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