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