##// END OF EJS Templates
Allow loading files and links together
neko259 -
r1762:76498950 default
parent child Browse files
Show More
@@ -1,488 +1,483
1 import hashlib
1 import hashlib
2 import logging
2 import logging
3 import re
3 import re
4 import time
4 import time
5
5
6 import pytz
6 import pytz
7 from django import forms
7 from django import forms
8 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
8 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
9 from django.forms.utils import ErrorList
9 from django.forms.utils import ErrorList
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
11
12 import boards.settings as board_settings
12 import boards.settings as board_settings
13 import neboard
13 import neboard
14 from boards.abstracts.attachment_alias import get_image_by_alias
14 from boards.abstracts.attachment_alias import get_image_by_alias
15 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.forms.fields import UrlFileField
16 from boards.forms.fields import UrlFileField
17 from boards.mdx_neboard import formatters
17 from boards.mdx_neboard import formatters
18 from boards.models import Tag
18 from boards.models import Tag
19 from boards.models.attachment.downloaders import download
19 from boards.models.attachment.downloaders import download
20 from boards.models.post import TITLE_MAX_LENGTH
20 from boards.models.post import TITLE_MAX_LENGTH
21 from boards.utils import validate_file_size, get_file_mimetype, \
21 from boards.utils import validate_file_size, get_file_mimetype, \
22 FILE_EXTENSION_DELIMITER
22 FILE_EXTENSION_DELIMITER
23 from neboard import settings
23 from neboard import settings
24
24
25 SECTION_FORMS = 'Forms'
25 SECTION_FORMS = 'Forms'
26
26
27 POW_HASH_LENGTH = 16
27 POW_HASH_LENGTH = 16
28 POW_LIFE_MINUTES = 5
28 POW_LIFE_MINUTES = 5
29
29
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
32 REGEX_URL = re.compile(r'^(http|https|ftp|magnet):\/\/', re.UNICODE)
32 REGEX_URL = re.compile(r'^(http|https|ftp|magnet):\/\/', re.UNICODE)
33
33
34 VETERAN_POSTING_DELAY = 5
34 VETERAN_POSTING_DELAY = 5
35
35
36 ATTRIBUTE_PLACEHOLDER = 'placeholder'
36 ATTRIBUTE_PLACEHOLDER = 'placeholder'
37 ATTRIBUTE_ROWS = 'rows'
37 ATTRIBUTE_ROWS = 'rows'
38
38
39 LAST_POST_TIME = 'last_post_time'
39 LAST_POST_TIME = 'last_post_time'
40 LAST_LOGIN_TIME = 'last_login_time'
40 LAST_LOGIN_TIME = 'last_login_time'
41 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
41 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
42 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
42 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
43
43
44 LABEL_TITLE = _('Title')
44 LABEL_TITLE = _('Title')
45 LABEL_TEXT = _('Text')
45 LABEL_TEXT = _('Text')
46 LABEL_TAG = _('Tag')
46 LABEL_TAG = _('Tag')
47 LABEL_SEARCH = _('Search')
47 LABEL_SEARCH = _('Search')
48 LABEL_FILE = _('File')
48 LABEL_FILE = _('File')
49
49
50 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
50 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
51 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
51 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
52 ERROR_MANY_FILES = _('Too many files.')
52 ERROR_MANY_FILES = _('Too many files.')
53
53
54 TAG_MAX_LENGTH = 20
54 TAG_MAX_LENGTH = 20
55
55
56 TEXTAREA_ROWS = 4
56 TEXTAREA_ROWS = 4
57
57
58 TRIPCODE_DELIM = '#'
58 TRIPCODE_DELIM = '#'
59
59
60 # TODO Maybe this may be converted into the database table?
60 # TODO Maybe this may be converted into the database table?
61 MIMETYPE_EXTENSIONS = {
61 MIMETYPE_EXTENSIONS = {
62 'image/jpeg': 'jpeg',
62 'image/jpeg': 'jpeg',
63 'image/png': 'png',
63 'image/png': 'png',
64 'image/gif': 'gif',
64 'image/gif': 'gif',
65 'video/webm': 'webm',
65 'video/webm': 'webm',
66 'application/pdf': 'pdf',
66 'application/pdf': 'pdf',
67 'x-diff': 'diff',
67 'x-diff': 'diff',
68 'image/svg+xml': 'svg',
68 'image/svg+xml': 'svg',
69 'application/x-shockwave-flash': 'swf',
69 'application/x-shockwave-flash': 'swf',
70 'image/x-ms-bmp': 'bmp',
70 'image/x-ms-bmp': 'bmp',
71 'image/bmp': 'bmp',
71 'image/bmp': 'bmp',
72 }
72 }
73
73
74
74
75 logger = logging.getLogger('boards.forms')
75 logger = logging.getLogger('boards.forms')
76
76
77
77
78 def get_timezones():
78 def get_timezones():
79 timezones = []
79 timezones = []
80 for tz in pytz.common_timezones:
80 for tz in pytz.common_timezones:
81 timezones.append((tz, tz),)
81 timezones.append((tz, tz),)
82 return timezones
82 return timezones
83
83
84
84
85 class FormatPanel(forms.Textarea):
85 class FormatPanel(forms.Textarea):
86 """
86 """
87 Panel for text formatting. Consists of buttons to add different tags to the
87 Panel for text formatting. Consists of buttons to add different tags to the
88 form text area.
88 form text area.
89 """
89 """
90
90
91 def render(self, name, value, attrs=None):
91 def render(self, name, value, attrs=None):
92 output = '<div id="mark-panel">'
92 output = '<div id="mark-panel">'
93 for formatter in formatters:
93 for formatter in formatters:
94 output += '<span class="mark_btn"' + \
94 output += '<span class="mark_btn"' + \
95 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
95 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
96 '\', \'' + formatter.format_right + '\')">' + \
96 '\', \'' + formatter.format_right + '\')">' + \
97 formatter.preview_left + formatter.name + \
97 formatter.preview_left + formatter.name + \
98 formatter.preview_right + '</span>'
98 formatter.preview_right + '</span>'
99
99
100 output += '</div>'
100 output += '</div>'
101 output += super(FormatPanel, self).render(name, value, attrs=attrs)
101 output += super(FormatPanel, self).render(name, value, attrs=attrs)
102
102
103 return output
103 return output
104
104
105
105
106 class PlainErrorList(ErrorList):
106 class PlainErrorList(ErrorList):
107 def __unicode__(self):
107 def __unicode__(self):
108 return self.as_text()
108 return self.as_text()
109
109
110 def as_text(self):
110 def as_text(self):
111 return ''.join(['(!) %s ' % e for e in self])
111 return ''.join(['(!) %s ' % e for e in self])
112
112
113
113
114 class NeboardForm(forms.Form):
114 class NeboardForm(forms.Form):
115 """
115 """
116 Form with neboard-specific formatting.
116 Form with neboard-specific formatting.
117 """
117 """
118 required_css_class = 'required-field'
118 required_css_class = 'required-field'
119
119
120 def as_div(self):
120 def as_div(self):
121 """
121 """
122 Returns this form rendered as HTML <as_div>s.
122 Returns this form rendered as HTML <as_div>s.
123 """
123 """
124
124
125 return self._html_output(
125 return self._html_output(
126 # TODO Do not show hidden rows in the list here
126 # TODO Do not show hidden rows in the list here
127 normal_row='<div class="form-row">'
127 normal_row='<div class="form-row">'
128 '<div class="form-label">'
128 '<div class="form-label">'
129 '%(label)s'
129 '%(label)s'
130 '</div>'
130 '</div>'
131 '<div class="form-input">'
131 '<div class="form-input">'
132 '%(field)s'
132 '%(field)s'
133 '</div>'
133 '</div>'
134 '</div>'
134 '</div>'
135 '<div class="form-row">'
135 '<div class="form-row">'
136 '%(help_text)s'
136 '%(help_text)s'
137 '</div>',
137 '</div>',
138 error_row='<div class="form-row">'
138 error_row='<div class="form-row">'
139 '<div class="form-label"></div>'
139 '<div class="form-label"></div>'
140 '<div class="form-errors">%s</div>'
140 '<div class="form-errors">%s</div>'
141 '</div>',
141 '</div>',
142 row_ender='</div>',
142 row_ender='</div>',
143 help_text_html='%s',
143 help_text_html='%s',
144 errors_on_separate_row=True)
144 errors_on_separate_row=True)
145
145
146 def as_json_errors(self):
146 def as_json_errors(self):
147 errors = []
147 errors = []
148
148
149 for name, field in list(self.fields.items()):
149 for name, field in list(self.fields.items()):
150 if self[name].errors:
150 if self[name].errors:
151 errors.append({
151 errors.append({
152 'field': name,
152 'field': name,
153 'errors': self[name].errors.as_text(),
153 'errors': self[name].errors.as_text(),
154 })
154 })
155
155
156 return errors
156 return errors
157
157
158
158
159 class PostForm(NeboardForm):
159 class PostForm(NeboardForm):
160
160
161 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
161 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
162 label=LABEL_TITLE,
162 label=LABEL_TITLE,
163 widget=forms.TextInput(
163 widget=forms.TextInput(
164 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
164 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
165 text = forms.CharField(
165 text = forms.CharField(
166 widget=FormatPanel(attrs={
166 widget=FormatPanel(attrs={
167 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
167 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
168 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
168 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
169 }),
169 }),
170 required=False, label=LABEL_TEXT)
170 required=False, label=LABEL_TEXT)
171 file = UrlFileField(required=False, label=LABEL_FILE)
171 file = UrlFileField(required=False, label=LABEL_FILE)
172
172
173 # This field is for spam prevention only
173 # This field is for spam prevention only
174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
175 widget=forms.TextInput(attrs={
175 widget=forms.TextInput(attrs={
176 'class': 'form-email'}))
176 'class': 'form-email'}))
177 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
177 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
178
178
179 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
179 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
180 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
180 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
181 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
181 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
182
182
183 session = None
183 session = None
184 need_to_ban = False
184 need_to_ban = False
185 image = None
185 image = None
186
186
187 def clean_title(self):
187 def clean_title(self):
188 title = self.cleaned_data['title']
188 title = self.cleaned_data['title']
189 if title:
189 if title:
190 if len(title) > TITLE_MAX_LENGTH:
190 if len(title) > TITLE_MAX_LENGTH:
191 raise forms.ValidationError(_('Title must have less than %s '
191 raise forms.ValidationError(_('Title must have less than %s '
192 'characters') %
192 'characters') %
193 str(TITLE_MAX_LENGTH))
193 str(TITLE_MAX_LENGTH))
194 return title
194 return title
195
195
196 def clean_text(self):
196 def clean_text(self):
197 text = self.cleaned_data['text'].strip()
197 text = self.cleaned_data['text'].strip()
198 if text:
198 if text:
199 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
199 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
200 if len(text) > max_length:
200 if len(text) > max_length:
201 raise forms.ValidationError(_('Text must have less than %s '
201 raise forms.ValidationError(_('Text must have less than %s '
202 'characters') % str(max_length))
202 'characters') % str(max_length))
203 return text
203 return text
204
204
205 def clean_file(self):
205 def clean_file(self):
206 return self._clean_files(self.cleaned_data['file'])
206 return self._clean_files(self.cleaned_data['file'])
207
207
208 def clean(self):
208 def clean(self):
209 cleaned_data = super(PostForm, self).clean()
209 cleaned_data = super(PostForm, self).clean()
210
210
211 if cleaned_data['email']:
211 if cleaned_data['email']:
212 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
212 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
213 self.need_to_ban = True
213 self.need_to_ban = True
214 raise forms.ValidationError('A human cannot enter a hidden field')
214 raise forms.ValidationError('A human cannot enter a hidden field')
215
215
216 if not self.errors:
216 if not self.errors:
217 self._clean_text_file()
217 self._clean_text_file()
218
218
219 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
219 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
220 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
220 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
221
221
222 settings_manager = get_settings_manager(self)
222 settings_manager = get_settings_manager(self)
223 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
223 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
224 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
224 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
225 if pow_difficulty > 0:
225 if pow_difficulty > 0:
226 # PoW-based
226 # PoW-based
227 if cleaned_data['timestamp'] \
227 if cleaned_data['timestamp'] \
228 and cleaned_data['iteration'] and cleaned_data['guess'] \
228 and cleaned_data['iteration'] and cleaned_data['guess'] \
229 and not settings_manager.get_setting('confirmed_user'):
229 and not settings_manager.get_setting('confirmed_user'):
230 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
230 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
231 else:
231 else:
232 # Time-based
232 # Time-based
233 self._validate_posting_speed()
233 self._validate_posting_speed()
234 settings_manager.set_setting('confirmed_user', True)
234 settings_manager.set_setting('confirmed_user', True)
235
235
236 return cleaned_data
236 return cleaned_data
237
237
238 def get_files(self):
238 def get_files(self):
239 """
239 """
240 Gets file from form or URL.
240 Gets file from form or URL.
241 """
241 """
242
242
243 files = []
243 files = []
244 for file in self.cleaned_data['file']:
244 for file in self.cleaned_data['file']:
245 if isinstance(file, UploadedFile):
245 if isinstance(file, UploadedFile):
246 files.append(file)
246 files.append(file)
247
247
248 return files
248 return files
249
249
250 def get_file_urls(self):
250 def get_file_urls(self):
251 files = []
251 files = []
252 for file in self.cleaned_data['file']:
252 for file in self.cleaned_data['file']:
253 if type(file) == str:
253 if type(file) == str:
254 files.append(file)
254 files.append(file)
255
255
256 return files
256 return files
257
257
258 def get_tripcode(self):
258 def get_tripcode(self):
259 title = self.cleaned_data['title']
259 title = self.cleaned_data['title']
260 if title is not None and TRIPCODE_DELIM in title:
260 if title is not None and TRIPCODE_DELIM in title:
261 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
261 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
262 tripcode = hashlib.md5(code.encode()).hexdigest()
262 tripcode = hashlib.md5(code.encode()).hexdigest()
263 else:
263 else:
264 tripcode = ''
264 tripcode = ''
265 return tripcode
265 return tripcode
266
266
267 def get_title(self):
267 def get_title(self):
268 title = self.cleaned_data['title']
268 title = self.cleaned_data['title']
269 if title is not None and TRIPCODE_DELIM in title:
269 if title is not None and TRIPCODE_DELIM in title:
270 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
270 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
271 else:
271 else:
272 return title
272 return title
273
273
274 def get_images(self):
274 def get_images(self):
275 if self.image:
275 if self.image:
276 return [self.image]
276 return [self.image]
277 else:
277 else:
278 return []
278 return []
279
279
280 def is_subscribe(self):
280 def is_subscribe(self):
281 return self.cleaned_data['subscribe']
281 return self.cleaned_data['subscribe']
282
282
283 def _update_file_extension(self, file):
283 def _update_file_extension(self, file):
284 if file:
284 if file:
285 mimetype = get_file_mimetype(file)
285 mimetype = get_file_mimetype(file)
286 extension = MIMETYPE_EXTENSIONS.get(mimetype)
286 extension = MIMETYPE_EXTENSIONS.get(mimetype)
287 if extension:
287 if extension:
288 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
288 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
289 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
289 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
290
290
291 file.name = new_filename
291 file.name = new_filename
292 else:
292 else:
293 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
293 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
294
294
295 def _clean_files(self, inputs):
295 def _clean_files(self, inputs):
296 files = []
296 files = []
297
297
298 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
298 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
299 if type(inputs) == list:
299 if len(inputs) > max_file_count:
300 if len(inputs) > max_file_count:
300 raise forms.ValidationError(ERROR_MANY_FILES)
301 raise forms.ValidationError(ERROR_MANY_FILES)
301 for input in inputs:
302 for file in inputs:
302 if isinstance(input, UploadedFile):
303 files.append(self._clean_file_file(file))
303 files.append(self._clean_file_file(input))
304 elif inputs:
304 else:
305 inputs = inputs.replace('\r\n', '\n')
305 files.append(self._clean_file_url(input))
306 url_list = inputs.split('\n')
307 if len(url_list) > max_file_count:
308 raise forms.ValidationError(ERROR_MANY_FILES)
309 for url in url_list:
310 files.append(self._clean_file_url(url))
311
306
312 return files
307 return files
313
308
314 def _clean_file_file(self, file):
309 def _clean_file_file(self, file):
315 validate_file_size(file.size)
310 validate_file_size(file.size)
316 self._update_file_extension(file)
311 self._update_file_extension(file)
317
312
318 return file
313 return file
319
314
320 def _clean_file_url(self, url):
315 def _clean_file_url(self, url):
321 file = None
316 file = None
322
317
323 if url:
318 if url:
324 try:
319 try:
325 file = get_image_by_alias(url, self.session)
320 file = get_image_by_alias(url, self.session)
326 self.image = file
321 self.image = file
327
322
328 if file is not None:
323 if file is not None:
329 return
324 return
330
325
331 if file is None:
326 if file is None:
332 file = self._get_file_from_url(url)
327 file = self._get_file_from_url(url)
333 if not file:
328 if not file:
334 raise forms.ValidationError(_('Invalid URL'))
329 raise forms.ValidationError(_('Invalid URL'))
335 else:
330 else:
336 validate_file_size(file.size)
331 validate_file_size(file.size)
337 self._update_file_extension(file)
332 self._update_file_extension(file)
338 except forms.ValidationError as e:
333 except forms.ValidationError as e:
339 # Assume we will get the plain URL instead of a file and save it
334 # Assume we will get the plain URL instead of a file and save it
340 if REGEX_URL.match(url):
335 if REGEX_URL.match(url):
341 logger.info('Error in forms: {}'.format(e))
336 logger.info('Error in forms: {}'.format(e))
342 return url
337 return url
343 else:
338 else:
344 raise e
339 raise e
345
340
346 return file
341 return file
347
342
348 def _clean_text_file(self):
343 def _clean_text_file(self):
349 text = self.cleaned_data.get('text')
344 text = self.cleaned_data.get('text')
350 file = self.get_files()
345 file = self.get_files()
351 file_url = self.get_file_urls()
346 file_url = self.get_file_urls()
352 images = self.get_images()
347 images = self.get_images()
353
348
354 if (not text) and (not file) and (not file_url) and len(images) == 0:
349 if (not text) and (not file) and (not file_url) and len(images) == 0:
355 error_message = _('Either text or file must be entered.')
350 error_message = _('Either text or file must be entered.')
356 self._errors['text'] = self.error_class([error_message])
351 self._errors['text'] = self.error_class([error_message])
357
352
358 def _validate_posting_speed(self):
353 def _validate_posting_speed(self):
359 can_post = True
354 can_post = True
360
355
361 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
356 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
362
357
363 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
358 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
364 now = time.time()
359 now = time.time()
365
360
366 current_delay = 0
361 current_delay = 0
367
362
368 if LAST_POST_TIME not in self.session:
363 if LAST_POST_TIME not in self.session:
369 self.session[LAST_POST_TIME] = now
364 self.session[LAST_POST_TIME] = now
370
365
371 need_delay = True
366 need_delay = True
372 else:
367 else:
373 last_post_time = self.session.get(LAST_POST_TIME)
368 last_post_time = self.session.get(LAST_POST_TIME)
374 current_delay = int(now - last_post_time)
369 current_delay = int(now - last_post_time)
375
370
376 need_delay = current_delay < posting_delay
371 need_delay = current_delay < posting_delay
377
372
378 if need_delay:
373 if need_delay:
379 delay = posting_delay - current_delay
374 delay = posting_delay - current_delay
380 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
375 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
381 delay) % {'delay': delay}
376 delay) % {'delay': delay}
382 self._errors['text'] = self.error_class([error_message])
377 self._errors['text'] = self.error_class([error_message])
383
378
384 can_post = False
379 can_post = False
385
380
386 if can_post:
381 if can_post:
387 self.session[LAST_POST_TIME] = now
382 self.session[LAST_POST_TIME] = now
388
383
389 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
384 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
390 """
385 """
391 Gets an file file from URL.
386 Gets an file file from URL.
392 """
387 """
393
388
394 try:
389 try:
395 return download(url)
390 return download(url)
396 except forms.ValidationError as e:
391 except forms.ValidationError as e:
397 raise e
392 raise e
398 except Exception as e:
393 except Exception as e:
399 raise forms.ValidationError(e)
394 raise forms.ValidationError(e)
400
395
401 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
396 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
402 payload = timestamp + message.replace('\r\n', '\n')
397 payload = timestamp + message.replace('\r\n', '\n')
403 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
398 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
404 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
399 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
405 if len(target) < POW_HASH_LENGTH:
400 if len(target) < POW_HASH_LENGTH:
406 target = '0' * (POW_HASH_LENGTH - len(target)) + target
401 target = '0' * (POW_HASH_LENGTH - len(target)) + target
407
402
408 computed_guess = hashlib.sha256((payload + iteration).encode())\
403 computed_guess = hashlib.sha256((payload + iteration).encode())\
409 .hexdigest()[0:POW_HASH_LENGTH]
404 .hexdigest()[0:POW_HASH_LENGTH]
410 if guess != computed_guess or guess > target:
405 if guess != computed_guess or guess > target:
411 self._errors['text'] = self.error_class(
406 self._errors['text'] = self.error_class(
412 [_('Invalid PoW.')])
407 [_('Invalid PoW.')])
413
408
414
409
415 class ThreadForm(PostForm):
410 class ThreadForm(PostForm):
416
411
417 tags = forms.CharField(
412 tags = forms.CharField(
418 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
413 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
419 max_length=100, label=_('Tags'), required=True)
414 max_length=100, label=_('Tags'), required=True)
420 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
415 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
421
416
422 def clean_tags(self):
417 def clean_tags(self):
423 tags = self.cleaned_data['tags'].strip()
418 tags = self.cleaned_data['tags'].strip()
424
419
425 if not tags or not REGEX_TAGS.match(tags):
420 if not tags or not REGEX_TAGS.match(tags):
426 raise forms.ValidationError(
421 raise forms.ValidationError(
427 _('Inappropriate characters in tags.'))
422 _('Inappropriate characters in tags.'))
428
423
429 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
424 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
430 .strip().lower()
425 .strip().lower()
431
426
432 required_tag_exists = False
427 required_tag_exists = False
433 tag_set = set()
428 tag_set = set()
434 for tag_string in tags.split():
429 for tag_string in tags.split():
435 if tag_string.strip().lower() == default_tag_name:
430 if tag_string.strip().lower() == default_tag_name:
436 required_tag_exists = True
431 required_tag_exists = True
437 tag, created = Tag.objects.get_or_create(
432 tag, created = Tag.objects.get_or_create(
438 name=tag_string.strip().lower(), required=True)
433 name=tag_string.strip().lower(), required=True)
439 else:
434 else:
440 tag, created = Tag.objects.get_or_create(
435 tag, created = Tag.objects.get_or_create(
441 name=tag_string.strip().lower())
436 name=tag_string.strip().lower())
442 tag_set.add(tag)
437 tag_set.add(tag)
443
438
444 # If this is a new tag, don't check for its parents because nobody
439 # If this is a new tag, don't check for its parents because nobody
445 # added them yet
440 # added them yet
446 if not created:
441 if not created:
447 tag_set |= set(tag.get_all_parents())
442 tag_set |= set(tag.get_all_parents())
448
443
449 for tag in tag_set:
444 for tag in tag_set:
450 if tag.required:
445 if tag.required:
451 required_tag_exists = True
446 required_tag_exists = True
452 break
447 break
453
448
454 # Use default tag if no section exists
449 # Use default tag if no section exists
455 if not required_tag_exists:
450 if not required_tag_exists:
456 default_tag, created = Tag.objects.get_or_create(
451 default_tag, created = Tag.objects.get_or_create(
457 name=default_tag_name, required=True)
452 name=default_tag_name, required=True)
458 tag_set.add(default_tag)
453 tag_set.add(default_tag)
459
454
460 return tag_set
455 return tag_set
461
456
462 def clean(self):
457 def clean(self):
463 cleaned_data = super(ThreadForm, self).clean()
458 cleaned_data = super(ThreadForm, self).clean()
464
459
465 return cleaned_data
460 return cleaned_data
466
461
467 def is_monochrome(self):
462 def is_monochrome(self):
468 return self.cleaned_data['monochrome']
463 return self.cleaned_data['monochrome']
469
464
470
465
471 class SettingsForm(NeboardForm):
466 class SettingsForm(NeboardForm):
472
467
473 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
468 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
474 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
469 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
475 username = forms.CharField(label=_('User name'), required=False)
470 username = forms.CharField(label=_('User name'), required=False)
476 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
471 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
477
472
478 def clean_username(self):
473 def clean_username(self):
479 username = self.cleaned_data['username']
474 username = self.cleaned_data['username']
480
475
481 if username and not REGEX_USERNAMES.match(username):
476 if username and not REGEX_USERNAMES.match(username):
482 raise forms.ValidationError(_('Inappropriate characters.'))
477 raise forms.ValidationError(_('Inappropriate characters.'))
483
478
484 return username
479 return username
485
480
486
481
487 class SearchForm(NeboardForm):
482 class SearchForm(NeboardForm):
488 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
483 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,83 +1,90
1 from django import forms
1 from django import forms
2 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
2 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
3
3
4
4
5 ATTRIBUTE_PLACEHOLDER = 'placeholder'
5 ATTRIBUTE_PLACEHOLDER = 'placeholder'
6 ATTRIBUTE_ROWS = 'rows'
6 ATTRIBUTE_ROWS = 'rows'
7
7
8 URL_ROWS = 2
8 URL_ROWS = 2
9
9
10
10
11 class MultipleFileInput(forms.FileInput):
11 class MultipleFileInput(forms.FileInput):
12 """
12 """
13 Input that allows to enter many files at once, utilizing the html5 "multiple"
13 Input that allows to enter many files at once, utilizing the html5 "multiple"
14 attribute of a file input.
14 attribute of a file input.
15 """
15 """
16 def value_from_datadict(self, data, files, name):
16 def value_from_datadict(self, data, files, name):
17 if len(files) == 0:
17 if len(files) == 0:
18 return None
18 return None
19 else:
19 else:
20 return files.getlist(name)
20 return files.getlist(name)
21
21
22 def render(self, name, value, attrs=None):
22 def render(self, name, value, attrs=None):
23 if not attrs:
23 if not attrs:
24 attrs = dict()
24 attrs = dict()
25 attrs['multiple'] = ''
25 attrs['multiple'] = ''
26
26
27 return super().render(name, None, attrs=attrs)
27 return super().render(name, None, attrs=attrs)
28
28
29
29
30 class MultipleFileField(forms.FileField):
30 class MultipleFileField(forms.FileField):
31 """
31 """
32 Field that allows to enter multiple files from one input.
32 Field that allows to enter multiple files from one input.
33 """
33 """
34 def to_python(self, data):
34 def to_python(self, data):
35 if not data or len(data) == 0:
35 if not data or len(data) == 0:
36 return None
36 return None
37
37
38 for file in data:
38 for file in data:
39 super().to_python(file)
39 super().to_python(file)
40
40
41 return data
41 return data
42
42
43
43
44 class UrlFileWidget(forms.MultiWidget):
44 class UrlFileWidget(forms.MultiWidget):
45 """
45 """
46 Widget with a file input and a text input that allows to enter either a
46 Widget with a file input and a text input that allows to enter either a
47 file from a local system, or a URL from which the file would be downloaded.
47 file from a local system, or a URL from which the file would be downloaded.
48 """
48 """
49 def __init__(self, *args, **kwargs):
49 def __init__(self, *args, **kwargs):
50 widgets = (
50 widgets = (
51 MultipleFileInput(attrs={'accept': 'file/*'}),
51 MultipleFileInput(attrs={'accept': 'file/*'}),
52 forms.Textarea(attrs={
52 forms.Textarea(attrs={
53 ATTRIBUTE_PLACEHOLDER: 'http://example.com/image.png',
53 ATTRIBUTE_PLACEHOLDER: 'http://example.com/image.png',
54 ATTRIBUTE_ROWS: URL_ROWS,
54 ATTRIBUTE_ROWS: URL_ROWS,
55 }),
55 }),
56 )
56 )
57 super().__init__(widgets, *args, **kwargs)
57 super().__init__(widgets, *args, **kwargs)
58
58
59 def decompress(self, value):
59 def decompress(self, value):
60 return [None, None]
60 return [None, None]
61
61
62
62
63 class UrlFileField(forms.MultiValueField):
63 class UrlFileField(forms.MultiValueField):
64 """
64 """
65 Field with a file input and a text input that allows to enter either a
65 Field with a file input and a text input that allows to enter either a
66 file from a local system, or a URL from which the file would be downloaded.
66 file from a local system, or a URL from which the file would be downloaded.
67 """
67 """
68 widget = UrlFileWidget
68 widget = UrlFileWidget
69
69
70 def __init__(self, *args, **kwargs):
70 def __init__(self, *args, **kwargs):
71 fields = (
71 fields = (
72 MultipleFileField(required=False, label=_('File')),
72 MultipleFileField(required=False, label=_('File')),
73 forms.CharField(required=False, label=_('File URL')),
73 forms.CharField(required=False, label=_('File URL')),
74 )
74 )
75
75
76 super().__init__(
76 super().__init__(
77 fields=fields,
77 fields=fields,
78 require_all_fields=False, *args, **kwargs)
78 require_all_fields=False, *args, **kwargs)
79
79
80 def compress(self, data_list):
80 def compress(self, data_list):
81 if data_list and len(data_list) >= 2:
81 all_data = []
82 return data_list[0] or data_list[1]
82 for data in data_list:
83 if type(data) == list:
84 all_data += data
85 elif type(data) == str:
86 input = data.replace('\r\n', '\n')
87 url_list = input.split('\n')
88 all_data += url_list
83
89
90 return all_data
General Comments 0
You need to be logged in to leave comments. Login now