##// END OF EJS Templates
Add any number of files and URLs to post, not just one
neko259 -
r1753:22f62124 default
parent child Browse files
Show More
@@ -1,478 +1,482
1 1 import hashlib
2 2 import re
3 3 import time
4 4 import logging
5 5
6 6 import pytz
7 7
8 8 from django import forms
9 9 from django.core.files.uploadedfile import SimpleUploadedFile
10 10 from django.core.exceptions import ObjectDoesNotExist
11 11 from django.core.files.uploadedfile import UploadedFile
12 12 from django.forms.utils import ErrorList
13 13 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
14 14 from django.utils import timezone
15 15
16 16 from boards.abstracts.settingsmanager import get_settings_manager
17 17 from boards.abstracts.attachment_alias import get_image_by_alias
18 18 from boards.mdx_neboard import formatters
19 19 from boards.models.attachment.downloaders import download
20 20 from boards.models.post import TITLE_MAX_LENGTH
21 21 from boards.models import Tag, Post
22 22 from boards.utils import validate_file_size, get_file_mimetype, \
23 23 FILE_EXTENSION_DELIMITER
24 24 from boards.abstracts.fields import UrlFileField
25 25 from neboard import settings
26 26 import boards.settings as board_settings
27 27 import neboard
28 28
29 29 POW_HASH_LENGTH = 16
30 30 POW_LIFE_MINUTES = 5
31 31
32 32 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
33 33 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
34 34 REGEX_URL = re.compile(r'^(http|https|ftp|magnet):\/\/', re.UNICODE)
35 35
36 36 VETERAN_POSTING_DELAY = 5
37 37
38 38 ATTRIBUTE_PLACEHOLDER = 'placeholder'
39 39 ATTRIBUTE_ROWS = 'rows'
40 40
41 41 LAST_POST_TIME = 'last_post_time'
42 42 LAST_LOGIN_TIME = 'last_login_time'
43 43 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
44 44 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
45 45
46 46 LABEL_TITLE = _('Title')
47 47 LABEL_TEXT = _('Text')
48 48 LABEL_TAG = _('Tag')
49 49 LABEL_SEARCH = _('Search')
50 50
51 51 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
52 52 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
53 53
54 54 TAG_MAX_LENGTH = 20
55 55
56 56 TEXTAREA_ROWS = 4
57 57
58 58 TRIPCODE_DELIM = '#'
59 59
60 60 # TODO Maybe this may be converted into the database table?
61 61 MIMETYPE_EXTENSIONS = {
62 62 'image/jpeg': 'jpeg',
63 63 'image/png': 'png',
64 64 'image/gif': 'gif',
65 65 'video/webm': 'webm',
66 66 'application/pdf': 'pdf',
67 67 'x-diff': 'diff',
68 68 'image/svg+xml': 'svg',
69 69 'application/x-shockwave-flash': 'swf',
70 70 'image/x-ms-bmp': 'bmp',
71 71 'image/bmp': 'bmp',
72 72 }
73 73
74 74
75 75 logger = logging.getLogger('boards.forms')
76 76
77 77
78 78 def get_timezones():
79 79 timezones = []
80 80 for tz in pytz.common_timezones:
81 81 timezones.append((tz, tz),)
82 82 return timezones
83 83
84 84
85 85 class FormatPanel(forms.Textarea):
86 86 """
87 87 Panel for text formatting. Consists of buttons to add different tags to the
88 88 form text area.
89 89 """
90 90
91 91 def render(self, name, value, attrs=None):
92 92 output = '<div id="mark-panel">'
93 93 for formatter in formatters:
94 94 output += '<span class="mark_btn"' + \
95 95 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
96 96 '\', \'' + formatter.format_right + '\')">' + \
97 97 formatter.preview_left + formatter.name + \
98 98 formatter.preview_right + '</span>'
99 99
100 100 output += '</div>'
101 101 output += super(FormatPanel, self).render(name, value, attrs=attrs)
102 102
103 103 return output
104 104
105 105
106 106 class PlainErrorList(ErrorList):
107 107 def __unicode__(self):
108 108 return self.as_text()
109 109
110 110 def as_text(self):
111 111 return ''.join(['(!) %s ' % e for e in self])
112 112
113 113
114 114 class NeboardForm(forms.Form):
115 115 """
116 116 Form with neboard-specific formatting.
117 117 """
118 118 required_css_class = 'required-field'
119 119
120 120 def as_div(self):
121 121 """
122 122 Returns this form rendered as HTML <as_div>s.
123 123 """
124 124
125 125 return self._html_output(
126 126 # TODO Do not show hidden rows in the list here
127 127 normal_row='<div class="form-row">'
128 128 '<div class="form-label">'
129 129 '%(label)s'
130 130 '</div>'
131 131 '<div class="form-input">'
132 132 '%(field)s'
133 133 '</div>'
134 134 '</div>'
135 135 '<div class="form-row">'
136 136 '%(help_text)s'
137 137 '</div>',
138 138 error_row='<div class="form-row">'
139 139 '<div class="form-label"></div>'
140 140 '<div class="form-errors">%s</div>'
141 141 '</div>',
142 142 row_ender='</div>',
143 143 help_text_html='%s',
144 144 errors_on_separate_row=True)
145 145
146 146 def as_json_errors(self):
147 147 errors = []
148 148
149 149 for name, field in list(self.fields.items()):
150 150 if self[name].errors:
151 151 errors.append({
152 152 'field': name,
153 153 'errors': self[name].errors.as_text(),
154 154 })
155 155
156 156 return errors
157 157
158 158
159 159 class PostForm(NeboardForm):
160 160
161 161 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
162 162 label=LABEL_TITLE,
163 163 widget=forms.TextInput(
164 164 attrs={ATTRIBUTE_PLACEHOLDER:
165 165 'test#tripcode'}))
166 166 text = forms.CharField(
167 167 widget=FormatPanel(attrs={
168 168 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
169 169 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
170 170 }),
171 171 required=False, label=LABEL_TEXT)
172 172 file = UrlFileField(required=False, label=_('File'))
173 173
174 174 # This field is for spam prevention only
175 175 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
176 176 widget=forms.TextInput(attrs={
177 177 'class': 'form-email'}))
178 178 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
179 179
180 180 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
181 181 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
182 182 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
183 183
184 184 session = None
185 185 need_to_ban = False
186 186 image = None
187 187
188 188 def _update_file_extension(self, file):
189 189 if file:
190 mimetype = get_file_mimetype(file)
190 mimetype =get_file_mimetype(file)
191 191 extension = MIMETYPE_EXTENSIONS.get(mimetype)
192 192 if extension:
193 193 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
194 194 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
195 195
196 196 file.name = new_filename
197 197 else:
198 198 logger = logging.getLogger('boards.forms.extension')
199 199
200 200 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
201 201
202 202 def clean_title(self):
203 203 title = self.cleaned_data['title']
204 204 if title:
205 205 if len(title) > TITLE_MAX_LENGTH:
206 206 raise forms.ValidationError(_('Title must have less than %s '
207 207 'characters') %
208 208 str(TITLE_MAX_LENGTH))
209 209 return title
210 210
211 211 def clean_text(self):
212 212 text = self.cleaned_data['text'].strip()
213 213 if text:
214 214 max_length = board_settings.get_int('Forms', 'MaxTextLength')
215 215 if len(text) > max_length:
216 216 raise forms.ValidationError(_('Text must have less than %s '
217 217 'characters') % str(max_length))
218 218 return text
219 219
220 220 def clean_file(self):
221 221 file = self.cleaned_data['file']
222 222
223 223 if isinstance(file, UploadedFile):
224 224 file = self._clean_file_file(file)
225 225 else:
226 226 file = self._clean_file_url(file)
227 227
228 228 return file
229 229
230 230 def _clean_file_file(self, file):
231 231 validate_file_size(file.size)
232 232 self._update_file_extension(file)
233 233
234 234 return file
235 235
236 236
237 237 def _clean_file_url(self, url):
238 238 file = None
239 239
240 240 if url:
241 241 try:
242 242 file = get_image_by_alias(url, self.session)
243 243 self.image = file
244 244
245 245 if file is not None:
246 246 return
247 247
248 248 if file is None:
249 249 file = self._get_file_from_url(url)
250 250 if not file:
251 251 raise forms.ValidationError(_('Invalid URL'))
252 252 else:
253 253 validate_file_size(file.size)
254 254 self._update_file_extension(file)
255 255 except forms.ValidationError as e:
256 256 # Assume we will get the plain URL instead of a file and save it
257 257 if REGEX_URL.match(url):
258 258 logger.info('Error in forms: {}'.format(e))
259 259 return url
260 260 else:
261 261 raise e
262 262
263 263 return file
264 264
265 265 def clean(self):
266 266 cleaned_data = super(PostForm, self).clean()
267 267
268 268 if cleaned_data['email']:
269 269 if board_settings.get_bool('Forms', 'Autoban'):
270 270 self.need_to_ban = True
271 271 raise forms.ValidationError('A human cannot enter a hidden field')
272 272
273 273 if not self.errors:
274 274 self._clean_text_file()
275 275
276 276 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
277 277 limit_first = board_settings.get_bool('Forms', 'LimitFirstPosting')
278 278
279 279 settings_manager = get_settings_manager(self)
280 280 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
281 281 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
282 282 if pow_difficulty > 0:
283 283 # PoW-based
284 284 if cleaned_data['timestamp'] \
285 285 and cleaned_data['iteration'] and cleaned_data['guess'] \
286 286 and not settings_manager.get_setting('confirmed_user'):
287 287 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
288 288 else:
289 289 # Time-based
290 290 self._validate_posting_speed()
291 291 settings_manager.set_setting('confirmed_user', True)
292 292
293 293 return cleaned_data
294 294
295 def get_file(self):
295 def get_files(self):
296 296 """
297 297 Gets file from form or URL.
298 298 """
299 299
300 300 file = self.cleaned_data['file']
301 301 if isinstance(file, UploadedFile):
302 return file
302 return [file]
303 else:
304 return []
303 305
304 def get_file_url(self):
306 def get_file_urls(self):
305 307 file = self.cleaned_data['file']
306 308 if type(file) == str:
307 return file
309 return [file]
310 else:
311 return []
308 312
309 313 def get_tripcode(self):
310 314 title = self.cleaned_data['title']
311 315 if title is not None and TRIPCODE_DELIM in title:
312 316 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
313 317 tripcode = hashlib.md5(code.encode()).hexdigest()
314 318 else:
315 319 tripcode = ''
316 320 return tripcode
317 321
318 322 def get_title(self):
319 323 title = self.cleaned_data['title']
320 324 if title is not None and TRIPCODE_DELIM in title:
321 325 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
322 326 else:
323 327 return title
324 328
325 329 def get_images(self):
326 330 if self.image:
327 331 return [self.image]
328 332 else:
329 333 return []
330 334
331 335 def is_subscribe(self):
332 336 return self.cleaned_data['subscribe']
333 337
334 338 def _clean_text_file(self):
335 339 text = self.cleaned_data.get('text')
336 file = self.get_file()
337 file_url = self.get_file_url()
340 file = self.get_files()
341 file_url = self.get_file_urls()
338 342 images = self.get_images()
339 343
340 344 if (not text) and (not file) and (not file_url) and len(images) == 0:
341 345 error_message = _('Either text or file must be entered.')
342 346 self._errors['text'] = self.error_class([error_message])
343 347
344 348 def _validate_posting_speed(self):
345 349 can_post = True
346 350
347 351 posting_delay = board_settings.get_int('Forms', 'PostingDelay')
348 352
349 353 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
350 354 now = time.time()
351 355
352 356 current_delay = 0
353 357
354 358 if LAST_POST_TIME not in self.session:
355 359 self.session[LAST_POST_TIME] = now
356 360
357 361 need_delay = True
358 362 else:
359 363 last_post_time = self.session.get(LAST_POST_TIME)
360 364 current_delay = int(now - last_post_time)
361 365
362 366 need_delay = current_delay < posting_delay
363 367
364 368 if need_delay:
365 369 delay = posting_delay - current_delay
366 370 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
367 371 delay) % {'delay': delay}
368 372 self._errors['text'] = self.error_class([error_message])
369 373
370 374 can_post = False
371 375
372 376 if can_post:
373 377 self.session[LAST_POST_TIME] = now
374 378
375 379 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
376 380 """
377 381 Gets an file file from URL.
378 382 """
379 383
380 384 try:
381 385 return download(url)
382 386 except forms.ValidationError as e:
383 387 raise e
384 388 except Exception as e:
385 389 raise forms.ValidationError(e)
386 390
387 391 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
388 392 post_time = timezone.datetime.fromtimestamp(
389 393 int(timestamp[:-3]), tz=timezone.get_current_timezone())
390 394
391 395 payload = timestamp + message.replace('\r\n', '\n')
392 396 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
393 397 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
394 398 if len(target) < POW_HASH_LENGTH:
395 399 target = '0' * (POW_HASH_LENGTH - len(target)) + target
396 400
397 401 computed_guess = hashlib.sha256((payload + iteration).encode())\
398 402 .hexdigest()[0:POW_HASH_LENGTH]
399 403 if guess != computed_guess or guess > target:
400 404 self._errors['text'] = self.error_class(
401 405 [_('Invalid PoW.')])
402 406
403 407
404 408
405 409 class ThreadForm(PostForm):
406 410
407 411 tags = forms.CharField(
408 412 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
409 413 max_length=100, label=_('Tags'), required=True)
410 414 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
411 415
412 416 def clean_tags(self):
413 417 tags = self.cleaned_data['tags'].strip()
414 418
415 419 if not tags or not REGEX_TAGS.match(tags):
416 420 raise forms.ValidationError(
417 421 _('Inappropriate characters in tags.'))
418 422
419 423 default_tag_name = board_settings.get('Forms', 'DefaultTag')\
420 424 .strip().lower()
421 425
422 426 required_tag_exists = False
423 427 tag_set = set()
424 428 for tag_string in tags.split():
425 429 if tag_string.strip().lower() == default_tag_name:
426 430 required_tag_exists = True
427 431 tag, created = Tag.objects.get_or_create(
428 432 name=tag_string.strip().lower(), required=True)
429 433 else:
430 434 tag, created = Tag.objects.get_or_create(
431 435 name=tag_string.strip().lower())
432 436 tag_set.add(tag)
433 437
434 438 # If this is a new tag, don't check for its parents because nobody
435 439 # added them yet
436 440 if not created:
437 441 tag_set |= set(tag.get_all_parents())
438 442
439 443 for tag in tag_set:
440 444 if tag.required:
441 445 required_tag_exists = True
442 446 break
443 447
444 448 # Use default tag if no section exists
445 449 if not required_tag_exists:
446 450 default_tag, created = Tag.objects.get_or_create(
447 451 name=default_tag_name, required=True)
448 452 tag_set.add(default_tag)
449 453
450 454 return tag_set
451 455
452 456 def clean(self):
453 457 cleaned_data = super(ThreadForm, self).clean()
454 458
455 459 return cleaned_data
456 460
457 461 def is_monochrome(self):
458 462 return self.cleaned_data['monochrome']
459 463
460 464
461 465 class SettingsForm(NeboardForm):
462 466
463 467 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
464 468 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
465 469 username = forms.CharField(label=_('User name'), required=False)
466 470 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
467 471
468 472 def clean_username(self):
469 473 username = self.cleaned_data['username']
470 474
471 475 if username and not REGEX_USERNAMES.match(username):
472 476 raise forms.ValidationError(_('Inappropriate characters.'))
473 477
474 478 return username
475 479
476 480
477 481 class SearchForm(NeboardForm):
478 482 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,190 +1,190
1 1 import logging
2 2
3 3 from datetime import datetime, timedelta, date
4 4 from datetime import time as dtime
5 5
6 6 from boards.abstracts.exceptions import BannedException, ArchiveException
7 7 from django.db import models, transaction
8 8 from django.utils import timezone
9 9 from django.dispatch import Signal
10 10
11 11 import boards
12 12
13 13 from boards.models.user import Ban
14 14 from boards.mdx_neboard import Parser
15 15 from boards.models import Attachment
16 16 from boards import utils
17 17
18 18 __author__ = 'neko259'
19 19
20 20 POSTS_PER_DAY_RANGE = 7
21 21 NO_IP = '0.0.0.0'
22 22
23 23
24 24 post_import_deps = Signal()
25 25
26 26
27 27 class PostManager(models.Manager):
28 28 @transaction.atomic
29 def create_post(self, title: str, text: str, file=None, thread=None,
29 def create_post(self, title: str, text: str, files=[], thread=None,
30 30 ip=NO_IP, tags: list=None,
31 31 tripcode='', monochrome=False, images=[],
32 file_url=None):
32 file_urls=[]):
33 33 """
34 34 Creates new post
35 35 """
36 36
37 37 if thread is not None and thread.is_archived():
38 38 raise ArchiveException('Cannot post into an archived thread')
39 39
40 40 if not utils.is_anonymous_mode():
41 41 is_banned = Ban.objects.filter(ip=ip).exists()
42 42 else:
43 43 is_banned = False
44 44
45 45 if is_banned:
46 46 raise BannedException("This user is banned")
47 47
48 48 if not tags:
49 49 tags = []
50 50
51 51 posting_time = timezone.now()
52 52 new_thread = False
53 53 if not thread:
54 54 thread = boards.models.thread.Thread.objects.create(
55 55 bump_time=posting_time, last_edit_time=posting_time,
56 56 monochrome=monochrome)
57 57 list(map(thread.tags.add, tags))
58 58 new_thread = True
59 59
60 60 pre_text = Parser().preparse(text)
61 61
62 62 post = self.create(title=title,
63 63 text=pre_text,
64 64 pub_time=posting_time,
65 65 poster_ip=ip,
66 66 thread=thread,
67 67 last_edit_time=posting_time,
68 68 tripcode=tripcode,
69 69 opening=new_thread)
70 70
71 71 logger = logging.getLogger('boards.post.create')
72 72
73 73 logger.info('Created post [{}] with text [{}] by {}'.format(post,
74 74 post.get_text(),post.poster_ip))
75 75
76 if file:
76 for file in files:
77 77 self._add_file_to_post(file, post)
78 78 for image in images:
79 79 post.attachments.add(image)
80 if file_url:
80 for file_url in file_urls:
81 81 post.attachments.add(Attachment.objects.create_from_url(file_url))
82 82
83 83 post.set_global_id()
84 84
85 85 # Thread needs to be bumped only when the post is already created
86 86 if not new_thread:
87 87 thread.last_edit_time = posting_time
88 88 thread.bump()
89 89 thread.save()
90 90
91 91 return post
92 92
93 93 def delete_posts_by_ip(self, ip):
94 94 """
95 95 Deletes all posts of the author with same IP
96 96 """
97 97
98 98 posts = self.filter(poster_ip=ip)
99 99 for post in posts:
100 100 post.delete()
101 101
102 102 @utils.cached_result()
103 103 def get_posts_per_day(self) -> float:
104 104 """
105 105 Gets average count of posts per day for the last 7 days
106 106 """
107 107
108 108 day_end = date.today()
109 109 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
110 110
111 111 day_time_start = timezone.make_aware(datetime.combine(
112 112 day_start, dtime()), timezone.get_current_timezone())
113 113 day_time_end = timezone.make_aware(datetime.combine(
114 114 day_end, dtime()), timezone.get_current_timezone())
115 115
116 116 posts_per_period = float(self.filter(
117 117 pub_time__lte=day_time_end,
118 118 pub_time__gte=day_time_start).count())
119 119
120 120 ppd = posts_per_period / POSTS_PER_DAY_RANGE
121 121
122 122 return ppd
123 123
124 124 def get_post_per_days(self, days) -> int:
125 125 day_end = date.today() + timedelta(1)
126 126 day_start = day_end - timedelta(days)
127 127
128 128 day_time_start = timezone.make_aware(datetime.combine(
129 129 day_start, dtime()), timezone.get_current_timezone())
130 130 day_time_end = timezone.make_aware(datetime.combine(
131 131 day_end, dtime()), timezone.get_current_timezone())
132 132
133 133 return self.filter(
134 134 pub_time__lte=day_time_end,
135 135 pub_time__gte=day_time_start).count()
136 136
137 137
138 138 @transaction.atomic
139 139 def import_post(self, title: str, text: str, pub_time: str, global_id,
140 140 opening_post=None, tags=list(), files=list(),
141 141 tripcode=None, version=1):
142 142 is_opening = opening_post is None
143 143 if is_opening:
144 144 thread = boards.models.thread.Thread.objects.create(
145 145 bump_time=pub_time, last_edit_time=pub_time)
146 146 list(map(thread.tags.add, tags))
147 147 else:
148 148 thread = opening_post.get_thread()
149 149
150 150 post = self.create(title=title,
151 151 text=text,
152 152 pub_time=pub_time,
153 153 poster_ip=NO_IP,
154 154 last_edit_time=pub_time,
155 155 global_id=global_id,
156 156 opening=is_opening,
157 157 thread=thread,
158 158 tripcode=tripcode,
159 159 version=version)
160 160
161 161 for file in files:
162 162 self._add_file_to_post(file, post)
163 163
164 164 url_to_post = '[post]{}[/post]'.format(str(global_id))
165 165 replies = self.filter(text__contains=url_to_post)
166 166 for reply in replies:
167 167 post_import_deps.send(reply)
168 168
169 169 @transaction.atomic
170 170 def update_post(self, post, title: str, text: str, pub_time: str,
171 171 tags=list(), files=list(), tripcode=None, version=1):
172 172 post.title = title
173 173 post.text = text
174 174 post.pub_time = pub_time
175 175 post.tripcode = tripcode
176 176 post.version = version
177 177 post.save()
178 178
179 179 post.clear_cache()
180 180
181 181 post.attachments.clear()
182 182 for file in files:
183 183 self._add_file_to_post(file, post)
184 184
185 185 thread = post.get_thread()
186 186 thread.tags.clear()
187 187 list(map(thread.tags.add, tags))
188 188
189 189 def _add_file_to_post(self, file, post):
190 190 post.attachments.add(Attachment.objects.create_with_hash(file))
@@ -1,191 +1,191
1 1 from django.core.urlresolvers import reverse
2 2 from django.core.files import File
3 3 from django.core.files.temp import NamedTemporaryFile
4 4 from django.core.paginator import EmptyPage
5 5 from django.db import transaction
6 6 from django.http import Http404
7 7 from django.shortcuts import render, redirect
8 8 from django.utils.decorators import method_decorator
9 9 from django.views.decorators.csrf import csrf_protect
10 10
11 11 from boards import utils, settings
12 12 from boards.abstracts.paginator import get_paginator
13 13 from boards.abstracts.settingsmanager import get_settings_manager,\
14 14 SETTING_ONLY_FAVORITES
15 15 from boards.forms import ThreadForm, PlainErrorList
16 16 from boards.models import Post, Thread, Ban
17 17 from boards.views.banned import BannedView
18 18 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 19 from boards.views.posting_mixin import PostMixin
20 20 from boards.views.mixins import FileUploadMixin, PaginatedMixin,\
21 21 DispatcherMixin, PARAMETER_METHOD
22 22
23 23 FORM_TAGS = 'tags'
24 24 FORM_TEXT = 'text'
25 25 FORM_TITLE = 'title'
26 26 FORM_IMAGE = 'image'
27 27 FORM_THREADS = 'threads'
28 28
29 29 TAG_DELIMITER = ' '
30 30
31 31 PARAMETER_CURRENT_PAGE = 'current_page'
32 32 PARAMETER_PAGINATOR = 'paginator'
33 33 PARAMETER_THREADS = 'threads'
34 34 PARAMETER_ADDITIONAL = 'additional_params'
35 35 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
36 36 PARAMETER_RSS_URL = 'rss_url'
37 37
38 38 TEMPLATE = 'boards/all_threads.html'
39 39 DEFAULT_PAGE = 1
40 40
41 41 FORM_TAGS = 'tags'
42 42
43 43
44 44 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin, DispatcherMixin):
45 45
46 46 tag_name = ''
47 47
48 48 def __init__(self):
49 49 self.settings_manager = None
50 50 super(AllThreadsView, self).__init__()
51 51
52 52 @method_decorator(csrf_protect)
53 53 def get(self, request, form: ThreadForm=None):
54 54 page = request.GET.get('page', DEFAULT_PAGE)
55 55
56 56 params = self.get_context_data(request=request)
57 57
58 58 if not form:
59 59 form = ThreadForm(error_class=PlainErrorList,
60 60 initial={FORM_TAGS: self.tag_name})
61 61
62 62 self.settings_manager = get_settings_manager(request)
63 63
64 64 threads = self.get_threads()
65 65
66 66 order = request.GET.get('order', 'bump')
67 67 if order == 'bump':
68 68 threads = threads.order_by('-bump_time')
69 69 else:
70 70 threads = threads.filter(replies__opening=True)\
71 71 .order_by('-replies__pub_time')
72 72 filter = request.GET.get('filter')
73 73 threads = threads.distinct()
74 74
75 75 paginator = get_paginator(threads,
76 76 settings.get_int('View', 'ThreadsPerPage'))
77 77 paginator.current_page = int(page)
78 78
79 79 try:
80 80 threads = paginator.page(page).object_list
81 81 except EmptyPage:
82 82 raise Http404()
83 83
84 84 params[PARAMETER_THREADS] = threads
85 85 params[CONTEXT_FORM] = form
86 86 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
87 87 params[PARAMETER_RSS_URL] = self.get_rss_url()
88 88
89 89 paginator.set_url(self.get_reverse_url(), request.GET.dict())
90 90 self.get_page_context(paginator, params, page)
91 91
92 92 return render(request, TEMPLATE, params)
93 93
94 94 @method_decorator(csrf_protect)
95 95 def post(self, request):
96 96 if PARAMETER_METHOD in request.POST:
97 97 self.dispatch_method(request)
98 98
99 99 return redirect('index') # FIXME Different for different modes
100 100
101 101 form = ThreadForm(request.POST, request.FILES,
102 102 error_class=PlainErrorList)
103 103 form.session = request.session
104 104
105 105 if form.is_valid():
106 106 return self.create_thread(request, form)
107 107 if form.need_to_ban:
108 108 # Ban user because he is suspected to be a bot
109 109 self._ban_current_user(request)
110 110
111 111 return self.get(request, form)
112 112
113 113 def get_page_context(self, paginator, params, page):
114 114 """
115 115 Get pagination context variables
116 116 """
117 117
118 118 params[PARAMETER_PAGINATOR] = paginator
119 119 current_page = paginator.page(int(page))
120 120 params[PARAMETER_CURRENT_PAGE] = current_page
121 121 self.set_page_urls(paginator, params)
122 122
123 123 def get_reverse_url(self):
124 124 return reverse('index')
125 125
126 126 @transaction.atomic
127 127 def create_thread(self, request, form: ThreadForm, html_response=True):
128 128 """
129 129 Creates a new thread with an opening post.
130 130 """
131 131
132 132 ip = utils.get_client_ip(request)
133 133 is_banned = Ban.objects.filter(ip=ip).exists()
134 134
135 135 if is_banned:
136 136 if html_response:
137 137 return redirect(BannedView().as_view())
138 138 else:
139 139 return
140 140
141 141 data = form.cleaned_data
142 142
143 143 title = form.get_title()
144 144 text = data[FORM_TEXT]
145 file = form.get_file()
146 file_url = form.get_file_url()
145 files = form.get_files()
146 file_urls = form.get_file_urls()
147 147 images = form.get_images()
148 148
149 149 text = self._remove_invalid_links(text)
150 150
151 151 tags = data[FORM_TAGS]
152 152 monochrome = form.is_monochrome()
153 153
154 post = Post.objects.create_post(title=title, text=text, file=file,
154 post = Post.objects.create_post(title=title, text=text, files=files,
155 155 ip=ip, tags=tags,
156 156 tripcode=form.get_tripcode(),
157 157 monochrome=monochrome, images=images,
158 file_url = file_url)
158 file_urls = file_urls)
159 159
160 160 # This is required to update the threads to which posts we have replied
161 161 # when creating this one
162 162 post.notify_clients()
163 163
164 164 if form.is_subscribe():
165 165 settings_manager = get_settings_manager(request)
166 166 settings_manager.add_or_read_fav_thread(post)
167 167
168 168 if html_response:
169 169 return redirect(post.get_absolute_url())
170 170
171 171 def get_threads(self):
172 172 """
173 173 Gets list of threads that will be shown on a page.
174 174 """
175 175
176 176 threads = Thread.objects\
177 177 .exclude(tags__in=self.settings_manager.get_hidden_tags())
178 178 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
179 179 fav_tags = self.settings_manager.get_fav_tags()
180 180 if len(fav_tags) > 0:
181 181 threads = threads.filter(tags__in=fav_tags)
182 182
183 183 return threads
184 184
185 185 def get_rss_url(self):
186 186 return self.get_reverse_url() + 'rss/'
187 187
188 188 def toggle_fav(self, request):
189 189 settings_manager = get_settings_manager(request)
190 190 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
191 191 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
@@ -1,181 +1,181
1 1 from django.contrib.auth.decorators import permission_required
2 2
3 3 from django.core.exceptions import ObjectDoesNotExist
4 4 from django.core.urlresolvers import reverse
5 5 from django.http import Http404
6 6 from django.shortcuts import get_object_or_404, render, redirect
7 7 from django.template.context_processors import csrf
8 8 from django.utils.decorators import method_decorator
9 9 from django.views.decorators.csrf import csrf_protect
10 10 from django.views.generic.edit import FormMixin
11 11 from django.utils import timezone
12 12 from django.utils.dateformat import format
13 13
14 14 from boards import utils, settings
15 15 from boards.abstracts.settingsmanager import get_settings_manager
16 16 from boards.forms import PostForm, PlainErrorList
17 17 from boards.models import Post
18 18 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 19 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
20 20 from boards.views.posting_mixin import PostMixin
21 21 import neboard
22 22
23 23 REQ_POST_ID = 'post_id'
24 24
25 25 CONTEXT_LASTUPDATE = "last_update"
26 26 CONTEXT_THREAD = 'thread'
27 27 CONTEXT_WS_TOKEN = 'ws_token'
28 28 CONTEXT_WS_PROJECT = 'ws_project'
29 29 CONTEXT_WS_HOST = 'ws_host'
30 30 CONTEXT_WS_PORT = 'ws_port'
31 31 CONTEXT_WS_TIME = 'ws_token_time'
32 32 CONTEXT_MODE = 'mode'
33 33 CONTEXT_OP = 'opening_post'
34 34 CONTEXT_FAVORITE = 'is_favorite'
35 35 CONTEXT_RSS_URL = 'rss_url'
36 36
37 37 FORM_TITLE = 'title'
38 38 FORM_TEXT = 'text'
39 39 FORM_IMAGE = 'image'
40 40 FORM_THREADS = 'threads'
41 41
42 42
43 43 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
44 44
45 45 @method_decorator(csrf_protect)
46 46 def get(self, request, post_id, form: PostForm=None):
47 47 try:
48 48 opening_post = Post.objects.get(id=post_id)
49 49 except ObjectDoesNotExist:
50 50 raise Http404
51 51
52 52 # If the tag is favorite, update the counter
53 53 settings_manager = get_settings_manager(request)
54 54 favorite = settings_manager.thread_is_fav(opening_post)
55 55 if favorite:
56 56 settings_manager.add_or_read_fav_thread(opening_post)
57 57
58 58 # If this is not OP, don't show it as it is
59 59 if not opening_post.is_opening():
60 60 return redirect(opening_post.get_thread().get_opening_post()
61 61 .get_absolute_url())
62 62
63 63 if not form:
64 64 form = PostForm(error_class=PlainErrorList)
65 65
66 66 thread_to_show = opening_post.get_thread()
67 67
68 68 params = dict()
69 69
70 70 params[CONTEXT_FORM] = form
71 71 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
72 72 params[CONTEXT_THREAD] = thread_to_show
73 73 params[CONTEXT_MODE] = self.get_mode()
74 74 params[CONTEXT_OP] = opening_post
75 75 params[CONTEXT_FAVORITE] = favorite
76 76 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
77 77
78 78 if settings.get_bool('External', 'WebsocketsEnabled'):
79 79 token_time = format(timezone.now(), u'U')
80 80
81 81 params[CONTEXT_WS_TIME] = token_time
82 82 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
83 83 timestamp=token_time)
84 84 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
85 85 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
86 86 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
87 87
88 88 params.update(self.get_data(thread_to_show))
89 89
90 90 return render(request, self.get_template(), params)
91 91
92 92 @method_decorator(csrf_protect)
93 93 def post(self, request, post_id):
94 94 opening_post = get_object_or_404(Post, id=post_id)
95 95
96 96 # If this is not OP, don't show it as it is
97 97 if not opening_post.is_opening():
98 98 raise Http404
99 99
100 100 if PARAMETER_METHOD in request.POST:
101 101 self.dispatch_method(request, opening_post)
102 102
103 103 return redirect('thread', post_id) # FIXME Different for different modes
104 104
105 105 if not opening_post.get_thread().is_archived():
106 106 form = PostForm(request.POST, request.FILES,
107 107 error_class=PlainErrorList)
108 108 form.session = request.session
109 109
110 110 if form.is_valid():
111 111 return self.new_post(request, form, opening_post)
112 112 if form.need_to_ban:
113 113 # Ban user because he is suspected to be a bot
114 114 self._ban_current_user(request)
115 115
116 116 return self.get(request, post_id, form)
117 117
118 118 def new_post(self, request, form: PostForm, opening_post: Post=None,
119 119 html_response=True):
120 120 """
121 121 Adds a new post (in thread or as a reply).
122 122 """
123 123
124 124 ip = utils.get_client_ip(request)
125 125
126 126 data = form.cleaned_data
127 127
128 128 title = form.get_title()
129 129 text = data[FORM_TEXT]
130 file = form.get_file()
131 file_url = form.get_file_url()
130 files = form.get_files()
131 file_urls = form.get_file_urls()
132 132 images = form.get_images()
133 133
134 134 text = self._remove_invalid_links(text)
135 135
136 136 post_thread = opening_post.get_thread()
137 137
138 post = Post.objects.create_post(title=title, text=text, file=file,
138 post = Post.objects.create_post(title=title, text=text, files=files,
139 139 thread=post_thread, ip=ip,
140 140 tripcode=form.get_tripcode(),
141 images=images, file_url=file_url)
141 images=images, file_urls=file_urls)
142 142 post.notify_clients()
143 143
144 144 if form.is_subscribe():
145 145 settings_manager = get_settings_manager(request)
146 146 settings_manager.add_or_read_fav_thread(
147 147 post_thread.get_opening_post())
148 148
149 149 if html_response:
150 150 if opening_post:
151 151 return redirect(post.get_absolute_url())
152 152 else:
153 153 return post
154 154
155 155 def get_data(self, thread) -> dict:
156 156 """
157 157 Returns context params for the view.
158 158 """
159 159
160 160 return dict()
161 161
162 162 def get_template(self) -> str:
163 163 """
164 164 Gets template to show the thread mode on.
165 165 """
166 166
167 167 pass
168 168
169 169 def get_mode(self) -> str:
170 170 pass
171 171
172 172 def subscribe(self, request, opening_post):
173 173 settings_manager = get_settings_manager(request)
174 174 settings_manager.add_or_read_fav_thread(opening_post)
175 175
176 176 def unsubscribe(self, request, opening_post):
177 177 settings_manager = get_settings_manager(request)
178 178 settings_manager.del_fav_thread(opening_post)
179 179
180 180 def get_rss_url(self, opening_id):
181 181 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
General Comments 0
You need to be logged in to leave comments. Login now