pep8 and other code-style changes
[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         if books.exists():
210             return books
211         else:
212             return rc.NOT_FOUND
213
214     def create(self, request, *args, **kwargs):
215         return rc.FORBIDDEN
216
217
218 class BooksHandler(BookDetailHandler):
219     allowed_methods = ('GET', 'POST')
220     model = Book
221     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
222     anonymous = AnonymousBooksHandler
223
224     def create(self, request, *args, **kwargs):
225         if not request.user.has_perm('catalogue.add_book'):
226             return rc.FORBIDDEN
227
228         data = json.loads(request.POST.get('data'))
229         form = BookImportForm(data)
230         if form.is_valid():
231             form.save()
232             return rc.CREATED
233         else:
234             return rc.NOT_FOUND
235
236
237 class EBooksHandler(AnonymousBooksHandler):
238     fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
239
240
241 # add categorized tags fields for Book
242 def _tags_getter(category):
243     @classmethod
244     def get_tags(cls, book):
245         return book.tags.filter(category=category)
246     return get_tags
247
248
249 def _tag_getter(category):
250     @classmethod
251     def get_tag(cls, book):
252         return ', '.join(tag.name for tag in book.tags.filter(category=category))
253     return get_tag
254
255
256 def add_tag_getters():
257     for plural, singular in category_singular.items():
258         setattr(BookDetails, plural, _tags_getter(singular))
259         setattr(BookDetails, singular, _tag_getter(singular))
260
261 add_tag_getters()
262
263
264 # add fields for files in Book
265 def _file_getter(book_format):
266     field = "%s_file" % book_format
267
268     @classmethod
269     def get_file(cls, book):
270         f = getattr(book, field)
271         if f:
272             return MEDIA_BASE + f.url
273         else:
274             return ''
275     return get_file
276
277
278 def add_file_getters():
279     for book_format in Book.formats:
280         setattr(BookDetails, book_format, _file_getter(book_format))
281
282 add_file_getters()
283
284
285 class CollectionDetails(object):
286     """Custom Collection fields."""
287
288     @classmethod
289     def href(cls, collection):
290         """ Returns URI in the API for the collection. """
291
292         return API_BASE + reverse("api_collection", args=[collection.slug])
293
294     @classmethod
295     def url(cls, collection):
296         """ Returns URL on the site. """
297
298         return WL_BASE + collection.get_absolute_url()
299
300     @classmethod
301     def books(cls, collection):
302         return Book.objects.filter(collection.get_query())
303
304
305 class CollectionDetailHandler(BaseHandler, CollectionDetails):
306     allowed_methods = ('GET',)
307     fields = ['url', 'title', 'description', 'books']
308
309     @piwik_track
310     def read(self, request, slug):
311         """ Returns details of a collection, identified by slug. """
312         try:
313             return Collection.objects.get(slug=slug)
314         except Collection.DoesNotExist:
315             return rc.NOT_FOUND
316
317
318 class CollectionsHandler(BaseHandler, CollectionDetails):
319     allowed_methods = ('GET',)
320     model = Collection
321     fields = ['url', 'href', 'title']
322
323     @piwik_track
324     def read(self, request):
325         """ Returns all collections. """
326         return Collection.objects.all()
327
328
329 class TagDetails(object):
330     """Custom Tag fields."""
331
332     @classmethod
333     def href(cls, tag):
334         """ Returns URI in the API for the tag. """
335
336         return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
337
338     @classmethod
339     def url(cls, tag):
340         """ Returns URL on the site. """
341
342         return WL_BASE + tag.get_absolute_url()
343
344
345 class TagDetailHandler(BaseHandler, TagDetails):
346     """ Responsible for details of a single Tag object. """
347
348     fields = ['name', 'url', 'sort_key', 'description']
349
350     @piwik_track
351     def read(self, request, category, slug):
352         """ Returns details of a tag, identified by category and slug. """
353
354         try:
355             category_sng = category_singular[category]
356         except KeyError:
357             return rc.NOT_FOUND
358
359         try:
360             return Tag.objects.get(category=category_sng, slug=slug)
361         except Tag.DoesNotExist:
362             return rc.NOT_FOUND
363
364
365 class TagsHandler(BaseHandler, TagDetails):
366     """ Main handler for Tag objects.
367
368     Responsible for lists of Tag objects
369     and fields used for representing Tags.
370
371     """
372     allowed_methods = ('GET',)
373     model = Tag
374     fields = ['name', 'href', 'url']
375
376     @piwik_track
377     def read(self, request, category=None, pk=None):
378         """ Lists all tags in the category (eg. all themes). """
379         if pk is not None:
380             try:
381                 return Tag.objects.exclude(category='set').get(pk=pk)
382             except Book.DoesNotExist:
383                 return rc.NOT_FOUND
384
385         try:
386             category_sng = category_singular[category]
387         except KeyError:
388             return rc.NOT_FOUND
389
390         tags = Tag.objects.filter(category=category_sng).exclude(items=None)
391         if tags.exists():
392             return tags
393         else:
394             return rc.NOT_FOUND
395
396
397 class FragmentDetails(object):
398     """Custom Fragment fields."""
399
400     @classmethod
401     def href(cls, fragment):
402         """ Returns URI in the API for the fragment. """
403
404         return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
405
406     @classmethod
407     def url(cls, fragment):
408         """ Returns URL on the site for the fragment. """
409
410         return WL_BASE + fragment.get_absolute_url()
411
412     @classmethod
413     def themes(cls, fragment):
414         """ Returns a list of theme tags for the fragment. """
415
416         return fragment.tags.filter(category='theme')
417
418
419 class FragmentDetailHandler(BaseHandler, FragmentDetails):
420     fields = ['book', 'anchor', 'text', 'url', 'themes']
421
422     @piwik_track
423     def read(self, request, book, anchor):
424         """ Returns details of a fragment, identified by book slug and anchor. """
425         try:
426             return Fragment.objects.get(book__slug=book, anchor=anchor)
427         except Fragment.DoesNotExist:
428             return rc.NOT_FOUND
429
430
431 class FragmentsHandler(BaseHandler, FragmentDetails):
432     """ Main handler for Fragments.
433
434     Responsible for lists of Fragment objects
435     and fields used for representing Fragments.
436
437     """
438     model = Fragment
439     fields = ['book', 'url', 'anchor', 'href']
440     allowed_methods = ('GET',)
441
442     categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
443
444     @piwik_track
445     def read(self, request, tags):
446         """ Lists all fragments with given book, tags, themes.
447
448         :param tags: should be a path of categories and slugs, i.e.:
449              books/book-slug/authors/an-author/themes/a-theme/
450
451         """
452         try:
453             tags, ancestors = read_tags(tags, allowed=self.categories)
454         except ValueError:
455             return rc.NOT_FOUND
456         fragments = Fragment.tagged.with_all(tags).select_related('book')
457         if fragments.exists():
458             return fragments
459         else:
460             return rc.NOT_FOUND
461
462
463 class PictureHandler(BaseHandler):
464     model = Picture
465     fields = ('slug', 'title')
466     allowed_methods = ('POST',)
467
468     def create(self, request):
469         if not request.user.has_perm('picture.add_picture'):
470             return rc.FORBIDDEN
471
472         data = json.loads(request.POST.get('data'))
473         form = PictureImportForm(data)
474         if form.is_valid():
475             form.save()
476             return rc.CREATED
477         else:
478             return rc.NOT_FOUND