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