##// END OF EJS Templates
Added tripcodes
neko259 -
r1293:0b9a5210 default
parent child Browse files
Show More
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0023_auto_20150818_1026'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='post',
16 name='tripcode',
17 field=models.CharField(max_length=50, null=True),
18 ),
19 ]
@@ -1,377 +1,383 b''
1 import hashlib
1 import re
2 import re
2 import time
3 import time
3 import pytz
4 import pytz
4
5
5 from django import forms
6 from django import forms
6 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.exceptions import ObjectDoesNotExist
8 from django.forms.util import ErrorList
9 from django.forms.util import ErrorList
9 from django.utils.translation import ugettext_lazy as _
10 from django.utils.translation import ugettext_lazy as _
10 import requests
11 import requests
11
12
12 from boards.mdx_neboard import formatters
13 from boards.mdx_neboard import formatters
13 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models import Tag, Post
15 from boards.models import Tag, Post
15 from neboard import settings
16 from neboard import settings
16 import boards.settings as board_settings
17 import boards.settings as board_settings
17
18
18 HEADER_CONTENT_LENGTH = 'content-length'
19 HEADER_CONTENT_LENGTH = 'content-length'
19 HEADER_CONTENT_TYPE = 'content-type'
20 HEADER_CONTENT_TYPE = 'content-type'
20
21
21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
22 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
22
23
23 VETERAN_POSTING_DELAY = 5
24 VETERAN_POSTING_DELAY = 5
24
25
25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
26 ATTRIBUTE_PLACEHOLDER = 'placeholder'
26 ATTRIBUTE_ROWS = 'rows'
27 ATTRIBUTE_ROWS = 'rows'
27
28
28 LAST_POST_TIME = 'last_post_time'
29 LAST_POST_TIME = 'last_post_time'
29 LAST_LOGIN_TIME = 'last_login_time'
30 LAST_LOGIN_TIME = 'last_login_time'
30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
31 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
32 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
32
33
33 LABEL_TITLE = _('Title')
34 LABEL_TITLE = _('Title')
34 LABEL_TEXT = _('Text')
35 LABEL_TEXT = _('Text')
35 LABEL_TAG = _('Tag')
36 LABEL_TAG = _('Tag')
36 LABEL_SEARCH = _('Search')
37 LABEL_SEARCH = _('Search')
37
38
38 ERROR_SPEED = _('Please wait %s seconds before sending message')
39 ERROR_SPEED = _('Please wait %s seconds before sending message')
39
40
40 TAG_MAX_LENGTH = 20
41 TAG_MAX_LENGTH = 20
41
42
42 FILE_DOWNLOAD_CHUNK_BYTES = 100000
43 FILE_DOWNLOAD_CHUNK_BYTES = 100000
43
44
44 HTTP_RESULT_OK = 200
45 HTTP_RESULT_OK = 200
45
46
46 TEXTAREA_ROWS = 4
47 TEXTAREA_ROWS = 4
47
48
48
49
49 def get_timezones():
50 def get_timezones():
50 timezones = []
51 timezones = []
51 for tz in pytz.common_timezones:
52 for tz in pytz.common_timezones:
52 timezones.append((tz, tz),)
53 timezones.append((tz, tz),)
53 return timezones
54 return timezones
54
55
55
56
56 class FormatPanel(forms.Textarea):
57 class FormatPanel(forms.Textarea):
57 """
58 """
58 Panel for text formatting. Consists of buttons to add different tags to the
59 Panel for text formatting. Consists of buttons to add different tags to the
59 form text area.
60 form text area.
60 """
61 """
61
62
62 def render(self, name, value, attrs=None):
63 def render(self, name, value, attrs=None):
63 output = '<div id="mark-panel">'
64 output = '<div id="mark-panel">'
64 for formatter in formatters:
65 for formatter in formatters:
65 output += '<span class="mark_btn"' + \
66 output += '<span class="mark_btn"' + \
66 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
67 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
67 '\', \'' + formatter.format_right + '\')">' + \
68 '\', \'' + formatter.format_right + '\')">' + \
68 formatter.preview_left + formatter.name + \
69 formatter.preview_left + formatter.name + \
69 formatter.preview_right + '</span>'
70 formatter.preview_right + '</span>'
70
71
71 output += '</div>'
72 output += '</div>'
72 output += super(FormatPanel, self).render(name, value, attrs=None)
73 output += super(FormatPanel, self).render(name, value, attrs=None)
73
74
74 return output
75 return output
75
76
76
77
77 class PlainErrorList(ErrorList):
78 class PlainErrorList(ErrorList):
78 def __unicode__(self):
79 def __unicode__(self):
79 return self.as_text()
80 return self.as_text()
80
81
81 def as_text(self):
82 def as_text(self):
82 return ''.join(['(!) %s ' % e for e in self])
83 return ''.join(['(!) %s ' % e for e in self])
83
84
84
85
85 class NeboardForm(forms.Form):
86 class NeboardForm(forms.Form):
86 """
87 """
87 Form with neboard-specific formatting.
88 Form with neboard-specific formatting.
88 """
89 """
89
90
90 def as_div(self):
91 def as_div(self):
91 """
92 """
92 Returns this form rendered as HTML <as_div>s.
93 Returns this form rendered as HTML <as_div>s.
93 """
94 """
94
95
95 return self._html_output(
96 return self._html_output(
96 # TODO Do not show hidden rows in the list here
97 # TODO Do not show hidden rows in the list here
97 normal_row='<div class="form-row">'
98 normal_row='<div class="form-row">'
98 '<div class="form-label">'
99 '<div class="form-label">'
99 '%(label)s'
100 '%(label)s'
100 '</div>'
101 '</div>'
101 '<div class="form-input">'
102 '<div class="form-input">'
102 '%(field)s'
103 '%(field)s'
103 '</div>'
104 '</div>'
104 '</div>'
105 '</div>'
105 '<div class="form-row">'
106 '<div class="form-row">'
106 '%(help_text)s'
107 '%(help_text)s'
107 '</div>',
108 '</div>',
108 error_row='<div class="form-row">'
109 error_row='<div class="form-row">'
109 '<div class="form-label"></div>'
110 '<div class="form-label"></div>'
110 '<div class="form-errors">%s</div>'
111 '<div class="form-errors">%s</div>'
111 '</div>',
112 '</div>',
112 row_ender='</div>',
113 row_ender='</div>',
113 help_text_html='%s',
114 help_text_html='%s',
114 errors_on_separate_row=True)
115 errors_on_separate_row=True)
115
116
116 def as_json_errors(self):
117 def as_json_errors(self):
117 errors = []
118 errors = []
118
119
119 for name, field in list(self.fields.items()):
120 for name, field in list(self.fields.items()):
120 if self[name].errors:
121 if self[name].errors:
121 errors.append({
122 errors.append({
122 'field': name,
123 'field': name,
123 'errors': self[name].errors.as_text(),
124 'errors': self[name].errors.as_text(),
124 })
125 })
125
126
126 return errors
127 return errors
127
128
128
129
129 class PostForm(NeboardForm):
130 class PostForm(NeboardForm):
130
131
131 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
132 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
132 label=LABEL_TITLE)
133 label=LABEL_TITLE)
133 text = forms.CharField(
134 text = forms.CharField(
134 widget=FormatPanel(attrs={
135 widget=FormatPanel(attrs={
135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
136 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
137 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
137 }),
138 }),
138 required=False, label=LABEL_TEXT)
139 required=False, label=LABEL_TEXT)
139 file = forms.FileField(required=False, label=_('File'),
140 file = forms.FileField(required=False, label=_('File'),
140 widget=forms.ClearableFileInput(
141 widget=forms.ClearableFileInput(
141 attrs={'accept': 'file/*'}))
142 attrs={'accept': 'file/*'}))
142 file_url = forms.CharField(required=False, label=_('File URL'),
143 file_url = forms.CharField(required=False, label=_('File URL'),
143 widget=forms.TextInput(
144 widget=forms.TextInput(
144 attrs={ATTRIBUTE_PLACEHOLDER:
145 attrs={ATTRIBUTE_PLACEHOLDER:
145 'http://example.com/image.png'}))
146 'http://example.com/image.png'}))
146
147
147 # This field is for spam prevention only
148 # This field is for spam prevention only
148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
149 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
149 widget=forms.TextInput(attrs={
150 widget=forms.TextInput(attrs={
150 'class': 'form-email'}))
151 'class': 'form-email'}))
151 threads = forms.CharField(required=False, label=_('Additional threads'),
152 threads = forms.CharField(required=False, label=_('Additional threads'),
152 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
153 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
153 '123 456 789'}))
154 '123 456 789'}))
155 tripcode = forms.BooleanField(label=_('Tripcode'), required=False)
154
156
155 session = None
157 session = None
156 need_to_ban = False
158 need_to_ban = False
157
159
158 def clean_title(self):
160 def clean_title(self):
159 title = self.cleaned_data['title']
161 title = self.cleaned_data['title']
160 if title:
162 if title:
161 if len(title) > TITLE_MAX_LENGTH:
163 if len(title) > TITLE_MAX_LENGTH:
162 raise forms.ValidationError(_('Title must have less than %s '
164 raise forms.ValidationError(_('Title must have less than %s '
163 'characters') %
165 'characters') %
164 str(TITLE_MAX_LENGTH))
166 str(TITLE_MAX_LENGTH))
165 return title
167 return title
166
168
167 def clean_text(self):
169 def clean_text(self):
168 text = self.cleaned_data['text'].strip()
170 text = self.cleaned_data['text'].strip()
169 if text:
171 if text:
170 max_length = board_settings.get_int('Forms', 'MaxTextLength')
172 max_length = board_settings.get_int('Forms', 'MaxTextLength')
171 if len(text) > max_length:
173 if len(text) > max_length:
172 raise forms.ValidationError(_('Text must have less than %s '
174 raise forms.ValidationError(_('Text must have less than %s '
173 'characters') % str(max_length))
175 'characters') % str(max_length))
174 return text
176 return text
175
177
176 def clean_file(self):
178 def clean_file(self):
177 file = self.cleaned_data['file']
179 file = self.cleaned_data['file']
178
180
179 if file:
181 if file:
180 self.validate_file_size(file.size)
182 self.validate_file_size(file.size)
181
183
182 return file
184 return file
183
185
184 def clean_file_url(self):
186 def clean_file_url(self):
185 url = self.cleaned_data['file_url']
187 url = self.cleaned_data['file_url']
186
188
187 file = None
189 file = None
188 if url:
190 if url:
189 file = self._get_file_from_url(url)
191 file = self._get_file_from_url(url)
190
192
191 if not file:
193 if not file:
192 raise forms.ValidationError(_('Invalid URL'))
194 raise forms.ValidationError(_('Invalid URL'))
193 else:
195 else:
194 self.validate_file_size(file.size)
196 self.validate_file_size(file.size)
195
197
196 return file
198 return file
197
199
198 def clean_threads(self):
200 def clean_threads(self):
199 threads_str = self.cleaned_data['threads']
201 threads_str = self.cleaned_data['threads']
200
202
201 if len(threads_str) > 0:
203 if len(threads_str) > 0:
202 threads_id_list = threads_str.split(' ')
204 threads_id_list = threads_str.split(' ')
203
205
204 threads = list()
206 threads = list()
205
207
206 for thread_id in threads_id_list:
208 for thread_id in threads_id_list:
207 try:
209 try:
208 thread = Post.objects.get(id=int(thread_id))
210 thread = Post.objects.get(id=int(thread_id))
209 if not thread.is_opening() or thread.get_thread().archived:
211 if not thread.is_opening() or thread.get_thread().archived:
210 raise ObjectDoesNotExist()
212 raise ObjectDoesNotExist()
211 threads.append(thread)
213 threads.append(thread)
212 except (ObjectDoesNotExist, ValueError):
214 except (ObjectDoesNotExist, ValueError):
213 raise forms.ValidationError(_('Invalid additional thread list'))
215 raise forms.ValidationError(_('Invalid additional thread list'))
214
216
215 return threads
217 return threads
216
218
217 def clean(self):
219 def clean(self):
218 cleaned_data = super(PostForm, self).clean()
220 cleaned_data = super(PostForm, self).clean()
219
221
220 if cleaned_data['email']:
222 if cleaned_data['email']:
221 self.need_to_ban = True
223 self.need_to_ban = True
222 raise forms.ValidationError('A human cannot enter a hidden field')
224 raise forms.ValidationError('A human cannot enter a hidden field')
223
225
224 if not self.errors:
226 if not self.errors:
225 self._clean_text_file()
227 self._clean_text_file()
226
228
227 if not self.errors and self.session:
229 if not self.errors and self.session:
228 self._validate_posting_speed()
230 self._validate_posting_speed()
229
231
230 return cleaned_data
232 return cleaned_data
231
233
232 def get_file(self):
234 def get_file(self):
233 """
235 """
234 Gets file from form or URL.
236 Gets file from form or URL.
235 """
237 """
236
238
237 file = self.cleaned_data['file']
239 file = self.cleaned_data['file']
238 return file or self.cleaned_data['file_url']
240 return file or self.cleaned_data['file_url']
239
241
242 def get_tripcode(self):
243 if self.cleaned_data['tripcode']:
244 return hashlib.sha1(self.session.session_key.encode()).hexdigest()
245
240 def _clean_text_file(self):
246 def _clean_text_file(self):
241 text = self.cleaned_data.get('text')
247 text = self.cleaned_data.get('text')
242 file = self.get_file()
248 file = self.get_file()
243
249
244 if (not text) and (not file):
250 if (not text) and (not file):
245 error_message = _('Either text or file must be entered.')
251 error_message = _('Either text or file must be entered.')
246 self._errors['text'] = self.error_class([error_message])
252 self._errors['text'] = self.error_class([error_message])
247
253
248 def _validate_posting_speed(self):
254 def _validate_posting_speed(self):
249 can_post = True
255 can_post = True
250
256
251 posting_delay = settings.POSTING_DELAY
257 posting_delay = settings.POSTING_DELAY
252
258
253 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
259 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
254 now = time.time()
260 now = time.time()
255
261
256 current_delay = 0
262 current_delay = 0
257 need_delay = False
263 need_delay = False
258
264
259 if not LAST_POST_TIME in self.session:
265 if not LAST_POST_TIME in self.session:
260 self.session[LAST_POST_TIME] = now
266 self.session[LAST_POST_TIME] = now
261
267
262 need_delay = True
268 need_delay = True
263 else:
269 else:
264 last_post_time = self.session.get(LAST_POST_TIME)
270 last_post_time = self.session.get(LAST_POST_TIME)
265 current_delay = int(now - last_post_time)
271 current_delay = int(now - last_post_time)
266
272
267 need_delay = current_delay < posting_delay
273 need_delay = current_delay < posting_delay
268
274
269 if need_delay:
275 if need_delay:
270 error_message = ERROR_SPEED % str(posting_delay
276 error_message = ERROR_SPEED % str(posting_delay
271 - current_delay)
277 - current_delay)
272 self._errors['text'] = self.error_class([error_message])
278 self._errors['text'] = self.error_class([error_message])
273
279
274 can_post = False
280 can_post = False
275
281
276 if can_post:
282 if can_post:
277 self.session[LAST_POST_TIME] = now
283 self.session[LAST_POST_TIME] = now
278
284
279 def validate_file_size(self, size: int):
285 def validate_file_size(self, size: int):
280 max_size = board_settings.get_int('Forms', 'MaxFileSize')
286 max_size = board_settings.get_int('Forms', 'MaxFileSize')
281 if size > max_size:
287 if size > max_size:
282 raise forms.ValidationError(
288 raise forms.ValidationError(
283 _('File must be less than %s bytes')
289 _('File must be less than %s bytes')
284 % str(max_size))
290 % str(max_size))
285
291
286 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
292 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
287 """
293 """
288 Gets an file file from URL.
294 Gets an file file from URL.
289 """
295 """
290
296
291 img_temp = None
297 img_temp = None
292
298
293 try:
299 try:
294 # Verify content headers
300 # Verify content headers
295 response_head = requests.head(url, verify=False)
301 response_head = requests.head(url, verify=False)
296 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
302 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
297 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
303 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
298 if length_header:
304 if length_header:
299 length = int(length_header)
305 length = int(length_header)
300 self.validate_file_size(length)
306 self.validate_file_size(length)
301 # Get the actual content into memory
307 # Get the actual content into memory
302 response = requests.get(url, verify=False, stream=True)
308 response = requests.get(url, verify=False, stream=True)
303
309
304 # Download file, stop if the size exceeds limit
310 # Download file, stop if the size exceeds limit
305 size = 0
311 size = 0
306 content = b''
312 content = b''
307 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
313 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
308 size += len(chunk)
314 size += len(chunk)
309 self.validate_file_size(size)
315 self.validate_file_size(size)
310 content += chunk
316 content += chunk
311
317
312 if response.status_code == HTTP_RESULT_OK and content:
318 if response.status_code == HTTP_RESULT_OK and content:
313 # Set a dummy file name that will be replaced
319 # Set a dummy file name that will be replaced
314 # anyway, just keep the valid extension
320 # anyway, just keep the valid extension
315 filename = 'file.' + content_type.split('/')[1]
321 filename = 'file.' + content_type.split('/')[1]
316 img_temp = SimpleUploadedFile(filename, content,
322 img_temp = SimpleUploadedFile(filename, content,
317 content_type)
323 content_type)
318 except Exception as e:
324 except Exception as e:
319 # Just return no file
325 # Just return no file
320 pass
326 pass
321
327
322 return img_temp
328 return img_temp
323
329
324
330
325 class ThreadForm(PostForm):
331 class ThreadForm(PostForm):
326
332
327 tags = forms.CharField(
333 tags = forms.CharField(
328 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
334 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
329 max_length=100, label=_('Tags'), required=True)
335 max_length=100, label=_('Tags'), required=True)
330
336
331 def clean_tags(self):
337 def clean_tags(self):
332 tags = self.cleaned_data['tags'].strip()
338 tags = self.cleaned_data['tags'].strip()
333
339
334 if not tags or not REGEX_TAGS.match(tags):
340 if not tags or not REGEX_TAGS.match(tags):
335 raise forms.ValidationError(
341 raise forms.ValidationError(
336 _('Inappropriate characters in tags.'))
342 _('Inappropriate characters in tags.'))
337
343
338 required_tag_exists = False
344 required_tag_exists = False
339 for tag in tags.split():
345 for tag in tags.split():
340 try:
346 try:
341 Tag.objects.get(name=tag.strip().lower(), required=True)
347 Tag.objects.get(name=tag.strip().lower(), required=True)
342 required_tag_exists = True
348 required_tag_exists = True
343 break
349 break
344 except ObjectDoesNotExist:
350 except ObjectDoesNotExist:
345 pass
351 pass
346
352
347 if not required_tag_exists:
353 if not required_tag_exists:
348 all_tags = Tag.objects.filter(required=True)
354 all_tags = Tag.objects.filter(required=True)
349 raise forms.ValidationError(
355 raise forms.ValidationError(
350 _('Need at least one section.'))
356 _('Need at least one section.'))
351
357
352 return tags
358 return tags
353
359
354 def clean(self):
360 def clean(self):
355 cleaned_data = super(ThreadForm, self).clean()
361 cleaned_data = super(ThreadForm, self).clean()
356
362
357 return cleaned_data
363 return cleaned_data
358
364
359
365
360 class SettingsForm(NeboardForm):
366 class SettingsForm(NeboardForm):
361
367
362 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
368 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
363 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('image view mode'))
369 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('image view mode'))
364 username = forms.CharField(label=_('User name'), required=False)
370 username = forms.CharField(label=_('User name'), required=False)
365 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
371 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
366
372
367 def clean_username(self):
373 def clean_username(self):
368 username = self.cleaned_data['username']
374 username = self.cleaned_data['username']
369
375
370 if username and not REGEX_TAGS.match(username):
376 if username and not REGEX_TAGS.match(username):
371 raise forms.ValidationError(_('Inappropriate characters.'))
377 raise forms.ValidationError(_('Inappropriate characters.'))
372
378
373 return username
379 return username
374
380
375
381
376 class SearchForm(NeboardForm):
382 class SearchForm(NeboardForm):
377 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
383 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,438 +1,447 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import logging
3 import logging
4 import re
4 import re
5 import uuid
5 import uuid
6
6
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.db.models import TextField, QuerySet
10 from django.db.models import TextField, QuerySet
11 from django.template.loader import render_to_string
11 from django.template.loader import render_to_string
12 from django.utils import timezone
12 from django.utils import timezone
13
13
14 from boards import settings
14 from boards import settings
15 from boards.mdx_neboard import Parser
15 from boards.mdx_neboard import Parser
16 from boards.models import PostImage, Attachment
16 from boards.models import PostImage, Attachment
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18 from boards import utils
18 from boards import utils
19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
19 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
20 from boards.models.user import Notification, Ban
20 from boards.models.user import Notification, Ban
21 import boards.models.thread
21 import boards.models.thread
22
22
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 POSTS_PER_DAY_RANGE = 7
26 POSTS_PER_DAY_RANGE = 7
27
27
28 BAN_REASON_AUTO = 'Auto'
28 BAN_REASON_AUTO = 'Auto'
29
29
30 IMAGE_THUMB_SIZE = (200, 150)
30 IMAGE_THUMB_SIZE = (200, 150)
31
31
32 TITLE_MAX_LENGTH = 200
32 TITLE_MAX_LENGTH = 200
33
33
34 # TODO This should be removed
34 # TODO This should be removed
35 NO_IP = '0.0.0.0'
35 NO_IP = '0.0.0.0'
36
36
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
37 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
39
39
40 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TRUNCATED = 'truncated'
41 PARAMETER_TAG = 'tag'
41 PARAMETER_TAG = 'tag'
42 PARAMETER_OFFSET = 'offset'
42 PARAMETER_OFFSET = 'offset'
43 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_DIFF_TYPE = 'type'
44 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_CSS_CLASS = 'css_class'
45 PARAMETER_THREAD = 'thread'
45 PARAMETER_THREAD = 'thread'
46 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_IS_OPENING = 'is_opening'
47 PARAMETER_MODERATOR = 'moderator'
47 PARAMETER_MODERATOR = 'moderator'
48 PARAMETER_POST = 'post'
48 PARAMETER_POST = 'post'
49 PARAMETER_OP_ID = 'opening_post_id'
49 PARAMETER_OP_ID = 'opening_post_id'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
50 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
51 PARAMETER_REPLY_LINK = 'reply_link'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
52 PARAMETER_NEED_OP_DATA = 'need_op_data'
53
53
54 POST_VIEW_PARAMS = (
54 POST_VIEW_PARAMS = (
55 'need_op_data',
55 'need_op_data',
56 'reply_link',
56 'reply_link',
57 'moderator',
57 'moderator',
58 'need_open_link',
58 'need_open_link',
59 'truncated',
59 'truncated',
60 'mode_tree',
60 'mode_tree',
61 )
61 )
62
62
63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
63 REFMAP_STR = '<a href="{}">&gt;&gt;{}</a>'
64
64
65 IMAGE_TYPES = (
65 IMAGE_TYPES = (
66 'jpeg',
66 'jpeg',
67 'jpg',
67 'jpg',
68 'png',
68 'png',
69 'bmp',
69 'bmp',
70 'gif',
70 'gif',
71 )
71 )
72
72
73
73
74 class PostManager(models.Manager):
74 class PostManager(models.Manager):
75 @transaction.atomic
75 @transaction.atomic
76 def create_post(self, title: str, text: str, file=None, thread=None,
76 def create_post(self, title: str, text: str, file=None, thread=None,
77 ip=NO_IP, tags: list=None, opening_posts: list=None):
77 ip=NO_IP, tags: list=None, opening_posts: list=None, tripcode=None):
78 """
78 """
79 Creates new post
79 Creates new post
80 """
80 """
81
81
82 is_banned = Ban.objects.filter(ip=ip).exists()
82 is_banned = Ban.objects.filter(ip=ip).exists()
83
83
84 # TODO Raise specific exception and catch it in the views
84 # TODO Raise specific exception and catch it in the views
85 if is_banned:
85 if is_banned:
86 raise Exception("This user is banned")
86 raise Exception("This user is banned")
87
87
88 if not tags:
88 if not tags:
89 tags = []
89 tags = []
90 if not opening_posts:
90 if not opening_posts:
91 opening_posts = []
91 opening_posts = []
92
92
93 posting_time = timezone.now()
93 posting_time = timezone.now()
94 new_thread = False
94 new_thread = False
95 if not thread:
95 if not thread:
96 thread = boards.models.thread.Thread.objects.create(
96 thread = boards.models.thread.Thread.objects.create(
97 bump_time=posting_time, last_edit_time=posting_time)
97 bump_time=posting_time, last_edit_time=posting_time)
98 list(map(thread.tags.add, tags))
98 list(map(thread.tags.add, tags))
99 boards.models.thread.Thread.objects.process_oldest_threads()
99 boards.models.thread.Thread.objects.process_oldest_threads()
100 new_thread = True
100 new_thread = True
101
101
102 pre_text = Parser().preparse(text)
102 pre_text = Parser().preparse(text)
103
103
104 post = self.create(title=title,
104 post = self.create(title=title,
105 text=pre_text,
105 text=pre_text,
106 pub_time=posting_time,
106 pub_time=posting_time,
107 poster_ip=ip,
107 poster_ip=ip,
108 thread=thread,
108 thread=thread,
109 last_edit_time=posting_time)
109 last_edit_time=posting_time,
110 tripcode=tripcode)
110 post.threads.add(thread)
111 post.threads.add(thread)
111
112
112 logger = logging.getLogger('boards.post.create')
113 logger = logging.getLogger('boards.post.create')
113
114
114 logger.info('Created post {} by {}'.format(post, post.poster_ip))
115 logger.info('Created post {} by {}'.format(post, post.poster_ip))
115
116
116 # TODO Move this to other place
117 # TODO Move this to other place
117 if file:
118 if file:
118 file_type = file.name.split('.')[-1].lower()
119 file_type = file.name.split('.')[-1].lower()
119 if file_type in IMAGE_TYPES:
120 if file_type in IMAGE_TYPES:
120 post.images.add(PostImage.objects.create_with_hash(file))
121 post.images.add(PostImage.objects.create_with_hash(file))
121 else:
122 else:
122 post.attachments.add(Attachment.objects.create_with_hash(file))
123 post.attachments.add(Attachment.objects.create_with_hash(file))
123
124
124 post.build_url()
125 post.build_url()
125 post.connect_replies()
126 post.connect_replies()
126 post.connect_threads(opening_posts)
127 post.connect_threads(opening_posts)
127 post.connect_notifications()
128 post.connect_notifications()
128
129
129 # Thread needs to be bumped only when the post is already created
130 # Thread needs to be bumped only when the post is already created
130 if not new_thread:
131 if not new_thread:
131 thread.last_edit_time = posting_time
132 thread.last_edit_time = posting_time
132 thread.bump()
133 thread.bump()
133 thread.save()
134 thread.save()
134
135
135 return post
136 return post
136
137
137 def delete_posts_by_ip(self, ip):
138 def delete_posts_by_ip(self, ip):
138 """
139 """
139 Deletes all posts of the author with same IP
140 Deletes all posts of the author with same IP
140 """
141 """
141
142
142 posts = self.filter(poster_ip=ip)
143 posts = self.filter(poster_ip=ip)
143 for post in posts:
144 for post in posts:
144 post.delete()
145 post.delete()
145
146
146 @utils.cached_result()
147 @utils.cached_result()
147 def get_posts_per_day(self) -> float:
148 def get_posts_per_day(self) -> float:
148 """
149 """
149 Gets average count of posts per day for the last 7 days
150 Gets average count of posts per day for the last 7 days
150 """
151 """
151
152
152 day_end = date.today()
153 day_end = date.today()
153 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
154 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
154
155
155 day_time_start = timezone.make_aware(datetime.combine(
156 day_time_start = timezone.make_aware(datetime.combine(
156 day_start, dtime()), timezone.get_current_timezone())
157 day_start, dtime()), timezone.get_current_timezone())
157 day_time_end = timezone.make_aware(datetime.combine(
158 day_time_end = timezone.make_aware(datetime.combine(
158 day_end, dtime()), timezone.get_current_timezone())
159 day_end, dtime()), timezone.get_current_timezone())
159
160
160 posts_per_period = float(self.filter(
161 posts_per_period = float(self.filter(
161 pub_time__lte=day_time_end,
162 pub_time__lte=day_time_end,
162 pub_time__gte=day_time_start).count())
163 pub_time__gte=day_time_start).count())
163
164
164 ppd = posts_per_period / POSTS_PER_DAY_RANGE
165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
165
166
166 return ppd
167 return ppd
167
168
168
169
169 class Post(models.Model, Viewable):
170 class Post(models.Model, Viewable):
170 """A post is a message."""
171 """A post is a message."""
171
172
172 objects = PostManager()
173 objects = PostManager()
173
174
174 class Meta:
175 class Meta:
175 app_label = APP_LABEL_BOARDS
176 app_label = APP_LABEL_BOARDS
176 ordering = ('id',)
177 ordering = ('id',)
177
178
178 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
179 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
179 pub_time = models.DateTimeField()
180 pub_time = models.DateTimeField()
180 text = TextField(blank=True, null=True)
181 text = TextField(blank=True, null=True)
181 _text_rendered = TextField(blank=True, null=True, editable=False)
182 _text_rendered = TextField(blank=True, null=True, editable=False)
182
183
183 images = models.ManyToManyField(PostImage, null=True, blank=True,
184 images = models.ManyToManyField(PostImage, null=True, blank=True,
184 related_name='post_images', db_index=True)
185 related_name='post_images', db_index=True)
185 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
186 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
186 related_name='attachment_posts')
187 related_name='attachment_posts')
187
188
188 poster_ip = models.GenericIPAddressField()
189 poster_ip = models.GenericIPAddressField()
189
190
190 # TODO This field can be removed cause UID is used for update now
191 # TODO This field can be removed cause UID is used for update now
191 last_edit_time = models.DateTimeField()
192 last_edit_time = models.DateTimeField()
192
193
193 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
194 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
194 null=True,
195 null=True,
195 blank=True, related_name='refposts',
196 blank=True, related_name='refposts',
196 db_index=True)
197 db_index=True)
197 refmap = models.TextField(null=True, blank=True)
198 refmap = models.TextField(null=True, blank=True)
198 threads = models.ManyToManyField('Thread', db_index=True)
199 threads = models.ManyToManyField('Thread', db_index=True)
199 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
200 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
200
201
201 url = models.TextField()
202 url = models.TextField()
202 uid = models.TextField(db_index=True)
203 uid = models.TextField(db_index=True)
203
204
205 tripcode = models.CharField(max_length=50, null=True)
206
204 def __str__(self):
207 def __str__(self):
205 return 'P#{}/{}'.format(self.id, self.title)
208 return 'P#{}/{}'.format(self.id, self.title)
206
209
207 def get_referenced_posts(self):
210 def get_referenced_posts(self):
208 threads = self.get_threads().all()
211 threads = self.get_threads().all()
209 return self.referenced_posts.filter(threads__in=threads)\
212 return self.referenced_posts.filter(threads__in=threads)\
210 .order_by('pub_time').distinct().all()
213 .order_by('pub_time').distinct().all()
211
214
212 def get_title(self) -> str:
215 def get_title(self) -> str:
213 """
216 """
214 Gets original post title or part of its text.
217 Gets original post title or part of its text.
215 """
218 """
216
219
217 title = self.title
220 title = self.title
218 if not title:
221 if not title:
219 title = self.get_text()
222 title = self.get_text()
220
223
221 return title
224 return title
222
225
223 def build_refmap(self) -> None:
226 def build_refmap(self) -> None:
224 """
227 """
225 Builds a replies map string from replies list. This is a cache to stop
228 Builds a replies map string from replies list. This is a cache to stop
226 the server from recalculating the map on every post show.
229 the server from recalculating the map on every post show.
227 """
230 """
228
231
229 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
232 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
230 for refpost in self.referenced_posts.all()]
233 for refpost in self.referenced_posts.all()]
231
234
232 self.refmap = ', '.join(post_urls)
235 self.refmap = ', '.join(post_urls)
233
236
234 def is_referenced(self) -> bool:
237 def is_referenced(self) -> bool:
235 return self.refmap and len(self.refmap) > 0
238 return self.refmap and len(self.refmap) > 0
236
239
237 def is_opening(self) -> bool:
240 def is_opening(self) -> bool:
238 """
241 """
239 Checks if this is an opening post or just a reply.
242 Checks if this is an opening post or just a reply.
240 """
243 """
241
244
242 return self.get_thread().get_opening_post_id() == self.id
245 return self.get_thread().get_opening_post_id() == self.id
243
246
244 def get_absolute_url(self):
247 def get_absolute_url(self):
245 if self.url:
248 if self.url:
246 return self.url
249 return self.url
247 else:
250 else:
248 opening_id = self.get_thread().get_opening_post_id()
251 opening_id = self.get_thread().get_opening_post_id()
249 post_url = reverse('thread', kwargs={'post_id': opening_id})
252 post_url = reverse('thread', kwargs={'post_id': opening_id})
250 if self.id != opening_id:
253 if self.id != opening_id:
251 post_url += '#' + str(self.id)
254 post_url += '#' + str(self.id)
252 return post_url
255 return post_url
253
256
254
257
255 def get_thread(self):
258 def get_thread(self):
256 return self.thread
259 return self.thread
257
260
258 def get_threads(self) -> QuerySet:
261 def get_threads(self) -> QuerySet:
259 """
262 """
260 Gets post's thread.
263 Gets post's thread.
261 """
264 """
262
265
263 return self.threads
266 return self.threads
264
267
265 def get_view(self, *args, **kwargs) -> str:
268 def get_view(self, *args, **kwargs) -> str:
266 """
269 """
267 Renders post's HTML view. Some of the post params can be passed over
270 Renders post's HTML view. Some of the post params can be passed over
268 kwargs for the means of caching (if we view the thread, some params
271 kwargs for the means of caching (if we view the thread, some params
269 are same for every post and don't need to be computed over and over.
272 are same for every post and don't need to be computed over and over.
270 """
273 """
271
274
272 thread = self.get_thread()
275 thread = self.get_thread()
273 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
276 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
274
277
275 if is_opening:
278 if is_opening:
276 opening_post_id = self.id
279 opening_post_id = self.id
277 else:
280 else:
278 opening_post_id = thread.get_opening_post_id()
281 opening_post_id = thread.get_opening_post_id()
279
282
280 css_class = 'post'
283 css_class = 'post'
281 if thread.archived:
284 if thread.archived:
282 css_class += ' archive_post'
285 css_class += ' archive_post'
283 elif not thread.can_bump():
286 elif not thread.can_bump():
284 css_class += ' dead_post'
287 css_class += ' dead_post'
285
288
286 params = dict()
289 params = dict()
287 for param in POST_VIEW_PARAMS:
290 for param in POST_VIEW_PARAMS:
288 if param in kwargs:
291 if param in kwargs:
289 params[param] = kwargs[param]
292 params[param] = kwargs[param]
290
293
291 params.update({
294 params.update({
292 PARAMETER_POST: self,
295 PARAMETER_POST: self,
293 PARAMETER_IS_OPENING: is_opening,
296 PARAMETER_IS_OPENING: is_opening,
294 PARAMETER_THREAD: thread,
297 PARAMETER_THREAD: thread,
295 PARAMETER_CSS_CLASS: css_class,
298 PARAMETER_CSS_CLASS: css_class,
296 PARAMETER_OP_ID: opening_post_id,
299 PARAMETER_OP_ID: opening_post_id,
297 })
300 })
298
301
299 return render_to_string('boards/post.html', params)
302 return render_to_string('boards/post.html', params)
300
303
301 def get_search_view(self, *args, **kwargs):
304 def get_search_view(self, *args, **kwargs):
302 return self.get_view(need_op_data=True, *args, **kwargs)
305 return self.get_view(need_op_data=True, *args, **kwargs)
303
306
304 def get_first_image(self) -> PostImage:
307 def get_first_image(self) -> PostImage:
305 return self.images.earliest('id')
308 return self.images.earliest('id')
306
309
307 def delete(self, using=None):
310 def delete(self, using=None):
308 """
311 """
309 Deletes all post images and the post itself.
312 Deletes all post images and the post itself.
310 """
313 """
311
314
312 for image in self.images.all():
315 for image in self.images.all():
313 image_refs_count = image.post_images.count()
316 image_refs_count = image.post_images.count()
314 if image_refs_count == 1:
317 if image_refs_count == 1:
315 image.delete()
318 image.delete()
316
319
317 for attachment in self.attachments.all():
320 for attachment in self.attachments.all():
318 attachment_refs_count = attachment.attachment_posts.count()
321 attachment_refs_count = attachment.attachment_posts.count()
319 if attachment_refs_count == 1:
322 if attachment_refs_count == 1:
320 attachment.delete()
323 attachment.delete()
321
324
322 thread = self.get_thread()
325 thread = self.get_thread()
323 thread.last_edit_time = timezone.now()
326 thread.last_edit_time = timezone.now()
324 thread.save()
327 thread.save()
325
328
326 super(Post, self).delete(using)
329 super(Post, self).delete(using)
327
330
328 logging.getLogger('boards.post.delete').info(
331 logging.getLogger('boards.post.delete').info(
329 'Deleted post {}'.format(self))
332 'Deleted post {}'.format(self))
330
333
331 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
334 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
332 include_last_update=False) -> str:
335 include_last_update=False) -> str:
333 """
336 """
334 Gets post HTML or JSON data that can be rendered on a page or used by
337 Gets post HTML or JSON data that can be rendered on a page or used by
335 API.
338 API.
336 """
339 """
337
340
338 return get_exporter(format_type).export(self, request,
341 return get_exporter(format_type).export(self, request,
339 include_last_update)
342 include_last_update)
340
343
341 def notify_clients(self, recursive=True):
344 def notify_clients(self, recursive=True):
342 """
345 """
343 Sends post HTML data to the thread web socket.
346 Sends post HTML data to the thread web socket.
344 """
347 """
345
348
346 if not settings.get_bool('External', 'WebsocketsEnabled'):
349 if not settings.get_bool('External', 'WebsocketsEnabled'):
347 return
350 return
348
351
349 thread_ids = list()
352 thread_ids = list()
350 for thread in self.get_threads().all():
353 for thread in self.get_threads().all():
351 thread_ids.append(thread.id)
354 thread_ids.append(thread.id)
352
355
353 thread.notify_clients()
356 thread.notify_clients()
354
357
355 if recursive:
358 if recursive:
356 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
359 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
357 post_id = reply_number.group(1)
360 post_id = reply_number.group(1)
358
361
359 try:
362 try:
360 ref_post = Post.objects.get(id=post_id)
363 ref_post = Post.objects.get(id=post_id)
361
364
362 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
365 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
363 # If post is in this thread, its thread was already notified.
366 # If post is in this thread, its thread was already notified.
364 # Otherwise, notify its thread separately.
367 # Otherwise, notify its thread separately.
365 ref_post.notify_clients(recursive=False)
368 ref_post.notify_clients(recursive=False)
366 except ObjectDoesNotExist:
369 except ObjectDoesNotExist:
367 pass
370 pass
368
371
369 def build_url(self):
372 def build_url(self):
370 self.url = self.get_absolute_url()
373 self.url = self.get_absolute_url()
371 self.save(update_fields=['url'])
374 self.save(update_fields=['url'])
372
375
373 def save(self, force_insert=False, force_update=False, using=None,
376 def save(self, force_insert=False, force_update=False, using=None,
374 update_fields=None):
377 update_fields=None):
375 self._text_rendered = Parser().parse(self.get_raw_text())
378 self._text_rendered = Parser().parse(self.get_raw_text())
376
379
377 self.uid = str(uuid.uuid4())
380 self.uid = str(uuid.uuid4())
378 if update_fields is not None and 'uid' not in update_fields:
381 if update_fields is not None and 'uid' not in update_fields:
379 update_fields += ['uid']
382 update_fields += ['uid']
380
383
381 if self.id:
384 if self.id:
382 for thread in self.get_threads().all():
385 for thread in self.get_threads().all():
383 thread.last_edit_time = self.last_edit_time
386 thread.last_edit_time = self.last_edit_time
384
387
385 thread.save(update_fields=['last_edit_time', 'bumpable'])
388 thread.save(update_fields=['last_edit_time', 'bumpable'])
386
389
387 super().save(force_insert, force_update, using, update_fields)
390 super().save(force_insert, force_update, using, update_fields)
388
391
389 def get_text(self) -> str:
392 def get_text(self) -> str:
390 return self._text_rendered
393 return self._text_rendered
391
394
392 def get_raw_text(self) -> str:
395 def get_raw_text(self) -> str:
393 return self.text
396 return self.text
394
397
395 def get_absolute_id(self) -> str:
398 def get_absolute_id(self) -> str:
396 """
399 """
397 If the post has many threads, shows its main thread OP id in the post
400 If the post has many threads, shows its main thread OP id in the post
398 ID.
401 ID.
399 """
402 """
400
403
401 if self.get_threads().count() > 1:
404 if self.get_threads().count() > 1:
402 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
405 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
403 else:
406 else:
404 return str(self.id)
407 return str(self.id)
405
408
406 def connect_notifications(self):
409 def connect_notifications(self):
407 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
410 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
408 user_name = reply_number.group(1).lower()
411 user_name = reply_number.group(1).lower()
409 Notification.objects.get_or_create(name=user_name, post=self)
412 Notification.objects.get_or_create(name=user_name, post=self)
410
413
411 def connect_replies(self):
414 def connect_replies(self):
412 """
415 """
413 Connects replies to a post to show them as a reflink map
416 Connects replies to a post to show them as a reflink map
414 """
417 """
415
418
416 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
419 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
417 post_id = reply_number.group(1)
420 post_id = reply_number.group(1)
418
421
419 try:
422 try:
420 referenced_post = Post.objects.get(id=post_id)
423 referenced_post = Post.objects.get(id=post_id)
421
424
422 referenced_post.referenced_posts.add(self)
425 referenced_post.referenced_posts.add(self)
423 referenced_post.last_edit_time = self.pub_time
426 referenced_post.last_edit_time = self.pub_time
424 referenced_post.build_refmap()
427 referenced_post.build_refmap()
425 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
428 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
426 except ObjectDoesNotExist:
429 except ObjectDoesNotExist:
427 pass
430 pass
428
431
429 def connect_threads(self, opening_posts):
432 def connect_threads(self, opening_posts):
430 for opening_post in opening_posts:
433 for opening_post in opening_posts:
431 threads = opening_post.get_threads().all()
434 threads = opening_post.get_threads().all()
432 for thread in threads:
435 for thread in threads:
433 if thread.can_bump():
436 if thread.can_bump():
434 thread.update_bump_status()
437 thread.update_bump_status()
435
438
436 thread.last_edit_time = self.last_edit_time
439 thread.last_edit_time = self.last_edit_time
437 thread.save(update_fields=['last_edit_time', 'bumpable'])
440 thread.save(update_fields=['last_edit_time', 'bumpable'])
438 self.threads.add(opening_post.get_thread())
441 self.threads.add(opening_post.get_thread())
442
443 def get_tripcode_color(self):
444 return self.tripcode[:6]
445
446 def get_short_tripcode(self):
447 return self.tripcode[:10] No newline at end of file
@@ -1,547 +1,547 b''
1 * {
1 * {
2 text-decoration: none;
2 text-decoration: none;
3 font-weight: inherit;
3 font-weight: inherit;
4 }
4 }
5
5
6 b, strong {
6 b, strong {
7 font-weight: bold;
7 font-weight: bold;
8 }
8 }
9
9
10 html {
10 html {
11 background: #555;
11 background: #555;
12 color: #ffffff;
12 color: #ffffff;
13 }
13 }
14
14
15 body {
15 body {
16 margin: 0;
16 margin: 0;
17 }
17 }
18
18
19 #admin_panel {
19 #admin_panel {
20 background: #FF0000;
20 background: #FF0000;
21 color: #00FF00
21 color: #00FF00
22 }
22 }
23
23
24 .input_field_error {
24 .input_field_error {
25 color: #FF0000;
25 color: #FF0000;
26 }
26 }
27
27
28 .title {
28 .title {
29 font-weight: bold;
29 font-weight: bold;
30 color: #ffcc00;
30 color: #ffcc00;
31 }
31 }
32
32
33 .link, a {
33 .link, a {
34 color: #afdcec;
34 color: #afdcec;
35 }
35 }
36
36
37 .block {
37 .block {
38 display: inline-block;
38 display: inline-block;
39 vertical-align: top;
39 vertical-align: top;
40 }
40 }
41
41
42 .tag {
42 .tag {
43 color: #FFD37D;
43 color: #FFD37D;
44 }
44 }
45
45
46 .post_id {
46 .post_id {
47 color: #fff380;
47 color: #fff380;
48 }
48 }
49
49
50 .post, .dead_post, .archive_post, #posts-table {
50 .post, .dead_post, .archive_post, #posts-table {
51 background: #333;
51 background: #333;
52 padding: 10px;
52 padding: 10px;
53 clear: left;
53 clear: left;
54 word-wrap: break-word;
54 word-wrap: break-word;
55 border-top: 1px solid #777;
55 border-top: 1px solid #777;
56 border-bottom: 1px solid #777;
56 border-bottom: 1px solid #777;
57 }
57 }
58
58
59 .post + .post {
59 .post + .post {
60 border-top: none;
60 border-top: none;
61 }
61 }
62
62
63 .dead_post + .dead_post {
63 .dead_post + .dead_post {
64 border-top: none;
64 border-top: none;
65 }
65 }
66
66
67 .archive_post + .archive_post {
67 .archive_post + .archive_post {
68 border-top: none;
68 border-top: none;
69 }
69 }
70
70
71 .metadata {
71 .metadata {
72 padding-top: 5px;
72 padding-top: 5px;
73 margin-top: 10px;
73 margin-top: 10px;
74 border-top: solid 1px #666;
74 border-top: solid 1px #666;
75 color: #ddd;
75 color: #ddd;
76 }
76 }
77
77
78 .navigation_panel, .tag_info {
78 .navigation_panel, .tag_info {
79 background: #222;
79 background: #222;
80 margin-bottom: 5px;
80 margin-bottom: 5px;
81 margin-top: 5px;
81 margin-top: 5px;
82 padding: 10px;
82 padding: 10px;
83 border-bottom: solid 1px #888;
83 border-bottom: solid 1px #888;
84 border-top: solid 1px #888;
84 border-top: solid 1px #888;
85 color: #eee;
85 color: #eee;
86 }
86 }
87
87
88 .navigation_panel .link:first-child {
88 .navigation_panel .link:first-child {
89 border-right: 1px solid #fff;
89 border-right: 1px solid #fff;
90 font-weight: bold;
90 font-weight: bold;
91 margin-right: 1ex;
91 margin-right: 1ex;
92 padding-right: 1ex;
92 padding-right: 1ex;
93 }
93 }
94
94
95 .navigation_panel .right-link {
95 .navigation_panel .right-link {
96 border-left: 1px solid #fff;
96 border-left: 1px solid #fff;
97 border-right: none;
97 border-right: none;
98 float: right;
98 float: right;
99 margin-left: 1ex;
99 margin-left: 1ex;
100 margin-right: 0;
100 margin-right: 0;
101 padding-left: 1ex;
101 padding-left: 1ex;
102 padding-right: 0;
102 padding-right: 0;
103 }
103 }
104
104
105 .navigation_panel .link {
105 .navigation_panel .link {
106 font-weight: bold;
106 font-weight: bold;
107 }
107 }
108
108
109 .navigation_panel::after, .post::after {
109 .navigation_panel::after, .post::after {
110 clear: both;
110 clear: both;
111 content: ".";
111 content: ".";
112 display: block;
112 display: block;
113 height: 0;
113 height: 0;
114 line-height: 0;
114 line-height: 0;
115 visibility: hidden;
115 visibility: hidden;
116 }
116 }
117
117
118 .header {
118 .header {
119 border-bottom: solid 2px #ccc;
119 border-bottom: solid 2px #ccc;
120 margin-bottom: 5px;
120 margin-bottom: 5px;
121 border-top: none;
121 border-top: none;
122 margin-top: 0;
122 margin-top: 0;
123 }
123 }
124
124
125 .footer {
125 .footer {
126 border-top: solid 2px #ccc;
126 border-top: solid 2px #ccc;
127 margin-top: 5px;
127 margin-top: 5px;
128 border-bottom: none;
128 border-bottom: none;
129 margin-bottom: 0;
129 margin-bottom: 0;
130 }
130 }
131
131
132 p, .br {
132 p, .br {
133 margin-top: .5em;
133 margin-top: .5em;
134 margin-bottom: .5em;
134 margin-bottom: .5em;
135 }
135 }
136
136
137 .post-form-w {
137 .post-form-w {
138 background: #333344;
138 background: #333344;
139 border-top: solid 1px #888;
139 border-top: solid 1px #888;
140 border-bottom: solid 1px #888;
140 border-bottom: solid 1px #888;
141 color: #fff;
141 color: #fff;
142 padding: 10px;
142 padding: 10px;
143 margin-bottom: 5px;
143 margin-bottom: 5px;
144 margin-top: 5px;
144 margin-top: 5px;
145 }
145 }
146
146
147 .form-row {
147 .form-row {
148 width: 100%;
148 width: 100%;
149 display: table-row;
149 display: table-row;
150 }
150 }
151
151
152 .form-label {
152 .form-label {
153 padding: .25em 1ex .25em 0;
153 padding: .25em 1ex .25em 0;
154 vertical-align: top;
154 vertical-align: top;
155 display: table-cell;
155 display: table-cell;
156 }
156 }
157
157
158 .form-input {
158 .form-input {
159 padding: .25em 0;
159 padding: .25em 0;
160 width: 100%;
160 width: 100%;
161 display: table-cell;
161 display: table-cell;
162 }
162 }
163
163
164 .form-errors {
164 .form-errors {
165 font-weight: bolder;
165 font-weight: bolder;
166 vertical-align: middle;
166 vertical-align: middle;
167 display: table-cell;
167 display: table-cell;
168 }
168 }
169
169
170 .post-form input:not([name="image"]), .post-form textarea, .post-form select {
170 .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select {
171 background: #333;
171 background: #333;
172 color: #fff;
172 color: #fff;
173 border: solid 1px;
173 border: solid 1px;
174 padding: 0;
174 padding: 0;
175 font: medium sans-serif;
175 font: medium sans-serif;
176 width: 100%;
176 width: 100%;
177 }
177 }
178
178
179 .post-form textarea {
179 .post-form textarea {
180 resize: vertical;
180 resize: vertical;
181 }
181 }
182
182
183 .form-submit {
183 .form-submit {
184 display: table;
184 display: table;
185 margin-bottom: 1ex;
185 margin-bottom: 1ex;
186 margin-top: 1ex;
186 margin-top: 1ex;
187 }
187 }
188
188
189 .form-title {
189 .form-title {
190 font-weight: bold;
190 font-weight: bold;
191 font-size: 2ex;
191 font-size: 2ex;
192 margin-bottom: 0.5ex;
192 margin-bottom: 0.5ex;
193 }
193 }
194
194
195 .post-form input[type="submit"], input[type="submit"] {
195 input[type="submit"] {
196 background: #222;
196 background: #222;
197 border: solid 2px #fff;
197 border: solid 2px #fff;
198 color: #fff;
198 color: #fff;
199 padding: 0.5ex;
199 padding: 0.5ex;
200 }
200 }
201
201
202 input[type="submit"]:hover {
202 input[type="submit"]:hover {
203 background: #060;
203 background: #060;
204 }
204 }
205
205
206 blockquote {
206 blockquote {
207 border-left: solid 2px;
207 border-left: solid 2px;
208 padding-left: 5px;
208 padding-left: 5px;
209 color: #B1FB17;
209 color: #B1FB17;
210 margin: 0;
210 margin: 0;
211 }
211 }
212
212
213 .post > .image {
213 .post > .image {
214 float: left;
214 float: left;
215 margin: 0 1ex .5ex 0;
215 margin: 0 1ex .5ex 0;
216 min-width: 1px;
216 min-width: 1px;
217 text-align: center;
217 text-align: center;
218 display: table-row;
218 display: table-row;
219 }
219 }
220
220
221 .post > .metadata {
221 .post > .metadata {
222 clear: left;
222 clear: left;
223 }
223 }
224
224
225 .get {
225 .get {
226 font-weight: bold;
226 font-weight: bold;
227 color: #d55;
227 color: #d55;
228 }
228 }
229
229
230 * {
230 * {
231 text-decoration: none;
231 text-decoration: none;
232 }
232 }
233
233
234 .dead_post > .post-info {
234 .dead_post > .post-info {
235 font-style: italic;
235 font-style: italic;
236 }
236 }
237
237
238 .archive_post > .post-info {
238 .archive_post > .post-info {
239 text-decoration: line-through;
239 text-decoration: line-through;
240 }
240 }
241
241
242 .mark_btn {
242 .mark_btn {
243 border: 1px solid;
243 border: 1px solid;
244 padding: 2px 2ex;
244 padding: 2px 2ex;
245 display: inline-block;
245 display: inline-block;
246 margin: 0 5px 4px 0;
246 margin: 0 5px 4px 0;
247 }
247 }
248
248
249 .mark_btn:hover {
249 .mark_btn:hover {
250 background: #555;
250 background: #555;
251 }
251 }
252
252
253 .quote {
253 .quote {
254 color: #92cf38;
254 color: #92cf38;
255 font-style: italic;
255 font-style: italic;
256 }
256 }
257
257
258 .multiquote {
258 .multiquote {
259 padding: 3px;
259 padding: 3px;
260 display: inline-block;
260 display: inline-block;
261 background: #222;
261 background: #222;
262 border-style: solid;
262 border-style: solid;
263 border-width: 1px 1px 1px 4px;
263 border-width: 1px 1px 1px 4px;
264 font-size: 0.9em;
264 font-size: 0.9em;
265 }
265 }
266
266
267 .spoiler {
267 .spoiler {
268 background: black;
268 background: black;
269 color: black;
269 color: black;
270 padding: 0 1ex 0 1ex;
270 padding: 0 1ex 0 1ex;
271 }
271 }
272
272
273 .spoiler:hover {
273 .spoiler:hover {
274 color: #ddd;
274 color: #ddd;
275 }
275 }
276
276
277 .comment {
277 .comment {
278 color: #eb2;
278 color: #eb2;
279 }
279 }
280
280
281 a:hover {
281 a:hover {
282 text-decoration: underline;
282 text-decoration: underline;
283 }
283 }
284
284
285 .last-replies {
285 .last-replies {
286 margin-left: 3ex;
286 margin-left: 3ex;
287 margin-right: 3ex;
287 margin-right: 3ex;
288 border-left: solid 1px #777;
288 border-left: solid 1px #777;
289 border-right: solid 1px #777;
289 border-right: solid 1px #777;
290 }
290 }
291
291
292 .last-replies > .post:first-child {
292 .last-replies > .post:first-child {
293 border-top: none;
293 border-top: none;
294 }
294 }
295
295
296 .thread {
296 .thread {
297 margin-bottom: 3ex;
297 margin-bottom: 3ex;
298 margin-top: 1ex;
298 margin-top: 1ex;
299 }
299 }
300
300
301 .post:target {
301 .post:target {
302 border: solid 2px white;
302 border: solid 2px white;
303 }
303 }
304
304
305 pre{
305 pre{
306 white-space:pre-wrap
306 white-space:pre-wrap
307 }
307 }
308
308
309 li {
309 li {
310 list-style-position: inside;
310 list-style-position: inside;
311 }
311 }
312
312
313 .fancybox-skin {
313 .fancybox-skin {
314 position: relative;
314 position: relative;
315 background-color: #fff;
315 background-color: #fff;
316 color: #ddd;
316 color: #ddd;
317 text-shadow: none;
317 text-shadow: none;
318 }
318 }
319
319
320 .fancybox-image {
320 .fancybox-image {
321 border: 1px solid black;
321 border: 1px solid black;
322 }
322 }
323
323
324 .image-mode-tab {
324 .image-mode-tab {
325 background: #444;
325 background: #444;
326 color: #eee;
326 color: #eee;
327 margin-top: 5px;
327 margin-top: 5px;
328 padding: 5px;
328 padding: 5px;
329 border-top: 1px solid #888;
329 border-top: 1px solid #888;
330 border-bottom: 1px solid #888;
330 border-bottom: 1px solid #888;
331 }
331 }
332
332
333 .image-mode-tab > label {
333 .image-mode-tab > label {
334 margin: 0 1ex;
334 margin: 0 1ex;
335 }
335 }
336
336
337 .image-mode-tab > label > input {
337 .image-mode-tab > label > input {
338 margin-right: .5ex;
338 margin-right: .5ex;
339 }
339 }
340
340
341 #posts-table {
341 #posts-table {
342 margin-top: 5px;
342 margin-top: 5px;
343 margin-bottom: 5px;
343 margin-bottom: 5px;
344 }
344 }
345
345
346 .tag_info > h2 {
346 .tag_info > h2 {
347 margin: 0;
347 margin: 0;
348 }
348 }
349
349
350 .post-info {
350 .post-info {
351 color: #ddd;
351 color: #ddd;
352 margin-bottom: 1ex;
352 margin-bottom: 1ex;
353 }
353 }
354
354
355 .moderator_info {
355 .moderator_info {
356 color: #e99d41;
356 color: #e99d41;
357 opacity: 0.4;
357 opacity: 0.4;
358 }
358 }
359
359
360 .moderator_info:hover {
360 .moderator_info:hover {
361 opacity: 1;
361 opacity: 1;
362 }
362 }
363
363
364 .refmap {
364 .refmap {
365 font-size: 0.9em;
365 font-size: 0.9em;
366 color: #ccc;
366 color: #ccc;
367 margin-top: 1em;
367 margin-top: 1em;
368 }
368 }
369
369
370 .fav {
370 .fav {
371 color: yellow;
371 color: yellow;
372 }
372 }
373
373
374 .not_fav {
374 .not_fav {
375 color: #ccc;
375 color: #ccc;
376 }
376 }
377
377
378 .role {
378 .role {
379 text-decoration: underline;
379 text-decoration: underline;
380 }
380 }
381
381
382 .form-email {
382 .form-email {
383 display: none;
383 display: none;
384 }
384 }
385
385
386 .bar-value {
386 .bar-value {
387 background: rgba(50, 55, 164, 0.45);
387 background: rgba(50, 55, 164, 0.45);
388 font-size: 0.9em;
388 font-size: 0.9em;
389 height: 1.5em;
389 height: 1.5em;
390 }
390 }
391
391
392 .bar-bg {
392 .bar-bg {
393 position: relative;
393 position: relative;
394 border-top: solid 1px #888;
394 border-top: solid 1px #888;
395 border-bottom: solid 1px #888;
395 border-bottom: solid 1px #888;
396 margin-top: 5px;
396 margin-top: 5px;
397 overflow: hidden;
397 overflow: hidden;
398 }
398 }
399
399
400 .bar-text {
400 .bar-text {
401 padding: 2px;
401 padding: 2px;
402 position: absolute;
402 position: absolute;
403 left: 0;
403 left: 0;
404 top: 0;
404 top: 0;
405 }
405 }
406
406
407 .page_link {
407 .page_link {
408 background: #444;
408 background: #444;
409 border-top: solid 1px #888;
409 border-top: solid 1px #888;
410 border-bottom: solid 1px #888;
410 border-bottom: solid 1px #888;
411 padding: 5px;
411 padding: 5px;
412 color: #eee;
412 color: #eee;
413 font-size: 2ex;
413 font-size: 2ex;
414 }
414 }
415
415
416 .skipped_replies {
416 .skipped_replies {
417 padding: 5px;
417 padding: 5px;
418 margin-left: 3ex;
418 margin-left: 3ex;
419 margin-right: 3ex;
419 margin-right: 3ex;
420 border-left: solid 1px #888;
420 border-left: solid 1px #888;
421 border-right: solid 1px #888;
421 border-right: solid 1px #888;
422 border-bottom: solid 1px #888;
422 border-bottom: solid 1px #888;
423 background: #000;
423 background: #000;
424 }
424 }
425
425
426 .current_page {
426 .current_page {
427 padding: 2px;
427 padding: 2px;
428 background-color: #afdcec;
428 background-color: #afdcec;
429 color: #000;
429 color: #000;
430 }
430 }
431
431
432 .current_mode {
432 .current_mode {
433 font-weight: bold;
433 font-weight: bold;
434 }
434 }
435
435
436 .gallery_image {
436 .gallery_image {
437 border: solid 1px;
437 border: solid 1px;
438 padding: 0.5ex;
438 padding: 0.5ex;
439 margin: 0.5ex;
439 margin: 0.5ex;
440 text-align: center;
440 text-align: center;
441 }
441 }
442
442
443 code {
443 code {
444 border: dashed 1px #ccc;
444 border: dashed 1px #ccc;
445 background: #111;
445 background: #111;
446 padding: 2px;
446 padding: 2px;
447 font-size: 1.2em;
447 font-size: 1.2em;
448 display: inline-block;
448 display: inline-block;
449 }
449 }
450
450
451 pre {
451 pre {
452 overflow: auto;
452 overflow: auto;
453 }
453 }
454
454
455 .img-full {
455 .img-full {
456 background: #222;
456 background: #222;
457 border: solid 1px white;
457 border: solid 1px white;
458 }
458 }
459
459
460 .tag_item {
460 .tag_item {
461 display: inline-block;
461 display: inline-block;
462 }
462 }
463
463
464 #id_models li {
464 #id_models li {
465 list-style: none;
465 list-style: none;
466 }
466 }
467
467
468 #id_q {
468 #id_q {
469 margin-left: 1ex;
469 margin-left: 1ex;
470 }
470 }
471
471
472 ul {
472 ul {
473 padding-left: 0px;
473 padding-left: 0px;
474 }
474 }
475
475
476 .quote-header {
476 .quote-header {
477 border-bottom: 2px solid #ddd;
477 border-bottom: 2px solid #ddd;
478 margin-bottom: 1ex;
478 margin-bottom: 1ex;
479 padding-bottom: .5ex;
479 padding-bottom: .5ex;
480 color: #ddd;
480 color: #ddd;
481 font-size: 1.2em;
481 font-size: 1.2em;
482 }
482 }
483
483
484 /* Reflink preview */
484 /* Reflink preview */
485 .post_preview {
485 .post_preview {
486 border-left: 1px solid #777;
486 border-left: 1px solid #777;
487 border-right: 1px solid #777;
487 border-right: 1px solid #777;
488 max-width: 600px;
488 max-width: 600px;
489 }
489 }
490
490
491 /* Code highlighter */
491 /* Code highlighter */
492 .hljs {
492 .hljs {
493 color: #fff;
493 color: #fff;
494 background: #000;
494 background: #000;
495 display: inline-block;
495 display: inline-block;
496 }
496 }
497
497
498 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
498 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
499 color: #fff;
499 color: #fff;
500 }
500 }
501
501
502 #up {
502 #up {
503 position: fixed;
503 position: fixed;
504 bottom: 5px;
504 bottom: 5px;
505 right: 5px;
505 right: 5px;
506 border: 1px solid #777;
506 border: 1px solid #777;
507 background: #000;
507 background: #000;
508 padding: 4px;
508 padding: 4px;
509 }
509 }
510
510
511 .user-cast {
511 .user-cast {
512 border: solid #ffffff 1px;
512 border: solid #ffffff 1px;
513 padding: .2ex;
513 padding: .2ex;
514 background: #152154;
514 background: #152154;
515 color: #fff;
515 color: #fff;
516 }
516 }
517
517
518 .highlight {
518 .highlight {
519 background: #222;
519 background: #222;
520 }
520 }
521
521
522 .post-button-form > button:hover {
522 .post-button-form > button:hover {
523 text-decoration: underline;
523 text-decoration: underline;
524 }
524 }
525
525
526 .tree_reply > .post {
526 .tree_reply > .post {
527 margin-top: 1ex;
527 margin-top: 1ex;
528 border-left: solid 1px #777;
528 border-left: solid 1px #777;
529 padding-right: 0;
529 padding-right: 0;
530 }
530 }
531
531
532 #preview-text {
532 #preview-text {
533 border: solid 1px white;
533 border: solid 1px white;
534 margin: 1ex 0 1ex 0;
534 margin: 1ex 0 1ex 0;
535 padding: 1ex;
535 padding: 1ex;
536 }
536 }
537
537
538 button {
538 button {
539 border: 1px solid white;
539 border: 1px solid white;
540 margin-bottom: .5ex;
540 margin-bottom: .5ex;
541 margin-top: .5ex;
541 margin-top: .5ex;
542 }
542 }
543
543
544 .image-metadata {
544 .image-metadata {
545 font-style: italic;
545 font-style: italic;
546 font-size: 0.9em;
546 font-size: 0.9em;
547 }
547 }
@@ -1,110 +1,113 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3
3
4 {% get_current_language as LANGUAGE_CODE %}
4 {% get_current_language as LANGUAGE_CODE %}
5
5
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
6 <div class="{{ css_class }}" id="{{ post.id }}" data-uid="{{ post.uid }}">
7 <div class="post-info">
7 <div class="post-info">
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
8 <a class="post_id" href="{{ post.get_absolute_url }}">#{{ post.get_absolute_id }}</a>
9 <span class="title">{{ post.title }}</span>
9 <span class="title">{{ post.title }}</span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
10 <span class="pub_time"><time datetime="{{ post.pub_time|date:'c' }}">{{ post.pub_time }}</time></span>
11 {% if post.tripcode %}
12 <span style="color: #{{post.get_tripcode_color}}">{{ post.get_short_tripcode }}</span>
13 {% endif %}
11 {% comment %}
14 {% comment %}
12 Thread death time needs to be shown only if the thread is alredy archived
15 Thread death time needs to be shown only if the thread is alredy archived
13 and this is an opening post (thread death time) or a post for popup
16 and this is an opening post (thread death time) or a post for popup
14 (we don't see OP here so we show the death time in the post itself).
17 (we don't see OP here so we show the death time in the post itself).
15 {% endcomment %}
18 {% endcomment %}
16 {% if thread.archived %}
19 {% if thread.archived %}
17 {% if is_opening %}
20 {% if is_opening %}
18 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
21 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
19 {% endif %}
22 {% endif %}
20 {% endif %}
23 {% endif %}
21 {% if is_opening %}
24 {% if is_opening %}
22 {% if need_open_link %}
25 {% if need_open_link %}
23 {% if thread.archived %}
26 {% if thread.archived %}
24 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
27 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
25 {% else %}
28 {% else %}
26 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
29 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
27 {% endif %}
30 {% endif %}
28 {% endif %}
31 {% endif %}
29 {% else %}
32 {% else %}
30 {% if need_op_data %}
33 {% if need_op_data %}
31 {% with thread.get_opening_post as op %}
34 {% with thread.get_opening_post as op %}
32 {% trans " in " %}<a href="{{ op.get_absolute_url }}">&gt;&gt;{{ op.id }}</a> <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
35 {% trans " in " %}<a href="{{ op.get_absolute_url }}">&gt;&gt;{{ op.id }}</a> <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
33 {% endwith %}
36 {% endwith %}
34 {% endif %}
37 {% endif %}
35 {% endif %}
38 {% endif %}
36 {% if reply_link and not thread.archived %}
39 {% if reply_link and not thread.archived %}
37 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
40 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
38 {% endif %}
41 {% endif %}
39
42
40 {% if moderator %}
43 {% if moderator %}
41 <span class="moderator_info">
44 <span class="moderator_info">
42 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
45 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
43 {% if is_opening %}
46 {% if is_opening %}
44 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
47 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
45 {% endif %}
48 {% endif %}
46 </span>
49 </span>
47 {% endif %}
50 {% endif %}
48 </div>
51 </div>
49 {% comment %}
52 {% comment %}
50 Post images. Currently only 1 image can be posted and shown, but post model
53 Post images. Currently only 1 image can be posted and shown, but post model
51 supports multiple.
54 supports multiple.
52 {% endcomment %}
55 {% endcomment %}
53 {% if post.images.exists %}
56 {% if post.images.exists %}
54 {% with post.images.first as image %}
57 {% with post.images.first as image %}
55 {% autoescape off %}
58 {% autoescape off %}
56 {{ image.get_view }}
59 {{ image.get_view }}
57 {% endautoescape %}
60 {% endautoescape %}
58 {% endwith %}
61 {% endwith %}
59 {% endif %}
62 {% endif %}
60 {% if post.attachments.exists %}
63 {% if post.attachments.exists %}
61 {% with post.attachments.first as file %}
64 {% with post.attachments.first as file %}
62 {% autoescape off %}
65 {% autoescape off %}
63 {{ file.get_view }}
66 {{ file.get_view }}
64 {% endautoescape %}
67 {% endautoescape %}
65 {% endwith %}
68 {% endwith %}
66 {% endif %}
69 {% endif %}
67 {% comment %}
70 {% comment %}
68 Post message (text)
71 Post message (text)
69 {% endcomment %}
72 {% endcomment %}
70 <div class="message">
73 <div class="message">
71 {% autoescape off %}
74 {% autoescape off %}
72 {% if truncated %}
75 {% if truncated %}
73 {{ post.get_text|truncatewords_html:50 }}
76 {{ post.get_text|truncatewords_html:50 }}
74 {% else %}
77 {% else %}
75 {{ post.get_text }}
78 {{ post.get_text }}
76 {% endif %}
79 {% endif %}
77 {% endautoescape %}
80 {% endautoescape %}
78 </div>
81 </div>
79 {% if post.is_referenced %}
82 {% if post.is_referenced %}
80 {% if mode_tree %}
83 {% if mode_tree %}
81 <div class="tree_reply">
84 <div class="tree_reply">
82 {% for refpost in post.get_referenced_posts %}
85 {% for refpost in post.get_referenced_posts %}
83 {% post_view refpost mode_tree=True %}
86 {% post_view refpost mode_tree=True %}
84 {% endfor %}
87 {% endfor %}
85 </div>
88 </div>
86 {% else %}
89 {% else %}
87 <div class="refmap">
90 <div class="refmap">
88 {% autoescape off %}
91 {% autoescape off %}
89 {% trans "Replies" %}: {{ post.refmap }}
92 {% trans "Replies" %}: {{ post.refmap }}
90 {% endautoescape %}
93 {% endautoescape %}
91 </div>
94 </div>
92 {% endif %}
95 {% endif %}
93 {% endif %}
96 {% endif %}
94 {% comment %}
97 {% comment %}
95 Thread metadata: counters, tags etc
98 Thread metadata: counters, tags etc
96 {% endcomment %}
99 {% endcomment %}
97 {% if is_opening %}
100 {% if is_opening %}
98 <div class="metadata">
101 <div class="metadata">
99 {% if is_opening and need_open_link %}
102 {% if is_opening and need_open_link %}
100 {{ thread.get_reply_count }} {% trans 'messages' %},
103 {{ thread.get_reply_count }} {% trans 'messages' %},
101 {{ thread.get_images_count }} {% trans 'images' %}.
104 {{ thread.get_images_count }} {% trans 'images' %}.
102 {% endif %}
105 {% endif %}
103 <span class="tags">
106 <span class="tags">
104 {% autoescape off %}
107 {% autoescape off %}
105 {{ thread.get_tag_url_list }}
108 {{ thread.get_tag_url_list }}
106 {% endautoescape %}
109 {% endautoescape %}
107 </span>
110 </span>
108 </div>
111 </div>
109 {% endif %}
112 {% endif %}
110 </div>
113 </div>
@@ -1,169 +1,170 b''
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 import requests
8 import requests
9
9
10 from boards import utils, settings
10 from boards import utils, settings
11 from boards.abstracts.paginator import get_paginator
11 from boards.abstracts.paginator import get_paginator
12 from boards.abstracts.settingsmanager import get_settings_manager
12 from boards.abstracts.settingsmanager import get_settings_manager
13 from boards.forms import ThreadForm, PlainErrorList
13 from boards.forms import ThreadForm, PlainErrorList
14 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
14 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
15 from boards.views.banned import BannedView
15 from boards.views.banned import BannedView
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
16 from boards.views.base import BaseBoardView, CONTEXT_FORM
17 from boards.views.posting_mixin import PostMixin
17 from boards.views.posting_mixin import PostMixin
18
18
19
19
20 FORM_TAGS = 'tags'
20 FORM_TAGS = 'tags'
21 FORM_TEXT = 'text'
21 FORM_TEXT = 'text'
22 FORM_TITLE = 'title'
22 FORM_TITLE = 'title'
23 FORM_IMAGE = 'image'
23 FORM_IMAGE = 'image'
24 FORM_THREADS = 'threads'
24 FORM_THREADS = 'threads'
25
25
26 TAG_DELIMITER = ' '
26 TAG_DELIMITER = ' '
27
27
28 PARAMETER_CURRENT_PAGE = 'current_page'
28 PARAMETER_CURRENT_PAGE = 'current_page'
29 PARAMETER_PAGINATOR = 'paginator'
29 PARAMETER_PAGINATOR = 'paginator'
30 PARAMETER_THREADS = 'threads'
30 PARAMETER_THREADS = 'threads'
31 PARAMETER_BANNERS = 'banners'
31 PARAMETER_BANNERS = 'banners'
32
32
33 PARAMETER_PREV_LINK = 'prev_page_link'
33 PARAMETER_PREV_LINK = 'prev_page_link'
34 PARAMETER_NEXT_LINK = 'next_page_link'
34 PARAMETER_NEXT_LINK = 'next_page_link'
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, BaseBoardView):
40 class AllThreadsView(PostMixin, BaseBoardView):
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 def get(self, request, form: ThreadForm=None):
46 def get(self, request, form: ThreadForm=None):
47 page = request.GET.get('page', DEFAULT_PAGE)
47 page = request.GET.get('page', DEFAULT_PAGE)
48
48
49 params = self.get_context_data(request=request)
49 params = self.get_context_data(request=request)
50
50
51 if not form:
51 if not form:
52 form = ThreadForm(error_class=PlainErrorList)
52 form = ThreadForm(error_class=PlainErrorList)
53
53
54 self.settings_manager = get_settings_manager(request)
54 self.settings_manager = get_settings_manager(request)
55 paginator = get_paginator(self.get_threads(),
55 paginator = get_paginator(self.get_threads(),
56 settings.get_int('View', 'ThreadsPerPage'))
56 settings.get_int('View', 'ThreadsPerPage'))
57 paginator.current_page = int(page)
57 paginator.current_page = int(page)
58
58
59 try:
59 try:
60 threads = paginator.page(page).object_list
60 threads = paginator.page(page).object_list
61 except EmptyPage:
61 except EmptyPage:
62 raise Http404()
62 raise Http404()
63
63
64 params[PARAMETER_THREADS] = threads
64 params[PARAMETER_THREADS] = threads
65 params[CONTEXT_FORM] = form
65 params[CONTEXT_FORM] = form
66 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
66 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
67
67
68 self.get_page_context(paginator, params, page)
68 self.get_page_context(paginator, params, page)
69
69
70 return render(request, TEMPLATE, params)
70 return render(request, TEMPLATE, params)
71
71
72 def post(self, request):
72 def post(self, request):
73 form = ThreadForm(request.POST, request.FILES,
73 form = ThreadForm(request.POST, request.FILES,
74 error_class=PlainErrorList)
74 error_class=PlainErrorList)
75 form.session = request.session
75 form.session = request.session
76
76
77 if form.is_valid():
77 if form.is_valid():
78 return self.create_thread(request, form)
78 return self.create_thread(request, form)
79 if form.need_to_ban:
79 if form.need_to_ban:
80 # Ban user because he is suspected to be a bot
80 # Ban user because he is suspected to be a bot
81 self._ban_current_user(request)
81 self._ban_current_user(request)
82
82
83 return self.get(request, form)
83 return self.get(request, form)
84
84
85 def get_page_context(self, paginator, params, page):
85 def get_page_context(self, paginator, params, page):
86 """
86 """
87 Get pagination context variables
87 Get pagination context variables
88 """
88 """
89
89
90 params[PARAMETER_PAGINATOR] = paginator
90 params[PARAMETER_PAGINATOR] = paginator
91 current_page = paginator.page(int(page))
91 current_page = paginator.page(int(page))
92 params[PARAMETER_CURRENT_PAGE] = current_page
92 params[PARAMETER_CURRENT_PAGE] = current_page
93 if current_page.has_previous():
93 if current_page.has_previous():
94 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
94 params[PARAMETER_PREV_LINK] = self.get_previous_page_link(
95 current_page)
95 current_page)
96 if current_page.has_next():
96 if current_page.has_next():
97 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
97 params[PARAMETER_NEXT_LINK] = self.get_next_page_link(current_page)
98
98
99 def get_previous_page_link(self, current_page):
99 def get_previous_page_link(self, current_page):
100 return reverse('index') + '?page=' \
100 return reverse('index') + '?page=' \
101 + str(current_page.previous_page_number())
101 + str(current_page.previous_page_number())
102
102
103 def get_next_page_link(self, current_page):
103 def get_next_page_link(self, current_page):
104 return reverse('index') + '?page=' \
104 return reverse('index') + '?page=' \
105 + str(current_page.next_page_number())
105 + str(current_page.next_page_number())
106
106
107 @staticmethod
107 @staticmethod
108 def parse_tags_string(tag_strings):
108 def parse_tags_string(tag_strings):
109 """
109 """
110 Parses tag list string and returns tag object list.
110 Parses tag list string and returns tag object list.
111 """
111 """
112
112
113 tags = []
113 tags = []
114
114
115 if tag_strings:
115 if tag_strings:
116 tag_strings = tag_strings.split(TAG_DELIMITER)
116 tag_strings = tag_strings.split(TAG_DELIMITER)
117 for tag_name in tag_strings:
117 for tag_name in tag_strings:
118 tag_name = tag_name.strip().lower()
118 tag_name = tag_name.strip().lower()
119 if len(tag_name) > 0:
119 if len(tag_name) > 0:
120 tag, created = Tag.objects.get_or_create(name=tag_name)
120 tag, created = Tag.objects.get_or_create(name=tag_name)
121 tags.append(tag)
121 tags.append(tag)
122
122
123 return tags
123 return tags
124
124
125 @transaction.atomic
125 @transaction.atomic
126 def create_thread(self, request, form: ThreadForm, html_response=True):
126 def create_thread(self, request, form: ThreadForm, html_response=True):
127 """
127 """
128 Creates a new thread with an opening post.
128 Creates a new thread with an opening post.
129 """
129 """
130
130
131 ip = utils.get_client_ip(request)
131 ip = utils.get_client_ip(request)
132 is_banned = Ban.objects.filter(ip=ip).exists()
132 is_banned = Ban.objects.filter(ip=ip).exists()
133
133
134 if is_banned:
134 if is_banned:
135 if html_response:
135 if html_response:
136 return redirect(BannedView().as_view())
136 return redirect(BannedView().as_view())
137 else:
137 else:
138 return
138 return
139
139
140 data = form.cleaned_data
140 data = form.cleaned_data
141
141
142 title = data[FORM_TITLE]
142 title = data[FORM_TITLE]
143 text = data[FORM_TEXT]
143 text = data[FORM_TEXT]
144 file = form.get_file()
144 file = form.get_file()
145 threads = data[FORM_THREADS]
145 threads = data[FORM_THREADS]
146
146
147 text = self._remove_invalid_links(text)
147 text = self._remove_invalid_links(text)
148
148
149 tag_strings = data[FORM_TAGS]
149 tag_strings = data[FORM_TAGS]
150
150
151 tags = self.parse_tags_string(tag_strings)
151 tags = self.parse_tags_string(tag_strings)
152
152
153 post = Post.objects.create_post(title=title, text=text, file=file,
153 post = Post.objects.create_post(title=title, text=text, file=file,
154 ip=ip, tags=tags, opening_posts=threads)
154 ip=ip, tags=tags, opening_posts=threads,
155 tripcode=form.get_tripcode())
155
156
156 # This is required to update the threads to which posts we have replied
157 # This is required to update the threads to which posts we have replied
157 # when creating this one
158 # when creating this one
158 post.notify_clients()
159 post.notify_clients()
159
160
160 if html_response:
161 if html_response:
161 return redirect(post.get_absolute_url())
162 return redirect(post.get_absolute_url())
162
163
163 def get_threads(self):
164 def get_threads(self):
164 """
165 """
165 Gets list of threads that will be shown on a page.
166 Gets list of threads that will be shown on a page.
166 """
167 """
167
168
168 return Thread.objects.order_by('-bump_time')\
169 return Thread.objects.order_by('-bump_time')\
169 .exclude(tags__in=self.settings_manager.get_hidden_tags())
170 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,138 +1,140 b''
1 import hashlib
1 from django.core.exceptions import ObjectDoesNotExist
2 from django.core.exceptions import ObjectDoesNotExist
2 from django.http import Http404
3 from django.http import Http404
3 from django.shortcuts import get_object_or_404, render, redirect
4 from django.shortcuts import get_object_or_404, render, redirect
4 from django.views.generic.edit import FormMixin
5 from django.views.generic.edit import FormMixin
5 from django.utils import timezone
6 from django.utils import timezone
6 from django.utils.dateformat import format
7 from django.utils.dateformat import format
7
8
8 from boards import utils, settings
9 from boards import utils, settings
9 from boards.forms import PostForm, PlainErrorList
10 from boards.forms import PostForm, PlainErrorList
10 from boards.models import Post
11 from boards.models import Post
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.posting_mixin import PostMixin
13 from boards.views.posting_mixin import PostMixin
13
14
14 import neboard
15 import neboard
15
16
16
17
17 CONTEXT_LASTUPDATE = "last_update"
18 CONTEXT_LASTUPDATE = "last_update"
18 CONTEXT_THREAD = 'thread'
19 CONTEXT_THREAD = 'thread'
19 CONTEXT_WS_TOKEN = 'ws_token'
20 CONTEXT_WS_TOKEN = 'ws_token'
20 CONTEXT_WS_PROJECT = 'ws_project'
21 CONTEXT_WS_PROJECT = 'ws_project'
21 CONTEXT_WS_HOST = 'ws_host'
22 CONTEXT_WS_HOST = 'ws_host'
22 CONTEXT_WS_PORT = 'ws_port'
23 CONTEXT_WS_PORT = 'ws_port'
23 CONTEXT_WS_TIME = 'ws_token_time'
24 CONTEXT_WS_TIME = 'ws_token_time'
24 CONTEXT_MODE = 'mode'
25 CONTEXT_MODE = 'mode'
25 CONTEXT_OP = 'opening_post'
26 CONTEXT_OP = 'opening_post'
26
27
27 FORM_TITLE = 'title'
28 FORM_TITLE = 'title'
28 FORM_TEXT = 'text'
29 FORM_TEXT = 'text'
29 FORM_IMAGE = 'image'
30 FORM_IMAGE = 'image'
30 FORM_THREADS = 'threads'
31 FORM_THREADS = 'threads'
31
32
32
33
33 class ThreadView(BaseBoardView, PostMixin, FormMixin):
34 class ThreadView(BaseBoardView, PostMixin, FormMixin):
34
35
35 def get(self, request, post_id, form: PostForm=None):
36 def get(self, request, post_id, form: PostForm=None):
36 try:
37 try:
37 opening_post = Post.objects.get(id=post_id)
38 opening_post = Post.objects.get(id=post_id)
38 except ObjectDoesNotExist:
39 except ObjectDoesNotExist:
39 raise Http404
40 raise Http404
40
41
41 # If this is not OP, don't show it as it is
42 # If this is not OP, don't show it as it is
42 if not opening_post.is_opening():
43 if not opening_post.is_opening():
43 return redirect(opening_post.get_thread().get_opening_post()
44 return redirect(opening_post.get_thread().get_opening_post()
44 .get_absolute_url())
45 .get_absolute_url())
45
46
46 if not form:
47 if not form:
47 form = PostForm(error_class=PlainErrorList)
48 form = PostForm(error_class=PlainErrorList)
48
49
49 thread_to_show = opening_post.get_thread()
50 thread_to_show = opening_post.get_thread()
50
51
51 params = dict()
52 params = dict()
52
53
53 params[CONTEXT_FORM] = form
54 params[CONTEXT_FORM] = form
54 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
55 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
55 params[CONTEXT_THREAD] = thread_to_show
56 params[CONTEXT_THREAD] = thread_to_show
56 params[CONTEXT_MODE] = self.get_mode()
57 params[CONTEXT_MODE] = self.get_mode()
57 params[CONTEXT_OP] = opening_post
58 params[CONTEXT_OP] = opening_post
58
59
59 if settings.get_bool('External', 'WebsocketsEnabled'):
60 if settings.get_bool('External', 'WebsocketsEnabled'):
60 token_time = format(timezone.now(), u'U')
61 token_time = format(timezone.now(), u'U')
61
62
62 params[CONTEXT_WS_TIME] = token_time
63 params[CONTEXT_WS_TIME] = token_time
63 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
64 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
64 timestamp=token_time)
65 timestamp=token_time)
65 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
66 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
66 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
67 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
67 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
68 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
68
69
69 params.update(self.get_data(thread_to_show))
70 params.update(self.get_data(thread_to_show))
70
71
71 return render(request, self.get_template(), params)
72 return render(request, self.get_template(), params)
72
73
73 def post(self, request, post_id):
74 def post(self, request, post_id):
74 opening_post = get_object_or_404(Post, id=post_id)
75 opening_post = get_object_or_404(Post, id=post_id)
75
76
76 # If this is not OP, don't show it as it is
77 # If this is not OP, don't show it as it is
77 if not opening_post.is_opening():
78 if not opening_post.is_opening():
78 raise Http404
79 raise Http404
79
80
80 if not opening_post.get_thread().archived:
81 if not opening_post.get_thread().archived:
81 form = PostForm(request.POST, request.FILES,
82 form = PostForm(request.POST, request.FILES,
82 error_class=PlainErrorList)
83 error_class=PlainErrorList)
83 form.session = request.session
84 form.session = request.session
84
85
85 if form.is_valid():
86 if form.is_valid():
86 return self.new_post(request, form, opening_post)
87 return self.new_post(request, form, opening_post)
87 if form.need_to_ban:
88 if form.need_to_ban:
88 # Ban user because he is suspected to be a bot
89 # Ban user because he is suspected to be a bot
89 self._ban_current_user(request)
90 self._ban_current_user(request)
90
91
91 return self.get(request, post_id, form)
92 return self.get(request, post_id, form)
92
93
93 def new_post(self, request, form: PostForm, opening_post: Post=None,
94 def new_post(self, request, form: PostForm, opening_post: Post=None,
94 html_response=True):
95 html_response=True):
95 """
96 """
96 Adds a new post (in thread or as a reply).
97 Adds a new post (in thread or as a reply).
97 """
98 """
98
99
99 ip = utils.get_client_ip(request)
100 ip = utils.get_client_ip(request)
100
101
101 data = form.cleaned_data
102 data = form.cleaned_data
102
103
103 title = data[FORM_TITLE]
104 title = data[FORM_TITLE]
104 text = data[FORM_TEXT]
105 text = data[FORM_TEXT]
105 file = form.get_file()
106 file = form.get_file()
106 threads = data[FORM_THREADS]
107 threads = data[FORM_THREADS]
107
108
108 text = self._remove_invalid_links(text)
109 text = self._remove_invalid_links(text)
109
110
110 post_thread = opening_post.get_thread()
111 post_thread = opening_post.get_thread()
111
112
112 post = Post.objects.create_post(title=title, text=text, file=file,
113 post = Post.objects.create_post(title=title, text=text, file=file,
113 thread=post_thread, ip=ip,
114 thread=post_thread, ip=ip,
114 opening_posts=threads)
115 opening_posts=threads,
116 tripcode=form.get_tripcode())
115 post.notify_clients()
117 post.notify_clients()
116
118
117 if html_response:
119 if html_response:
118 if opening_post:
120 if opening_post:
119 return redirect(post.get_absolute_url())
121 return redirect(post.get_absolute_url())
120 else:
122 else:
121 return post
123 return post
122
124
123 def get_data(self, thread) -> dict:
125 def get_data(self, thread) -> dict:
124 """
126 """
125 Returns context params for the view.
127 Returns context params for the view.
126 """
128 """
127
129
128 return dict()
130 return dict()
129
131
130 def get_template(self) -> str:
132 def get_template(self) -> str:
131 """
133 """
132 Gets template to show the thread mode on.
134 Gets template to show the thread mode on.
133 """
135 """
134
136
135 pass
137 pass
136
138
137 def get_mode(self) -> str:
139 def get_mode(self) -> str:
138 pass
140 pass
General Comments 0
You need to be logged in to leave comments. Login now