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