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