##// END OF EJS Templates
Added support for youtu.be links. Show file size when size validation failed
neko259 -
r1433:8bc8ced0 default
parent child Browse files
Show More
@@ -1,440 +1,439 b''
1 1 import hashlib
2 2 import re
3 3 import time
4 4 import logging
5 5
6 6 import pytz
7 7
8 8 from django import forms
9 9 from django.core.files.uploadedfile import SimpleUploadedFile
10 10 from django.core.exceptions import ObjectDoesNotExist
11 11 from django.forms.util import ErrorList
12 12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 13 from django.utils import timezone
14 14
15 15 from boards.mdx_neboard import formatters
16 16 from boards.models.attachment.downloaders import Downloader
17 17 from boards.models.post import TITLE_MAX_LENGTH
18 18 from boards.models import Tag, Post
19 19 from boards.utils import validate_file_size, get_file_mimetype, \
20 20 FILE_EXTENSION_DELIMITER
21 21 from neboard import settings
22 22 import boards.settings as board_settings
23 23 import neboard
24 24
25 25 POW_HASH_LENGTH = 16
26 26 POW_LIFE_MINUTES = 1
27 27
28 28 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
29 29 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
30 30
31 31 VETERAN_POSTING_DELAY = 5
32 32
33 33 ATTRIBUTE_PLACEHOLDER = 'placeholder'
34 34 ATTRIBUTE_ROWS = 'rows'
35 35
36 36 LAST_POST_TIME = 'last_post_time'
37 37 LAST_LOGIN_TIME = 'last_login_time'
38 38 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
39 39 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
40 40
41 41 LABEL_TITLE = _('Title')
42 42 LABEL_TEXT = _('Text')
43 43 LABEL_TAG = _('Tag')
44 44 LABEL_SEARCH = _('Search')
45 45
46 46 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
47 47 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
48 48
49 49 TAG_MAX_LENGTH = 20
50 50
51 51 TEXTAREA_ROWS = 4
52 52
53 53 TRIPCODE_DELIM = '#'
54 54
55 55 # TODO Maybe this may be converted into the database table?
56 56 MIMETYPE_EXTENSIONS = {
57 57 'image/jpeg': 'jpeg',
58 58 'image/png': 'png',
59 59 'image/gif': 'gif',
60 60 'video/webm': 'webm',
61 61 'application/pdf': 'pdf',
62 62 'x-diff': 'diff',
63 63 'image/svg+xml': 'svg',
64 64 'application/x-shockwave-flash': 'swf',
65 65 }
66 66
67 67
68 68 def get_timezones():
69 69 timezones = []
70 70 for tz in pytz.common_timezones:
71 71 timezones.append((tz, tz),)
72 72 return timezones
73 73
74 74
75 75 class FormatPanel(forms.Textarea):
76 76 """
77 77 Panel for text formatting. Consists of buttons to add different tags to the
78 78 form text area.
79 79 """
80 80
81 81 def render(self, name, value, attrs=None):
82 82 output = '<div id="mark-panel">'
83 83 for formatter in formatters:
84 84 output += '<span class="mark_btn"' + \
85 85 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
86 86 '\', \'' + formatter.format_right + '\')">' + \
87 87 formatter.preview_left + formatter.name + \
88 88 formatter.preview_right + '</span>'
89 89
90 90 output += '</div>'
91 91 output += super(FormatPanel, self).render(name, value, attrs=attrs)
92 92
93 93 return output
94 94
95 95
96 96 class PlainErrorList(ErrorList):
97 97 def __unicode__(self):
98 98 return self.as_text()
99 99
100 100 def as_text(self):
101 101 return ''.join(['(!) %s ' % e for e in self])
102 102
103 103
104 104 class NeboardForm(forms.Form):
105 105 """
106 106 Form with neboard-specific formatting.
107 107 """
108 108
109 109 def as_div(self):
110 110 """
111 111 Returns this form rendered as HTML <as_div>s.
112 112 """
113 113
114 114 return self._html_output(
115 115 # TODO Do not show hidden rows in the list here
116 116 normal_row='<div class="form-row">'
117 117 '<div class="form-label">'
118 118 '%(label)s'
119 119 '</div>'
120 120 '<div class="form-input">'
121 121 '%(field)s'
122 122 '</div>'
123 123 '</div>'
124 124 '<div class="form-row">'
125 125 '%(help_text)s'
126 126 '</div>',
127 127 error_row='<div class="form-row">'
128 128 '<div class="form-label"></div>'
129 129 '<div class="form-errors">%s</div>'
130 130 '</div>',
131 131 row_ender='</div>',
132 132 help_text_html='%s',
133 133 errors_on_separate_row=True)
134 134
135 135 def as_json_errors(self):
136 136 errors = []
137 137
138 138 for name, field in list(self.fields.items()):
139 139 if self[name].errors:
140 140 errors.append({
141 141 'field': name,
142 142 'errors': self[name].errors.as_text(),
143 143 })
144 144
145 145 return errors
146 146
147 147
148 148 class PostForm(NeboardForm):
149 149
150 150 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
151 151 label=LABEL_TITLE,
152 152 widget=forms.TextInput(
153 153 attrs={ATTRIBUTE_PLACEHOLDER:
154 154 'test#tripcode'}))
155 155 text = forms.CharField(
156 156 widget=FormatPanel(attrs={
157 157 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
158 158 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
159 159 }),
160 160 required=False, label=LABEL_TEXT)
161 161 file = forms.FileField(required=False, label=_('File'),
162 162 widget=forms.ClearableFileInput(
163 163 attrs={'accept': 'file/*'}))
164 164 file_url = forms.CharField(required=False, label=_('File URL'),
165 165 widget=forms.TextInput(
166 166 attrs={ATTRIBUTE_PLACEHOLDER:
167 167 'http://example.com/image.png'}))
168 168
169 169 # This field is for spam prevention only
170 170 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
171 171 widget=forms.TextInput(attrs={
172 172 'class': 'form-email'}))
173 173 threads = forms.CharField(required=False, label=_('Additional threads'),
174 174 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
175 175 '123 456 789'}))
176 176
177 177 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
178 178 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
179 179 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
180 180
181 181 session = None
182 182 need_to_ban = False
183 183
184 184 def _update_file_extension(self, file):
185 185 if file:
186 186 mimetype = get_file_mimetype(file)
187 187 extension = MIMETYPE_EXTENSIONS.get(mimetype)
188 188 if extension:
189 189 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
190 190 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
191 191
192 192 file.name = new_filename
193 193 else:
194 194 logger = logging.getLogger('boards.forms.extension')
195 195
196 196 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
197 197
198 198 def clean_title(self):
199 199 title = self.cleaned_data['title']
200 200 if title:
201 201 if len(title) > TITLE_MAX_LENGTH:
202 202 raise forms.ValidationError(_('Title must have less than %s '
203 203 'characters') %
204 204 str(TITLE_MAX_LENGTH))
205 205 return title
206 206
207 207 def clean_text(self):
208 208 text = self.cleaned_data['text'].strip()
209 209 if text:
210 210 max_length = board_settings.get_int('Forms', 'MaxTextLength')
211 211 if len(text) > max_length:
212 212 raise forms.ValidationError(_('Text must have less than %s '
213 213 'characters') % str(max_length))
214 214 return text
215 215
216 216 def clean_file(self):
217 217 file = self.cleaned_data['file']
218 218
219 219 if file:
220 220 validate_file_size(file.size)
221 221 self._update_file_extension(file)
222 222
223 223 return file
224 224
225 225 def clean_file_url(self):
226 226 url = self.cleaned_data['file_url']
227 227
228 228 file = None
229 229 if url:
230 230 file = self._get_file_from_url(url)
231 231
232 232 if not file:
233 233 raise forms.ValidationError(_('Invalid URL'))
234 234 else:
235 235 validate_file_size(file.size)
236 236 self._update_file_extension(file)
237 237
238 238 return file
239 239
240 240 def clean_threads(self):
241 241 threads_str = self.cleaned_data['threads']
242 242
243 243 if len(threads_str) > 0:
244 244 threads_id_list = threads_str.split(' ')
245 245
246 246 threads = list()
247 247
248 248 for thread_id in threads_id_list:
249 249 try:
250 250 thread = Post.objects.get(id=int(thread_id))
251 251 if not thread.is_opening() or thread.get_thread().is_archived():
252 252 raise ObjectDoesNotExist()
253 253 threads.append(thread)
254 254 except (ObjectDoesNotExist, ValueError):
255 255 raise forms.ValidationError(_('Invalid additional thread list'))
256 256
257 257 return threads
258 258
259 259 def clean(self):
260 260 cleaned_data = super(PostForm, self).clean()
261 261
262 262 if cleaned_data['email']:
263 263 self.need_to_ban = True
264 264 raise forms.ValidationError('A human cannot enter a hidden field')
265 265
266 266 if not self.errors:
267 267 self._clean_text_file()
268 268
269 269 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
270 270 if not self.errors and limit_speed:
271 271 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
272 272 if pow_difficulty > 0 and cleaned_data['timestamp'] and cleaned_data['iteration'] and cleaned_data['guess']:
273 273 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
274 274 else:
275 275 self._validate_posting_speed()
276 276
277 277 return cleaned_data
278 278
279 279 def get_file(self):
280 280 """
281 281 Gets file from form or URL.
282 282 """
283 283
284 284 file = self.cleaned_data['file']
285 285 return file or self.cleaned_data['file_url']
286 286
287 287 def get_tripcode(self):
288 288 title = self.cleaned_data['title']
289 289 if title is not None and TRIPCODE_DELIM in title:
290 290 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
291 291 tripcode = hashlib.md5(code.encode()).hexdigest()
292 292 else:
293 293 tripcode = ''
294 294 return tripcode
295 295
296 296 def get_title(self):
297 297 title = self.cleaned_data['title']
298 298 if title is not None and TRIPCODE_DELIM in title:
299 299 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
300 300 else:
301 301 return title
302 302
303 303 def _clean_text_file(self):
304 304 text = self.cleaned_data.get('text')
305 305 file = self.get_file()
306 306
307 307 if (not text) and (not file):
308 308 error_message = _('Either text or file must be entered.')
309 309 self._errors['text'] = self.error_class([error_message])
310 310
311 311 def _validate_posting_speed(self):
312 312 can_post = True
313 313
314 314 posting_delay = settings.POSTING_DELAY
315 315
316 316 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
317 317 now = time.time()
318 318
319 319 current_delay = 0
320 320
321 321 if LAST_POST_TIME not in self.session:
322 322 self.session[LAST_POST_TIME] = now
323 323
324 324 need_delay = True
325 325 else:
326 326 last_post_time = self.session.get(LAST_POST_TIME)
327 327 current_delay = int(now - last_post_time)
328 328
329 329 need_delay = current_delay < posting_delay
330 330
331 331 if need_delay:
332 332 delay = posting_delay - current_delay
333 333 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
334 334 delay) % {'delay': delay}
335 335 self._errors['text'] = self.error_class([error_message])
336 336
337 337 can_post = False
338 338
339 339 if can_post:
340 340 self.session[LAST_POST_TIME] = now
341 341
342 342 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
343 343 """
344 344 Gets an file file from URL.
345 345 """
346 346
347 347 img_temp = None
348 348
349 349 try:
350 350 for downloader in Downloader.__subclasses__():
351 351 if downloader.handles(url):
352 352 return downloader.download(url)
353 353 # If nobody of the specific downloaders handles this, use generic
354 354 # one
355 355 return Downloader.download(url)
356 356 except forms.ValidationError as e:
357 357 raise e
358 358 except Exception as e:
359 # Just return no file
360 pass
359 raise forms.ValidationError(e)
361 360
362 361 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
363 362 post_time = timezone.datetime.fromtimestamp(
364 363 int(timestamp[:-3]), tz=timezone.get_current_timezone())
365 364 timedelta = (timezone.now() - post_time).seconds / 60
366 365 if timedelta > POW_LIFE_MINUTES:
367 366 self._errors['text'] = self.error_class([_('Stale PoW.')])
368 367
369 368 payload = timestamp + message.replace('\r\n', '\n')
370 369 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
371 370 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
372 371 if len(target) < POW_HASH_LENGTH:
373 372 target = '0' * (POW_HASH_LENGTH - len(target)) + target
374 373
375 374 computed_guess = hashlib.sha256((payload + iteration).encode())\
376 375 .hexdigest()[0:POW_HASH_LENGTH]
377 376 if guess != computed_guess or guess > target:
378 377 self._errors['text'] = self.error_class(
379 378 [_('Invalid PoW.')])
380 379
381 380
382 381 class ThreadForm(PostForm):
383 382
384 383 tags = forms.CharField(
385 384 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
386 385 max_length=100, label=_('Tags'), required=True)
387 386
388 387 def clean_tags(self):
389 388 tags = self.cleaned_data['tags'].strip()
390 389
391 390 if not tags or not REGEX_TAGS.match(tags):
392 391 raise forms.ValidationError(
393 392 _('Inappropriate characters in tags.'))
394 393
395 394 required_tag_exists = False
396 395 tag_set = set()
397 396 for tag_string in tags.split():
398 397 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
399 398 tag_set.add(tag)
400 399
401 400 # If this is a new tag, don't check for its parents because nobody
402 401 # added them yet
403 402 if not created:
404 403 tag_set |= set(tag.get_all_parents())
405 404
406 405 for tag in tag_set:
407 406 if tag.required:
408 407 required_tag_exists = True
409 408 break
410 409
411 410 if not required_tag_exists:
412 411 raise forms.ValidationError(
413 412 _('Need at least one section.'))
414 413
415 414 return tag_set
416 415
417 416 def clean(self):
418 417 cleaned_data = super(ThreadForm, self).clean()
419 418
420 419 return cleaned_data
421 420
422 421
423 422 class SettingsForm(NeboardForm):
424 423
425 424 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
426 425 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
427 426 username = forms.CharField(label=_('User name'), required=False)
428 427 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
429 428
430 429 def clean_username(self):
431 430 username = self.cleaned_data['username']
432 431
433 432 if username and not REGEX_USERNAMES.match(username):
434 433 raise forms.ValidationError(_('Inappropriate characters.'))
435 434
436 435 return username
437 436
438 437
439 438 class SearchForm(NeboardForm):
440 439 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
1 NO CONTENT: modified file, binary diff hidden
@@ -1,529 +1,529 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 10 "POT-Creation-Date: 2015-10-09 23:21+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 21 #: admin.py:22
22 22 msgid "{} posters were banned"
23 23 msgstr ""
24 24
25 25 #: authors.py:9
26 26 msgid "author"
27 27 msgstr "Π°Π²Ρ‚ΠΎΡ€"
28 28
29 29 #: authors.py:10
30 30 msgid "developer"
31 31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
32 32
33 33 #: authors.py:11
34 34 msgid "javascript developer"
35 35 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
36 36
37 37 #: authors.py:12
38 38 msgid "designer"
39 39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
40 40
41 41 #: forms.py:30
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
45 45
46 46 #: forms.py:31
47 47 msgid "music images i_dont_like_tags"
48 48 msgstr "ΠΌΡƒΠ·Ρ‹ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Ρ‚Π΅Π³ΠΈ_Π½Π΅_Π½ΡƒΠΆΠ½Ρ‹"
49 49
50 50 #: forms.py:33
51 51 msgid "Title"
52 52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53 53
54 54 #: forms.py:34
55 55 msgid "Text"
56 56 msgstr "ВСкст"
57 57
58 58 #: forms.py:35
59 59 msgid "Tag"
60 60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
61 61
62 62 #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7
63 63 msgid "Search"
64 64 msgstr "Поиск"
65 65
66 66 #: forms.py:139
67 67 msgid "File"
68 68 msgstr "Π€Π°ΠΉΠ»"
69 69
70 70 #: forms.py:142
71 71 msgid "File URL"
72 72 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
73 73
74 74 #: forms.py:148
75 75 msgid "e-mail"
76 76 msgstr ""
77 77
78 78 #: forms.py:151
79 79 msgid "Additional threads"
80 80 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
81 81
82 82 #: forms.py:162
83 83 #, python-format
84 84 msgid "Title must have less than %s characters"
85 85 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
86 86
87 87 #: forms.py:172
88 88 #, python-format
89 89 msgid "Text must have less than %s characters"
90 90 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
91 91
92 92 #: forms.py:192
93 93 msgid "Invalid URL"
94 94 msgstr "НСвСрный URL"
95 95
96 96 #: forms.py:213
97 97 msgid "Invalid additional thread list"
98 98 msgstr "НСвСрный список Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
99 99
100 100 #: forms.py:258
101 101 msgid "Either text or file must be entered."
102 102 msgstr "ВСкст ΠΈΠ»ΠΈ Ρ„Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
103 103
104 104 #: forms.py:317 templates/boards/all_threads.html:153
105 105 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
106 106 msgid "Tags"
107 107 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
108 108
109 109 #: forms.py:324
110 110 msgid "Inappropriate characters in tags."
111 111 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
112 112
113 113 #: forms.py:344
114 114 msgid "Need at least one section."
115 115 msgstr "НуТСн хотя Π±Ρ‹ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·Π΄Π΅Π»."
116 116
117 117 #: forms.py:356
118 118 msgid "Theme"
119 119 msgstr "Π’Π΅ΠΌΠ°"
120 120
121 121 #: forms.py:357
122 122 msgid "Image view mode"
123 123 msgstr "Π Π΅ΠΆΠΈΠΌ просмотра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
124 124
125 125 #: forms.py:358
126 126 msgid "User name"
127 127 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
128 128
129 129 #: forms.py:359
130 130 msgid "Time zone"
131 131 msgstr "Часовой пояс"
132 132
133 133 #: forms.py:365
134 134 msgid "Inappropriate characters."
135 135 msgstr "НСдопустимыС символы."
136 136
137 137 #: templates/boards/404.html:6
138 138 msgid "Not found"
139 139 msgstr "НС найдСно"
140 140
141 141 #: templates/boards/404.html:12
142 142 msgid "This page does not exist"
143 143 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
144 144
145 145 #: templates/boards/all_threads.html:35
146 146 msgid "Details"
147 147 msgstr "ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΡΡ‚ΠΈ"
148 148
149 149 #: templates/boards/all_threads.html:69
150 150 msgid "Edit tag"
151 151 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
152 152
153 153 #: templates/boards/all_threads.html:76
154 154 #, python-format
155 155 msgid "%(count)s active thread"
156 156 msgid_plural "%(count)s active threads"
157 157 msgstr[0] "%(count)s активная Ρ‚Π΅ΠΌΠ°"
158 158 msgstr[1] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
159 159 msgstr[2] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
160 160
161 161 #: templates/boards/all_threads.html:76
162 162 #, python-format
163 163 msgid "%(count)s thread in bumplimit"
164 164 msgid_plural "%(count)s threads in bumplimit"
165 165 msgstr[0] "%(count)s Ρ‚Π΅ΠΌΠ° Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
166 166 msgstr[1] "%(count)s Ρ‚Π΅ΠΌΡ‹ Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
167 167 msgstr[2] "%(count)s Ρ‚Π΅ΠΌ Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
168 168
169 169 #: templates/boards/all_threads.html:77
170 170 #, python-format
171 171 msgid "%(count)s archived thread"
172 172 msgid_plural "%(count)s archived thread"
173 173 msgstr[0] "%(count)s архивная Ρ‚Π΅ΠΌΠ°"
174 174 msgstr[1] "%(count)s Π°Ρ€Ρ…ΠΈΠ²Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
175 175 msgstr[2] "%(count)s Π°Ρ€Ρ…ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
176 176
177 177 #: templates/boards/all_threads.html:78 templates/boards/post.html:102
178 178 #, python-format
179 179 #| msgid "%(count)s message"
180 180 #| msgid_plural "%(count)s messages"
181 181 msgid "%(count)s message"
182 182 msgid_plural "%(count)s messages"
183 183 msgstr[0] "%(count)s сообщСниС"
184 184 msgstr[1] "%(count)s сообщСния"
185 185 msgstr[2] "%(count)s сообщСний"
186 186
187 187 #: templates/boards/all_threads.html:95 templates/boards/feed.html:30
188 188 #: templates/boards/notifications.html:17 templates/search/search.html:26
189 189 msgid "Previous page"
190 190 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
191 191
192 192 #: templates/boards/all_threads.html:109
193 193 #, python-format
194 194 msgid "Skipped %(count)s reply. Open thread to see all replies."
195 195 msgid_plural "Skipped %(count)s replies. Open thread to see all replies."
196 196 msgstr[0] "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ %(count)s ΠΎΡ‚Π²Π΅Ρ‚. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
197 197 msgstr[1] ""
198 198 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚Π°. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
199 199 msgstr[2] ""
200 200 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
201 201
202 202 #: templates/boards/all_threads.html:127 templates/boards/feed.html:40
203 203 #: templates/boards/notifications.html:27 templates/search/search.html:37
204 204 msgid "Next page"
205 205 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
206 206
207 207 #: templates/boards/all_threads.html:132
208 208 msgid "No threads exist. Create the first one!"
209 209 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
210 210
211 211 #: templates/boards/all_threads.html:138
212 212 msgid "Create new thread"
213 213 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
214 214
215 215 #: templates/boards/all_threads.html:143 templates/boards/preview.html:16
216 216 #: templates/boards/thread_normal.html:51
217 217 msgid "Post"
218 218 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
219 219
220 220 #: templates/boards/all_threads.html:144 templates/boards/preview.html:6
221 221 #: templates/boards/staticpages/help.html:21
222 222 #: templates/boards/thread_normal.html:52
223 223 msgid "Preview"
224 224 msgstr "ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
225 225
226 226 #: templates/boards/all_threads.html:149
227 227 msgid "Tags must be delimited by spaces. Text or image is required."
228 228 msgstr ""
229 229 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
230 230
231 231 #: templates/boards/all_threads.html:152 templates/boards/thread_normal.html:58
232 232 msgid "Text syntax"
233 233 msgstr "Бинтаксис тСкста"
234 234
235 235 #: templates/boards/all_threads.html:166 templates/boards/feed.html:53
236 236 msgid "Pages:"
237 237 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
238 238
239 239 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
240 240 msgid "Authors"
241 241 msgstr "Авторы"
242 242
243 243 #: templates/boards/authors.html:26
244 244 msgid "Distributed under the"
245 245 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
246 246
247 247 #: templates/boards/authors.html:28
248 248 msgid "license"
249 249 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
250 250
251 251 #: templates/boards/authors.html:30
252 252 msgid "Repository"
253 253 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
254 254
255 255 #: templates/boards/base.html:14 templates/boards/base.html.py:41
256 256 msgid "Feed"
257 257 msgstr "Π›Π΅Π½Ρ‚Π°"
258 258
259 259 #: templates/boards/base.html:31
260 260 msgid "All threads"
261 261 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
262 262
263 263 #: templates/boards/base.html:37
264 264 msgid "Add tags"
265 265 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΠΈ"
266 266
267 267 #: templates/boards/base.html:39
268 268 msgid "Tag management"
269 269 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠ°ΠΌΠΈ"
270 270
271 271 #: templates/boards/base.html:39
272 272 msgid "tags"
273 273 msgstr "ΠΌΠ΅Ρ‚ΠΊΠΈ"
274 274
275 275 #: templates/boards/base.html:40
276 276 msgid "search"
277 277 msgstr "поиск"
278 278
279 279 #: templates/boards/base.html:41 templates/boards/feed.html:11
280 280 msgid "feed"
281 281 msgstr "Π»Π΅Π½Ρ‚Π°"
282 282
283 283 #: templates/boards/base.html:42 templates/boards/random.html:6
284 284 msgid "Random images"
285 285 msgstr "Π‘Π»ΡƒΡ‡Π°ΠΉΠ½Ρ‹Π΅ изобраТСния"
286 286
287 287 #: templates/boards/base.html:42
288 288 msgid "random"
289 289 msgstr "случайныС"
290 290
291 291 #: templates/boards/base.html:44
292 292 msgid "favorites"
293 293 msgstr "ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ΅"
294 294
295 295 #: templates/boards/base.html:48 templates/boards/base.html.py:49
296 296 #: templates/boards/notifications.html:8
297 297 msgid "Notifications"
298 298 msgstr "УвСдомлСния"
299 299
300 300 #: templates/boards/base.html:56 templates/boards/settings.html:8
301 301 msgid "Settings"
302 302 msgstr "Настройки"
303 303
304 304 #: templates/boards/base.html:59
305 305 msgid "Loading..."
306 306 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
307 307
308 308 #: templates/boards/base.html:71
309 309 msgid "Admin"
310 310 msgstr "АдминистрированиС"
311 311
312 312 #: templates/boards/base.html:73
313 313 #, python-format
314 314 msgid "Speed: %(ppd)s posts per day"
315 315 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
316 316
317 317 #: templates/boards/base.html:75
318 318 msgid "Up"
319 319 msgstr "Π’Π²Π΅Ρ€Ρ…"
320 320
321 321 #: templates/boards/feed.html:45
322 322 msgid "No posts exist. Create the first one!"
323 323 msgstr "НСт сообщСний. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΠΎΠ΅!"
324 324
325 325 #: templates/boards/post.html:33
326 326 msgid "Open"
327 327 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
328 328
329 329 #: templates/boards/post.html:35 templates/boards/post.html.py:46
330 330 msgid "Reply"
331 331 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ"
332 332
333 333 #: templates/boards/post.html:41
334 334 msgid " in "
335 335 msgstr " Π² "
336 336
337 337 #: templates/boards/post.html:51
338 338 msgid "Edit"
339 339 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
340 340
341 341 #: templates/boards/post.html:53
342 342 msgid "Edit thread"
343 343 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
344 344
345 345 #: templates/boards/post.html:91
346 346 msgid "Replies"
347 347 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
348 348
349 349 #: templates/boards/post.html:103
350 350 #, python-format
351 351 msgid "%(count)s image"
352 352 msgid_plural "%(count)s images"
353 353 msgstr[0] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
354 354 msgstr[1] "%(count)s изобраТСния"
355 355 msgstr[2] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
356 356
357 357 #: templates/boards/rss/post.html:5
358 358 msgid "Post image"
359 359 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
360 360
361 361 #: templates/boards/settings.html:15
362 362 msgid "You are moderator."
363 363 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
364 364
365 365 #: templates/boards/settings.html:19
366 366 msgid "Hidden tags:"
367 367 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
368 368
369 369 #: templates/boards/settings.html:25
370 370 msgid "No hidden tags."
371 371 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
372 372
373 373 #: templates/boards/settings.html:34
374 374 msgid "Save"
375 375 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
376 376
377 377 #: templates/boards/staticpages/banned.html:6
378 378 msgid "Banned"
379 379 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
380 380
381 381 #: templates/boards/staticpages/banned.html:11
382 382 msgid "Your IP address has been banned. Contact the administrator"
383 383 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
384 384
385 385 #: templates/boards/staticpages/help.html:6
386 386 #: templates/boards/staticpages/help.html:10
387 387 msgid "Syntax"
388 388 msgstr "Бинтаксис"
389 389
390 390 #: templates/boards/staticpages/help.html:11
391 391 msgid "Italic text"
392 392 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
393 393
394 394 #: templates/boards/staticpages/help.html:12
395 395 msgid "Bold text"
396 396 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
397 397
398 398 #: templates/boards/staticpages/help.html:13
399 399 msgid "Spoiler"
400 400 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
401 401
402 402 #: templates/boards/staticpages/help.html:14
403 403 msgid "Link to a post"
404 404 msgstr "Бсылка Π½Π° сообщСниС"
405 405
406 406 #: templates/boards/staticpages/help.html:15
407 407 msgid "Strikethrough text"
408 408 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
409 409
410 410 #: templates/boards/staticpages/help.html:16
411 411 msgid "Comment"
412 412 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
413 413
414 414 #: templates/boards/staticpages/help.html:17
415 415 #: templates/boards/staticpages/help.html:18
416 416 msgid "Quote"
417 417 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
418 418
419 419 #: templates/boards/staticpages/help.html:21
420 420 msgid "You can try pasting the text and previewing the result here:"
421 421 msgstr "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ тСкст ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ здСсь:"
422 422
423 423 #: templates/boards/tags.html:17
424 424 msgid "Sections:"
425 425 msgstr "Π Π°Π·Π΄Π΅Π»Ρ‹:"
426 426
427 427 #: templates/boards/tags.html:30
428 428 msgid "Other tags:"
429 429 msgstr "Π”Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
430 430
431 431 #: templates/boards/tags.html:43
432 432 msgid "All tags..."
433 433 msgstr "ВсС ΠΌΠ΅Ρ‚ΠΊΠΈ..."
434 434
435 435 #: templates/boards/thread.html:14
436 436 msgid "Normal"
437 437 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
438 438
439 439 #: templates/boards/thread.html:15
440 440 msgid "Gallery"
441 441 msgstr "ГалСрСя"
442 442
443 443 #: templates/boards/thread.html:16
444 444 msgid "Tree"
445 445 msgstr "Π”Π΅Ρ€Π΅Π²ΠΎ"
446 446
447 447 #: templates/boards/thread.html:35
448 448 msgid "message"
449 449 msgid_plural "messages"
450 450 msgstr[0] "сообщСниС"
451 451 msgstr[1] "сообщСния"
452 452 msgstr[2] "сообщСний"
453 453
454 454 #: templates/boards/thread.html:38
455 455 msgid "image"
456 456 msgid_plural "images"
457 457 msgstr[0] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
458 458 msgstr[1] "изобраТСния"
459 459 msgstr[2] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
460 460
461 461 #: templates/boards/thread.html:40
462 462 msgid "Last update: "
463 463 msgstr "ПослСднСС обновлСниС: "
464 464
465 465 #: templates/boards/thread_gallery.html:36
466 466 msgid "No images."
467 467 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
468 468
469 469 #: templates/boards/thread_normal.html:30
470 470 msgid "posts to bumplimit"
471 471 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
472 472
473 473 #: templates/boards/thread_normal.html:44
474 474 msgid "Reply to thread"
475 475 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
476 476
477 477 #: templates/boards/thread_normal.html:44
478 478 msgid "to message "
479 479 msgstr "Π½Π° сообщСниС"
480 480
481 481 #: templates/boards/thread_normal.html:59
482 482 msgid "Close form"
483 483 msgstr "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
484 484
485 485 #: templates/search/search.html:17
486 486 msgid "Ok"
487 487 msgstr "Ок"
488 488
489 489 #: utils.py:120
490 490 #, python-format
491 msgid "File must be less than %s bytes"
492 msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
491 msgid "File must be less than %s but is %s."
492 msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s, Π½ΠΎ Π΅Π³ΠΎ Ρ€Π°Π·ΠΌΠ΅Ρ€ %s."
493 493
494 494 msgid "Please wait %(delay)d second before sending message"
495 495 msgid_plural "Please wait %(delay)d seconds before sending message"
496 496 msgstr[0] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунду ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
497 497 msgstr[1] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунды ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
498 498 msgstr[2] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунд ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
499 499
500 500 msgid "New threads"
501 501 msgstr "НовыС Ρ‚Π΅ΠΌΡ‹"
502 502
503 503 #, python-format
504 504 msgid "Max file size is %(size)s."
505 505 msgstr "ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π° %(size)s."
506 506
507 507 msgid "Size of media:"
508 508 msgstr "Π Π°Π·ΠΌΠ΅Ρ€ ΠΌΠ΅Π΄ΠΈΠ°:"
509 509
510 510 msgid "Statistics"
511 511 msgstr "Бтатистика"
512 512
513 513 msgid "Invalid PoW."
514 514 msgstr "НСвСрный PoW."
515 515
516 516 msgid "Stale PoW."
517 517 msgstr "PoW устарСл."
518 518
519 519 msgid "Show"
520 520 msgstr "ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ"
521 521
522 522 msgid "Hide"
523 523 msgstr "Π‘ΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒ"
524 524
525 525 msgid "Add to favorites"
526 526 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ΅"
527 527
528 528 msgid "Remove from favorites"
529 529 msgstr "Π£Π±Ρ€Π°Ρ‚ΡŒ ΠΈΠ· ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ" No newline at end of file
@@ -1,69 +1,69 b''
1 1 import os
2 2 import re
3 3
4 4 from django.core.files.uploadedfile import SimpleUploadedFile, \
5 5 TemporaryUploadedFile
6 6 from pytube import YouTube
7 7 import requests
8 8
9 9 from boards.utils import validate_file_size
10 10
11 11 YOUTUBE_VIDEO_FORMAT = 'webm'
12 12
13 13 HTTP_RESULT_OK = 200
14 14
15 15 HEADER_CONTENT_LENGTH = 'content-length'
16 16 HEADER_CONTENT_TYPE = 'content-type'
17 17
18 18 FILE_DOWNLOAD_CHUNK_BYTES = 200000
19 19
20 YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+')
20 YOUTUBE_URL = re.compile(r'https?://(www\.youtube\.com/watch\?v=|youtu.be/)\w+')
21 21
22 22
23 23 class Downloader:
24 24 @staticmethod
25 25 def handles(url: str) -> bool:
26 26 return False
27 27
28 28 @staticmethod
29 29 def download(url: str):
30 30 # Verify content headers
31 31 response_head = requests.head(url, verify=False)
32 32 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
33 33 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
34 34 if length_header:
35 35 length = int(length_header)
36 36 validate_file_size(length)
37 37 # Get the actual content into memory
38 38 response = requests.get(url, verify=False, stream=True)
39 39
40 40 # Download file, stop if the size exceeds limit
41 41 size = 0
42 42
43 43 # Set a dummy file name that will be replaced
44 44 # anyway, just keep the valid extension
45 45 filename = 'file.' + content_type.split('/')[1]
46 46
47 47 file = TemporaryUploadedFile(filename, content_type, 0, None, None)
48 48 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
49 49 size += len(chunk)
50 50 validate_file_size(size)
51 51 file.write(chunk)
52 52
53 53 if response.status_code == HTTP_RESULT_OK:
54 54 return file
55 55
56 56
57 57 class YouTubeDownloader(Downloader):
58 58 @staticmethod
59 59 def download(url: str):
60 60 yt = YouTube()
61 61 yt.from_url(url)
62 62 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
63 63 if len(videos) > 0:
64 64 video = videos[0]
65 65 return Downloader.download(video.url)
66 66
67 67 @staticmethod
68 68 def handles(url: str) -> bool:
69 69 return YOUTUBE_URL.match(url)
@@ -1,143 +1,144 b''
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 4 import hashlib
5 5 from random import random
6 6 import time
7 7 import hmac
8 8
9 9 from django.core.cache import cache
10 10 from django.db.models import Model
11 11 from django import forms
12 from django.template.defaultfilters import filesizeformat
12 13 from django.utils import timezone
13 14 from django.utils.translation import ugettext_lazy as _
14 15 import magic
15 16 from portage import os
16 17
17 18 import boards
18 19 from boards.settings import get_bool
19 20 from neboard import settings
20 21
21 22 CACHE_KEY_DELIMITER = '_'
22 23
23 24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
24 25 META_REMOTE_ADDR = 'REMOTE_ADDR'
25 26
26 27 SETTING_MESSAGES = 'Messages'
27 28 SETTING_ANON_MODE = 'AnonymousMode'
28 29
29 30 ANON_IP = '127.0.0.1'
30 31
31 32 UPLOAD_DIRS ={
32 33 'PostImage': 'images/',
33 34 'Attachment': 'files/',
34 35 }
35 36 FILE_EXTENSION_DELIMITER = '.'
36 37
37 38
38 39 def is_anonymous_mode():
39 40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40 41
41 42
42 43 def get_client_ip(request):
43 44 if is_anonymous_mode():
44 45 ip = ANON_IP
45 46 else:
46 47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 48 if x_forwarded_for:
48 49 ip = x_forwarded_for.split(',')[-1].strip()
49 50 else:
50 51 ip = request.META.get(META_REMOTE_ADDR)
51 52 return ip
52 53
53 54
54 55 # TODO The output format is not epoch because it includes microseconds
55 56 def datetime_to_epoch(datetime):
56 57 return int(time.mktime(timezone.localtime(
57 58 datetime,timezone.get_current_timezone()).timetuple())
58 59 * 1000000 + datetime.microsecond)
59 60
60 61
61 62 def get_websocket_token(user_id='', timestamp=''):
62 63 """
63 64 Create token to validate information provided by new connection.
64 65 """
65 66
66 67 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 68 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 69 sign.update(user_id.encode())
69 70 sign.update(timestamp.encode())
70 71 token = sign.hexdigest()
71 72
72 73 return token
73 74
74 75
75 76 # TODO Test this carefully
76 77 def cached_result(key_method=None):
77 78 """
78 79 Caches method result in the Django's cache system, persisted by object name,
79 80 object name, model id if object is a Django model, args and kwargs if any.
80 81 """
81 82 def _cached_result(function):
82 83 def inner_func(obj, *args, **kwargs):
83 84 cache_key_params = [obj.__class__.__name__, function.__name__]
84 85
85 86 cache_key_params += args
86 87 for key, value in kwargs:
87 88 cache_key_params.append(key + ':' + value)
88 89
89 90 if isinstance(obj, Model):
90 91 cache_key_params.append(str(obj.id))
91 92
92 93 if key_method is not None:
93 94 cache_key_params += [str(arg) for arg in key_method(obj)]
94 95
95 96 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
96 97
97 98 persisted_result = cache.get(cache_key)
98 99 if persisted_result is not None:
99 100 result = persisted_result
100 101 else:
101 102 result = function(obj, *args, **kwargs)
102 103 cache.set(cache_key, result)
103 104
104 105 return result
105 106
106 107 return inner_func
107 108 return _cached_result
108 109
109 110
110 111 def get_file_hash(file) -> str:
111 112 md5 = hashlib.md5()
112 113 for chunk in file.chunks():
113 114 md5.update(chunk)
114 115 return md5.hexdigest()
115 116
116 117
117 118 def validate_file_size(size: int):
118 119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
119 120 if size > max_size:
120 121 raise forms.ValidationError(
121 _('File must be less than %s bytes')
122 % str(max_size))
122 _('File must be less than %s but is %s.')
123 % (filesizeformat(max_size), filesizeformat(size)))
123 124
124 125
125 126 def get_extension(filename):
126 127 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
127 128
128 129
129 130 def get_upload_filename(model_instance, old_filename):
130 131 # TODO Use something other than random number in file name
131 132 extension = get_extension(old_filename)
132 133 new_name = '{}{}.{}'.format(
133 134 str(int(time.mktime(time.gmtime()))),
134 135 str(int(random() * 1000)),
135 136 extension)
136 137
137 138 directory = UPLOAD_DIRS[type(model_instance).__name__]
138 139
139 140 return os.path.join(directory, new_name)
140 141
141 142
142 143 def get_file_mimetype(file) -> str:
143 144 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
General Comments 0
You need to be logged in to leave comments. Login now