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 catalogue.models.tag import prefetch_relations
17 from picture.models import Picture
18 from picture.forms import PictureImportForm
20 from stats.utils import piwik_track
22 from . import emitters # Register our emitters
24 API_BASE = WL_BASE = MEDIA_BASE = lazy(
25 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
37 for k, v in category_singular.items():
38 category_plural[v] = k
40 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
43 def read_tags(tags, allowed):
44 """ Reads a path of filtering tags.
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
53 tags = tags.strip('/').split('/')
57 category = tags.pop(0)
61 category = category_singular[category]
63 raise ValueError('Unknown category.')
65 if category not in allowed:
66 raise ValueError('Category not allowed.')
68 if category == 'book':
70 books.append(Book.objects.get(slug=slug))
71 except Book.DoesNotExist:
72 raise ValueError('Unknown book.')
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
84 class BookMediaHandler(BaseHandler):
85 """ Responsible for representing media in Books. """
88 fields = ['name', 'type', 'url', 'artist', 'director']
92 """ Link to media on site. """
94 return MEDIA_BASE + media.file.url
97 def artist(cls, media):
98 return media.extra_info.get('artist_name', '')
101 def director(cls, media):
102 return media.extra_info.get('director_name', '')
105 class BookDetails(object):
106 """Custom fields used for representing Books."""
110 """ Returns an URI for a Book in the API. """
111 return API_BASE + reverse("api_book", args=[book.slug])
115 """ Returns Book's URL on the site. """
117 return WL_BASE + book.get_absolute_url()
120 def children(cls, book):
121 """ Returns all children for a book. """
123 return book.children.all()
126 def media(cls, book):
127 """ Returns all media for a book. """
128 return book.media.all()
131 def cover(cls, book):
132 return MEDIA_BASE + book.cover.url if book.cover else ''
135 def cover_thumb(cls, book):
136 return MEDIA_BASE + default.backend.get_thumbnail(
137 book.cover, "139x193").url if book.cover else ''
140 def cover_source_image(cls, book):
141 url = book.cover_source()
142 return url.rstrip('/') + '/file/'
145 class BookDetailHandler(BaseHandler, BookDetails):
146 """ Main handler for Book objects.
148 Responsible for single Book details.
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]
156 def read(self, request, book):
157 """ Returns details of a book, identified by a slug and lang. """
159 return Book.objects.get(slug=book)
160 except Book.DoesNotExist:
164 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
165 """ Main handler for Book objects.
167 Responsible for lists of Book objects.
169 allowed_methods = ('GET',)
171 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
174 def genres(cls, book):
175 """ Returns all media for a book. """
176 return book.tags.filter(category='genre')
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.
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
192 return Book.objects.get(pk=pk)
193 except Book.DoesNotExist:
197 tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
203 books = Book.tagged_top_level(tags)
204 return books if books else rc.NOT_FOUND
206 books = Book.tagged.with_all(tags)
208 books = books if books is not None else Book.objects.all()
209 books = books.order_by('slug')
212 books = books.filter(parent=None)
214 books = books.filter(media__type='mp3').distinct()
216 books = books.filter(media__type='daisy').distinct()
218 books = books.filter(recommended=True)
220 books = books.order_by('-created_at')
223 books = books.filter(slug__gt=after)
225 books = books.filter(slug__lt=before)
227 books = books.only('slug', 'title', 'cover', 'cover_thumb')
228 for category in book_tag_categories:
229 books = prefetch_relations(books, category)
233 books = list(reversed(books.order_by('-slug')[:count]))
235 books = books[:count]
242 def create(self, request, *args, **kwargs):
246 class BooksHandler(BookDetailHandler):
247 allowed_methods = ('GET', 'POST')
249 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
250 anonymous = AnonymousBooksHandler
252 def create(self, request, *args, **kwargs):
253 if not request.user.has_perm('catalogue.add_book'):
256 data = json.loads(request.POST.get('data'))
257 form = BookImportForm(data)
265 class EBooksHandler(AnonymousBooksHandler):
266 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
269 class FilterBooksHandler(AnonymousBooksHandler):
270 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'cover_source_image']
272 def read(self, request, title_part=None, author_part=None, is_lektura=None, is_audiobook=None,
273 after=None, before=None, count=None):
276 if is_lektura in ('true', 'false'):
277 is_lektura = is_lektura == 'true'
280 if is_audiobook in ('true', 'false'):
281 is_audiobook = is_audiobook == 'true'
282 books = Book.objects.distinct()
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:
291 books = books.filter(media__type='mp3')
293 books = books.exclude(media__type='mp3')
294 return super(FilterBooksHandler, self).read(request, books=books, after=after, before=before, count=count)
297 # add categorized tags fields for Book
298 def _tags_getter(category):
300 def get_tags(cls, book):
301 return book.tags.filter(category=category)
305 def _tag_getter(category):
307 def get_tag(cls, book):
308 return book.tag_unicode(category)
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))
320 # add fields for files in Book
321 def _file_getter(book_format):
322 field = "%s_file" % book_format
325 def get_file(cls, book):
326 f = getattr(book, field)
328 return MEDIA_BASE + f.url
334 def add_file_getters():
335 for book_format in Book.formats:
336 setattr(BookDetails, book_format, _file_getter(book_format))
341 class CollectionDetails(object):
342 """Custom Collection fields."""
345 def href(cls, collection):
346 """ Returns URI in the API for the collection. """
348 return API_BASE + reverse("api_collection", args=[collection.slug])
351 def url(cls, collection):
352 """ Returns URL on the site. """
354 return WL_BASE + collection.get_absolute_url()
357 def books(cls, collection):
358 return Book.objects.filter(collection.get_query())
361 class CollectionDetailHandler(BaseHandler, CollectionDetails):
362 allowed_methods = ('GET',)
363 fields = ['url', 'title', 'description', 'books']
366 def read(self, request, slug):
367 """ Returns details of a collection, identified by slug. """
369 return Collection.objects.get(slug=slug)
370 except Collection.DoesNotExist:
374 class CollectionsHandler(BaseHandler, CollectionDetails):
375 allowed_methods = ('GET',)
377 fields = ['url', 'href', 'title']
380 def read(self, request):
381 """ Returns all collections. """
382 return Collection.objects.all()
385 class TagDetails(object):
386 """Custom Tag fields."""
390 """ Returns URI in the API for the tag. """
392 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
396 """ Returns URL on the site. """
398 return WL_BASE + tag.get_absolute_url()
401 class TagDetailHandler(BaseHandler, TagDetails):
402 """ Responsible for details of a single Tag object. """
404 fields = ['name', 'url', 'sort_key', 'description']
407 def read(self, request, category, slug):
408 """ Returns details of a tag, identified by category and slug. """
411 category_sng = category_singular[category]
416 return Tag.objects.get(category=category_sng, slug=slug)
417 except Tag.DoesNotExist:
421 class TagsHandler(BaseHandler, TagDetails):
422 """ Main handler for Tag objects.
424 Responsible for lists of Tag objects
425 and fields used for representing Tags.
428 allowed_methods = ('GET',)
430 fields = ['name', 'href', 'url']
433 def read(self, request, category=None, pk=None):
434 """ Lists all tags in the category (eg. all themes). """
437 return Tag.objects.exclude(category='set').get(pk=pk)
438 except Book.DoesNotExist:
442 category_sng = category_singular[category]
446 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
453 class FragmentDetails(object):
454 """Custom Fragment fields."""
457 def href(cls, fragment):
458 """ Returns URI in the API for the fragment. """
460 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
463 def url(cls, fragment):
464 """ Returns URL on the site for the fragment. """
466 return WL_BASE + fragment.get_absolute_url()
469 def themes(cls, fragment):
470 """ Returns a list of theme tags for the fragment. """
472 return fragment.tags.filter(category='theme')
475 class FragmentDetailHandler(BaseHandler, FragmentDetails):
476 fields = ['book', 'anchor', 'text', 'url', 'themes']
479 def read(self, request, book, anchor):
480 """ Returns details of a fragment, identified by book slug and anchor. """
482 return Fragment.objects.get(book__slug=book, anchor=anchor)
483 except Fragment.DoesNotExist:
487 class FragmentsHandler(BaseHandler, FragmentDetails):
488 """ Main handler for Fragments.
490 Responsible for lists of Fragment objects
491 and fields used for representing Fragments.
495 fields = ['book', 'url', 'anchor', 'href']
496 allowed_methods = ('GET',)
498 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
501 def read(self, request, tags):
502 """ Lists all fragments with given book, tags, themes.
504 :param tags: should be a path of categories and slugs, i.e.:
505 books/book-slug/authors/an-author/themes/a-theme/
509 tags, ancestors = read_tags(tags, allowed=self.categories)
512 fragments = Fragment.tagged.with_all(tags).select_related('book')
513 if fragments.exists():
519 class PictureHandler(BaseHandler):
521 fields = ('slug', 'title')
522 allowed_methods = ('POST',)
524 def create(self, request):
525 if not request.user.has_perm('picture.add_picture'):
528 data = json.loads(request.POST.get('data'))
529 form = PictureImportForm(data)