##// 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
@@ -9,7 +9,7 b' CacheTimeout = 600'
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]
@@ -18,14 +18,6 b' import boards.settings as board_settings'
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
@@ -47,7 +39,7 b" ERROR_SPEED = _('Please wait %s seconds "
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
@@ -144,10 +136,10 b' class PostForm(NeboardForm):'
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'}))
@@ -181,27 +173,27 b' class PostForm(NeboardForm):'
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']
@@ -230,27 +222,27 b' class PostForm(NeboardForm):'
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):
@@ -284,16 +276,16 b' class PostForm(NeboardForm):'
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
@@ -302,30 +294,29 b' class PostForm(NeboardForm):'
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
@@ -369,7 +360,7 b' class ThreadForm(PostForm):'
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
@@ -1,6 +1,7 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
@@ -61,15 +61,13 b' class PostImage(models.Model, Viewable):'
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)
@@ -13,7 +13,7 b' 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
@@ -62,10 +62,17 b' POST_VIEW_PARAMS = ('
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
@@ -105,8 +112,13 b' class PostManager(models.Manager):'
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()
@@ -169,6 +181,8 b' class Post(models.Model, Viewable):'
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
@@ -51,12 +51,19 b''
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 %}
@@ -141,7 +141,7 b' class AllThreadsView(PostMixin, BaseBoar'
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)
@@ -150,7 +150,7 b' class AllThreadsView(PostMixin, BaseBoar'
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
@@ -102,14 +102,14 b' class ThreadView(BaseBoardView, PostMixi'
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()
General Comments 0
You need to be logged in to leave comments. Login now