##// END OF EJS Templates
Added support for different attachment types
neko259 -
r1273:2c3f55c9 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', '0021_tag_description'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='thread',
16 name='tags',
17 field=models.ManyToManyField(related_name='thread_tags', to='boards.Tag'),
18 ),
19 ]
@@ -0,0 +1,29 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5 import boards.models.attachment
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('boards', '0022_auto_20150812_1819'),
12 ]
13
14 operations = [
15 migrations.CreateModel(
16 name='Attachment',
17 fields=[
18 ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
19 ('file', models.FileField(upload_to=boards.models.attachment.Attachment._update_filename)),
20 ('mimetype', models.CharField(max_length=50)),
21 ('hash', models.CharField(max_length=36)),
22 ],
23 ),
24 migrations.AddField(
25 model_name='post',
26 name='attachments',
27 field=models.ManyToManyField(blank=True, null=True, related_name='attachment_posts', to='boards.Attachment'),
28 ),
29 ]
@@ -0,0 +1,75 b''
1 import hashlib
2 import os
3 import time
4
5 from random import random
6
7 from django.db import models
8
9 from boards.models.attachment.viewers import AbstractViewer, WebmViewer
10
11
12 FILES_DIRECTORY = 'files/'
13 FILE_EXTENSION_DELIMITER = '.'
14
15 VIEWERS = (
16 WebmViewer,
17 )
18
19
20 class AttachmentManager(models.Manager):
21 def create_with_hash(self, file):
22 file_hash = self.get_hash(file)
23 existing = self.filter(hash=file_hash)
24 if len(existing) > 0:
25 attachment = existing[0]
26 else:
27 file_type = file.name.split(FILE_EXTENSION_DELIMITER)[-1].lower()
28 attachment = Attachment.objects.create(file=file,
29 mimetype=file_type, hash=file_hash)
30
31 return attachment
32
33 def get_hash(self, file):
34 """
35 Gets hash of an file.
36 """
37 md5 = hashlib.md5()
38 for chunk in file.chunks():
39 md5.update(chunk)
40 return md5.hexdigest()
41
42
43 class Attachment(models.Model):
44 objects = AttachmentManager()
45
46 # TODO Dedup the method
47 def _update_filename(self, filename):
48 """
49 Gets unique filename
50 """
51
52 # TODO Use something other than random number in file name
53 new_name = '{}{}.{}'.format(
54 str(int(time.mktime(time.gmtime()))),
55 str(int(random() * 1000)),
56 filename.split(FILE_EXTENSION_DELIMITER)[-1:][0])
57
58 return os.path.join(FILES_DIRECTORY, new_name)
59
60 file = models.FileField(upload_to=_update_filename)
61 mimetype = models.CharField(max_length=50)
62 hash = models.CharField(max_length=36)
63
64 def get_view(self):
65 file_viewer = None
66 for viewer in VIEWERS:
67 if viewer.supports(self.mimetype):
68 file_viewer = viewer(self.file, self.mimetype)
69 break
70 if file_viewer is None:
71 file_viewer = AbstractViewer(self.file, self.mimetype)
72
73 return file_viewer.get_view()
74
75
@@ -0,0 +1,39 b''
1 from django.template.defaultfilters import filesizeformat
2
3
4 class AbstractViewer:
5 def __init__(self, file, file_type):
6 self.file = file
7 self.file_type = file_type
8
9 @staticmethod
10 def get_viewer(file_type, file):
11 for viewer in VIEWERS:
12 if viewer.supports(file_type):
13 return viewer(file)
14 return AbstractViewer(file)
15
16 @staticmethod
17 def supports(file_type):
18 return true
19
20 def get_view(self):
21 return '<div class="image"><a href="{}">'\
22 '<img src="/static/images/file.png" width="200" height="150"/>'\
23 '</a>'\
24 '<div class="image-metadata">{}, {}</div>'\
25 '</div>'.format(self.file.url, self.file_type,
26 filesizeformat(self.file.size))
27
28
29 class WebmViewer(AbstractViewer):
30 @staticmethod
31 def supports(file_type):
32 return file_type == 'webm'
33
34 def get_view(self):
35 return '<div class="image">'\
36 '<video width="200" height="150" controls/>'\
37 '<source src="{}">'\
38 '</div>'.format(self.file.url)
39
@@ -1,33 +1,33 b''
1 1 [Version]
2 2 Version = 2.8.3 Charlie
3 3 SiteName = Neboard DEV
4 4
5 5 [Cache]
6 6 # Timeout for caching, if cache is used
7 7 CacheTimeout = 600
8 8
9 9 [Forms]
10 10 # Max post length in characters
11 11 MaxTextLength = 30000
12 MaxImageSize = 8000000
12 MaxFileSize = 8000000
13 13 LimitPostingSpeed = false
14 14
15 15 [Messages]
16 16 # Thread bumplimit
17 17 MaxPostsPerThread = 10
18 18 # Old posts will be archived or deleted if this value is reached
19 19 MaxThreadCount = 5
20 20
21 21 [View]
22 22 DefaultTheme = md
23 23 DefaultImageViewer = simple
24 24 LastRepliesCount = 3
25 25 ThreadsPerPage = 3
26 26
27 27 [Storage]
28 28 # Enable archiving threads instead of deletion when the thread limit is reached
29 29 ArchiveThreads = true
30 30
31 31 [External]
32 32 # Thread update
33 33 WebsocketsEnabled = false
@@ -1,386 +1,377 b''
1 1 import re
2 2 import time
3 3 import pytz
4 4
5 5 from django import forms
6 6 from django.core.files.uploadedfile import SimpleUploadedFile
7 7 from django.core.exceptions import ObjectDoesNotExist
8 8 from django.forms.util import ErrorList
9 9 from django.utils.translation import ugettext_lazy as _
10 10 import requests
11 11
12 12 from boards.mdx_neboard import formatters
13 13 from boards.models.post import TITLE_MAX_LENGTH
14 14 from boards.models import Tag, Post
15 15 from neboard import settings
16 16 import boards.settings as board_settings
17 17
18 18 HEADER_CONTENT_LENGTH = 'content-length'
19 19 HEADER_CONTENT_TYPE = 'content-type'
20 20
21 CONTENT_TYPE_IMAGE = (
22 'image/jpeg',
23 'image/jpg',
24 'image/png',
25 'image/gif',
26 'image/bmp',
27 )
28
29 21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
30 22
31 23 VETERAN_POSTING_DELAY = 5
32 24
33 25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
34 26 ATTRIBUTE_ROWS = 'rows'
35 27
36 28 LAST_POST_TIME = 'last_post_time'
37 29 LAST_LOGIN_TIME = 'last_login_time'
38 30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
39 31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
40 32
41 33 LABEL_TITLE = _('Title')
42 34 LABEL_TEXT = _('Text')
43 35 LABEL_TAG = _('Tag')
44 36 LABEL_SEARCH = _('Search')
45 37
46 38 ERROR_SPEED = _('Please wait %s seconds before sending message')
47 39
48 40 TAG_MAX_LENGTH = 20
49 41
50 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000
42 FILE_DOWNLOAD_CHUNK_BYTES = 100000
51 43
52 44 HTTP_RESULT_OK = 200
53 45
54 46 TEXTAREA_ROWS = 4
55 47
56 48
57 49 def get_timezones():
58 50 timezones = []
59 51 for tz in pytz.common_timezones:
60 52 timezones.append((tz, tz),)
61 53 return timezones
62 54
63 55
64 56 class FormatPanel(forms.Textarea):
65 57 """
66 58 Panel for text formatting. Consists of buttons to add different tags to the
67 59 form text area.
68 60 """
69 61
70 62 def render(self, name, value, attrs=None):
71 63 output = '<div id="mark-panel">'
72 64 for formatter in formatters:
73 65 output += '<span class="mark_btn"' + \
74 66 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
75 67 '\', \'' + formatter.format_right + '\')">' + \
76 68 formatter.preview_left + formatter.name + \
77 69 formatter.preview_right + '</span>'
78 70
79 71 output += '</div>'
80 72 output += super(FormatPanel, self).render(name, value, attrs=None)
81 73
82 74 return output
83 75
84 76
85 77 class PlainErrorList(ErrorList):
86 78 def __unicode__(self):
87 79 return self.as_text()
88 80
89 81 def as_text(self):
90 82 return ''.join(['(!) %s ' % e for e in self])
91 83
92 84
93 85 class NeboardForm(forms.Form):
94 86 """
95 87 Form with neboard-specific formatting.
96 88 """
97 89
98 90 def as_div(self):
99 91 """
100 92 Returns this form rendered as HTML <as_div>s.
101 93 """
102 94
103 95 return self._html_output(
104 96 # TODO Do not show hidden rows in the list here
105 97 normal_row='<div class="form-row">'
106 98 '<div class="form-label">'
107 99 '%(label)s'
108 100 '</div>'
109 101 '<div class="form-input">'
110 102 '%(field)s'
111 103 '</div>'
112 104 '</div>'
113 105 '<div class="form-row">'
114 106 '%(help_text)s'
115 107 '</div>',
116 108 error_row='<div class="form-row">'
117 109 '<div class="form-label"></div>'
118 110 '<div class="form-errors">%s</div>'
119 111 '</div>',
120 112 row_ender='</div>',
121 113 help_text_html='%s',
122 114 errors_on_separate_row=True)
123 115
124 116 def as_json_errors(self):
125 117 errors = []
126 118
127 119 for name, field in list(self.fields.items()):
128 120 if self[name].errors:
129 121 errors.append({
130 122 'field': name,
131 123 'errors': self[name].errors.as_text(),
132 124 })
133 125
134 126 return errors
135 127
136 128
137 129 class PostForm(NeboardForm):
138 130
139 131 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
140 132 label=LABEL_TITLE)
141 133 text = forms.CharField(
142 134 widget=FormatPanel(attrs={
143 135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
144 136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
145 137 }),
146 138 required=False, label=LABEL_TEXT)
147 image = forms.ImageField(required=False, label=_('Image'),
139 file = forms.FileField(required=False, label=_('File'),
148 140 widget=forms.ClearableFileInput(
149 attrs={'accept': 'image/*'}))
150 image_url = forms.CharField(required=False, label=_('Image URL'),
141 attrs={'accept': 'file/*'}))
142 file_url = forms.CharField(required=False, label=_('File URL'),
151 143 widget=forms.TextInput(
152 144 attrs={ATTRIBUTE_PLACEHOLDER:
153 145 'http://example.com/image.png'}))
154 146
155 147 # This field is for spam prevention only
156 148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
157 149 widget=forms.TextInput(attrs={
158 150 'class': 'form-email'}))
159 151 threads = forms.CharField(required=False, label=_('Additional threads'),
160 152 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
161 153 '123 456 789'}))
162 154
163 155 session = None
164 156 need_to_ban = False
165 157
166 158 def clean_title(self):
167 159 title = self.cleaned_data['title']
168 160 if title:
169 161 if len(title) > TITLE_MAX_LENGTH:
170 162 raise forms.ValidationError(_('Title must have less than %s '
171 163 'characters') %
172 164 str(TITLE_MAX_LENGTH))
173 165 return title
174 166
175 167 def clean_text(self):
176 168 text = self.cleaned_data['text'].strip()
177 169 if text:
178 170 max_length = board_settings.get_int('Forms', 'MaxTextLength')
179 171 if len(text) > max_length:
180 172 raise forms.ValidationError(_('Text must have less than %s '
181 173 'characters') % str(max_length))
182 174 return text
183 175
184 def clean_image(self):
185 image = self.cleaned_data['image']
176 def clean_file(self):
177 file = self.cleaned_data['file']
186 178
187 if image:
188 self.validate_image_size(image.size)
179 if file:
180 self.validate_file_size(file.size)
189 181
190 return image
182 return file
191 183
192 def clean_image_url(self):
193 url = self.cleaned_data['image_url']
184 def clean_file_url(self):
185 url = self.cleaned_data['file_url']
194 186
195 image = None
187 file = None
196 188 if url:
197 image = self._get_image_from_url(url)
189 file = self._get_file_from_url(url)
198 190
199 if not image:
191 if not file:
200 192 raise forms.ValidationError(_('Invalid URL'))
201 193 else:
202 self.validate_image_size(image.size)
194 self.validate_file_size(file.size)
203 195
204 return image
196 return file
205 197
206 198 def clean_threads(self):
207 199 threads_str = self.cleaned_data['threads']
208 200
209 201 if len(threads_str) > 0:
210 202 threads_id_list = threads_str.split(' ')
211 203
212 204 threads = list()
213 205
214 206 for thread_id in threads_id_list:
215 207 try:
216 208 thread = Post.objects.get(id=int(thread_id))
217 209 if not thread.is_opening() or thread.get_thread().archived:
218 210 raise ObjectDoesNotExist()
219 211 threads.append(thread)
220 212 except (ObjectDoesNotExist, ValueError):
221 213 raise forms.ValidationError(_('Invalid additional thread list'))
222 214
223 215 return threads
224 216
225 217 def clean(self):
226 218 cleaned_data = super(PostForm, self).clean()
227 219
228 220 if cleaned_data['email']:
229 221 self.need_to_ban = True
230 222 raise forms.ValidationError('A human cannot enter a hidden field')
231 223
232 224 if not self.errors:
233 self._clean_text_image()
225 self._clean_text_file()
234 226
235 227 if not self.errors and self.session:
236 228 self._validate_posting_speed()
237 229
238 230 return cleaned_data
239 231
240 def get_image(self):
232 def get_file(self):
241 233 """
242 Gets image from file or URL.
234 Gets file from form or URL.
243 235 """
244 236
245 image = self.cleaned_data['image']
246 return image if image else self.cleaned_data['image_url']
237 file = self.cleaned_data['file']
238 return file or self.cleaned_data['file_url']
247 239
248 def _clean_text_image(self):
240 def _clean_text_file(self):
249 241 text = self.cleaned_data.get('text')
250 image = self.get_image()
242 file = self.get_file()
251 243
252 if (not text) and (not image):
253 error_message = _('Either text or image must be entered.')
244 if (not text) and (not file):
245 error_message = _('Either text or file must be entered.')
254 246 self._errors['text'] = self.error_class([error_message])
255 247
256 248 def _validate_posting_speed(self):
257 249 can_post = True
258 250
259 251 posting_delay = settings.POSTING_DELAY
260 252
261 253 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
262 254 now = time.time()
263 255
264 256 current_delay = 0
265 257 need_delay = False
266 258
267 259 if not LAST_POST_TIME in self.session:
268 260 self.session[LAST_POST_TIME] = now
269 261
270 262 need_delay = True
271 263 else:
272 264 last_post_time = self.session.get(LAST_POST_TIME)
273 265 current_delay = int(now - last_post_time)
274 266
275 267 need_delay = current_delay < posting_delay
276 268
277 269 if need_delay:
278 270 error_message = ERROR_SPEED % str(posting_delay
279 271 - current_delay)
280 272 self._errors['text'] = self.error_class([error_message])
281 273
282 274 can_post = False
283 275
284 276 if can_post:
285 277 self.session[LAST_POST_TIME] = now
286 278
287 def validate_image_size(self, size: int):
288 max_size = board_settings.get_int('Forms', 'MaxImageSize')
279 def validate_file_size(self, size: int):
280 max_size = board_settings.get_int('Forms', 'MaxFileSize')
289 281 if size > max_size:
290 282 raise forms.ValidationError(
291 _('Image must be less than %s bytes')
283 _('File must be less than %s bytes')
292 284 % str(max_size))
293 285
294 def _get_image_from_url(self, url: str) -> SimpleUploadedFile:
286 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
295 287 """
296 Gets an image file from URL.
288 Gets an file file from URL.
297 289 """
298 290
299 291 img_temp = None
300 292
301 293 try:
302 294 # Verify content headers
303 295 response_head = requests.head(url, verify=False)
304 296 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
305 if content_type in CONTENT_TYPE_IMAGE:
306 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
307 if length_header:
308 length = int(length_header)
309 self.validate_image_size(length)
310 # Get the actual content into memory
311 response = requests.get(url, verify=False, stream=True)
297 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
298 if length_header:
299 length = int(length_header)
300 self.validate_file_size(length)
301 # Get the actual content into memory
302 response = requests.get(url, verify=False, stream=True)
312 303
313 # Download image, stop if the size exceeds limit
314 size = 0
315 content = b''
316 for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES):
317 size += len(chunk)
318 self.validate_image_size(size)
319 content += chunk
304 # Download file, stop if the size exceeds limit
305 size = 0
306 content = b''
307 for chunk in response.iter_content(file_DOWNLOAD_CHUNK_BYTES):
308 size += len(chunk)
309 self.validate_file_size(size)
310 content += chunk
320 311
321 if response.status_code == HTTP_RESULT_OK and content:
322 # Set a dummy file name that will be replaced
323 # anyway, just keep the valid extension
324 filename = 'image.' + content_type.split('/')[1]
325 img_temp = SimpleUploadedFile(filename, content,
326 content_type)
312 if response.status_code == HTTP_RESULT_OK and content:
313 # Set a dummy file name that will be replaced
314 # anyway, just keep the valid extension
315 filename = 'file.' + content_type.split('/')[1]
316 img_temp = SimpleUploadedFile(filename, content,
317 content_type)
327 318 except Exception:
328 # Just return no image
319 # Just return no file
329 320 pass
330 321
331 322 return img_temp
332 323
333 324
334 325 class ThreadForm(PostForm):
335 326
336 327 tags = forms.CharField(
337 328 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
338 329 max_length=100, label=_('Tags'), required=True)
339 330
340 331 def clean_tags(self):
341 332 tags = self.cleaned_data['tags'].strip()
342 333
343 334 if not tags or not REGEX_TAGS.match(tags):
344 335 raise forms.ValidationError(
345 336 _('Inappropriate characters in tags.'))
346 337
347 338 required_tag_exists = False
348 339 for tag in tags.split():
349 340 try:
350 341 Tag.objects.get(name=tag.strip().lower(), required=True)
351 342 required_tag_exists = True
352 343 break
353 344 except ObjectDoesNotExist:
354 345 pass
355 346
356 347 if not required_tag_exists:
357 348 all_tags = Tag.objects.filter(required=True)
358 349 raise forms.ValidationError(
359 350 _('Need at least one section.'))
360 351
361 352 return tags
362 353
363 354 def clean(self):
364 355 cleaned_data = super(ThreadForm, self).clean()
365 356
366 357 return cleaned_data
367 358
368 359
369 360 class SettingsForm(NeboardForm):
370 361
371 362 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
372 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
363 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('image view mode'))
373 364 username = forms.CharField(label=_('User name'), required=False)
374 365 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
375 366
376 367 def clean_username(self):
377 368 username = self.cleaned_data['username']
378 369
379 370 if username and not REGEX_TAGS.match(username):
380 371 raise forms.ValidationError(_('Inappropriate characters.'))
381 372
382 373 return username
383 374
384 375
385 376 class SearchForm(NeboardForm):
386 377 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,8 +1,9 b''
1 1 __author__ = 'neko259'
2 2
3 3 from boards.models.image import PostImage
4 from boards.models.attachment import Attachment
4 5 from boards.models.thread import Thread
5 6 from boards.models.post import Post
6 7 from boards.models.tag import Tag
7 8 from boards.models.user import Ban
8 9 from boards.models.banner import Banner
@@ -1,123 +1,121 b''
1 1 import hashlib
2 2 import os
3 3 from random import random
4 4 import time
5 5
6 6 from django.db import models
7 7 from django.template.defaultfilters import filesizeformat
8 8
9 9 from boards import thumbs
10 10 import boards
11 11 from boards.models.base import Viewable
12 12
13 13 __author__ = 'neko259'
14 14
15 15
16 16 IMAGE_THUMB_SIZE = (200, 150)
17 17 IMAGES_DIRECTORY = 'images/'
18 18 FILE_EXTENSION_DELIMITER = '.'
19 19 HASH_LENGTH = 36
20 20
21 21 CSS_CLASS_IMAGE = 'image'
22 22 CSS_CLASS_THUMB = 'thumb'
23 23
24 24
25 25 class PostImageManager(models.Manager):
26 26 def create_with_hash(self, image):
27 27 image_hash = self.get_hash(image)
28 28 existing = self.filter(hash=image_hash)
29 29 if len(existing) > 0:
30 30 post_image = existing[0]
31 31 else:
32 32 post_image = PostImage.objects.create(image=image)
33 33
34 34 return post_image
35 35
36 36 def get_hash(self, image):
37 37 """
38 38 Gets hash of an image.
39 39 """
40 40 md5 = hashlib.md5()
41 41 for chunk in image.chunks():
42 42 md5.update(chunk)
43 43 return md5.hexdigest()
44 44
45 45 def get_random_images(self, count, include_archived=False, tags=None):
46 46 images = self.filter(post_images__thread__archived=include_archived)
47 47 if tags is not None:
48 48 images = images.filter(post_images__threads__tags__in=tags)
49 49 return images.order_by('?')[:count]
50 50
51 51
52 52 class PostImage(models.Model, Viewable):
53 53 objects = PostImageManager()
54 54
55 55 class Meta:
56 56 app_label = 'boards'
57 57 ordering = ('id',)
58 58
59 59 def _update_image_filename(self, filename):
60 60 """
61 61 Gets unique image filename
62 62 """
63 63
64 path = IMAGES_DIRECTORY
65
66 64 # TODO Use something other than random number in file name
67 65 new_name = '{}{}.{}'.format(
68 66 str(int(time.mktime(time.gmtime()))),
69 67 str(int(random() * 1000)),
70 68 filename.split(FILE_EXTENSION_DELIMITER)[-1:][0])
71 69
72 return os.path.join(path, new_name)
70 return os.path.join(IMAGES_DIRECTORY, new_name)
73 71
74 72 width = models.IntegerField(default=0)
75 73 height = models.IntegerField(default=0)
76 74
77 75 pre_width = models.IntegerField(default=0)
78 76 pre_height = models.IntegerField(default=0)
79 77
80 78 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
81 79 blank=True, sizes=(IMAGE_THUMB_SIZE,),
82 80 width_field='width',
83 81 height_field='height',
84 82 preview_width_field='pre_width',
85 83 preview_height_field='pre_height')
86 84 hash = models.CharField(max_length=HASH_LENGTH)
87 85
88 86 def save(self, *args, **kwargs):
89 87 """
90 88 Saves the model and computes the image hash for deduplication purposes.
91 89 """
92 90
93 91 if not self.pk and self.image:
94 92 self.hash = PostImage.objects.get_hash(self.image)
95 93 super(PostImage, self).save(*args, **kwargs)
96 94
97 95 def __str__(self):
98 96 return self.image.url
99 97
100 98 def get_view(self):
101 99 metadata = '{}, {}'.format(self.image.name.split('.')[-1],
102 100 filesizeformat(self.image.size))
103 101 return '<div class="{}">' \
104 102 '<a class="{}" href="{full}">' \
105 103 '<img class="post-image-preview"' \
106 104 ' src="{}"' \
107 105 ' alt="{}"' \
108 106 ' width="{}"' \
109 107 ' height="{}"' \
110 108 ' data-width="{}"' \
111 109 ' data-height="{}" />' \
112 110 '</a>' \
113 111 '<div class="image-metadata">{image_meta}</div>' \
114 112 '</div>'\
115 113 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB,
116 114 self.image.url_200x150,
117 115 str(self.hash), str(self.pre_width),
118 116 str(self.pre_height), str(self.width), str(self.height),
119 117 full=self.image.url, image_meta=metadata)
120 118
121 119 def get_random_associated_post(self):
122 120 posts = boards.models.Post.objects.filter(images__in=[self])
123 121 return posts.order_by('?').first()
@@ -1,418 +1,432 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 from boards.models import PostImage
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 IMAGE_TYPES = (
66 'jpeg',
67 'jpg',
68 'png',
69 'bmp',
70 )
71
65 72
66 73 class PostManager(models.Manager):
67 74 @transaction.atomic
68 def create_post(self, title: str, text: str, image=None, thread=None,
75 def create_post(self, title: str, text: str, file=None, thread=None,
69 76 ip=NO_IP, tags: list=None, opening_posts: list=None):
70 77 """
71 78 Creates new post
72 79 """
73 80
74 81 is_banned = Ban.objects.filter(ip=ip).exists()
75 82
76 83 # TODO Raise specific exception and catch it in the views
77 84 if is_banned:
78 85 raise Exception("This user is banned")
79 86
80 87 if not tags:
81 88 tags = []
82 89 if not opening_posts:
83 90 opening_posts = []
84 91
85 92 posting_time = timezone.now()
86 93 new_thread = False
87 94 if not thread:
88 95 thread = boards.models.thread.Thread.objects.create(
89 96 bump_time=posting_time, last_edit_time=posting_time)
90 97 list(map(thread.tags.add, tags))
91 98 boards.models.thread.Thread.objects.process_oldest_threads()
92 99 new_thread = True
93 100
94 101 pre_text = Parser().preparse(text)
95 102
96 103 post = self.create(title=title,
97 104 text=pre_text,
98 105 pub_time=posting_time,
99 106 poster_ip=ip,
100 107 thread=thread,
101 108 last_edit_time=posting_time)
102 109 post.threads.add(thread)
103 110
104 111 logger = logging.getLogger('boards.post.create')
105 112
106 113 logger.info('Created post {} by {}'.format(post, post.poster_ip))
107 114
108 if image:
109 post.images.add(PostImage.objects.create_with_hash(image))
115 # TODO Move this to other place
116 if file:
117 file_type = file.name.split('.')[-1].lower()
118 if file_type in IMAGE_TYPES:
119 post.images.add(PostImage.objects.create_with_hash(file))
120 else:
121 post.attachments.add(Attachment.objects.create_with_hash(file))
110 122
111 123 post.build_url()
112 124 post.connect_replies()
113 125 post.connect_threads(opening_posts)
114 126 post.connect_notifications()
115 127
116 128 # Thread needs to be bumped only when the post is already created
117 129 if not new_thread:
118 130 thread.last_edit_time = posting_time
119 131 thread.bump()
120 132 thread.save()
121 133
122 134 return post
123 135
124 136 def delete_posts_by_ip(self, ip):
125 137 """
126 138 Deletes all posts of the author with same IP
127 139 """
128 140
129 141 posts = self.filter(poster_ip=ip)
130 142 for post in posts:
131 143 post.delete()
132 144
133 145 @utils.cached_result()
134 146 def get_posts_per_day(self) -> float:
135 147 """
136 148 Gets average count of posts per day for the last 7 days
137 149 """
138 150
139 151 day_end = date.today()
140 152 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
141 153
142 154 day_time_start = timezone.make_aware(datetime.combine(
143 155 day_start, dtime()), timezone.get_current_timezone())
144 156 day_time_end = timezone.make_aware(datetime.combine(
145 157 day_end, dtime()), timezone.get_current_timezone())
146 158
147 159 posts_per_period = float(self.filter(
148 160 pub_time__lte=day_time_end,
149 161 pub_time__gte=day_time_start).count())
150 162
151 163 ppd = posts_per_period / POSTS_PER_DAY_RANGE
152 164
153 165 return ppd
154 166
155 167
156 168 class Post(models.Model, Viewable):
157 169 """A post is a message."""
158 170
159 171 objects = PostManager()
160 172
161 173 class Meta:
162 174 app_label = APP_LABEL_BOARDS
163 175 ordering = ('id',)
164 176
165 177 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
166 178 pub_time = models.DateTimeField()
167 179 text = TextField(blank=True, null=True)
168 180 _text_rendered = TextField(blank=True, null=True, editable=False)
169 181
170 182 images = models.ManyToManyField(PostImage, null=True, blank=True,
171 183 related_name='post_images', db_index=True)
184 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
185 related_name='attachment_posts')
172 186
173 187 poster_ip = models.GenericIPAddressField()
174 188
175 189 # TODO This field can be removed cause UID is used for update now
176 190 last_edit_time = models.DateTimeField()
177 191
178 192 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
179 193 null=True,
180 194 blank=True, related_name='refposts',
181 195 db_index=True)
182 196 refmap = models.TextField(null=True, blank=True)
183 197 threads = models.ManyToManyField('Thread', db_index=True)
184 198 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
185 199
186 200 url = models.TextField()
187 201 uid = models.TextField(db_index=True)
188 202
189 203 def __str__(self):
190 204 return 'P#{}/{}'.format(self.id, self.title)
191 205
192 206 def get_referenced_posts(self):
193 207 threads = self.get_threads().all()
194 208 return self.referenced_posts.filter(threads__in=threads)\
195 209 .order_by('pub_time').distinct().all()
196 210
197 211 def get_title(self) -> str:
198 212 """
199 213 Gets original post title or part of its text.
200 214 """
201 215
202 216 title = self.title
203 217 if not title:
204 218 title = self.get_text()
205 219
206 220 return title
207 221
208 222 def build_refmap(self) -> None:
209 223 """
210 224 Builds a replies map string from replies list. This is a cache to stop
211 225 the server from recalculating the map on every post show.
212 226 """
213 227
214 228 post_urls = [REFMAP_STR.format(refpost.get_absolute_url(), refpost.id)
215 229 for refpost in self.referenced_posts.all()]
216 230
217 231 self.refmap = ', '.join(post_urls)
218 232
219 233 def is_referenced(self) -> bool:
220 234 return self.refmap and len(self.refmap) > 0
221 235
222 236 def is_opening(self) -> bool:
223 237 """
224 238 Checks if this is an opening post or just a reply.
225 239 """
226 240
227 241 return self.get_thread().get_opening_post_id() == self.id
228 242
229 243 def get_absolute_url(self):
230 244 if self.url:
231 245 return self.url
232 246 else:
233 247 opening_id = self.get_thread().get_opening_post_id()
234 248 post_url = reverse('thread', kwargs={'post_id': opening_id})
235 249 if self.id != opening_id:
236 250 post_url += '#' + str(self.id)
237 251 return post_url
238 252
239 253
240 254 def get_thread(self):
241 255 return self.thread
242 256
243 257 def get_threads(self) -> QuerySet:
244 258 """
245 259 Gets post's thread.
246 260 """
247 261
248 262 return self.threads
249 263
250 264 def get_view(self, *args, **kwargs) -> str:
251 265 """
252 266 Renders post's HTML view. Some of the post params can be passed over
253 267 kwargs for the means of caching (if we view the thread, some params
254 268 are same for every post and don't need to be computed over and over.
255 269 """
256 270
257 271 thread = self.get_thread()
258 272 is_opening = kwargs.get(PARAMETER_IS_OPENING, self.is_opening())
259 273
260 274 if is_opening:
261 275 opening_post_id = self.id
262 276 else:
263 277 opening_post_id = thread.get_opening_post_id()
264 278
265 279 css_class = 'post'
266 280 if thread.archived:
267 281 css_class += ' archive_post'
268 282 elif not thread.can_bump():
269 283 css_class += ' dead_post'
270 284
271 285 params = dict()
272 286 for param in POST_VIEW_PARAMS:
273 287 if param in kwargs:
274 288 params[param] = kwargs[param]
275 289
276 290 params.update({
277 291 PARAMETER_POST: self,
278 292 PARAMETER_IS_OPENING: is_opening,
279 293 PARAMETER_THREAD: thread,
280 294 PARAMETER_CSS_CLASS: css_class,
281 295 PARAMETER_OP_ID: opening_post_id,
282 296 })
283 297
284 298 return render_to_string('boards/post.html', params)
285 299
286 300 def get_search_view(self, *args, **kwargs):
287 301 return self.get_view(need_op_data=True, *args, **kwargs)
288 302
289 303 def get_first_image(self) -> PostImage:
290 304 return self.images.earliest('id')
291 305
292 306 def delete(self, using=None):
293 307 """
294 308 Deletes all post images and the post itself.
295 309 """
296 310
297 311 for image in self.images.all():
298 312 image_refs_count = Post.objects.filter(images__in=[image]).count()
299 313 if image_refs_count == 1:
300 314 image.delete()
301 315
302 316 thread = self.get_thread()
303 317 thread.last_edit_time = timezone.now()
304 318 thread.save()
305 319
306 320 super(Post, self).delete(using)
307 321
308 322 logging.getLogger('boards.post.delete').info(
309 323 'Deleted post {}'.format(self))
310 324
311 325 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
312 326 include_last_update=False) -> str:
313 327 """
314 328 Gets post HTML or JSON data that can be rendered on a page or used by
315 329 API.
316 330 """
317 331
318 332 return get_exporter(format_type).export(self, request,
319 333 include_last_update)
320 334
321 335 def notify_clients(self, recursive=True):
322 336 """
323 337 Sends post HTML data to the thread web socket.
324 338 """
325 339
326 340 if not settings.get_bool('External', 'WebsocketsEnabled'):
327 341 return
328 342
329 343 thread_ids = list()
330 344 for thread in self.get_threads().all():
331 345 thread_ids.append(thread.id)
332 346
333 347 thread.notify_clients()
334 348
335 349 if recursive:
336 350 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
337 351 post_id = reply_number.group(1)
338 352
339 353 try:
340 354 ref_post = Post.objects.get(id=post_id)
341 355
342 356 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
343 357 # If post is in this thread, its thread was already notified.
344 358 # Otherwise, notify its thread separately.
345 359 ref_post.notify_clients(recursive=False)
346 360 except ObjectDoesNotExist:
347 361 pass
348 362
349 363 def build_url(self):
350 364 self.url = self.get_absolute_url()
351 365 self.save(update_fields=['url'])
352 366
353 367 def save(self, force_insert=False, force_update=False, using=None,
354 368 update_fields=None):
355 369 self._text_rendered = Parser().parse(self.get_raw_text())
356 370
357 371 self.uid = str(uuid.uuid4())
358 372 if update_fields is not None and 'uid' not in update_fields:
359 373 update_fields += ['uid']
360 374
361 375 if self.id:
362 376 for thread in self.get_threads().all():
363 377 thread.last_edit_time = self.last_edit_time
364 378
365 379 thread.save(update_fields=['last_edit_time', 'bumpable'])
366 380
367 381 super().save(force_insert, force_update, using, update_fields)
368 382
369 383 def get_text(self) -> str:
370 384 return self._text_rendered
371 385
372 386 def get_raw_text(self) -> str:
373 387 return self.text
374 388
375 389 def get_absolute_id(self) -> str:
376 390 """
377 391 If the post has many threads, shows its main thread OP id in the post
378 392 ID.
379 393 """
380 394
381 395 if self.get_threads().count() > 1:
382 396 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
383 397 else:
384 398 return str(self.id)
385 399
386 400 def connect_notifications(self):
387 401 for reply_number in re.finditer(REGEX_NOTIFICATION, self.get_raw_text()):
388 402 user_name = reply_number.group(1).lower()
389 403 Notification.objects.get_or_create(name=user_name, post=self)
390 404
391 405 def connect_replies(self):
392 406 """
393 407 Connects replies to a post to show them as a reflink map
394 408 """
395 409
396 410 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
397 411 post_id = reply_number.group(1)
398 412
399 413 try:
400 414 referenced_post = Post.objects.get(id=post_id)
401 415
402 416 referenced_post.referenced_posts.add(self)
403 417 referenced_post.last_edit_time = self.pub_time
404 418 referenced_post.build_refmap()
405 419 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
406 420 except ObjectDoesNotExist:
407 421 pass
408 422
409 423 def connect_threads(self, opening_posts):
410 424 for opening_post in opening_posts:
411 425 threads = opening_post.get_threads().all()
412 426 for thread in threads:
413 427 if thread.can_bump():
414 428 thread.update_bump_status()
415 429
416 430 thread.last_edit_time = self.last_edit_time
417 431 thread.save(update_fields=['last_edit_time', 'bumpable'])
418 432 self.threads.add(opening_post.get_thread())
@@ -1,103 +1,110 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 11 {% comment %}
12 12 Thread death time needs to be shown only if the thread is alredy archived
13 13 and this is an opening post (thread death time) or a post for popup
14 14 (we don't see OP here so we show the death time in the post itself).
15 15 {% endcomment %}
16 16 {% if thread.archived %}
17 17 {% if is_opening %}
18 18 β€” <time datetime="{{ thread.bump_time|date:'c' }}">{{ thread.bump_time }}</time>
19 19 {% endif %}
20 20 {% endif %}
21 21 {% if is_opening %}
22 22 {% if need_open_link %}
23 23 {% if thread.archived %}
24 24 <a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>
25 25 {% else %}
26 26 <a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>
27 27 {% endif %}
28 28 {% endif %}
29 29 {% else %}
30 30 {% if need_op_data %}
31 31 {% with thread.get_opening_post as op %}
32 32 {% trans " in " %}<a href="{{ op.get_absolute_url }}">&gt;&gt;{{ op.id }}</a> <span class="title">{{ op.get_title|striptags|truncatewords:5 }}</span>
33 33 {% endwith %}
34 34 {% endif %}
35 35 {% endif %}
36 36 {% if reply_link and not thread.archived %}
37 37 <a href="#form" onclick="addQuickReply('{{ post.id }}'); return false;">{% trans 'Reply' %}</a>
38 38 {% endif %}
39 39
40 40 {% if moderator %}
41 41 <span class="moderator_info">
42 42 | <a href="{% url 'admin:boards_post_change' post.id %}">{% trans 'Edit' %}</a>
43 43 {% if is_opening %}
44 44 | <a href="{% url 'admin:boards_thread_change' thread.id %}">{% trans 'Edit thread' %}</a>
45 45 {% endif %}
46 46 </span>
47 47 {% endif %}
48 48 </div>
49 49 {% comment %}
50 50 Post images. Currently only 1 image can be posted and shown, but post model
51 51 supports multiple.
52 52 {% endcomment %}
53 53 {% if post.images.exists %}
54 {% with post.images.all.0 as image %}
54 {% with post.images.first as image %}
55 55 {% autoescape off %}
56 56 {{ image.get_view }}
57 57 {% endautoescape %}
58 58 {% endwith %}
59 59 {% endif %}
60 {% if post.attachments.exists %}
61 {% with post.attachments.first as file %}
62 {% autoescape off %}
63 {{ file.get_view }}
64 {% endautoescape %}
65 {% endwith %}
66 {% endif %}
60 67 {% comment %}
61 68 Post message (text)
62 69 {% endcomment %}
63 70 <div class="message">
64 71 {% autoescape off %}
65 72 {% if truncated %}
66 73 {{ post.get_text|truncatewords_html:50 }}
67 74 {% else %}
68 75 {{ post.get_text }}
69 76 {% endif %}
70 77 {% endautoescape %}
71 78 {% if post.is_referenced %}
72 79 {% if mode_tree %}
73 80 <div class="tree_reply">
74 81 {% for refpost in post.get_referenced_posts %}
75 82 {% post_view refpost mode_tree=True %}
76 83 {% endfor %}
77 84 </div>
78 85 {% else %}
79 86 <div class="refmap">
80 87 {% autoescape off %}
81 88 {% trans "Replies" %}: {{ post.refmap }}
82 89 {% endautoescape %}
83 90 </div>
84 91 {% endif %}
85 92 {% endif %}
86 93 </div>
87 94 {% comment %}
88 95 Thread metadata: counters, tags etc
89 96 {% endcomment %}
90 97 {% if is_opening %}
91 98 <div class="metadata">
92 99 {% if is_opening and need_open_link %}
93 100 {{ thread.get_reply_count }} {% trans 'messages' %},
94 101 {{ thread.get_images_count }} {% trans 'images' %}.
95 102 {% endif %}
96 103 <span class="tags">
97 104 {% autoescape off %}
98 105 {{ thread.get_tag_url_list }}
99 106 {% endautoescape %}
100 107 </span>
101 108 </div>
102 109 {% endif %}
103 110 </div>
@@ -1,169 +1,169 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 image = form.get_image()
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 post = Post.objects.create_post(title=title, text=text, image=image,
153 post = Post.objects.create_post(title=title, text=text, file=file,
154 154 ip=ip, tags=tags, opening_posts=threads)
155 155
156 156 # This is required to update the threads to which posts we have replied
157 157 # when creating this one
158 158 post.notify_clients()
159 159
160 160 if html_response:
161 161 return redirect(post.get_absolute_url())
162 162
163 163 def get_threads(self):
164 164 """
165 165 Gets list of threads that will be shown on a page.
166 166 """
167 167
168 168 return Thread.objects.order_by('-bump_time')\
169 169 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,138 +1,138 b''
1 1 from django.core.exceptions import ObjectDoesNotExist
2 2 from django.http import Http404
3 3 from django.shortcuts import get_object_or_404, render, redirect
4 4 from django.views.generic.edit import FormMixin
5 5 from django.utils import timezone
6 6 from django.utils.dateformat import format
7 7
8 8 from boards import utils, settings
9 9 from boards.forms import PostForm, PlainErrorList
10 10 from boards.models import Post
11 11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 12 from boards.views.posting_mixin import PostMixin
13 13
14 14 import neboard
15 15
16 16
17 17 CONTEXT_LASTUPDATE = "last_update"
18 18 CONTEXT_THREAD = 'thread'
19 19 CONTEXT_WS_TOKEN = 'ws_token'
20 20 CONTEXT_WS_PROJECT = 'ws_project'
21 21 CONTEXT_WS_HOST = 'ws_host'
22 22 CONTEXT_WS_PORT = 'ws_port'
23 23 CONTEXT_WS_TIME = 'ws_token_time'
24 24 CONTEXT_MODE = 'mode'
25 25 CONTEXT_OP = 'opening_post'
26 26
27 27 FORM_TITLE = 'title'
28 28 FORM_TEXT = 'text'
29 29 FORM_IMAGE = 'image'
30 30 FORM_THREADS = 'threads'
31 31
32 32
33 33 class ThreadView(BaseBoardView, PostMixin, FormMixin):
34 34
35 35 def get(self, request, post_id, form: PostForm=None):
36 36 try:
37 37 opening_post = Post.objects.get(id=post_id)
38 38 except ObjectDoesNotExist:
39 39 raise Http404
40 40
41 41 # If this is not OP, don't show it as it is
42 42 if not opening_post.is_opening():
43 43 return redirect(opening_post.get_thread().get_opening_post()
44 44 .get_absolute_url())
45 45
46 46 if not form:
47 47 form = PostForm(error_class=PlainErrorList)
48 48
49 49 thread_to_show = opening_post.get_thread()
50 50
51 51 params = dict()
52 52
53 53 params[CONTEXT_FORM] = form
54 54 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
55 55 params[CONTEXT_THREAD] = thread_to_show
56 56 params[CONTEXT_MODE] = self.get_mode()
57 57 params[CONTEXT_OP] = opening_post
58 58
59 59 if settings.get_bool('External', 'WebsocketsEnabled'):
60 60 token_time = format(timezone.now(), u'U')
61 61
62 62 params[CONTEXT_WS_TIME] = token_time
63 63 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
64 64 timestamp=token_time)
65 65 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
66 66 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
67 67 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
68 68
69 69 params.update(self.get_data(thread_to_show))
70 70
71 71 return render(request, self.get_template(), params)
72 72
73 73 def post(self, request, post_id):
74 74 opening_post = get_object_or_404(Post, id=post_id)
75 75
76 76 # If this is not OP, don't show it as it is
77 77 if not opening_post.is_opening():
78 78 raise Http404
79 79
80 80 if not opening_post.get_thread().archived:
81 81 form = PostForm(request.POST, request.FILES,
82 82 error_class=PlainErrorList)
83 83 form.session = request.session
84 84
85 85 if form.is_valid():
86 86 return self.new_post(request, form, opening_post)
87 87 if form.need_to_ban:
88 88 # Ban user because he is suspected to be a bot
89 89 self._ban_current_user(request)
90 90
91 91 return self.get(request, post_id, form)
92 92
93 93 def new_post(self, request, form: PostForm, opening_post: Post=None,
94 94 html_response=True):
95 95 """
96 96 Adds a new post (in thread or as a reply).
97 97 """
98 98
99 99 ip = utils.get_client_ip(request)
100 100
101 101 data = form.cleaned_data
102 102
103 103 title = data[FORM_TITLE]
104 104 text = data[FORM_TEXT]
105 image = form.get_image()
105 file = form.get_file()
106 106 threads = data[FORM_THREADS]
107 107
108 108 text = self._remove_invalid_links(text)
109 109
110 110 post_thread = opening_post.get_thread()
111 111
112 post = Post.objects.create_post(title=title, text=text, image=image,
112 post = Post.objects.create_post(title=title, text=text, file=file,
113 113 thread=post_thread, ip=ip,
114 114 opening_posts=threads)
115 115 post.notify_clients()
116 116
117 117 if html_response:
118 118 if opening_post:
119 119 return redirect(post.get_absolute_url())
120 120 else:
121 121 return post
122 122
123 123 def get_data(self, thread) -> dict:
124 124 """
125 125 Returns context params for the view.
126 126 """
127 127
128 128 return dict()
129 129
130 130 def get_template(self) -> str:
131 131 """
132 132 Gets template to show the thread mode on.
133 133 """
134 134
135 135 pass
136 136
137 137 def get_mode(self) -> str:
138 138 pass
General Comments 0
You need to be logged in to leave comments. Login now