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