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