##// END OF EJS Templates
Pass textarea attributes to fix w3c validation of label
neko259 -
r1408:9d9f4258 default
parent child Browse files
Show More
@@ -1,406 +1,406 b''
1 1 import hashlib
2 2 import re
3 3 import time
4 4 import logging
5 5 import pytz
6 6
7 7 from django import forms
8 8 from django.core.files.uploadedfile import SimpleUploadedFile
9 9 from django.core.exceptions import ObjectDoesNotExist
10 10 from django.forms.util import ErrorList
11 11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12 12
13 13 from boards.mdx_neboard import formatters
14 14 from boards.models.attachment.downloaders import Downloader
15 15 from boards.models.post import TITLE_MAX_LENGTH
16 16 from boards.models import Tag, Post
17 17 from boards.utils import validate_file_size, get_file_mimetype, \
18 18 FILE_EXTENSION_DELIMITER
19 19 from neboard import settings
20 20 import boards.settings as board_settings
21 21 import neboard
22 22
23 23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
24 24
25 25 VETERAN_POSTING_DELAY = 5
26 26
27 27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
28 28 ATTRIBUTE_ROWS = 'rows'
29 29
30 30 LAST_POST_TIME = 'last_post_time'
31 31 LAST_LOGIN_TIME = 'last_login_time'
32 32 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
33 33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
34 34
35 35 LABEL_TITLE = _('Title')
36 36 LABEL_TEXT = _('Text')
37 37 LABEL_TAG = _('Tag')
38 38 LABEL_SEARCH = _('Search')
39 39
40 40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
41 41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
42 42
43 43 TAG_MAX_LENGTH = 20
44 44
45 45 TEXTAREA_ROWS = 4
46 46
47 47 TRIPCODE_DELIM = '#'
48 48
49 49 # TODO Maybe this may be converted into the database table?
50 50 MIMETYPE_EXTENSIONS = {
51 51 'image/jpeg': 'jpeg',
52 52 'image/png': 'png',
53 53 'image/gif': 'gif',
54 54 'video/webm': 'webm',
55 55 'application/pdf': 'pdf',
56 56 'x-diff': 'diff',
57 57 'image/svg+xml': 'svg',
58 58 'application/x-shockwave-flash': 'swf',
59 59 }
60 60
61 61
62 62 def get_timezones():
63 63 timezones = []
64 64 for tz in pytz.common_timezones:
65 65 timezones.append((tz, tz),)
66 66 return timezones
67 67
68 68
69 69 class FormatPanel(forms.Textarea):
70 70 """
71 71 Panel for text formatting. Consists of buttons to add different tags to the
72 72 form text area.
73 73 """
74 74
75 75 def render(self, name, value, attrs=None):
76 76 output = '<div id="mark-panel">'
77 77 for formatter in formatters:
78 78 output += '<span class="mark_btn"' + \
79 79 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
80 80 '\', \'' + formatter.format_right + '\')">' + \
81 81 formatter.preview_left + formatter.name + \
82 82 formatter.preview_right + '</span>'
83 83
84 84 output += '</div>'
85 output += super(FormatPanel, self).render(name, value, attrs=None)
85 output += super(FormatPanel, self).render(name, value, attrs=attrs)
86 86
87 87 return output
88 88
89 89
90 90 class PlainErrorList(ErrorList):
91 91 def __unicode__(self):
92 92 return self.as_text()
93 93
94 94 def as_text(self):
95 95 return ''.join(['(!) %s ' % e for e in self])
96 96
97 97
98 98 class NeboardForm(forms.Form):
99 99 """
100 100 Form with neboard-specific formatting.
101 101 """
102 102
103 103 def as_div(self):
104 104 """
105 105 Returns this form rendered as HTML <as_div>s.
106 106 """
107 107
108 108 return self._html_output(
109 109 # TODO Do not show hidden rows in the list here
110 110 normal_row='<div class="form-row">'
111 111 '<div class="form-label">'
112 112 '%(label)s'
113 113 '</div>'
114 114 '<div class="form-input">'
115 115 '%(field)s'
116 116 '</div>'
117 117 '</div>'
118 118 '<div class="form-row">'
119 119 '%(help_text)s'
120 120 '</div>',
121 121 error_row='<div class="form-row">'
122 122 '<div class="form-label"></div>'
123 123 '<div class="form-errors">%s</div>'
124 124 '</div>',
125 125 row_ender='</div>',
126 126 help_text_html='%s',
127 127 errors_on_separate_row=True)
128 128
129 129 def as_json_errors(self):
130 130 errors = []
131 131
132 132 for name, field in list(self.fields.items()):
133 133 if self[name].errors:
134 134 errors.append({
135 135 'field': name,
136 136 'errors': self[name].errors.as_text(),
137 137 })
138 138
139 139 return errors
140 140
141 141
142 142 class PostForm(NeboardForm):
143 143
144 144 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
145 145 label=LABEL_TITLE,
146 146 widget=forms.TextInput(
147 147 attrs={ATTRIBUTE_PLACEHOLDER:
148 148 'test#tripcode'}))
149 149 text = forms.CharField(
150 150 widget=FormatPanel(attrs={
151 151 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
152 152 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
153 153 }),
154 154 required=False, label=LABEL_TEXT)
155 155 file = forms.FileField(required=False, label=_('File'),
156 156 widget=forms.ClearableFileInput(
157 157 attrs={'accept': 'file/*'}))
158 158 file_url = forms.CharField(required=False, label=_('File URL'),
159 159 widget=forms.TextInput(
160 160 attrs={ATTRIBUTE_PLACEHOLDER:
161 161 'http://example.com/image.png'}))
162 162
163 163 # This field is for spam prevention only
164 164 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
165 165 widget=forms.TextInput(attrs={
166 166 'class': 'form-email'}))
167 167 threads = forms.CharField(required=False, label=_('Additional threads'),
168 168 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
169 169 '123 456 789'}))
170 170
171 171 session = None
172 172 need_to_ban = False
173 173
174 174 def _update_file_extension(self, file):
175 175 if file:
176 176 mimetype = get_file_mimetype(file)
177 177 extension = MIMETYPE_EXTENSIONS.get(mimetype)
178 178 if extension:
179 179 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
180 180 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
181 181
182 182 file.name = new_filename
183 183 else:
184 184 logger = logging.getLogger('boards.forms.extension')
185 185
186 186 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
187 187
188 188 def clean_title(self):
189 189 title = self.cleaned_data['title']
190 190 if title:
191 191 if len(title) > TITLE_MAX_LENGTH:
192 192 raise forms.ValidationError(_('Title must have less than %s '
193 193 'characters') %
194 194 str(TITLE_MAX_LENGTH))
195 195 return title
196 196
197 197 def clean_text(self):
198 198 text = self.cleaned_data['text'].strip()
199 199 if text:
200 200 max_length = board_settings.get_int('Forms', 'MaxTextLength')
201 201 if len(text) > max_length:
202 202 raise forms.ValidationError(_('Text must have less than %s '
203 203 'characters') % str(max_length))
204 204 return text
205 205
206 206 def clean_file(self):
207 207 file = self.cleaned_data['file']
208 208
209 209 if file:
210 210 validate_file_size(file.size)
211 211 self._update_file_extension(file)
212 212
213 213 return file
214 214
215 215 def clean_file_url(self):
216 216 url = self.cleaned_data['file_url']
217 217
218 218 file = None
219 219 if url:
220 220 file = self._get_file_from_url(url)
221 221
222 222 if not file:
223 223 raise forms.ValidationError(_('Invalid URL'))
224 224 else:
225 225 validate_file_size(file.size)
226 226 self._update_file_extension(file)
227 227
228 228 return file
229 229
230 230 def clean_threads(self):
231 231 threads_str = self.cleaned_data['threads']
232 232
233 233 if len(threads_str) > 0:
234 234 threads_id_list = threads_str.split(' ')
235 235
236 236 threads = list()
237 237
238 238 for thread_id in threads_id_list:
239 239 try:
240 240 thread = Post.objects.get(id=int(thread_id))
241 241 if not thread.is_opening() or thread.get_thread().archived:
242 242 raise ObjectDoesNotExist()
243 243 threads.append(thread)
244 244 except (ObjectDoesNotExist, ValueError):
245 245 raise forms.ValidationError(_('Invalid additional thread list'))
246 246
247 247 return threads
248 248
249 249 def clean(self):
250 250 cleaned_data = super(PostForm, self).clean()
251 251
252 252 if cleaned_data['email']:
253 253 self.need_to_ban = True
254 254 raise forms.ValidationError('A human cannot enter a hidden field')
255 255
256 256 if not self.errors:
257 257 self._clean_text_file()
258 258
259 259 if not self.errors and self.session:
260 260 self._validate_posting_speed()
261 261
262 262 return cleaned_data
263 263
264 264 def get_file(self):
265 265 """
266 266 Gets file from form or URL.
267 267 """
268 268
269 269 file = self.cleaned_data['file']
270 270 return file or self.cleaned_data['file_url']
271 271
272 272 def get_tripcode(self):
273 273 title = self.cleaned_data['title']
274 274 if title is not None and TRIPCODE_DELIM in title:
275 275 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
276 276 tripcode = hashlib.md5(code.encode()).hexdigest()
277 277 else:
278 278 tripcode = ''
279 279 return tripcode
280 280
281 281 def get_title(self):
282 282 title = self.cleaned_data['title']
283 283 if title is not None and TRIPCODE_DELIM in title:
284 284 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
285 285 else:
286 286 return title
287 287
288 288 def _clean_text_file(self):
289 289 text = self.cleaned_data.get('text')
290 290 file = self.get_file()
291 291
292 292 if (not text) and (not file):
293 293 error_message = _('Either text or file must be entered.')
294 294 self._errors['text'] = self.error_class([error_message])
295 295
296 296 def _validate_posting_speed(self):
297 297 can_post = True
298 298
299 299 posting_delay = settings.POSTING_DELAY
300 300
301 301 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
302 302 now = time.time()
303 303
304 304 current_delay = 0
305 305
306 306 if LAST_POST_TIME not in self.session:
307 307 self.session[LAST_POST_TIME] = now
308 308
309 309 need_delay = True
310 310 else:
311 311 last_post_time = self.session.get(LAST_POST_TIME)
312 312 current_delay = int(now - last_post_time)
313 313
314 314 need_delay = current_delay < posting_delay
315 315
316 316 if need_delay:
317 317 delay = posting_delay - current_delay
318 318 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
319 319 delay) % {'delay': delay}
320 320 self._errors['text'] = self.error_class([error_message])
321 321
322 322 can_post = False
323 323
324 324 if can_post:
325 325 self.session[LAST_POST_TIME] = now
326 326
327 327 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
328 328 """
329 329 Gets an file file from URL.
330 330 """
331 331
332 332 img_temp = None
333 333
334 334 try:
335 335 for downloader in Downloader.__subclasses__():
336 336 if downloader.handles(url):
337 337 return downloader.download(url)
338 338 # If nobody of the specific downloaders handles this, use generic
339 339 # one
340 340 return Downloader.download(url)
341 341 except forms.ValidationError as e:
342 342 raise e
343 343 except Exception as e:
344 344 # Just return no file
345 345 pass
346 346
347 347
348 348 class ThreadForm(PostForm):
349 349
350 350 tags = forms.CharField(
351 351 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
352 352 max_length=100, label=_('Tags'), required=True)
353 353
354 354 def clean_tags(self):
355 355 tags = self.cleaned_data['tags'].strip()
356 356
357 357 if not tags or not REGEX_TAGS.match(tags):
358 358 raise forms.ValidationError(
359 359 _('Inappropriate characters in tags.'))
360 360
361 361 required_tag_exists = False
362 362 tag_set = set()
363 363 for tag_string in tags.split():
364 364 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
365 365 tag_set.add(tag)
366 366
367 367 # If this is a new tag, don't check for its parents because nobody
368 368 # added them yet
369 369 if not created:
370 370 tag_set |= set(tag.get_all_parents())
371 371
372 372 for tag in tag_set:
373 373 if tag.required:
374 374 required_tag_exists = True
375 375 break
376 376
377 377 if not required_tag_exists:
378 378 raise forms.ValidationError(
379 379 _('Need at least one section.'))
380 380
381 381 return tag_set
382 382
383 383 def clean(self):
384 384 cleaned_data = super(ThreadForm, self).clean()
385 385
386 386 return cleaned_data
387 387
388 388
389 389 class SettingsForm(NeboardForm):
390 390
391 391 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
392 392 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
393 393 username = forms.CharField(label=_('User name'), required=False)
394 394 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
395 395
396 396 def clean_username(self):
397 397 username = self.cleaned_data['username']
398 398
399 399 if username and not REGEX_TAGS.match(username):
400 400 raise forms.ValidationError(_('Inappropriate characters.'))
401 401
402 402 return username
403 403
404 404
405 405 class SearchForm(NeboardForm):
406 406 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