##// END OF EJS Templates
Show the list of required tags when non was entered at thread creation
neko259 -
r1099:e990ce46 default
parent child Browse files
Show More
@@ -1,369 +1,372 b''
1 import re
1 import re
2 import time
2 import time
3 import pytz
3 import pytz
4
4
5 from django import forms
5 from django import forms
6 from django.core.files.uploadedfile import SimpleUploadedFile
6 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.forms.util import ErrorList
8 from django.forms.util import ErrorList
9 from django.utils.translation import ugettext_lazy as _
9 from django.utils.translation import ugettext_lazy as _
10 import requests
10 import requests
11
11
12 from boards.mdx_neboard import formatters
12 from boards.mdx_neboard import formatters
13 from boards.models.post import TITLE_MAX_LENGTH
13 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models import Tag, Post
14 from boards.models import Tag, Post
15 from neboard import settings
15 from neboard import settings
16 import boards.settings as board_settings
16 import boards.settings as board_settings
17
17
18
18
19 CONTENT_TYPE_IMAGE = (
19 CONTENT_TYPE_IMAGE = (
20 'image/jpeg',
20 'image/jpeg',
21 'image/png',
21 'image/png',
22 'image/gif',
22 'image/gif',
23 'image/bmp',
23 'image/bmp',
24 )
24 )
25
25
26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
27
27
28 VETERAN_POSTING_DELAY = 5
28 VETERAN_POSTING_DELAY = 5
29
29
30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
31 ATTRIBUTE_ROWS = 'rows'
31 ATTRIBUTE_ROWS = 'rows'
32
32
33 LAST_POST_TIME = 'last_post_time'
33 LAST_POST_TIME = 'last_post_time'
34 LAST_LOGIN_TIME = 'last_login_time'
34 LAST_LOGIN_TIME = 'last_login_time'
35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
37
37
38 LABEL_TITLE = _('Title')
38 LABEL_TITLE = _('Title')
39 LABEL_TEXT = _('Text')
39 LABEL_TEXT = _('Text')
40 LABEL_TAG = _('Tag')
40 LABEL_TAG = _('Tag')
41 LABEL_SEARCH = _('Search')
41 LABEL_SEARCH = _('Search')
42
42
43 TAG_MAX_LENGTH = 20
43 TAG_MAX_LENGTH = 20
44
44
45 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
45 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
46
46
47 HTTP_RESULT_OK = 200
47 HTTP_RESULT_OK = 200
48
48
49 TEXTAREA_ROWS = 4
49 TEXTAREA_ROWS = 4
50
50
51
51
52 def get_timezones():
52 def get_timezones():
53 timezones = []
53 timezones = []
54 for tz in pytz.common_timezones:
54 for tz in pytz.common_timezones:
55 timezones.append((tz, tz),)
55 timezones.append((tz, tz),)
56 return timezones
56 return timezones
57
57
58
58
59 class FormatPanel(forms.Textarea):
59 class FormatPanel(forms.Textarea):
60 """
60 """
61 Panel for text formatting. Consists of buttons to add different tags to the
61 Panel for text formatting. Consists of buttons to add different tags to the
62 form text area.
62 form text area.
63 """
63 """
64
64
65 def render(self, name, value, attrs=None):
65 def render(self, name, value, attrs=None):
66 output = '<div id="mark-panel">'
66 output = '<div id="mark-panel">'
67 for formatter in formatters:
67 for formatter in formatters:
68 output += '<span class="mark_btn"' + \
68 output += '<span class="mark_btn"' + \
69 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
69 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
70 '\', \'' + formatter.format_right + '\')">' + \
70 '\', \'' + formatter.format_right + '\')">' + \
71 formatter.preview_left + formatter.name + \
71 formatter.preview_left + formatter.name + \
72 formatter.preview_right + '</span>'
72 formatter.preview_right + '</span>'
73
73
74 output += '</div>'
74 output += '</div>'
75 output += super(FormatPanel, self).render(name, value, attrs=None)
75 output += super(FormatPanel, self).render(name, value, attrs=None)
76
76
77 return output
77 return output
78
78
79
79
80 class PlainErrorList(ErrorList):
80 class PlainErrorList(ErrorList):
81 def __unicode__(self):
81 def __unicode__(self):
82 return self.as_text()
82 return self.as_text()
83
83
84 def as_text(self):
84 def as_text(self):
85 return ''.join(['(!) %s ' % e for e in self])
85 return ''.join(['(!) %s ' % e for e in self])
86
86
87
87
88 class NeboardForm(forms.Form):
88 class NeboardForm(forms.Form):
89 """
89 """
90 Form with neboard-specific formatting.
90 Form with neboard-specific formatting.
91 """
91 """
92
92
93 def as_div(self):
93 def as_div(self):
94 """
94 """
95 Returns this form rendered as HTML <as_div>s.
95 Returns this form rendered as HTML <as_div>s.
96 """
96 """
97
97
98 return self._html_output(
98 return self._html_output(
99 # TODO Do not show hidden rows in the list here
99 # TODO Do not show hidden rows in the list here
100 normal_row='<div class="form-row"><div class="form-label">'
100 normal_row='<div class="form-row"><div class="form-label">'
101 '%(label)s'
101 '%(label)s'
102 '</div></div>'
102 '</div></div>'
103 '<div class="form-row"><div class="form-input">'
103 '<div class="form-row"><div class="form-input">'
104 '%(field)s'
104 '%(field)s'
105 '</div></div>'
105 '</div></div>'
106 '<div class="form-row">'
106 '<div class="form-row">'
107 '%(help_text)s'
107 '%(help_text)s'
108 '</div>',
108 '</div>',
109 error_row='<div class="form-row">'
109 error_row='<div class="form-row">'
110 '<div class="form-label"></div>'
110 '<div class="form-label"></div>'
111 '<div class="form-errors">%s</div>'
111 '<div class="form-errors">%s</div>'
112 '</div>',
112 '</div>',
113 row_ender='</div>',
113 row_ender='</div>',
114 help_text_html='%s',
114 help_text_html='%s',
115 errors_on_separate_row=True)
115 errors_on_separate_row=True)
116
116
117 def as_json_errors(self):
117 def as_json_errors(self):
118 errors = []
118 errors = []
119
119
120 for name, field in list(self.fields.items()):
120 for name, field in list(self.fields.items()):
121 if self[name].errors:
121 if self[name].errors:
122 errors.append({
122 errors.append({
123 'field': name,
123 'field': name,
124 'errors': self[name].errors.as_text(),
124 'errors': self[name].errors.as_text(),
125 })
125 })
126
126
127 return errors
127 return errors
128
128
129
129
130 class PostForm(NeboardForm):
130 class PostForm(NeboardForm):
131
131
132 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
132 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
133 label=LABEL_TITLE)
133 label=LABEL_TITLE)
134 text = forms.CharField(
134 text = forms.CharField(
135 widget=FormatPanel(attrs={
135 widget=FormatPanel(attrs={
136 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
136 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
137 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
137 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
138 }),
138 }),
139 required=False, label=LABEL_TEXT)
139 required=False, label=LABEL_TEXT)
140 image = forms.ImageField(required=False, label=_('Image'),
140 image = forms.ImageField(required=False, label=_('Image'),
141 widget=forms.ClearableFileInput(
141 widget=forms.ClearableFileInput(
142 attrs={'accept': 'image/*'}))
142 attrs={'accept': 'image/*'}))
143 image_url = forms.CharField(required=False, label=_('Image URL'),
143 image_url = forms.CharField(required=False, label=_('Image URL'),
144 widget=forms.TextInput(
144 widget=forms.TextInput(
145 attrs={ATTRIBUTE_PLACEHOLDER:
145 attrs={ATTRIBUTE_PLACEHOLDER:
146 'http://example.com/image.png'}))
146 'http://example.com/image.png'}))
147
147
148 # This field is for spam prevention only
148 # This field is for spam prevention only
149 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
149 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
150 widget=forms.TextInput(attrs={
150 widget=forms.TextInput(attrs={
151 'class': 'form-email'}))
151 'class': 'form-email'}))
152 threads = forms.CharField(required=False, label=_('Additional threads'),
152 threads = forms.CharField(required=False, label=_('Additional threads'),
153 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
153 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
154 '123 456 789'}))
154 '123 456 789'}))
155
155
156 session = None
156 session = None
157 need_to_ban = False
157 need_to_ban = False
158
158
159 def clean_title(self):
159 def clean_title(self):
160 title = self.cleaned_data['title']
160 title = self.cleaned_data['title']
161 if title:
161 if title:
162 if len(title) > TITLE_MAX_LENGTH:
162 if len(title) > TITLE_MAX_LENGTH:
163 raise forms.ValidationError(_('Title must have less than %s '
163 raise forms.ValidationError(_('Title must have less than %s '
164 'characters') %
164 'characters') %
165 str(TITLE_MAX_LENGTH))
165 str(TITLE_MAX_LENGTH))
166 return title
166 return title
167
167
168 def clean_text(self):
168 def clean_text(self):
169 text = self.cleaned_data['text'].strip()
169 text = self.cleaned_data['text'].strip()
170 if text:
170 if text:
171 if len(text) > board_settings.MAX_TEXT_LENGTH:
171 if len(text) > board_settings.MAX_TEXT_LENGTH:
172 raise forms.ValidationError(_('Text must have less than %s '
172 raise forms.ValidationError(_('Text must have less than %s '
173 'characters') %
173 'characters') %
174 str(board_settings
174 str(board_settings
175 .MAX_TEXT_LENGTH))
175 .MAX_TEXT_LENGTH))
176 return text
176 return text
177
177
178 def clean_image(self):
178 def clean_image(self):
179 image = self.cleaned_data['image']
179 image = self.cleaned_data['image']
180
180
181 if image:
181 if image:
182 self.validate_image_size(image.size)
182 self.validate_image_size(image.size)
183
183
184 return image
184 return image
185
185
186 def clean_image_url(self):
186 def clean_image_url(self):
187 url = self.cleaned_data['image_url']
187 url = self.cleaned_data['image_url']
188
188
189 image = None
189 image = None
190 if url:
190 if url:
191 image = self._get_image_from_url(url)
191 image = self._get_image_from_url(url)
192
192
193 if not image:
193 if not image:
194 raise forms.ValidationError(_('Invalid URL'))
194 raise forms.ValidationError(_('Invalid URL'))
195 else:
195 else:
196 self.validate_image_size(image.size)
196 self.validate_image_size(image.size)
197
197
198 return image
198 return image
199
199
200 def clean_threads(self):
200 def clean_threads(self):
201 threads_str = self.cleaned_data['threads']
201 threads_str = self.cleaned_data['threads']
202
202
203 if len(threads_str) > 0:
203 if len(threads_str) > 0:
204 threads_id_list = threads_str.split(' ')
204 threads_id_list = threads_str.split(' ')
205
205
206 threads = list()
206 threads = list()
207
207
208 for thread_id in threads_id_list:
208 for thread_id in threads_id_list:
209 try:
209 try:
210 thread = Post.objects.get(id=int(thread_id))
210 thread = Post.objects.get(id=int(thread_id))
211 if not thread.is_opening():
211 if not thread.is_opening():
212 raise ObjectDoesNotExist()
212 raise ObjectDoesNotExist()
213 threads.append(thread)
213 threads.append(thread)
214 except (ObjectDoesNotExist, ValueError):
214 except (ObjectDoesNotExist, ValueError):
215 raise forms.ValidationError(_('Invalid additional thread list'))
215 raise forms.ValidationError(_('Invalid additional thread list'))
216
216
217 return threads
217 return threads
218
218
219 def clean(self):
219 def clean(self):
220 cleaned_data = super(PostForm, self).clean()
220 cleaned_data = super(PostForm, self).clean()
221
221
222 if not self.session:
222 if not self.session:
223 raise forms.ValidationError('Humans have sessions')
223 raise forms.ValidationError('Humans have sessions')
224
224
225 if cleaned_data['email']:
225 if cleaned_data['email']:
226 self.need_to_ban = True
226 self.need_to_ban = True
227 raise forms.ValidationError('A human cannot enter a hidden field')
227 raise forms.ValidationError('A human cannot enter a hidden field')
228
228
229 if not self.errors:
229 if not self.errors:
230 self._clean_text_image()
230 self._clean_text_image()
231
231
232 if not self.errors and self.session:
232 if not self.errors and self.session:
233 self._validate_posting_speed()
233 self._validate_posting_speed()
234
234
235 return cleaned_data
235 return cleaned_data
236
236
237 def get_image(self):
237 def get_image(self):
238 """
238 """
239 Gets image from file or URL.
239 Gets image from file or URL.
240 """
240 """
241
241
242 image = self.cleaned_data['image']
242 image = self.cleaned_data['image']
243 return image if image else self.cleaned_data['image_url']
243 return image if image else self.cleaned_data['image_url']
244
244
245 def _clean_text_image(self):
245 def _clean_text_image(self):
246 text = self.cleaned_data.get('text')
246 text = self.cleaned_data.get('text')
247 image = self.get_image()
247 image = self.get_image()
248
248
249 if (not text) and (not image):
249 if (not text) and (not image):
250 error_message = _('Either text or image must be entered.')
250 error_message = _('Either text or image must be entered.')
251 self._errors['text'] = self.error_class([error_message])
251 self._errors['text'] = self.error_class([error_message])
252
252
253 def _validate_posting_speed(self):
253 def _validate_posting_speed(self):
254 can_post = True
254 can_post = True
255
255
256 posting_delay = settings.POSTING_DELAY
256 posting_delay = settings.POSTING_DELAY
257
257
258 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
258 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
259 self.session:
259 self.session:
260 now = time.time()
260 now = time.time()
261 last_post_time = self.session[LAST_POST_TIME]
261 last_post_time = self.session[LAST_POST_TIME]
262
262
263 current_delay = int(now - last_post_time)
263 current_delay = int(now - last_post_time)
264
264
265 if current_delay < posting_delay:
265 if current_delay < posting_delay:
266 error_message = _('Wait %s seconds after last posting') % str(
266 error_message = _('Wait %s seconds after last posting') % str(
267 posting_delay - current_delay)
267 posting_delay - current_delay)
268 self._errors['text'] = self.error_class([error_message])
268 self._errors['text'] = self.error_class([error_message])
269
269
270 can_post = False
270 can_post = False
271
271
272 if can_post:
272 if can_post:
273 self.session[LAST_POST_TIME] = time.time()
273 self.session[LAST_POST_TIME] = time.time()
274
274
275 def validate_image_size(self, size: int):
275 def validate_image_size(self, size: int):
276 if size > board_settings.MAX_IMAGE_SIZE:
276 if size > board_settings.MAX_IMAGE_SIZE:
277 raise forms.ValidationError(
277 raise forms.ValidationError(
278 _('Image must be less than %s bytes')
278 _('Image must be less than %s bytes')
279 % str(board_settings.MAX_IMAGE_SIZE))
279 % str(board_settings.MAX_IMAGE_SIZE))
280
280
281 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
281 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
282 """
282 """
283 Gets an image file from URL.
283 Gets an image file from URL.
284 """
284 """
285
285
286 img_temp = None
286 img_temp = None
287
287
288 try:
288 try:
289 # Verify content headers
289 # Verify content headers
290 response_head = requests.head(url, verify=False)
290 response_head = requests.head(url, verify=False)
291 content_type = response_head.headers['content-type'].split(';')[0]
291 content_type = response_head.headers['content-type'].split(';')[0]
292 if content_type in CONTENT_TYPE_IMAGE:
292 if content_type in CONTENT_TYPE_IMAGE:
293 length_header = response_head.headers.get('content-length')
293 length_header = response_head.headers.get('content-length')
294 if length_header:
294 if length_header:
295 length = int(length_header)
295 length = int(length_header)
296 self.validate_image_size(length)
296 self.validate_image_size(length)
297 # Get the actual content into memory
297 # Get the actual content into memory
298 response = requests.get(url, verify=False, stream=True)
298 response = requests.get(url, verify=False, stream=True)
299
299
300 # Download image, stop if the size exceeds limit
300 # Download image, stop if the size exceeds limit
301 size = 0
301 size = 0
302 content = b''
302 content = b''
303 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
303 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
304 size += len(chunk)
304 size += len(chunk)
305 self.validate_image_size(size)
305 self.validate_image_size(size)
306 content += chunk
306 content += chunk
307
307
308 if response.status_code == HTTP_RESULT_OK and content:
308 if response.status_code == HTTP_RESULT_OK and content:
309 # Set a dummy file name that will be replaced
309 # Set a dummy file name that will be replaced
310 # anyway, just keep the valid extension
310 # anyway, just keep the valid extension
311 filename = 'image.' + content_type.split('/')[1]
311 filename = 'image.' + content_type.split('/')[1]
312 img_temp = SimpleUploadedFile(filename, content,
312 img_temp = SimpleUploadedFile(filename, content,
313 content_type)
313 content_type)
314 except Exception:
314 except Exception:
315 # Just return no image
315 # Just return no image
316 pass
316 pass
317
317
318 return img_temp
318 return img_temp
319
319
320
320
321 class ThreadForm(PostForm):
321 class ThreadForm(PostForm):
322
322
323 tags = forms.CharField(
323 tags = forms.CharField(
324 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
324 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
325 max_length=100, label=_('Tags'), required=True)
325 max_length=100, label=_('Tags'), required=True)
326
326
327 def clean_tags(self):
327 def clean_tags(self):
328 tags = self.cleaned_data['tags'].strip()
328 tags = self.cleaned_data['tags'].strip()
329
329
330 if not tags or not REGEX_TAGS.match(tags):
330 if not tags or not REGEX_TAGS.match(tags):
331 raise forms.ValidationError(
331 raise forms.ValidationError(
332 _('Inappropriate characters in tags.'))
332 _('Inappropriate characters in tags.'))
333
333
334 required_tag_exists = False
334 required_tag_exists = False
335 for tag in tags.split():
335 for tag in tags.split():
336 tag_model = Tag.objects.filter(name=tag.strip().lower(),
336 tag_model = Tag.objects.filter(name=tag.strip().lower(),
337 required=True)
337 required=True)
338 if tag_model.exists():
338 if tag_model.exists():
339 required_tag_exists = True
339 required_tag_exists = True
340 break
340 break
341
341
342 if not required_tag_exists:
342 if not required_tag_exists:
343 raise forms.ValidationError(_('Need at least 1 required tag.'))
343 all_tags = Tag.objects.filter(required=True)
344 raise forms.ValidationError(
345 _('Need at least one of the tags: ')
346 + ', '.join([tag.name for tag in all_tags]))
344
347
345 return tags
348 return tags
346
349
347 def clean(self):
350 def clean(self):
348 cleaned_data = super(ThreadForm, self).clean()
351 cleaned_data = super(ThreadForm, self).clean()
349
352
350 return cleaned_data
353 return cleaned_data
351
354
352
355
353 class SettingsForm(NeboardForm):
356 class SettingsForm(NeboardForm):
354
357
355 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
358 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
356 username = forms.CharField(label=_('User name'), required=False)
359 username = forms.CharField(label=_('User name'), required=False)
357 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
360 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
358
361
359 def clean_username(self):
362 def clean_username(self):
360 username = self.cleaned_data['username']
363 username = self.cleaned_data['username']
361
364
362 if username and not REGEX_TAGS.match(username):
365 if username and not REGEX_TAGS.match(username):
363 raise forms.ValidationError(_('Inappropriate characters.'))
366 raise forms.ValidationError(_('Inappropriate characters.'))
364
367
365 return username
368 return username
366
369
367
370
368 class SearchForm(NeboardForm):
371 class SearchForm(NeboardForm):
369 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
372 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,401 +1,402 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-04-14 13:07+0300\n"
10 "POT-Creation-Date: 2015-04-14 21:16+0300\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: admin.py:22
21 #: admin.py:22
22 msgid "{} posters were banned"
22 msgid "{} posters were banned"
23 msgstr ""
23 msgstr ""
24
24
25 #: authors.py:9
25 #: authors.py:9
26 msgid "author"
26 msgid "author"
27 msgstr "автор"
27 msgstr "автор"
28
28
29 #: authors.py:10
29 #: authors.py:10
30 msgid "developer"
30 msgid "developer"
31 msgstr "разработчик"
31 msgstr "разработчик"
32
32
33 #: authors.py:11
33 #: authors.py:11
34 msgid "javascript developer"
34 msgid "javascript developer"
35 msgstr "разработчик javascript"
35 msgstr "разработчик javascript"
36
36
37 #: authors.py:12
37 #: authors.py:12
38 msgid "designer"
38 msgid "designer"
39 msgstr "дизайнер"
39 msgstr "дизайнер"
40
40
41 #: forms.py:35
41 #: forms.py:35
42 msgid "Type message here. Use formatting panel for more advanced usage."
42 msgid "Type message here. Use formatting panel for more advanced usage."
43 msgstr ""
43 msgstr ""
44 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
44 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
45
45
46 #: forms.py:36
46 #: forms.py:36
47 msgid "music images i_dont_like_tags"
47 msgid "music images i_dont_like_tags"
48 msgstr "музыка картинки теги_не_нужны"
48 msgstr "музыка картинки теги_не_нужны"
49
49
50 #: forms.py:38
50 #: forms.py:38
51 msgid "Title"
51 msgid "Title"
52 msgstr "Заголовок"
52 msgstr "Заголовок"
53
53
54 #: forms.py:39
54 #: forms.py:39
55 msgid "Text"
55 msgid "Text"
56 msgstr "Текст"
56 msgstr "Текст"
57
57
58 #: forms.py:40
58 #: forms.py:40
59 msgid "Tag"
59 msgid "Tag"
60 msgstr "Метка"
60 msgstr "Метка"
61
61
62 #: forms.py:41 templates/boards/base.html:40 templates/search/search.html:7
62 #: forms.py:41 templates/boards/base.html:40 templates/search/search.html:7
63 msgid "Search"
63 msgid "Search"
64 msgstr "Поиск"
64 msgstr "Поиск"
65
65
66 #: forms.py:140
66 #: forms.py:140
67 msgid "Image"
67 msgid "Image"
68 msgstr "Изображение"
68 msgstr "Изображение"
69
69
70 #: forms.py:143
70 #: forms.py:143
71 msgid "Image URL"
71 msgid "Image URL"
72 msgstr "URL изображения"
72 msgstr "URL изображения"
73
73
74 #: forms.py:149
74 #: forms.py:149
75 msgid "e-mail"
75 msgid "e-mail"
76 msgstr ""
76 msgstr ""
77
77
78 #: forms.py:152
78 #: forms.py:152
79 msgid "Additional threads"
79 msgid "Additional threads"
80 msgstr "Дополнительные темы"
80 msgstr "Дополнительные темы"
81
81
82 #: forms.py:163
82 #: forms.py:163
83 #, python-format
83 #, python-format
84 msgid "Title must have less than %s characters"
84 msgid "Title must have less than %s characters"
85 msgstr "Заголовок должен иметь меньше %s символов"
85 msgstr "Заголовок должен иметь меньше %s символов"
86
86
87 #: forms.py:172
87 #: forms.py:172
88 #, python-format
88 #, python-format
89 msgid "Text must have less than %s characters"
89 msgid "Text must have less than %s characters"
90 msgstr "Текст должен быть короче %s символов"
90 msgstr "Текст должен быть короче %s символов"
91
91
92 #: forms.py:194
92 #: forms.py:194
93 msgid "Invalid URL"
93 msgid "Invalid URL"
94 msgstr "Неверный URL"
94 msgstr "Неверный URL"
95
95
96 #: forms.py:215
96 #: forms.py:215
97 msgid "Invalid additional thread list"
97 msgid "Invalid additional thread list"
98 msgstr "Неверный список дополнительных тем"
98 msgstr "Неверный список дополнительных тем"
99
99
100 #: forms.py:250
100 #: forms.py:250
101 msgid "Either text or image must be entered."
101 msgid "Either text or image must be entered."
102 msgstr "Текст или картинка должны быть введены."
102 msgstr "Текст или картинка должны быть введены."
103
103
104 #: forms.py:266
104 #: forms.py:266
105 #, python-format
105 #, python-format
106 msgid "Wait %s seconds after last posting"
106 msgid "Wait %s seconds after last posting"
107 msgstr "Подождите %s секунд после последнего постинга"
107 msgstr "Подождите %s секунд после последнего постинга"
108
108
109 #: forms.py:278
109 #: forms.py:278
110 #, python-format
110 #, python-format
111 msgid "Image must be less than %s bytes"
111 msgid "Image must be less than %s bytes"
112 msgstr "Изображение должно быть менее %s байт"
112 msgstr "Изображение должно быть менее %s байт"
113
113
114 #: forms.py:325 templates/boards/posting_general.html:148
114 #: forms.py:325 templates/boards/posting_general.html:148
115 #: templates/boards/rss/post.html:10 templates/boards/tags.html:7
115 #: templates/boards/rss/post.html:10 templates/boards/tags.html:7
116 msgid "Tags"
116 msgid "Tags"
117 msgstr "Метки"
117 msgstr "Метки"
118
118
119 #: forms.py:332
119 #: forms.py:332
120 msgid "Inappropriate characters in tags."
120 msgid "Inappropriate characters in tags."
121 msgstr "Недопустимые символы в метках."
121 msgstr "Недопустимые символы в метках."
122
122
123 #: forms.py:343
123 #: forms.py:345
124 msgid "Need at least 1 required tag."
124 #| msgid "Need at least 1 required tag."
125 msgstr "Нужна хотя бы 1 обязательная метка."
125 msgid "Need at least one of the tags: "
126 msgstr "Нужна хотя бы одна из меток: "
126
127
127 #: forms.py:355
128 #: forms.py:358
128 msgid "Theme"
129 msgid "Theme"
129 msgstr "Тема"
130 msgstr "Тема"
130
131
131 #: forms.py:356
132 #: forms.py:359
132 msgid "User name"
133 msgid "User name"
133 msgstr "Имя пользователя"
134 msgstr "Имя пользователя"
134
135
135 #: forms.py:357
136 #: forms.py:360
136 msgid "Time zone"
137 msgid "Time zone"
137 msgstr "Часовой пояс"
138 msgstr "Часовой пояс"
138
139
139 #: forms.py:363
140 #: forms.py:366
140 msgid "Inappropriate characters."
141 msgid "Inappropriate characters."
141 msgstr "Недопустимые символы."
142 msgstr "Недопустимые символы."
142
143
143 #: templates/boards/404.html:6
144 #: templates/boards/404.html:6
144 msgid "Not found"
145 msgid "Not found"
145 msgstr "Не найдено"
146 msgstr "Не найдено"
146
147
147 #: templates/boards/404.html:12
148 #: templates/boards/404.html:12
148 msgid "This page does not exist"
149 msgid "This page does not exist"
149 msgstr "Этой страницы не существует"
150 msgstr "Этой страницы не существует"
150
151
151 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
152 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
152 msgid "Authors"
153 msgid "Authors"
153 msgstr "Авторы"
154 msgstr "Авторы"
154
155
155 #: templates/boards/authors.html:26
156 #: templates/boards/authors.html:26
156 msgid "Distributed under the"
157 msgid "Distributed under the"
157 msgstr "Распространяется под"
158 msgstr "Распространяется под"
158
159
159 #: templates/boards/authors.html:28
160 #: templates/boards/authors.html:28
160 msgid "license"
161 msgid "license"
161 msgstr "лицензией"
162 msgstr "лицензией"
162
163
163 #: templates/boards/authors.html:30
164 #: templates/boards/authors.html:30
164 msgid "Repository"
165 msgid "Repository"
165 msgstr "Репозиторий"
166 msgstr "Репозиторий"
166
167
167 #: templates/boards/base.html:13
168 #: templates/boards/base.html:13
168 msgid "Feed"
169 msgid "Feed"
169 msgstr "Лента"
170 msgstr "Лента"
170
171
171 #: templates/boards/base.html:30
172 #: templates/boards/base.html:30
172 msgid "All threads"
173 msgid "All threads"
173 msgstr "Все темы"
174 msgstr "Все темы"
174
175
175 #: templates/boards/base.html:36
176 #: templates/boards/base.html:36
176 msgid "Add tags"
177 msgid "Add tags"
177 msgstr "Добавить метки"
178 msgstr "Добавить метки"
178
179
179 #: templates/boards/base.html:38
180 #: templates/boards/base.html:38
180 msgid "Tag management"
181 msgid "Tag management"
181 msgstr "Управление метками"
182 msgstr "Управление метками"
182
183
183 #: templates/boards/base.html:43 templates/boards/base.html.py:44
184 #: templates/boards/base.html:43 templates/boards/base.html.py:44
184 #: templates/boards/notifications.html:8
185 #: templates/boards/notifications.html:8
185 msgid "Notifications"
186 msgid "Notifications"
186 msgstr "Уведомления"
187 msgstr "Уведомления"
187
188
188 #: templates/boards/base.html:51 templates/boards/settings.html:9
189 #: templates/boards/base.html:51 templates/boards/settings.html:9
189 msgid "Settings"
190 msgid "Settings"
190 msgstr "Настройки"
191 msgstr "Настройки"
191
192
192 #: templates/boards/base.html:64
193 #: templates/boards/base.html:64
193 msgid "Admin"
194 msgid "Admin"
194 msgstr "Администрирование"
195 msgstr "Администрирование"
195
196
196 #: templates/boards/base.html:66
197 #: templates/boards/base.html:66
197 #, python-format
198 #, python-format
198 msgid "Speed: %(ppd)s posts per day"
199 msgid "Speed: %(ppd)s posts per day"
199 msgstr "Скорость: %(ppd)s сообщений в день"
200 msgstr "Скорость: %(ppd)s сообщений в день"
200
201
201 #: templates/boards/base.html:68
202 #: templates/boards/base.html:68
202 msgid "Up"
203 msgid "Up"
203 msgstr "Вверх"
204 msgstr "Вверх"
204
205
205 #: templates/boards/notifications.html:17
206 #: templates/boards/notifications.html:17
206 #: templates/boards/posting_general.html:81 templates/search/search.html:26
207 #: templates/boards/posting_general.html:81 templates/search/search.html:26
207 msgid "Previous page"
208 msgid "Previous page"
208 msgstr "Предыдущая страница"
209 msgstr "Предыдущая страница"
209
210
210 #: templates/boards/notifications.html:27
211 #: templates/boards/notifications.html:27
211 #: templates/boards/posting_general.html:121 templates/search/search.html:37
212 #: templates/boards/posting_general.html:121 templates/search/search.html:37
212 msgid "Next page"
213 msgid "Next page"
213 msgstr "Следующая страница"
214 msgstr "Следующая страница"
214
215
215 #: templates/boards/post.html:32
216 #: templates/boards/post.html:25
216 msgid "Open"
217 msgid "Open"
217 msgstr "Открыть"
218 msgstr "Открыть"
218
219
219 #: templates/boards/post.html:34 templates/boards/post.html.py:38
220 #: templates/boards/post.html:27 templates/boards/post.html.py:31
220 msgid "Reply"
221 msgid "Reply"
221 msgstr "Ответ"
222 msgstr "Ответ"
222
223
223 #: templates/boards/post.html:43
224 #: templates/boards/post.html:36
224 msgid "Edit"
225 msgid "Edit"
225 msgstr "Изменить"
226 msgstr "Изменить"
226
227
227 #: templates/boards/post.html:45
228 #: templates/boards/post.html:38
228 msgid "Edit thread"
229 msgid "Edit thread"
229 msgstr "Изменить тему"
230 msgstr "Изменить тему"
230
231
231 #: templates/boards/post.html:75
232 #: templates/boards/post.html:70
232 msgid "Replies"
233 msgid "Replies"
233 msgstr "Ответы"
234 msgstr "Ответы"
234
235
235 #: templates/boards/post.html:86 templates/boards/thread.html:31
236 #: templates/boards/post.html:82 templates/boards/thread.html:31
236 msgid "messages"
237 msgid "messages"
237 msgstr "сообщений"
238 msgstr "сообщений"
238
239
239 #: templates/boards/post.html:87 templates/boards/thread.html:32
240 #: templates/boards/post.html:83 templates/boards/thread.html:32
240 msgid "images"
241 msgid "images"
241 msgstr "изображений"
242 msgstr "изображений"
242
243
243 #: templates/boards/posting_general.html:65
244 #: templates/boards/posting_general.html:65
244 msgid "Edit tag"
245 msgid "Edit tag"
245 msgstr "Изменить метку"
246 msgstr "Изменить метку"
246
247
247 #: templates/boards/posting_general.html:68
248 #: templates/boards/posting_general.html:68
248 #, python-format
249 #, python-format
249 msgid "This tag has %(thread_count)s threads and %(post_count)s posts."
250 msgid "This tag has %(thread_count)s threads and %(post_count)s posts."
250 msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений."
251 msgstr "С этой меткой есть %(thread_count)s тем и %(post_count)s сообщений."
251
252
252 #: templates/boards/posting_general.html:96
253 #: templates/boards/posting_general.html:96
253 #, python-format
254 #, python-format
254 msgid "Skipped %(count)s replies. Open thread to see all replies."
255 msgid "Skipped %(count)s replies. Open thread to see all replies."
255 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
256 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
256
257
257 #: templates/boards/posting_general.html:126
258 #: templates/boards/posting_general.html:126
258 msgid "No threads exist. Create the first one!"
259 msgid "No threads exist. Create the first one!"
259 msgstr "Нет тем. Создайте первую!"
260 msgstr "Нет тем. Создайте первую!"
260
261
261 #: templates/boards/posting_general.html:132
262 #: templates/boards/posting_general.html:132
262 msgid "Create new thread"
263 msgid "Create new thread"
263 msgstr "Создать новую тему"
264 msgstr "Создать новую тему"
264
265
265 #: templates/boards/posting_general.html:137 templates/boards/preview.html:16
266 #: templates/boards/posting_general.html:137 templates/boards/preview.html:16
266 #: templates/boards/thread_normal.html:46
267 #: templates/boards/thread_normal.html:46
267 msgid "Post"
268 msgid "Post"
268 msgstr "Отправить"
269 msgstr "Отправить"
269
270
270 #: templates/boards/posting_general.html:142
271 #: templates/boards/posting_general.html:142
271 msgid "Tags must be delimited by spaces. Text or image is required."
272 msgid "Tags must be delimited by spaces. Text or image is required."
272 msgstr ""
273 msgstr ""
273 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
274 "Метки должны быть разделены пробелами. Текст или изображение обязательны."
274
275
275 #: templates/boards/posting_general.html:145
276 #: templates/boards/posting_general.html:145
276 #: templates/boards/thread_normal.html:51
277 #: templates/boards/thread_normal.html:51
277 msgid "Text syntax"
278 msgid "Text syntax"
278 msgstr "Синтаксис текста"
279 msgstr "Синтаксис текста"
279
280
280 #: templates/boards/posting_general.html:161
281 #: templates/boards/posting_general.html:161
281 msgid "Pages:"
282 msgid "Pages:"
282 msgstr "Страницы: "
283 msgstr "Страницы: "
283
284
284 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20
285 #: templates/boards/preview.html:6 templates/boards/staticpages/help.html:20
285 msgid "Preview"
286 msgid "Preview"
286 msgstr "Предпросмотр"
287 msgstr "Предпросмотр"
287
288
288 #: templates/boards/rss/post.html:5
289 #: templates/boards/rss/post.html:5
289 msgid "Post image"
290 msgid "Post image"
290 msgstr "Изображение сообщения"
291 msgstr "Изображение сообщения"
291
292
292 #: templates/boards/settings.html:17
293 #: templates/boards/settings.html:17
293 msgid "You are moderator."
294 msgid "You are moderator."
294 msgstr "Вы модератор."
295 msgstr "Вы модератор."
295
296
296 #: templates/boards/settings.html:21
297 #: templates/boards/settings.html:21
297 msgid "Hidden tags:"
298 msgid "Hidden tags:"
298 msgstr "Скрытые метки:"
299 msgstr "Скрытые метки:"
299
300
300 #: templates/boards/settings.html:29
301 #: templates/boards/settings.html:29
301 msgid "No hidden tags."
302 msgid "No hidden tags."
302 msgstr "Нет скрытых меток."
303 msgstr "Нет скрытых меток."
303
304
304 #: templates/boards/settings.html:38
305 #: templates/boards/settings.html:38
305 msgid "Save"
306 msgid "Save"
306 msgstr "Сохранить"
307 msgstr "Сохранить"
307
308
308 #: templates/boards/staticpages/banned.html:6
309 #: templates/boards/staticpages/banned.html:6
309 msgid "Banned"
310 msgid "Banned"
310 msgstr "Заблокирован"
311 msgstr "Заблокирован"
311
312
312 #: templates/boards/staticpages/banned.html:11
313 #: templates/boards/staticpages/banned.html:11
313 msgid "Your IP address has been banned. Contact the administrator"
314 msgid "Your IP address has been banned. Contact the administrator"
314 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
315 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
315
316
316 #: templates/boards/staticpages/help.html:6
317 #: templates/boards/staticpages/help.html:6
317 #: templates/boards/staticpages/help.html:10
318 #: templates/boards/staticpages/help.html:10
318 msgid "Syntax"
319 msgid "Syntax"
319 msgstr "Синтаксис"
320 msgstr "Синтаксис"
320
321
321 #: templates/boards/staticpages/help.html:11
322 #: templates/boards/staticpages/help.html:11
322 msgid "Italic text"
323 msgid "Italic text"
323 msgstr "Курсивный текст"
324 msgstr "Курсивный текст"
324
325
325 #: templates/boards/staticpages/help.html:12
326 #: templates/boards/staticpages/help.html:12
326 msgid "Bold text"
327 msgid "Bold text"
327 msgstr "Полужирный текст"
328 msgstr "Полужирный текст"
328
329
329 #: templates/boards/staticpages/help.html:13
330 #: templates/boards/staticpages/help.html:13
330 msgid "Spoiler"
331 msgid "Spoiler"
331 msgstr "Спойлер"
332 msgstr "Спойлер"
332
333
333 #: templates/boards/staticpages/help.html:14
334 #: templates/boards/staticpages/help.html:14
334 msgid "Link to a post"
335 msgid "Link to a post"
335 msgstr "Ссылка на сообщение"
336 msgstr "Ссылка на сообщение"
336
337
337 #: templates/boards/staticpages/help.html:15
338 #: templates/boards/staticpages/help.html:15
338 msgid "Strikethrough text"
339 msgid "Strikethrough text"
339 msgstr "Зачеркнутый текст"
340 msgstr "Зачеркнутый текст"
340
341
341 #: templates/boards/staticpages/help.html:16
342 #: templates/boards/staticpages/help.html:16
342 msgid "Comment"
343 msgid "Comment"
343 msgstr "Комментарий"
344 msgstr "Комментарий"
344
345
345 #: templates/boards/staticpages/help.html:17
346 #: templates/boards/staticpages/help.html:17
346 #: templates/boards/staticpages/help.html:18
347 #: templates/boards/staticpages/help.html:18
347 msgid "Quote"
348 msgid "Quote"
348 msgstr "Цитата"
349 msgstr "Цитата"
349
350
350 #: templates/boards/staticpages/help.html:20
351 #: templates/boards/staticpages/help.html:20
351 msgid "You can try pasting the text and previewing the result here:"
352 msgid "You can try pasting the text and previewing the result here:"
352 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
353 msgstr "Вы можете попробовать вставить текст и проверить результат здесь:"
353
354
354 #: templates/boards/tags.html:23
355 #: templates/boards/tags.html:23
355 msgid "No tags found."
356 msgid "No tags found."
356 msgstr "Метки не найдены."
357 msgstr "Метки не найдены."
357
358
358 #: templates/boards/tags.html:26
359 #: templates/boards/tags.html:26
359 msgid "All tags"
360 msgid "All tags"
360 msgstr "Все метки"
361 msgstr "Все метки"
361
362
362 #: templates/boards/thread.html:33
363 #: templates/boards/thread.html:33
363 msgid "Last update: "
364 msgid "Last update: "
364 msgstr "Последнее обновление: "
365 msgstr "Последнее обновление: "
365
366
366 #: templates/boards/thread_gallery.html:21
367 #: templates/boards/thread_gallery.html:21
367 #: templates/boards/thread_normal.html:16
368 #: templates/boards/thread_normal.html:16
368 msgid "Normal mode"
369 msgid "Normal mode"
369 msgstr "Нормальный режим"
370 msgstr "Нормальный режим"
370
371
371 #: templates/boards/thread_gallery.html:22
372 #: templates/boards/thread_gallery.html:22
372 #: templates/boards/thread_normal.html:17
373 #: templates/boards/thread_normal.html:17
373 msgid "Gallery mode"
374 msgid "Gallery mode"
374 msgstr "Режим галереи"
375 msgstr "Режим галереи"
375
376
376 #: templates/boards/thread_gallery.html:52
377 #: templates/boards/thread_gallery.html:52
377 msgid "No images."
378 msgid "No images."
378 msgstr "Нет изображений."
379 msgstr "Нет изображений."
379
380
380 #: templates/boards/thread_normal.html:25
381 #: templates/boards/thread_normal.html:25
381 msgid "posts to bumplimit"
382 msgid "posts to bumplimit"
382 msgstr "сообщений до бамплимита"
383 msgstr "сообщений до бамплимита"
383
384
384 #: templates/boards/thread_normal.html:39
385 #: templates/boards/thread_normal.html:39
385 msgid "Reply to thread"
386 msgid "Reply to thread"
386 msgstr "Ответить в тему"
387 msgstr "Ответить в тему"
387
388
388 #: templates/boards/thread_normal.html:52
389 #: templates/boards/thread_normal.html:52
389 msgid "Close form"
390 msgid "Close form"
390 msgstr "Закрыть форму"
391 msgstr "Закрыть форму"
391
392
392 #: templates/boards/thread_normal.html:68
393 #: templates/boards/thread_normal.html:68
393 msgid "Update"
394 msgid "Update"
394 msgstr "Обновить"
395 msgstr "Обновить"
395
396
396 #: templates/search/search.html:17
397 #: templates/search/search.html:17
397 msgid "Ok"
398 msgid "Ok"
398 msgstr "Ок"
399 msgstr "Ок"
399
400
400 #~ msgid "tag1 several_words_tag"
401 #~ msgid "tag1 several_words_tag"
401 #~ msgstr "метка1 метка_из_нескольких_слов"
402 #~ msgstr "метка1 метка_из_нескольких_слов"
General Comments 0
You need to be logged in to leave comments. Login now