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