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.
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
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
19 from stats.utils import piwik_track
21 from . import emitters # Register our emitters
23 API_BASE = WL_BASE = MEDIA_BASE = lazy(
24 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
36 for k, v in category_singular.items():
37 category_plural[v] = k
39 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
42 def read_tags(tags, allowed):
43 """ Reads a path of filtering tags.
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
52 tags = tags.strip('/').split('/')
56 category = tags.pop(0)
60 category = category_singular[category]
62 raise ValueError('Unknown category.')
64 if category not in allowed:
65 raise ValueError('Category not allowed.')
67 if category == 'book':
69 books.append(Book.objects.get(slug=slug))
70 except Book.DoesNotExist:
71 raise ValueError('Unknown book.')
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
83 class BookMediaHandler(BaseHandler):
84 """ Responsible for representing media in Books. """
87 fields = ['name', 'type', 'url', 'artist', 'director']
91 """ Link to media on site. """
93 return MEDIA_BASE + media.file.url
96 def artist(cls, media):
97 return media.extra_info.get('artist_name', '')
100 def director(cls, media):
101 return media.extra_info.get('director_name', '')
104 class BookDetails(object):
105 """Custom fields used for representing Books."""
109 """ Returns an URI for a Book in the API. """
110 return API_BASE + reverse("api_book", args=[book.slug])
114 """ Returns Book's URL on the site. """
116 return WL_BASE + book.get_absolute_url()
119 def children(cls, book):
120 """ Returns all children for a book. """
122 return book.children.all()
125 def media(cls, book):
126 """ Returns all media for a book. """
127 return book.media.all()
130 def cover(cls, book):
131 return MEDIA_BASE + book.cover.url if book.cover else ''
134 def cover_thumb(cls, book):
135 return MEDIA_BASE + default.backend.get_thumbnail(
136 book.cover, "139x193").url if book.cover else ''
139 class BookDetailHandler(BaseHandler, BookDetails):
140 """ Main handler for Book objects.
142 Responsible for single Book details.
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]
150 def read(self, request, book):
151 """ Returns details of a book, identified by a slug and lang. """
153 return Book.objects.get(slug=book)
154 except Book.DoesNotExist:
158 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
159 """ Main handler for Book objects.
161 Responsible for lists of Book objects.
163 allowed_methods = ('GET',)
165 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
168 def genres(cls, book):
169 """ Returns all media for a book. """
170 return book.tags.filter(category='genre')
173 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None):
174 """ Lists all books with given tags.
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
184 return Book.objects.get(pk=pk)
185 except Book.DoesNotExist:
189 tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
195 books = Book.tagged_top_level(tags)
196 return books if books else rc.NOT_FOUND
198 books = Book.tagged.with_all(tags)
200 books = Book.objects.all()
203 books = books.filter(parent=None)
205 books = books.filter(media__type='mp3').distinct()
207 books = books.filter(media__type='daisy').distinct()
214 def create(self, request, *args, **kwargs):
218 class BooksHandler(BookDetailHandler):
219 allowed_methods = ('GET', 'POST')
221 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
222 anonymous = AnonymousBooksHandler
224 def create(self, request, *args, **kwargs):
225 if not request.user.has_perm('catalogue.add_book'):
228 data = json.loads(request.POST.get('data'))
229 form = BookImportForm(data)
237 class EBooksHandler(AnonymousBooksHandler):
238 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
241 # add categorized tags fields for Book
242 def _tags_getter(category):
244 def get_tags(cls, book):
245 return book.tags.filter(category=category)
249 def _tag_getter(category):
251 def get_tag(cls, book):
252 return ', '.join(tag.name for tag in book.tags.filter(category=category))
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))
264 # add fields for files in Book
265 def _file_getter(book_format):
266 field = "%s_file" % book_format
269 def get_file(cls, book):
270 f = getattr(book, field)
272 return MEDIA_BASE + f.url
278 def add_file_getters():
279 for book_format in Book.formats:
280 setattr(BookDetails, book_format, _file_getter(book_format))
285 class CollectionDetails(object):
286 """Custom Collection fields."""
289 def href(cls, collection):
290 """ Returns URI in the API for the collection. """
292 return API_BASE + reverse("api_collection", args=[collection.slug])
295 def url(cls, collection):
296 """ Returns URL on the site. """
298 return WL_BASE + collection.get_absolute_url()
301 def books(cls, collection):
302 return Book.objects.filter(collection.get_query())
305 class CollectionDetailHandler(BaseHandler, CollectionDetails):
306 allowed_methods = ('GET',)
307 fields = ['url', 'title', 'description', 'books']
310 def read(self, request, slug):
311 """ Returns details of a collection, identified by slug. """
313 return Collection.objects.get(slug=slug)
314 except Collection.DoesNotExist:
318 class CollectionsHandler(BaseHandler, CollectionDetails):
319 allowed_methods = ('GET',)
321 fields = ['url', 'href', 'title']
324 def read(self, request):
325 """ Returns all collections. """
326 return Collection.objects.all()
329 class TagDetails(object):
330 """Custom Tag fields."""
334 """ Returns URI in the API for the tag. """
336 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
340 """ Returns URL on the site. """
342 return WL_BASE + tag.get_absolute_url()
345 class TagDetailHandler(BaseHandler, TagDetails):
346 """ Responsible for details of a single Tag object. """
348 fields = ['name', 'url', 'sort_key', 'description']
351 def read(self, request, category, slug):
352 """ Returns details of a tag, identified by category and slug. """
355 category_sng = category_singular[category]
360 return Tag.objects.get(category=category_sng, slug=slug)
361 except Tag.DoesNotExist:
365 class TagsHandler(BaseHandler, TagDetails):
366 """ Main handler for Tag objects.
368 Responsible for lists of Tag objects
369 and fields used for representing Tags.
372 allowed_methods = ('GET',)
374 fields = ['name', 'href', 'url']
377 def read(self, request, category=None, pk=None):
378 """ Lists all tags in the category (eg. all themes). """
381 return Tag.objects.exclude(category='set').get(pk=pk)
382 except Book.DoesNotExist:
386 category_sng = category_singular[category]
390 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
397 class FragmentDetails(object):
398 """Custom Fragment fields."""
401 def href(cls, fragment):
402 """ Returns URI in the API for the fragment. """
404 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
407 def url(cls, fragment):
408 """ Returns URL on the site for the fragment. """
410 return WL_BASE + fragment.get_absolute_url()
413 def themes(cls, fragment):
414 """ Returns a list of theme tags for the fragment. """
416 return fragment.tags.filter(category='theme')
419 class FragmentDetailHandler(BaseHandler, FragmentDetails):
420 fields = ['book', 'anchor', 'text', 'url', 'themes']
423 def read(self, request, book, anchor):
424 """ Returns details of a fragment, identified by book slug and anchor. """
426 return Fragment.objects.get(book__slug=book, anchor=anchor)
427 except Fragment.DoesNotExist:
431 class FragmentsHandler(BaseHandler, FragmentDetails):
432 """ Main handler for Fragments.
434 Responsible for lists of Fragment objects
435 and fields used for representing Fragments.
439 fields = ['book', 'url', 'anchor', 'href']
440 allowed_methods = ('GET',)
442 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
445 def read(self, request, tags):
446 """ Lists all fragments with given book, tags, themes.
448 :param tags: should be a path of categories and slugs, i.e.:
449 books/book-slug/authors/an-author/themes/a-theme/
453 tags, ancestors = read_tags(tags, allowed=self.categories)
456 fragments = Fragment.tagged.with_all(tags).select_related('book')
457 if fragments.exists():
463 class PictureHandler(BaseHandler):
465 fields = ('slug', 'title')
466 allowed_methods = ('POST',)
468 def create(self, request):
469 if not request.user.has_perm('picture.add_picture'):
472 data = json.loads(request.POST.get('data'))
473 form = PictureImportForm(data)