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