##// END OF EJS Templates
Fixed shadowing of an internal keyword. Fixed empty URL being treated as a file input
neko259 -
r1763:271906c8 default
parent child Browse files
Show More
@@ -1,483 +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 len(inputs) > max_file_count:
299 if len(inputs) > max_file_count:
300 raise forms.ValidationError(ERROR_MANY_FILES)
300 raise forms.ValidationError(ERROR_MANY_FILES)
301 for input in inputs:
301 for file_input in inputs:
302 if isinstance(input, UploadedFile):
302 if isinstance(file_input, UploadedFile):
303 files.append(self._clean_file_file(input))
303 files.append(self._clean_file_file(file_input))
304 else:
304 else:
305 files.append(self._clean_file_url(input))
305 files.append(self._clean_file_url(file_input))
306
306
307 return files
307 return files
308
308
309 def _clean_file_file(self, file):
309 def _clean_file_file(self, file):
310 validate_file_size(file.size)
310 validate_file_size(file.size)
311 self._update_file_extension(file)
311 self._update_file_extension(file)
312
312
313 return file
313 return file
314
314
315 def _clean_file_url(self, url):
315 def _clean_file_url(self, url):
316 file = None
316 file = None
317
317
318 if url:
318 if url:
319 try:
319 try:
320 file = get_image_by_alias(url, self.session)
320 file = get_image_by_alias(url, self.session)
321 self.image = file
321 self.image = file
322
322
323 if file is not None:
323 if file is not None:
324 return
324 return
325
325
326 if file is None:
326 if file is None:
327 file = self._get_file_from_url(url)
327 file = self._get_file_from_url(url)
328 if not file:
328 if not file:
329 raise forms.ValidationError(_('Invalid URL'))
329 raise forms.ValidationError(_('Invalid URL'))
330 else:
330 else:
331 validate_file_size(file.size)
331 validate_file_size(file.size)
332 self._update_file_extension(file)
332 self._update_file_extension(file)
333 except forms.ValidationError as e:
333 except forms.ValidationError as e:
334 # 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
335 if REGEX_URL.match(url):
335 if REGEX_URL.match(url):
336 logger.info('Error in forms: {}'.format(e))
336 logger.info('Error in forms: {}'.format(e))
337 return url
337 return url
338 else:
338 else:
339 raise e
339 raise e
340
340
341 return file
341 return file
342
342
343 def _clean_text_file(self):
343 def _clean_text_file(self):
344 text = self.cleaned_data.get('text')
344 text = self.cleaned_data.get('text')
345 file = self.get_files()
345 file = self.get_files()
346 file_url = self.get_file_urls()
346 file_url = self.get_file_urls()
347 images = self.get_images()
347 images = self.get_images()
348
348
349 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:
350 error_message = _('Either text or file must be entered.')
350 error_message = _('Either text or file must be entered.')
351 self._errors['text'] = self.error_class([error_message])
351 self._errors['text'] = self.error_class([error_message])
352
352
353 def _validate_posting_speed(self):
353 def _validate_posting_speed(self):
354 can_post = True
354 can_post = True
355
355
356 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
356 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
357
357
358 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
358 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
359 now = time.time()
359 now = time.time()
360
360
361 current_delay = 0
361 current_delay = 0
362
362
363 if LAST_POST_TIME not in self.session:
363 if LAST_POST_TIME not in self.session:
364 self.session[LAST_POST_TIME] = now
364 self.session[LAST_POST_TIME] = now
365
365
366 need_delay = True
366 need_delay = True
367 else:
367 else:
368 last_post_time = self.session.get(LAST_POST_TIME)
368 last_post_time = self.session.get(LAST_POST_TIME)
369 current_delay = int(now - last_post_time)
369 current_delay = int(now - last_post_time)
370
370
371 need_delay = current_delay < posting_delay
371 need_delay = current_delay < posting_delay
372
372
373 if need_delay:
373 if need_delay:
374 delay = posting_delay - current_delay
374 delay = posting_delay - current_delay
375 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
375 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
376 delay) % {'delay': delay}
376 delay) % {'delay': delay}
377 self._errors['text'] = self.error_class([error_message])
377 self._errors['text'] = self.error_class([error_message])
378
378
379 can_post = False
379 can_post = False
380
380
381 if can_post:
381 if can_post:
382 self.session[LAST_POST_TIME] = now
382 self.session[LAST_POST_TIME] = now
383
383
384 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
384 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
385 """
385 """
386 Gets an file file from URL.
386 Gets an file file from URL.
387 """
387 """
388
388
389 try:
389 try:
390 return download(url)
390 return download(url)
391 except forms.ValidationError as e:
391 except forms.ValidationError as e:
392 raise e
392 raise e
393 except Exception as e:
393 except Exception as e:
394 raise forms.ValidationError(e)
394 raise forms.ValidationError(e)
395
395
396 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):
397 payload = timestamp + message.replace('\r\n', '\n')
397 payload = timestamp + message.replace('\r\n', '\n')
398 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
398 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
399 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
399 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
400 if len(target) < POW_HASH_LENGTH:
400 if len(target) < POW_HASH_LENGTH:
401 target = '0' * (POW_HASH_LENGTH - len(target)) + target
401 target = '0' * (POW_HASH_LENGTH - len(target)) + target
402
402
403 computed_guess = hashlib.sha256((payload + iteration).encode())\
403 computed_guess = hashlib.sha256((payload + iteration).encode())\
404 .hexdigest()[0:POW_HASH_LENGTH]
404 .hexdigest()[0:POW_HASH_LENGTH]
405 if guess != computed_guess or guess > target:
405 if guess != computed_guess or guess > target:
406 self._errors['text'] = self.error_class(
406 self._errors['text'] = self.error_class(
407 [_('Invalid PoW.')])
407 [_('Invalid PoW.')])
408
408
409
409
410 class ThreadForm(PostForm):
410 class ThreadForm(PostForm):
411
411
412 tags = forms.CharField(
412 tags = forms.CharField(
413 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
413 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
414 max_length=100, label=_('Tags'), required=True)
414 max_length=100, label=_('Tags'), required=True)
415 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
415 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
416
416
417 def clean_tags(self):
417 def clean_tags(self):
418 tags = self.cleaned_data['tags'].strip()
418 tags = self.cleaned_data['tags'].strip()
419
419
420 if not tags or not REGEX_TAGS.match(tags):
420 if not tags or not REGEX_TAGS.match(tags):
421 raise forms.ValidationError(
421 raise forms.ValidationError(
422 _('Inappropriate characters in tags.'))
422 _('Inappropriate characters in tags.'))
423
423
424 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
424 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
425 .strip().lower()
425 .strip().lower()
426
426
427 required_tag_exists = False
427 required_tag_exists = False
428 tag_set = set()
428 tag_set = set()
429 for tag_string in tags.split():
429 for tag_string in tags.split():
430 if tag_string.strip().lower() == default_tag_name:
430 if tag_string.strip().lower() == default_tag_name:
431 required_tag_exists = True
431 required_tag_exists = True
432 tag, created = Tag.objects.get_or_create(
432 tag, created = Tag.objects.get_or_create(
433 name=tag_string.strip().lower(), required=True)
433 name=tag_string.strip().lower(), required=True)
434 else:
434 else:
435 tag, created = Tag.objects.get_or_create(
435 tag, created = Tag.objects.get_or_create(
436 name=tag_string.strip().lower())
436 name=tag_string.strip().lower())
437 tag_set.add(tag)
437 tag_set.add(tag)
438
438
439 # 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
440 # added them yet
440 # added them yet
441 if not created:
441 if not created:
442 tag_set |= set(tag.get_all_parents())
442 tag_set |= set(tag.get_all_parents())
443
443
444 for tag in tag_set:
444 for tag in tag_set:
445 if tag.required:
445 if tag.required:
446 required_tag_exists = True
446 required_tag_exists = True
447 break
447 break
448
448
449 # Use default tag if no section exists
449 # Use default tag if no section exists
450 if not required_tag_exists:
450 if not required_tag_exists:
451 default_tag, created = Tag.objects.get_or_create(
451 default_tag, created = Tag.objects.get_or_create(
452 name=default_tag_name, required=True)
452 name=default_tag_name, required=True)
453 tag_set.add(default_tag)
453 tag_set.add(default_tag)
454
454
455 return tag_set
455 return tag_set
456
456
457 def clean(self):
457 def clean(self):
458 cleaned_data = super(ThreadForm, self).clean()
458 cleaned_data = super(ThreadForm, self).clean()
459
459
460 return cleaned_data
460 return cleaned_data
461
461
462 def is_monochrome(self):
462 def is_monochrome(self):
463 return self.cleaned_data['monochrome']
463 return self.cleaned_data['monochrome']
464
464
465
465
466 class SettingsForm(NeboardForm):
466 class SettingsForm(NeboardForm):
467
467
468 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
468 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
469 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'))
470 username = forms.CharField(label=_('User name'), required=False)
470 username = forms.CharField(label=_('User name'), required=False)
471 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
471 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
472
472
473 def clean_username(self):
473 def clean_username(self):
474 username = self.cleaned_data['username']
474 username = self.cleaned_data['username']
475
475
476 if username and not REGEX_USERNAMES.match(username):
476 if username and not REGEX_USERNAMES.match(username):
477 raise forms.ValidationError(_('Inappropriate characters.'))
477 raise forms.ValidationError(_('Inappropriate characters.'))
478
478
479 return username
479 return username
480
480
481
481
482 class SearchForm(NeboardForm):
482 class SearchForm(NeboardForm):
483 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
483 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,90 +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 all_data = []
81 all_data = []
82 for data in data_list:
82 for data in data_list:
83 if type(data) == list:
83 if type(data) == list:
84 all_data += data
84 all_data += data
85 elif type(data) == str:
85 elif type(data) == str and len(data) > 0:
86 input = data.replace('\r\n', '\n')
86 file_input = data.replace('\r\n', '\n')
87 url_list = input.split('\n')
87 url_list = file_input.split('\n')
88 all_data += url_list
88 all_data += url_list
89
89
90 return all_data
90 return all_data
General Comments 0
You need to be logged in to leave comments. Login now