##// END OF EJS Templates
Refactored required tags validation a bit
neko259 -
r1100:ee1fd200 default
parent child Browse files
Show More
@@ -1,372 +1,372 b''
1 1 import re
2 2 import time
3 3 import pytz
4 4
5 5 from django import forms
6 6 from django.core.files.uploadedfile import SimpleUploadedFile
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.forms.util import ErrorList
9 9 from django.utils.translation import ugettext_lazy as _
10 10 import requests
11 11
12 12 from boards.mdx_neboard import formatters
13 13 from boards.models.post import TITLE_MAX_LENGTH
14 14 from boards.models import Tag, Post
15 15 from neboard import settings
16 16 import boards.settings as board_settings
17 17
18 18
19 19 CONTENT_TYPE_IMAGE = (
20 20 'image/jpeg',
21 21 'image/png',
22 22 'image/gif',
23 23 'image/bmp',
24 24 )
25 25
26 26 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
27 27
28 28 VETERAN_POSTING_DELAY = 5
29 29
30 30 ATTRIBUTE_PLACEHOLDER = 'placeholder'
31 31 ATTRIBUTE_ROWS = 'rows'
32 32
33 33 LAST_POST_TIME = 'last_post_time'
34 34 LAST_LOGIN_TIME = 'last_login_time'
35 35 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
36 36 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
37 37
38 38 LABEL_TITLE = _('Title')
39 39 LABEL_TEXT = _('Text')
40 40 LABEL_TAG = _('Tag')
41 41 LABEL_SEARCH = _('Search')
42 42
43 43 TAG_MAX_LENGTH = 20
44 44
45 45 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
46 46
47 47 HTTP_RESULT_OK = 200
48 48
49 49 TEXTAREA_ROWS = 4
50 50
51 51
52 52 def get_timezones():
53 53 timezones = []
54 54 for tz in pytz.common_timezones:
55 55 timezones.append((tz, tz),)
56 56 return timezones
57 57
58 58
59 59 class FormatPanel(forms.Textarea):
60 60 """
61 61 Panel for text formatting. Consists of buttons to add different tags to the
62 62 form text area.
63 63 """
64 64
65 65 def render(self, name, value, attrs=None):
66 66 output = '<div id="mark-panel">'
67 67 for formatter in formatters:
68 68 output += '<span class="mark_btn"' + \
69 69 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
70 70 '\', \'' + formatter.format_right + '\')">' + \
71 71 formatter.preview_left + formatter.name + \
72 72 formatter.preview_right + '</span>'
73 73
74 74 output += '</div>'
75 75 output += super(FormatPanel, self).render(name, value, attrs=None)
76 76
77 77 return output
78 78
79 79
80 80 class PlainErrorList(ErrorList):
81 81 def __unicode__(self):
82 82 return self.as_text()
83 83
84 84 def as_text(self):
85 85 return ''.join(['(!) %s ' % e for e in self])
86 86
87 87
88 88 class NeboardForm(forms.Form):
89 89 """
90 90 Form with neboard-specific formatting.
91 91 """
92 92
93 93 def as_div(self):
94 94 """
95 95 Returns this form rendered as HTML <as_div>s.
96 96 """
97 97
98 98 return self._html_output(
99 99 # TODO Do not show hidden rows in the list here
100 100 normal_row='<div class="form-row"><div class="form-label">'
101 101 '%(label)s'
102 102 '</div></div>'
103 103 '<div class="form-row"><div class="form-input">'
104 104 '%(field)s'
105 105 '</div></div>'
106 106 '<div class="form-row">'
107 107 '%(help_text)s'
108 108 '</div>',
109 109 error_row='<div class="form-row">'
110 110 '<div class="form-label"></div>'
111 111 '<div class="form-errors">%s</div>'
112 112 '</div>',
113 113 row_ender='</div>',
114 114 help_text_html='%s',
115 115 errors_on_separate_row=True)
116 116
117 117 def as_json_errors(self):
118 118 errors = []
119 119
120 120 for name, field in list(self.fields.items()):
121 121 if self[name].errors:
122 122 errors.append({
123 123 'field': name,
124 124 'errors': self[name].errors.as_text(),
125 125 })
126 126
127 127 return errors
128 128
129 129
130 130 class PostForm(NeboardForm):
131 131
132 132 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
133 133 label=LABEL_TITLE)
134 134 text = forms.CharField(
135 135 widget=FormatPanel(attrs={
136 136 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
137 137 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
138 138 }),
139 139 required=False, label=LABEL_TEXT)
140 140 image = forms.ImageField(required=False, label=_('Image'),
141 141 widget=forms.ClearableFileInput(
142 142 attrs={'accept': 'image/*'}))
143 143 image_url = forms.CharField(required=False, label=_('Image URL'),
144 144 widget=forms.TextInput(
145 145 attrs={ATTRIBUTE_PLACEHOLDER:
146 146 'http://example.com/image.png'}))
147 147
148 148 # This field is for spam prevention only
149 149 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
150 150 widget=forms.TextInput(attrs={
151 151 'class': 'form-email'}))
152 152 threads = forms.CharField(required=False, label=_('Additional threads'),
153 153 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
154 154 '123 456 789'}))
155 155
156 156 session = None
157 157 need_to_ban = False
158 158
159 159 def clean_title(self):
160 160 title = self.cleaned_data['title']
161 161 if title:
162 162 if len(title) > TITLE_MAX_LENGTH:
163 163 raise forms.ValidationError(_('Title must have less than %s '
164 164 'characters') %
165 165 str(TITLE_MAX_LENGTH))
166 166 return title
167 167
168 168 def clean_text(self):
169 169 text = self.cleaned_data['text'].strip()
170 170 if text:
171 171 if len(text) > board_settings.MAX_TEXT_LENGTH:
172 172 raise forms.ValidationError(_('Text must have less than %s '
173 173 'characters') %
174 174 str(board_settings
175 175 .MAX_TEXT_LENGTH))
176 176 return text
177 177
178 178 def clean_image(self):
179 179 image = self.cleaned_data['image']
180 180
181 181 if image:
182 182 self.validate_image_size(image.size)
183 183
184 184 return image
185 185
186 186 def clean_image_url(self):
187 187 url = self.cleaned_data['image_url']
188 188
189 189 image = None
190 190 if url:
191 191 image = self._get_image_from_url(url)
192 192
193 193 if not image:
194 194 raise forms.ValidationError(_('Invalid URL'))
195 195 else:
196 196 self.validate_image_size(image.size)
197 197
198 198 return image
199 199
200 200 def clean_threads(self):
201 201 threads_str = self.cleaned_data['threads']
202 202
203 203 if len(threads_str) > 0:
204 204 threads_id_list = threads_str.split(' ')
205 205
206 206 threads = list()
207 207
208 208 for thread_id in threads_id_list:
209 209 try:
210 210 thread = Post.objects.get(id=int(thread_id))
211 211 if not thread.is_opening():
212 212 raise ObjectDoesNotExist()
213 213 threads.append(thread)
214 214 except (ObjectDoesNotExist, ValueError):
215 215 raise forms.ValidationError(_('Invalid additional thread list'))
216 216
217 217 return threads
218 218
219 219 def clean(self):
220 220 cleaned_data = super(PostForm, self).clean()
221 221
222 222 if not self.session:
223 223 raise forms.ValidationError('Humans have sessions')
224 224
225 225 if cleaned_data['email']:
226 226 self.need_to_ban = True
227 227 raise forms.ValidationError('A human cannot enter a hidden field')
228 228
229 229 if not self.errors:
230 230 self._clean_text_image()
231 231
232 232 if not self.errors and self.session:
233 233 self._validate_posting_speed()
234 234
235 235 return cleaned_data
236 236
237 237 def get_image(self):
238 238 """
239 239 Gets image from file or URL.
240 240 """
241 241
242 242 image = self.cleaned_data['image']
243 243 return image if image else self.cleaned_data['image_url']
244 244
245 245 def _clean_text_image(self):
246 246 text = self.cleaned_data.get('text')
247 247 image = self.get_image()
248 248
249 249 if (not text) and (not image):
250 250 error_message = _('Either text or image must be entered.')
251 251 self._errors['text'] = self.error_class([error_message])
252 252
253 253 def _validate_posting_speed(self):
254 254 can_post = True
255 255
256 256 posting_delay = settings.POSTING_DELAY
257 257
258 258 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
259 259 self.session:
260 260 now = time.time()
261 261 last_post_time = self.session[LAST_POST_TIME]
262 262
263 263 current_delay = int(now - last_post_time)
264 264
265 265 if current_delay < posting_delay:
266 266 error_message = _('Wait %s seconds after last posting') % str(
267 267 posting_delay - current_delay)
268 268 self._errors['text'] = self.error_class([error_message])
269 269
270 270 can_post = False
271 271
272 272 if can_post:
273 273 self.session[LAST_POST_TIME] = time.time()
274 274
275 275 def validate_image_size(self, size: int):
276 276 if size > board_settings.MAX_IMAGE_SIZE:
277 277 raise forms.ValidationError(
278 278 _('Image must be less than %s bytes')
279 279 % str(board_settings.MAX_IMAGE_SIZE))
280 280
281 281 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
282 282 """
283 283 Gets an image file from URL.
284 284 """
285 285
286 286 img_temp = None
287 287
288 288 try:
289 289 # Verify content headers
290 290 response_head = requests.head(url, verify=False)
291 291 content_type = response_head.headers['content-type'].split(';')[0]
292 292 if content_type in CONTENT_TYPE_IMAGE:
293 293 length_header = response_head.headers.get('content-length')
294 294 if length_header:
295 295 length = int(length_header)
296 296 self.validate_image_size(length)
297 297 # Get the actual content into memory
298 298 response = requests.get(url, verify=False, stream=True)
299 299
300 300 # Download image, stop if the size exceeds limit
301 301 size = 0
302 302 content = b''
303 303 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
304 304 size += len(chunk)
305 305 self.validate_image_size(size)
306 306 content += chunk
307 307
308 308 if response.status_code == HTTP_RESULT_OK and content:
309 309 # Set a dummy file name that will be replaced
310 310 # anyway, just keep the valid extension
311 311 filename = 'image.' + content_type.split('/')[1]
312 312 img_temp = SimpleUploadedFile(filename, content,
313 313 content_type)
314 314 except Exception:
315 315 # Just return no image
316 316 pass
317 317
318 318 return img_temp
319 319
320 320
321 321 class ThreadForm(PostForm):
322 322
323 323 tags = forms.CharField(
324 324 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
325 325 max_length=100, label=_('Tags'), required=True)
326 326
327 327 def clean_tags(self):
328 328 tags = self.cleaned_data['tags'].strip()
329 329
330 330 if not tags or not REGEX_TAGS.match(tags):
331 331 raise forms.ValidationError(
332 332 _('Inappropriate characters in tags.'))
333 333
334 334 required_tag_exists = False
335 335 for tag in tags.split():
336 tag_model = Tag.objects.filter(name=tag.strip().lower(),
337 required=True)
338 if tag_model.exists():
339 required_tag_exists = True
336 try:
337 Tag.objects.get(name=tag.strip().lower(), required=True)
340 338 break
339 except ObjectDoesNotExist:
340 pass
341 341
342 342 if not required_tag_exists:
343 343 all_tags = Tag.objects.filter(required=True)
344 344 raise forms.ValidationError(
345 345 _('Need at least one of the tags: ')
346 346 + ', '.join([tag.name for tag in all_tags]))
347 347
348 348 return tags
349 349
350 350 def clean(self):
351 351 cleaned_data = super(ThreadForm, self).clean()
352 352
353 353 return cleaned_data
354 354
355 355
356 356 class SettingsForm(NeboardForm):
357 357
358 358 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
359 359 username = forms.CharField(label=_('User name'), required=False)
360 360 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
361 361
362 362 def clean_username(self):
363 363 username = self.cleaned_data['username']
364 364
365 365 if username and not REGEX_TAGS.match(username):
366 366 raise forms.ValidationError(_('Inappropriate characters.'))
367 367
368 368 return username
369 369
370 370
371 371 class SearchForm(NeboardForm):
372 372 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