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