Switching the hook 'get_media_manager' to a more "directed" tuple-hook
[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 """
6403bc92 206 manager = hook_handle(('media_manager', self.media_type))
58a94757 207 if manager:
4259ad5b
CAW
208 return manager(self)
209
5f8b4ae8
SS
210 # Not found? Then raise an error
211 raise FileTypeNotSupported(
e6991972
RE
212 "MediaManager not in enabled types. Check media_type plugins are"
213 " enabled in config?")
5f8b4ae8 214
f42e49c3
E
215 def get_fail_exception(self):
216 """
217 Get the exception that's appropriate for this error
218 """
51eb0267
JW
219 if self.fail_error:
220 return common.import_component(self.fail_error)
17c23e15
AW
221
222 def get_license_data(self):
223 """Return license dict for requested license"""
138a18fd 224 return licenses.get_license_by_url(self.license or "")
feba5c52 225
5bad26bc
E
226 def exif_display_iter(self):
227 if not self.media_data:
228 return
229 exif_all = self.media_data.get("exif_all")
230
b3566e1d
GS
231 for key in exif_all:
232 label = re.sub('(.)([A-Z][a-z]+)', r'\1 \2', key)
233 yield label.replace('EXIF', '').replace('Image', ''), exif_all[key]
5bad26bc 234
420e1374
GS
235 def exif_display_data_short(self):
236 """Display a very short practical version of exif info"""
420e1374
GS
237 if not self.media_data:
238 return
907bba31 239
420e1374 240 exif_all = self.media_data.get("exif_all")
907bba31 241
14aa2eaa
JW
242 exif_short = {}
243
907bba31
JW
244 if 'Image DateTimeOriginal' in exif_all:
245 # format date taken
246 takendate = datetime.datetime.strptime(
247 exif_all['Image DateTimeOriginal']['printable'],
248 '%Y:%m:%d %H:%M:%S').date()
249 taken = takendate.strftime('%B %d %Y')
250
14aa2eaa
JW
251 exif_short.update({'Date Taken': taken})
252
1b6a2b85 253 aperture = None
907bba31
JW
254 if 'EXIF FNumber' in exif_all:
255 fnum = str(exif_all['EXIF FNumber']['printable']).split('/')
256
1b6a2b85
JW
257 # calculate aperture
258 if len(fnum) == 2:
259 aperture = "f/%.1f" % (float(fnum[0])/float(fnum[1]))
260 elif fnum[0] != 'None':
261 aperture = "f/%s" % (fnum[0])
907bba31 262
14aa2eaa
JW
263 if aperture:
264 exif_short.update({'Aperture': aperture})
265
266 short_keys = [
267 ('Camera', 'Image Model', None),
268 ('Exposure', 'EXIF ExposureTime', lambda x: '%s sec' % x),
269 ('ISO Speed', 'EXIF ISOSpeedRatings', None),
270 ('Focal Length', 'EXIF FocalLength', lambda x: '%s mm' % x)]
271
272 for label, key, fmt_func in short_keys:
273 try:
274 val = fmt_func(exif_all[key]['printable']) if fmt_func \
275 else exif_all[key]['printable']
276 exif_short.update({label: val})
277 except KeyError:
278 pass
279
280 return exif_short
281
feba5c52
E
282
283class MediaCommentMixin(object):
284 @property
285 def content_html(self):
286 """
287 the actual html-rendered version of the comment displayed.
288 Run through Markdown and the HTML cleaner.
289 """
290 return cleaned_markdown_conversion(self.content)
be5be115 291
2d7b6bde
JW
292 def __repr__(self):
293 return '<{klass} #{id} {author} "{comment}">'.format(
294 klass=self.__class__.__name__,
295 id=self.id,
296 author=self.get_author,
297 comment=self.content)
298
be5be115 299
455fd36f
E
300class CollectionMixin(GenerateSlugMixin):
301 def check_slug_used(self, slug):
be5be115
AW
302 # import this here due to a cyclic import issue
303 # (db.models -> db.mixin -> db.util -> db.models)
304 from mediagoblin.db.util import check_collection_slug_used
305
455fd36f 306 return check_collection_slug_used(self.creator, slug, self.id)
be5be115
AW
307
308 @property
309 def description_html(self):
310 """
311 Rendered version of the description, run through
312 Markdown and cleaned with our cleaning tool.
313 """
314 return cleaned_markdown_conversion(self.description)
315
316 @property
317 def slug_or_id(self):
5c2b8486 318 return (self.slug or self.id)
be5be115
AW
319
320 def url_for_self(self, urlgen, **extra_args):
321 """
322 Generate an appropriate url for ourselves
323
5c2b8486 324 Use a slug if we have one, else use our 'id'.
be5be115
AW
325 """
326 creator = self.get_creator
327
328 return urlgen(
256f816f 329 'mediagoblin.user_pages.user_collection',
be5be115
AW
330 user=creator.username,
331 collection=self.slug_or_id,
332 **extra_args)
333
6d1e55b2 334
be5be115
AW
335class CollectionItemMixin(object):
336 @property
337 def note_html(self):
338 """
339 the actual html-rendered version of the note displayed.
340 Run through Markdown and the HTML cleaner.
341 """
342 return cleaned_markdown_conversion(self.note)