cover images in filter API
[wolnelektury.git] / src / api / handlers.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 import json
6
7 from django.contrib.sites.models import Site
8 from django.core.urlresolvers import reverse
9 from django.utils.functional import lazy
10 from piston.handler import AnonymousBaseHandler, BaseHandler
11 from piston.utils import rc
12 from sorl.thumbnail import default
13
14 from catalogue.forms import BookImportForm
15 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
16 from catalogue.models.tag import prefetch_relations
17 from picture.models import Picture
18 from picture.forms import PictureImportForm
19
20 from stats.utils import piwik_track
21
22 from . import emitters  # Register our emitters
23
24 API_BASE = WL_BASE = MEDIA_BASE = lazy(
25     lambda: u'http://' + Site.objects.get_current().domain, unicode)()
26
27
28 category_singular = {
29     'authors': 'author',
30     'kinds': 'kind',
31     'genres': 'genre',
32     'epochs': 'epoch',
33     'themes': 'theme',
34     'books': 'book',
35 }
36 category_plural = {}
37 for k, v in category_singular.items():
38     category_plural[v] = k
39
40 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
41
42
43 def read_tags(tags, allowed):
44     """ Reads a path of filtering tags.
45
46     :param str tags: a path of category and slug pairs, like: authors/an-author/...
47     :returns: list of Tag objects
48     :raises: ValueError when tags can't be found
49     """
50     if not tags:
51         return [], []
52
53     tags = tags.strip('/').split('/')
54     real_tags = []
55     books = []
56     while tags:
57         category = tags.pop(0)
58         slug = tags.pop(0)
59
60         try:
61             category = category_singular[category]
62         except KeyError:
63             raise ValueError('Unknown category.')
64
65         if category not in allowed:
66             raise ValueError('Category not allowed.')
67
68         if category == 'book':
69             try:
70                 books.append(Book.objects.get(slug=slug))
71             except Book.DoesNotExist:
72                 raise ValueError('Unknown book.')
73
74         try:
75             real_tags.append(Tag.objects.get(category=category, slug=slug))
76         except Tag.DoesNotExist:
77             raise ValueError('Tag not found')
78     return real_tags, books
79
80
81 # RESTful handlers
82
83
84 class BookMediaHandler(BaseHandler):
85     """ Responsible for representing media in Books. """
86
87     model = BookMedia
88     fields = ['name', 'type', 'url', 'artist', 'director']
89
90     @classmethod
91     def url(cls, media):
92         """ Link to media on site. """
93
94         return MEDIA_BASE + media.file.url
95
96     @classmethod
97     def artist(cls, media):
98         return media.extra_info.get('artist_name', '')
99
100     @classmethod
101     def director(cls, media):
102         return media.extra_info.get('director_name', '')
103
104
105 class BookDetails(object):
106     """Custom fields used for representing Books."""
107
108     @classmethod
109     def href(cls, book):
110         """ Returns an URI for a Book in the API. """
111         return API_BASE + reverse("api_book", args=[book.slug])
112
113     @classmethod
114     def url(cls, book):
115         """ Returns Book's URL on the site. """
116
117         return WL_BASE + book.get_absolute_url()
118
119     @classmethod
120     def children(cls, book):
121         """ Returns all children for a book. """
122
123         return book.children.all()
124
125     @classmethod
126     def media(cls, book):
127         """ Returns all media for a book. """
128         return book.media.all()
129
130     @classmethod
131     def cover(cls, book):
132         return MEDIA_BASE + book.cover.url if book.cover else ''
133
134     @classmethod
135     def cover_thumb(cls, book):
136         return MEDIA_BASE + default.backend.get_thumbnail(
137                     book.cover, "139x193").url if book.cover else ''
138
139     @classmethod
140     def cover_source_image(cls, book):
141         url = book.cover_source()
142         return url.rstrip('/') + '/file/'
143
144
145 class BookDetailHandler(BaseHandler, BookDetails):
146     """ Main handler for Book objects.
147
148     Responsible for single Book details.
149     """
150     allowed_methods = ['GET']
151     fields = ['title', 'parent', 'children'] + Book.formats + [
152         'media', 'url', 'cover', 'cover_thumb'] + [
153             category_plural[c] for c in book_tag_categories]
154
155     @piwik_track
156     def read(self, request, book):
157         """ Returns details of a book, identified by a slug and lang. """
158         try:
159             return Book.objects.get(slug=book)
160         except Book.DoesNotExist:
161             return rc.NOT_FOUND
162
163
164 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
165     """ Main handler for Book objects.
166
167     Responsible for lists of Book objects.
168     """
169     allowed_methods = ('GET',)
170     model = Book
171     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
172
173     @classmethod
174     def genres(cls, book):
175         """ Returns all media for a book. """
176         return book.tags.filter(category='genre')
177
178     @piwik_track
179     def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
180              recommended=False, newest=False, books=None,
181              after=None, before=None, count=None):
182         """ Lists all books with given tags.
183
184         :param tags: filtering tags; should be a path of categories
185              and slugs, i.e.: authors/an-author/epoch/an-epoch/
186         :param top_level: if True and a book is included in the results,
187              it's children are aren't. By default all books matching the tags
188              are returned.
189         """
190         if pk is not None:
191             try:
192                 return Book.objects.get(pk=pk)
193             except Book.DoesNotExist:
194                 return rc.NOT_FOUND
195
196         try:
197             tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
198         except ValueError:
199             return rc.NOT_FOUND
200
201         if tags:
202             if top_level:
203                 books = Book.tagged_top_level(tags)
204                 return books if books else rc.NOT_FOUND
205             else:
206                 books = Book.tagged.with_all(tags)
207         else:
208             books = books if books is not None else Book.objects.all()
209         books = books.order_by('slug')
210
211         if top_level:
212             books = books.filter(parent=None)
213         if audiobooks:
214             books = books.filter(media__type='mp3').distinct()
215         if daisy:
216             books = books.filter(media__type='daisy').distinct()
217         if recommended:
218             books = books.filter(recommended=True)
219         if newest:
220             books = books.order_by('-created_at')
221
222         if after:
223             books = books.filter(slug__gt=after)
224         if before:
225             books = books.filter(slug__lt=before)
226
227         books = books.only('slug', 'title', 'cover', 'cover_thumb')
228         for category in book_tag_categories:
229             books = prefetch_relations(books, category)
230
231         if count:
232             if before:
233                 books = list(reversed(books.order_by('-slug')[:count]))
234             else:
235                 books = books[:count]
236
237         if books:
238             return books
239         else:
240             return rc.NOT_FOUND
241
242     def create(self, request, *args, **kwargs):
243         return rc.FORBIDDEN
244
245
246 class BooksHandler(BookDetailHandler):
247     allowed_methods = ('GET', 'POST')
248     model = Book
249     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
250     anonymous = AnonymousBooksHandler
251
252     def create(self, request, *args, **kwargs):
253         if not request.user.has_perm('catalogue.add_book'):
254             return rc.FORBIDDEN
255
256         data = json.loads(request.POST.get('data'))
257         form = BookImportForm(data)
258         if form.is_valid():
259             form.save()
260             return rc.CREATED
261         else:
262             return rc.NOT_FOUND
263
264
265 class EBooksHandler(AnonymousBooksHandler):
266     fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
267
268
269 class FilterBooksHandler(AnonymousBooksHandler):
270     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'cover_source_image']
271
272     def read(self, request, title_part=None, author_part=None, is_lektura=None, is_audiobook=None,
273              after=None, before=None, count=None):
274         if count is None:
275             count = 50
276         if is_lektura in ('true', 'false'):
277             is_lektura = is_lektura == 'true'
278         else:
279             is_lektura = None
280         if is_audiobook in ('true', 'false'):
281             is_audiobook = is_audiobook == 'true'
282         books = Book.objects.distinct()
283         if title_part:
284             books = books.filter(title__iregex='\m' + title_part)
285         if author_part is not None:
286             books = books.filter(cached_author__iregex='\m' + author_part)
287         if is_lektura is not None:
288             books = books.filter(has_audience=is_lektura)
289         if is_audiobook is not None:
290             if is_audiobook:
291                 books = books.filter(media__type='mp3')
292             else:
293                 books = books.exclude(media__type='mp3')
294         return super(FilterBooksHandler, self).read(request, books=books, after=after, before=before, count=count)
295
296
297 # add categorized tags fields for Book
298 def _tags_getter(category):
299     @classmethod
300     def get_tags(cls, book):
301         return book.tags.filter(category=category)
302     return get_tags
303
304
305 def _tag_getter(category):
306     @classmethod
307     def get_tag(cls, book):
308         return book.tag_unicode(category)
309     return get_tag
310
311
312 def add_tag_getters():
313     for plural, singular in category_singular.items():
314         setattr(BookDetails, plural, _tags_getter(singular))
315         setattr(BookDetails, singular, _tag_getter(singular))
316
317 add_tag_getters()
318
319
320 # add fields for files in Book
321 def _file_getter(book_format):
322     field = "%s_file" % book_format
323
324     @classmethod
325     def get_file(cls, book):
326         f = getattr(book, field)
327         if f:
328             return MEDIA_BASE + f.url
329         else:
330             return ''
331     return get_file
332
333
334 def add_file_getters():
335     for book_format in Book.formats:
336         setattr(BookDetails, book_format, _file_getter(book_format))
337
338 add_file_getters()
339
340
341 class CollectionDetails(object):
342     """Custom Collection fields."""
343
344     @classmethod
345     def href(cls, collection):
346         """ Returns URI in the API for the collection. """
347
348         return API_BASE + reverse("api_collection", args=[collection.slug])
349
350     @classmethod
351     def url(cls, collection):
352         """ Returns URL on the site. """
353
354         return WL_BASE + collection.get_absolute_url()
355
356     @classmethod
357     def books(cls, collection):
358         return Book.objects.filter(collection.get_query())
359
360
361 class CollectionDetailHandler(BaseHandler, CollectionDetails):
362     allowed_methods = ('GET',)
363     fields = ['url', 'title', 'description', 'books']
364
365     @piwik_track
366     def read(self, request, slug):
367         """ Returns details of a collection, identified by slug. """
368         try:
369             return Collection.objects.get(slug=slug)
370         except Collection.DoesNotExist:
371             return rc.NOT_FOUND
372
373
374 class CollectionsHandler(BaseHandler, CollectionDetails):
375     allowed_methods = ('GET',)
376     model = Collection
377     fields = ['url', 'href', 'title']
378
379     @piwik_track
380     def read(self, request):
381         """ Returns all collections. """
382         return Collection.objects.all()
383
384
385 class TagDetails(object):
386     """Custom Tag fields."""
387
388     @classmethod
389     def href(cls, tag):
390         """ Returns URI in the API for the tag. """
391
392         return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
393
394     @classmethod
395     def url(cls, tag):
396         """ Returns URL on the site. """
397
398         return WL_BASE + tag.get_absolute_url()
399
400
401 class TagDetailHandler(BaseHandler, TagDetails):
402     """ Responsible for details of a single Tag object. """
403
404     fields = ['name', 'url', 'sort_key', 'description']
405
406     @piwik_track
407     def read(self, request, category, slug):
408         """ Returns details of a tag, identified by category and slug. """
409
410         try:
411             category_sng = category_singular[category]
412         except KeyError:
413             return rc.NOT_FOUND
414
415         try:
416             return Tag.objects.get(category=category_sng, slug=slug)
417         except Tag.DoesNotExist:
418             return rc.NOT_FOUND
419
420
421 class TagsHandler(BaseHandler, TagDetails):
422     """ Main handler for Tag objects.
423
424     Responsible for lists of Tag objects
425     and fields used for representing Tags.
426
427     """
428     allowed_methods = ('GET',)
429     model = Tag
430     fields = ['name', 'href', 'url']
431
432     @piwik_track
433     def read(self, request, category=None, pk=None):
434         """ Lists all tags in the category (eg. all themes). """
435         if pk is not None:
436             try:
437                 return Tag.objects.exclude(category='set').get(pk=pk)
438             except Book.DoesNotExist:
439                 return rc.NOT_FOUND
440
441         try:
442             category_sng = category_singular[category]
443         except KeyError:
444             return rc.NOT_FOUND
445
446         tags = Tag.objects.filter(category=category_sng).exclude(items=None)
447         if tags.exists():
448             return tags
449         else:
450             return rc.NOT_FOUND
451
452
453 class FragmentDetails(object):
454     """Custom Fragment fields."""
455
456     @classmethod
457     def href(cls, fragment):
458         """ Returns URI in the API for the fragment. """
459
460         return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
461
462     @classmethod
463     def url(cls, fragment):
464         """ Returns URL on the site for the fragment. """
465
466         return WL_BASE + fragment.get_absolute_url()
467
468     @classmethod
469     def themes(cls, fragment):
470         """ Returns a list of theme tags for the fragment. """
471
472         return fragment.tags.filter(category='theme')
473
474
475 class FragmentDetailHandler(BaseHandler, FragmentDetails):
476     fields = ['book', 'anchor', 'text', 'url', 'themes']
477
478     @piwik_track
479     def read(self, request, book, anchor):
480         """ Returns details of a fragment, identified by book slug and anchor. """
481         try:
482             return Fragment.objects.get(book__slug=book, anchor=anchor)
483         except Fragment.DoesNotExist:
484             return rc.NOT_FOUND
485
486
487 class FragmentsHandler(BaseHandler, FragmentDetails):
488     """ Main handler for Fragments.
489
490     Responsible for lists of Fragment objects
491     and fields used for representing Fragments.
492
493     """
494     model = Fragment
495     fields = ['book', 'url', 'anchor', 'href']
496     allowed_methods = ('GET',)
497
498     categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
499
500     @piwik_track
501     def read(self, request, tags):
502         """ Lists all fragments with given book, tags, themes.
503
504         :param tags: should be a path of categories and slugs, i.e.:
505              books/book-slug/authors/an-author/themes/a-theme/
506
507         """
508         try:
509             tags, ancestors = read_tags(tags, allowed=self.categories)
510         except ValueError:
511             return rc.NOT_FOUND
512         fragments = Fragment.tagged.with_all(tags).select_related('book')
513         if fragments.exists():
514             return fragments
515         else:
516             return rc.NOT_FOUND
517
518
519 class PictureHandler(BaseHandler):
520     model = Picture
521     fields = ('slug', 'title')
522     allowed_methods = ('POST',)
523
524     def create(self, request):
525         if not request.user.has_perm('picture.add_picture'):
526             return rc.FORBIDDEN
527
528         data = json.loads(request.POST.get('data'))
529         form = PictureImportForm(data)
530         if form.is_valid():
531             form.save()
532             return rc.CREATED
533         else:
534             return rc.NOT_FOUND