Using cache middleware instead of various caching micro-strategies,
[wolnelektury.git] / apps / 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
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 not category 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
140
141 class BookDetailHandler(BaseHandler, BookDetails):
142     """ Main handler for Book objects.
143
144     Responsible for single Book details.
145     """
146     allowed_methods = ['GET']
147     fields = ['title', 'parent', 'children'] + Book.formats + [
148         'media', 'url', 'cover', 'cover_thumb'] + [
149             category_plural[c] for c in book_tag_categories]
150
151     @piwik_track
152     def read(self, request, book):
153         """ Returns details of a book, identified by a slug and lang. """
154         try:
155             return Book.objects.get(slug=book)
156         except Book.DoesNotExist:
157             return rc.NOT_FOUND
158
159
160 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
161     """ Main handler for Book objects.
162
163     Responsible for lists of Book objects.
164     """
165     allowed_methods = ('GET',)
166     model = Book
167     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
168
169     @classmethod
170     def genres(cls, book):
171         """ Returns all media for a book. """
172         return book.tags.filter(category='genre')
173
174     @piwik_track
175     def read(self, request, tags=None, top_level=False,
176                 audiobooks=False, daisy=False, pk=None):
177         """ Lists all books with given tags.
178
179         :param tags: filtering tags; should be a path of categories
180              and slugs, i.e.: authors/an-author/epoch/an-epoch/
181         :param top_level: if True and a book is included in the results,
182              it's children are aren't. By default all books matching the tags
183              are returned.
184         """
185         if pk is not None:
186             try:
187                 return Book.objects.get(pk=pk)
188             except Book.DoesNotExist:
189                 return rc.NOT_FOUND
190
191         try:
192             tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
193         except ValueError:
194             return rc.NOT_FOUND
195
196         if tags:
197             if top_level:
198                 books = Book.tagged_top_level(tags)
199                 return books if books else rc.NOT_FOUND
200             else:
201                 books = Book.tagged.with_all(tags)
202         else:
203             books = Book.objects.all()
204
205         if top_level:
206             books = books.filter(parent=None)
207         if audiobooks:
208             books = books.filter(media__type='mp3').distinct()
209         if daisy:
210             books = books.filter(media__type='daisy').distinct()
211
212         if books.exists():
213             return books
214         else:
215             return rc.NOT_FOUND
216
217     def create(self, request, *args, **kwargs):
218         return rc.FORBIDDEN
219
220
221 class BooksHandler(BookDetailHandler):
222     allowed_methods = ('GET', 'POST')
223     model = Book
224     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
225     anonymous = AnonymousBooksHandler
226
227     def create(self, request, *args, **kwargs):
228         if not request.user.has_perm('catalogue.add_book'):
229             return rc.FORBIDDEN
230
231         data = json.loads(request.POST.get('data'))
232         form = BookImportForm(data)
233         if form.is_valid():
234             form.save()
235             return rc.CREATED
236         else:
237             return rc.NOT_FOUND
238
239
240 class EBooksHandler(AnonymousBooksHandler):
241     fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
242
243
244 # add categorized tags fields for Book
245 def _tags_getter(category):
246     @classmethod
247     def get_tags(cls, book):
248         return book.tags.filter(category=category)
249     return get_tags
250 def _tag_getter(category):
251     @classmethod
252     def get_tag(cls, book):
253         return ', '.join(tag.name for tag in book.tags.filter(category=category))
254     return get_tag
255 for plural, singular in category_singular.items():
256     setattr(BookDetails, plural, _tags_getter(singular))
257     setattr(BookDetails, singular, _tag_getter(singular))
258
259 # add fields for files in Book
260 def _file_getter(format):
261     field = "%s_file" % format
262     @classmethod
263     def get_file(cls, book):
264         f = getattr(book, field)
265         if f:
266             return MEDIA_BASE + f.url
267         else:
268             return ''
269     return get_file
270 for format in Book.formats:
271     setattr(BookDetails, format, _file_getter(format))
272
273
274 class CollectionDetails(object):
275     """Custom Collection fields."""
276
277     @classmethod
278     def href(cls, collection):
279         """ Returns URI in the API for the collection. """
280
281         return API_BASE + reverse("api_collection", args=[collection.slug])
282
283     @classmethod
284     def url(cls, collection):
285         """ Returns URL on the site. """
286
287         return WL_BASE + collection.get_absolute_url()
288
289     @classmethod
290     def books(cls, collection):
291         return Book.objects.filter(collection.get_query())
292
293
294
295 class CollectionDetailHandler(BaseHandler, CollectionDetails):
296     allowed_methods = ('GET',)
297     fields = ['url', 'title', 'description', 'books']
298
299     @piwik_track
300     def read(self, request, slug):
301         """ Returns details of a collection, identified by slug. """
302         try:
303             return Collection.objects.get(slug=slug)
304         except Collection.DoesNotExist:
305             return rc.NOT_FOUND
306
307
308 class CollectionsHandler(BaseHandler, CollectionDetails):
309     allowed_methods = ('GET',)
310     model = Collection
311     fields = ['url', 'href', 'title']
312
313     @piwik_track
314     def read(self, request):
315         """ Returns all collections. """
316         return Collection.objects.all()
317
318
319 class TagDetails(object):
320     """Custom Tag fields."""
321
322     @classmethod
323     def href(cls, tag):
324         """ Returns URI in the API for the tag. """
325
326         return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
327
328     @classmethod
329     def url(cls, tag):
330         """ Returns URL on the site. """
331
332         return WL_BASE + tag.get_absolute_url()
333
334
335 class TagDetailHandler(BaseHandler, TagDetails):
336     """ Responsible for details of a single Tag object. """
337
338     fields = ['name', 'url', 'sort_key', 'description']
339
340     @piwik_track
341     def read(self, request, category, slug):
342         """ Returns details of a tag, identified by category and slug. """
343
344         try:
345             category_sng = category_singular[category]
346         except KeyError, e:
347             return rc.NOT_FOUND
348
349         try:
350             return Tag.objects.get(category=category_sng, slug=slug)
351         except Tag.DoesNotExist:
352             return rc.NOT_FOUND
353
354
355 class TagsHandler(BaseHandler, TagDetails):
356     """ Main handler for Tag objects.
357
358     Responsible for lists of Tag objects
359     and fields used for representing Tags.
360
361     """
362     allowed_methods = ('GET',)
363     model = Tag
364     fields = ['name', 'href', 'url']
365
366     @piwik_track
367     def read(self, request, category=None, pk=None):
368         """ Lists all tags in the category (eg. all themes). """
369         if pk is not None:
370             try:
371                 return Tag.objects.exclude(category='set').get(pk=pk)
372             except Book.DoesNotExist:
373                 return rc.NOT_FOUND
374
375         try:
376             category_sng = category_singular[category]
377         except KeyError, e:
378             return rc.NOT_FOUND
379
380         tags = Tag.objects.filter(category=category_sng).exclude(items=None)
381         if tags.exists():
382             return tags
383         else:
384             return rc.NOT_FOUND
385
386
387 class FragmentDetails(object):
388     """Custom Fragment fields."""
389
390     @classmethod
391     def href(cls, fragment):
392         """ Returns URI in the API for the fragment. """
393
394         return API_BASE + reverse("api_fragment",
395             args=[fragment.book.slug, fragment.anchor])
396
397     @classmethod
398     def url(cls, fragment):
399         """ Returns URL on the site for the fragment. """
400
401         return WL_BASE + fragment.get_absolute_url()
402
403     @classmethod
404     def themes(cls, fragment):
405         """ Returns a list of theme tags for the fragment. """
406
407         return fragment.tags.filter(category='theme')
408
409
410 class FragmentDetailHandler(BaseHandler, FragmentDetails):
411     fields = ['book', 'anchor', 'text', 'url', 'themes']
412
413     @piwik_track
414     def read(self, request, book, anchor):
415         """ Returns details of a fragment, identified by book slug and anchor. """
416         try:
417             return Fragment.objects.get(book__slug=book, anchor=anchor)
418         except Fragment.DoesNotExist:
419             return rc.NOT_FOUND
420
421
422 class FragmentsHandler(BaseHandler, FragmentDetails):
423     """ Main handler for Fragments.
424
425     Responsible for lists of Fragment objects
426     and fields used for representing Fragments.
427
428     """
429     model = Fragment
430     fields = ['book', 'url', 'anchor', 'href']
431     allowed_methods = ('GET',)
432
433     categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
434
435     @piwik_track
436     def read(self, request, tags):
437         """ Lists all fragments with given book, tags, themes.
438
439         :param tags: should be a path of categories and slugs, i.e.:
440              books/book-slug/authors/an-author/themes/a-theme/
441
442         """
443         try:
444             tags, ancestors = read_tags(tags, allowed=self.categories)
445         except ValueError:
446             return rc.NOT_FOUND
447         fragments = Fragment.tagged.with_all(tags).select_related('book')
448         if fragments.exists():
449             return fragments
450         else:
451             return rc.NOT_FOUND
452
453
454 class PictureHandler(BaseHandler):
455     model = Picture
456     fields = ('slug', 'title')
457     allowed_methods = ('POST',)
458
459     def create(self, request):
460         if not request.user.has_perm('picture.add_picture'):
461             return rc.FORBIDDEN
462
463         data = json.loads(request.POST.get('data'))
464         form = PictureImportForm(data)
465         if form.is_valid():
466             form.save()
467             return rc.CREATED
468         else:
469             return rc.NOT_FOUND