removed sniff_handler from Imange Media Manager class
[mediagoblin.git] / mediagoblin / db / mixin.py
CommitLineData
f42e49c3 1# GNU MediaGoblin -- federated, autonomous media hosting
7f4ebeed 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
f42e49c3
E
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""
18This module contains some Mixin classes for the db objects.
19
20A bunch of functions on the db objects are really more like
21"utility functions": They could live outside the classes
22and be called "by hand" passing the appropiate reference.
23They usually only use the public API of the object and
24rarely use database related stuff.
25
26These functions now live here and get "mixed in" into the
27real objects.
28"""
29
a81082fc 30import uuid
907bba31 31import re
2d7b6bde
JW
32from datetime import datetime
33
5f8b4ae8
SS
34from werkzeug.utils import cached_property
35
814334f6 36from mediagoblin import mg_globals
58a94757 37from mediagoblin.media_types import FileTypeNotSupported
17c23e15 38from mediagoblin.tools import common, licenses
58a94757 39from mediagoblin.tools.pluginapi import hook_handle
e61ab099 40from mediagoblin.tools.text import cleaned_markdown_conversion
814334f6 41from mediagoblin.tools.url import slugify
f42e49c3
E
42
43
44class UserMixin(object):
e61ab099
E
45 @property
46 def bio_html(self):
47 return cleaned_markdown_conversion(self.bio)
48
f42e49c3 49
29c65044 50class GenerateSlugMixin(object):
44123853
E
51 """
52 Mixin to add a generate_slug method to objects.
53
54 Depends on:
55 - self.slug
56 - self.title
57 - self.check_slug_used(new_slug)
58 """
814334f6 59 def generate_slug(self):
88de830f 60 """
44123853 61 Generate a unique slug for this object.
98587109 62
88de830f
CAW
63 This one does not *force* slugs, but usually it will probably result
64 in a niceish one.
65
98587109
CAW
66 The end *result* of the algorithm will result in these resolutions for
67 these situations:
88de830f
CAW
68 - If we have a slug, make sure it's clean and sanitized, and if it's
69 unique, we'll use that.
70 - If we have a title, slugify it, and if it's unique, we'll use that.
71 - If we can't get any sort of thing that looks like it'll be a useful
72 slug out of a title or an existing slug, bail, and don't set the
73 slug at all. Don't try to create something just because. Make
74 sure we have a reasonable basis for a slug first.
75 - If we have a reasonable basis for a slug (either based on existing
76 slug or slugified title) but it's not unique, first try appending
77 the entry's id, if that exists
78 - If that doesn't result in something unique, tack on some randomly
79 generated bits until it's unique. That'll be a little bit of junk,
80 but at least it has the basis of a nice slug.
81 """
66d9f1b2
SS
82 #Is already a slug assigned? Check if it is valid
83 if self.slug:
84 self.slug = slugify(self.slug)
b1126f71 85
88de830f 86 # otherwise, try to use the title.
66d9f1b2 87 elif self.title:
88de830f 88 # assign slug based on title
66d9f1b2 89 self.slug = slugify(self.title)
b1126f71 90
b0118957
CAW
91 # We don't want any empty string slugs
92 if self.slug == u"":
93 self.slug = None
94
88de830f
CAW
95 # Do we have anything at this point?
96 # If not, we're not going to get a slug
97 # so just return... we're not going to force one.
98 if not self.slug:
99 return # giving up!
b1126f71 100
88de830f 101 # Otherwise, let's see if this is unique.
29c65044 102 if self.check_slug_used(self.slug):
88de830f 103 # It looks like it's being used... lame.
b1126f71 104
88de830f
CAW
105 # Can we just append the object's id to the end?
106 if self.id:
98587109 107 slug_with_id = u"%s-%s" % (self.slug, self.id)
29c65044 108 if not self.check_slug_used(slug_with_id):
88de830f
CAW
109 self.slug = slug_with_id
110 return # success!
b1126f71 111
88de830f
CAW
112 # okay, still no success;
113 # let's whack junk on there till it's unique.
a81082fc
CAW
114 self.slug += '-' + uuid.uuid4().hex[:4]
115 # keep going if necessary!
29c65044 116 while self.check_slug_used(self.slug):
a81082fc 117 self.slug += uuid.uuid4().hex[:4]
814334f6 118
29c65044
E
119
120class MediaEntryMixin(GenerateSlugMixin):
121 def check_slug_used(self, slug):
122 # import this here due to a cyclic import issue
123 # (db.models -> db.mixin -> db.util -> db.models)
124 from mediagoblin.db.util import check_media_slug_used
125
126 return check_media_slug_used(self.uploader, slug, self.id)
127
1e72e075
E
128 @property
129 def description_html(self):
130 """
131 Rendered version of the description, run through
132 Markdown and cleaned with our cleaning tool.
133 """
134 return cleaned_markdown_conversion(self.description)
135
e77df64f
CAW
136 def get_display_media(self):
137 """Find the best media for display.
f42e49c3 138
53024776 139 We try checking self.media_manager.fetching_order if it exists to
e77df64f 140 pull down the order.
f42e49c3
E
141
142 Returns:
ddbf6af1
CAW
143 (media_size, media_path)
144 or, if not found, None.
e77df64f 145
f42e49c3 146 """
e8676fa3 147 fetch_order = self.media_manager.media_fetch_order
f42e49c3 148
ddbf6af1
CAW
149 # No fetching order found? well, give up!
150 if not fetch_order:
151 return None
152
153 media_sizes = self.media_files.keys()
154
155 for media_size in fetch_order:
f42e49c3 156 if media_size in media_sizes:
ddbf6af1 157 return media_size, self.media_files[media_size]
f42e49c3
E
158
159 def main_mediafile(self):
160 pass
161
3e907d55
E
162 @property
163 def slug_or_id(self):
7de20e52
CAW
164 if self.slug:
165 return self.slug
166 else:
167 return u'id:%s' % self.id
5f8b4ae8 168
cb7ae1e4 169 def url_for_self(self, urlgen, **extra_args):
f42e49c3
E
170 """
171 Generate an appropriate url for ourselves
172
5c2b8486 173 Use a slug if we have one, else use our 'id'.
f42e49c3
E
174 """
175 uploader = self.get_uploader
176
3e907d55
E
177 return urlgen(
178 'mediagoblin.user_pages.media_home',
179 user=uploader.username,
180 media=self.slug_or_id,
181 **extra_args)
f42e49c3 182
2e4ad359
SS
183 @property
184 def thumb_url(self):
185 """Return the thumbnail URL (for usage in templates)
186 Will return either the real thumbnail or a default fallback icon."""
187 # TODO: implement generic fallback in case MEDIA_MANAGER does
188 # not specify one?
189 if u'thumb' in self.media_files:
190 thumb_url = mg_globals.app.public_store.file_url(
191 self.media_files[u'thumb'])
192 else:
df1c4976 193 # No thumbnail in media available. Get the media's
2e4ad359 194 # MEDIA_MANAGER for the fallback icon and return static URL
5f8b4ae8
SS
195 # Raises FileTypeNotSupported in case no such manager is enabled
196 manager = self.media_manager
df1c4976 197 thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb'])
2e4ad359
SS
198 return thumb_url
199
5f8b4ae8
SS
200 @cached_property
201 def media_manager(self):
202 """Returns the MEDIA_MANAGER of the media's media_type
203
204 Raises FileTypeNotSupported in case no such manager is enabled
205 """
58a94757
RE
206 manager = hook_handle('get_media_manager', self.media_type)
207 if manager:
208 return manager
5f8b4ae8
SS
209 # Not found? Then raise an error
210 raise FileTypeNotSupported(
58a94757 211 "MediaManager not in enabled types. Check media_types in config?")
5f8b4ae8 212
f42e49c3
E
213 def get_fail_exception(self):
214 """
215 Get the exception that's appropriate for this error
216 """
51eb0267
JW
217 if self.fail_error:
218 return common.import_component(self.fail_error)
17c23e15
AW
219
220 def get_license_data(self):
221 """Return license dict for requested license"""
138a18fd 222 return licenses.get_license_by_url(self.license or "")
feba5c52 223
5bad26bc
E
224 def exif_display_iter(self):
225 if not self.media_data:
226 return
227 exif_all = self.media_data.get("exif_all")
228
b3566e1d
GS
229 for key in exif_all:
230 label = re.sub('(.)([A-Z][a-z]+)', r'\1 \2', key)
231 yield label.replace('EXIF', '').replace('Image', ''), exif_all[key]
5bad26bc 232
420e1374
GS
233 def exif_display_data_short(self):
234 """Display a very short practical version of exif info"""
420e1374
GS
235 if not self.media_data:
236 return
907bba31 237
420e1374 238 exif_all = self.media_data.get("exif_all")
907bba31 239
14aa2eaa
JW
240 exif_short = {}
241
907bba31
JW
242 if 'Image DateTimeOriginal' in exif_all:
243 # format date taken
244 takendate = datetime.datetime.strptime(
245 exif_all['Image DateTimeOriginal']['printable'],
246 '%Y:%m:%d %H:%M:%S').date()
247 taken = takendate.strftime('%B %d %Y')
248
14aa2eaa
JW
249 exif_short.update({'Date Taken': taken})
250
1b6a2b85 251 aperture = None
907bba31
JW
252 if 'EXIF FNumber' in exif_all:
253 fnum = str(exif_all['EXIF FNumber']['printable']).split('/')
254
1b6a2b85
JW
255 # calculate aperture
256 if len(fnum) == 2:
257 aperture = "f/%.1f" % (float(fnum[0])/float(fnum[1]))
258 elif fnum[0] != 'None':
259 aperture = "f/%s" % (fnum[0])
907bba31 260
14aa2eaa
JW
261 if aperture:
262 exif_short.update({'Aperture': aperture})
263
264 short_keys = [
265 ('Camera', 'Image Model', None),
266 ('Exposure', 'EXIF ExposureTime', lambda x: '%s sec' % x),
267 ('ISO Speed', 'EXIF ISOSpeedRatings', None),
268 ('Focal Length', 'EXIF FocalLength', lambda x: '%s mm' % x)]
269
270 for label, key, fmt_func in short_keys:
271 try:
272 val = fmt_func(exif_all[key]['printable']) if fmt_func \
273 else exif_all[key]['printable']
274 exif_short.update({label: val})
275 except KeyError:
276 pass
277
278 return exif_short
279
feba5c52
E
280
281class MediaCommentMixin(object):
282 @property
283 def content_html(self):
284 """
285 the actual html-rendered version of the comment displayed.
286 Run through Markdown and the HTML cleaner.
287 """
288 return cleaned_markdown_conversion(self.content)
be5be115 289
2d7b6bde
JW
290 def __repr__(self):
291 return '<{klass} #{id} {author} "{comment}">'.format(
292 klass=self.__class__.__name__,
293 id=self.id,
294 author=self.get_author,
295 comment=self.content)
296
be5be115 297
455fd36f
E
298class CollectionMixin(GenerateSlugMixin):
299 def check_slug_used(self, slug):
be5be115
AW
300 # import this here due to a cyclic import issue
301 # (db.models -> db.mixin -> db.util -> db.models)
302 from mediagoblin.db.util import check_collection_slug_used
303
455fd36f 304 return check_collection_slug_used(self.creator, slug, self.id)
be5be115
AW
305
306 @property
307 def description_html(self):
308 """
309 Rendered version of the description, run through
310 Markdown and cleaned with our cleaning tool.
311 """
312 return cleaned_markdown_conversion(self.description)
313
314 @property
315 def slug_or_id(self):
5c2b8486 316 return (self.slug or self.id)
be5be115
AW
317
318 def url_for_self(self, urlgen, **extra_args):
319 """
320 Generate an appropriate url for ourselves
321
5c2b8486 322 Use a slug if we have one, else use our 'id'.
be5be115
AW
323 """
324 creator = self.get_creator
325
326 return urlgen(
256f816f 327 'mediagoblin.user_pages.user_collection',
be5be115
AW
328 user=creator.username,
329 collection=self.slug_or_id,
330 **extra_args)
331
6d1e55b2 332
be5be115
AW
333class CollectionItemMixin(object):
334 @property
335 def note_html(self):
336 """
337 the actual html-rendered version of the note displayed.
338 Run through Markdown and the HTML cleaner.
339 """
340 return cleaned_markdown_conversion(self.note)