##// END OF EJS Templates
Show attachment alias if it has one
neko259 -
r1912:6d13074b default
parent child Browse files
Show More
@@ -1,158 +1,159 b''
1 1 from itertools import zip_longest
2 2
3 3 import boards
4 4 from boards.models import STATUS_ARCHIVE
5 5 from django.core.files.images import get_image_dimensions
6 6 from django.db import models
7 7
8 8 from boards import utils
9 9 from boards.models.attachment.viewers import get_viewers, AbstractViewer, \
10 10 FILE_TYPES_IMAGE
11 11 from boards.utils import get_upload_filename, get_extension, cached_result, \
12 12 get_file_mimetype
13 13
14 14
15 15 class AttachmentManager(models.Manager):
16 16 def create_with_hash(self, file):
17 17 file_hash = utils.get_file_hash(file)
18 18 attachment = self.get_existing_duplicate(file_hash, file)
19 19 if not attachment:
20 20 file_type = get_file_mimetype(file)
21 21 attachment = self.create(file=file, mimetype=file_type,
22 22 hash=file_hash)
23 23
24 24 return attachment
25 25
26 26 def create_from_url(self, url):
27 27 existing = self.filter(url=url)
28 28 if len(existing) > 0:
29 29 attachment = existing[0]
30 30 else:
31 31 attachment = self.create(url=url)
32 32 return attachment
33 33
34 34 def get_random_images(self, count, tags=None):
35 35 images = self.filter(mimetype__in=FILE_TYPES_IMAGE).exclude(
36 36 attachment_posts__thread__status=STATUS_ARCHIVE)
37 37 if tags is not None:
38 38 images = images.filter(attachment_posts__threads__tags__in=tags)
39 39 return images.order_by('?')[:count]
40 40
41 41 def get_existing_duplicate(self, file_hash, file):
42 42 """
43 43 Gets an attachment with the same file if one exists.
44 44 """
45 45 existing = self.filter(hash=file_hash)
46 46 attachment = None
47 47 for existing_attachment in existing:
48 48 existing_file = existing_attachment.file
49 49
50 50 file_chunks = file.chunks()
51 51 existing_file_chunks = existing_file.chunks()
52 52
53 53 if self._compare_chunks(file_chunks, existing_file_chunks):
54 54 attachment = existing_attachment
55 55 return attachment
56 56
57 57 def _compare_chunks(self, chunks1, chunks2):
58 58 """
59 59 Compares 2 chunks of different sizes (e.g. first chunk array contains
60 60 all data in 1 chunk, and other one -- in a multiple of smaller ones.
61 61 """
62 62 equal = True
63 63
64 64 position1 = 0
65 65 position2 = 0
66 66 chunk1 = None
67 67 chunk2 = None
68 68 chunk1ended = False
69 69 chunk2ended = False
70 70 while True:
71 71 if not chunk1 or len(chunk1) <= position1:
72 72 try:
73 73 chunk1 = chunks1.__next__()
74 74 position1 = 0
75 75 except StopIteration:
76 76 chunk1ended = True
77 77 if not chunk2 or len(chunk2) <= position2:
78 78 try:
79 79 chunk2 = chunks2.__next__()
80 80 position2 = 0
81 81 except StopIteration:
82 82 chunk2ended = True
83 83
84 84 if chunk1ended and chunk2ended:
85 85 # Same size chunksm checked for equality previously
86 86 break
87 87 elif chunk1ended or chunk2ended:
88 88 # Different size chunks, not equal
89 89 equal = False
90 90 break
91 91 elif chunk1[position1] != chunk2[position2]:
92 92 # Different bytes, not equal
93 93 equal = False
94 94 break
95 95 else:
96 96 position1 += 1
97 97 position2 += 1
98 98 return equal
99 99
100 100
101 101 class Attachment(models.Model):
102 102 objects = AttachmentManager()
103 103
104 104 class Meta:
105 105 app_label = 'boards'
106 106 ordering = ('id',)
107 107
108 108 file = models.FileField(upload_to=get_upload_filename, null=True)
109 109 mimetype = models.CharField(max_length=200, null=True)
110 110 hash = models.CharField(max_length=36, null=True)
111 111 alias = models.TextField(unique=True, null=True)
112 112 url = models.TextField(blank=True, default='')
113 113
114 114 def get_view(self):
115 115 file_viewer = None
116 116 for viewer in get_viewers():
117 117 if viewer.supports(self.mimetype):
118 118 file_viewer = viewer
119 119 break
120 120 if file_viewer is None:
121 121 file_viewer = AbstractViewer
122 122
123 return file_viewer(self.file, self.mimetype, self.hash, self.url).get_view()
123 return file_viewer(self.file, self.mimetype, self.hash, self.url,
124 self.alias).get_view()
124 125
125 126 def __str__(self):
126 127 return self.url or self.file.url
127 128
128 129 def get_random_associated_post(self):
129 130 posts = boards.models.Post.objects.filter(attachments__in=[self])
130 131 return posts.order_by('?').first()
131 132
132 133 @cached_result()
133 134 def get_size(self):
134 135 if self.file:
135 136 if self.mimetype in FILE_TYPES_IMAGE:
136 137 return get_image_dimensions(self.file)
137 138 else:
138 139 return 200, 150
139 140
140 141 def get_thumb_url(self):
141 142 split = self.file.url.rsplit('.', 1)
142 143 w, h = 200, 150
143 144 return '%s.%sx%s.%s' % (split[0], w, h, split[1])
144 145
145 146 @cached_result()
146 147 def get_preview_size(self):
147 148 size = 200, 150
148 149 if self.mimetype in FILE_TYPES_IMAGE:
149 150 preview_path = self.file.path.replace('.', '.200x150.')
150 151 try:
151 152 size = get_image_dimensions(preview_path)
152 153 except Exception:
153 154 pass
154 155
155 156 return size
156 157
157 158 def is_internal(self):
158 159 return self.url is None or len(self.url) == 0
@@ -1,244 +1,247 b''
1 1 import re
2 2
3 3 from PIL import Image
4 4
5 5 from django.contrib.staticfiles import finders
6 6 from django.contrib.staticfiles.templatetags.staticfiles import static
7 7 from django.core.files.images import get_image_dimensions
8 8 from django.template.defaultfilters import filesizeformat
9 9 from django.core.urlresolvers import reverse
10 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 11
12 12 from boards.utils import get_domain, cached_result, get_extension
13 13 from boards import settings
14 14
15 15
16 16 FILE_STUB_IMAGE = 'images/file.png'
17 17 FILE_STUB_URL = 'url'
18 18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19 19
20 20
21 21 FILE_TYPES_VIDEO = (
22 22 'video/webm',
23 23 'video/mp4',
24 24 'video/mpeg',
25 25 'video/ogv',
26 26 )
27 27 FILE_TYPE_SVG = 'image/svg+xml'
28 28 FILE_TYPES_AUDIO = (
29 29 'audio/ogg',
30 30 'audio/mpeg',
31 31 'audio/opus',
32 32 'audio/x-flac',
33 33 'audio/mpeg',
34 34 )
35 35 FILE_TYPES_IMAGE = (
36 36 'image/jpeg',
37 37 'image/jpg',
38 38 'image/png',
39 39 'image/bmp',
40 40 'image/gif',
41 41 )
42 42
43 43 PLAIN_FILE_FORMATS = {
44 44 'zip': 'archive',
45 45 'tar': 'archive',
46 46 'gz': 'archive',
47 47 'mid' : 'midi',
48 48 }
49 49
50 50 URL_PROTOCOLS = {
51 51 'magnet': 'magnet',
52 52 }
53 53
54 54 CSS_CLASS_IMAGE = 'image'
55 55 CSS_CLASS_THUMB = 'thumb'
56 56
57 57 ABSTRACT_VIEW = '<div class="image">'\
58 58 '{}'\
59 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
59 '<div class="image-metadata">{}<a href="{}" download >{}, {}</a>'\
60 60 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
61 61 '</div>'
62 62 URL_VIEW = '<div class="image">' \
63 63 '{}' \
64 64 '<div class="image-metadata">{}</div>' \
65 65 '</div>'
66 66 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
67 67 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
68 68 '</a>'
69 69 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
70 70 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
71 71 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
72 72 '<img class="post-image-preview"' \
73 73 ' src="{}"' \
74 74 ' alt="{}"' \
75 75 ' width="{}"' \
76 76 ' height="{}"' \
77 77 ' data-width="{}"' \
78 78 ' data-height="{}" />' \
79 79 '</a>'
80 80 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
81 81 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
82 82 '</a>'
83 83 URL_FORMAT_VIEW = '<a href="{}">' \
84 84 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
85 85 '</a>'
86 86
87 87
88 88 def get_viewers():
89 89 return AbstractViewer.__subclasses__()
90 90
91 91
92 92 def get_static_dimensions(filename):
93 93 file_path = finders.find(filename)
94 94 return get_image_dimensions(file_path)
95 95
96 96
97 97 # TODO Move this to utils
98 98 def file_exists(filename):
99 99 return finders.find(filename) is not None
100 100
101 101
102 102 class AbstractViewer:
103 def __init__(self, file, file_type, hash, url):
103 def __init__(self, file, file_type, hash, url, alias):
104 104 self.file = file
105 105 self.file_type = file_type
106 106 self.hash = hash
107 107 self.url = url
108 108 self.extension = get_extension(self.file.name)
109 self.alias = alias
109 110
110 111 @staticmethod
111 112 def supports(file_type):
112 113 return True
113 114
114 115 def get_view(self):
115 116 search_host = settings.get('External', 'ImageSearchHost')
116 117 if search_host:
117 118 if search_host.endswith('/'):
118 119 search_host = search_host[:-1]
119 120 search_url = search_host + self.file.url
120 121 else:
121 122 search_url = ''
122 123
123 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
124 alias = self.alias + '<br />' if self.alias else ''
125
126 return ABSTRACT_VIEW.format(self.get_format_view(), alias, self.file.url,
124 127 self.file_type, filesizeformat(self.file.size),
125 128 self.file_type, search_url, self.file.name)
126 129
127 130 def get_format_view(self):
128 131 image_name = PLAIN_FILE_FORMATS.get(self.extension, self.extension)
129 132 file_name = FILE_FILEFORMAT.format(image_name)
130 133
131 134 if file_exists(file_name):
132 135 image = file_name
133 136 else:
134 137 image = FILE_STUB_IMAGE
135 138
136 139 w, h = get_static_dimensions(image)
137 140
138 141 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
139 142
140 143
141 144 class VideoViewer(AbstractViewer):
142 145 @staticmethod
143 146 def supports(file_type):
144 147 return file_type in FILE_TYPES_VIDEO
145 148
146 149 def get_format_view(self):
147 150 return VIDEO_FORMAT_VIEW.format(self.file.url)
148 151
149 152
150 153 class AudioViewer(AbstractViewer):
151 154 @staticmethod
152 155 def supports(file_type):
153 156 return file_type in FILE_TYPES_AUDIO
154 157
155 158 def get_format_view(self):
156 159 return AUDIO_FORMAT_VIEW.format(self.file.url)
157 160
158 161
159 162 class SvgViewer(AbstractViewer):
160 163 @staticmethod
161 164 def supports(file_type):
162 165 return file_type == FILE_TYPE_SVG
163 166
164 167 def get_format_view(self):
165 168 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
166 169
167 170
168 171 class ImageViewer(AbstractViewer):
169 172 @staticmethod
170 173 def supports(file_type):
171 174 return file_type in FILE_TYPES_IMAGE
172 175
173 176 def get_format_view(self):
174 177 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
175 178 filesizeformat(self.file.size))
176 179
177 180 try:
178 181 width, height = get_image_dimensions(self.file.path)
179 182 except Exception:
180 183 # If the image is a decompression bomb, treat it as just a regular
181 184 # file
182 185 return super().get_format_view()
183 186
184 187 preview_path = self.file.path.replace('.', '.200x150.')
185 188 try:
186 189 pre_width, pre_height = get_image_dimensions(preview_path)
187 190 except Exception:
188 191 return super().get_format_view()
189 192
190 193 split = self.file.url.rsplit('.', 1)
191 194 w, h = 200, 150
192 195 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
193 196
194 197 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
195 198 thumb_url,
196 199 self.hash,
197 200 str(pre_width),
198 201 str(pre_height), str(width), str(height),
199 202 full=self.file.url, image_meta=metadata)
200 203
201 204
202 205 class UrlViewer(AbstractViewer):
203 206 @staticmethod
204 207 def supports(file_type):
205 208 return file_type is None
206 209
207 210 def get_view(self):
208 211 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
209 212
210 213 def get_format_view(self):
211 214 protocol = self.url.split(':')[0]
212 215
213 216 domain = get_domain(self.url)
214 217
215 218 if protocol in URL_PROTOCOLS:
216 219 url_image_name = URL_PROTOCOLS.get(protocol)
217 220 elif domain:
218 221 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
219 222 else:
220 223 url_image_name = FILE_STUB_URL
221 224
222 225 image_path = 'images/{}.png'.format(url_image_name)
223 226 image = static(image_path)
224 227 w, h = get_static_dimensions(image_path)
225 228
226 229 return URL_FORMAT_VIEW.format(self.url, image, w, h)
227 230
228 231 @cached_result()
229 232 def _find_image_for_domains(self, domain):
230 233 """
231 234 Searches for the domain image for every domain level except top.
232 235 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
233 236 example.co.uk, then co.uk
234 237 """
235 238 levels = domain.split('.')
236 239 while len(levels) > 1:
237 240 domain = '.'.join(levels)
238 241
239 242 filename = 'images/domains/{}.png'.format(domain)
240 243 if file_exists(filename):
241 244 return 'domains/' + domain
242 245 else:
243 246 del levels[0]
244 247
General Comments 0
You need to be logged in to leave comments. Login now