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.http.response import HttpResponse
10 from django.utils.functional import lazy
11 from django.db import models
12 from piston.handler import AnonymousBaseHandler, BaseHandler
13 from piston.utils import rc
14 from sorl.thumbnail import default
16 from api.models import BookUserData
17 from catalogue.forms import BookImportForm
18 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
19 from catalogue.models.tag import prefetch_relations
20 from catalogue.utils import is_subscribed
21 from picture.models import Picture
22 from picture.forms import PictureImportForm
24 from stats.utils import piwik_track
25 from wolnelektury.utils import re_escape
27 from . import emitters # Register our emitters
29 API_BASE = WL_BASE = MEDIA_BASE = lazy(
30 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
42 for k, v in category_singular.items():
43 category_plural[v] = k
45 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
48 def read_tags(tags, request, allowed):
49 """ Reads a path of filtering tags.
51 :param str tags: a path of category and slug pairs, like: authors/an-author/...
52 :returns: list of Tag objects
53 :raises: ValueError when tags can't be found
56 def process(category, slug):
57 if category == 'book':
59 books.append(Book.objects.get(slug=slug))
60 except Book.DoesNotExist:
61 raise ValueError('Unknown book.')
63 real_tags.append(Tag.objects.get(category=category, slug=slug))
64 except Tag.DoesNotExist:
65 raise ValueError('Tag not found')
70 tags = tags.strip('/').split('/')
74 category = tags.pop(0)
78 category = category_singular[category]
80 raise ValueError('Unknown category.')
82 if category not in allowed:
83 raise ValueError('Category not allowed.')
84 process(category, slug)
86 for key in request.GET:
87 if key in category_singular:
88 category = category_singular[key]
89 if category in allowed:
90 for slug in request.GET.getlist(key):
91 process(category, slug)
92 return real_tags, books
98 class BookMediaHandler(BaseHandler):
99 """ Responsible for representing media in Books. """
102 fields = ['name', 'type', 'url', 'artist', 'director']
106 """ Link to media on site. """
108 return MEDIA_BASE + media.file.url
111 def artist(cls, media):
112 return media.extra_info.get('artist_name', '')
115 def director(cls, media):
116 return media.extra_info.get('director_name', '')
119 class BookDetails(object):
120 """Custom fields used for representing Books."""
124 """ Returns an URI for a Book in the API. """
125 return API_BASE + reverse("api_book", args=[book.slug])
129 """ Returns Book's URL on the site. """
130 return WL_BASE + book.get_absolute_url()
133 def children(cls, book):
134 """ Returns all children for a book. """
135 return book.children.order_by('parent_number', 'sort_key')
138 def media(cls, book):
139 """ Returns all media for a book. """
140 return book.media.all()
143 def cover(cls, book):
144 return MEDIA_BASE + book.cover.url if book.cover else ''
147 def cover_thumb(cls, book):
148 return MEDIA_BASE + default.backend.get_thumbnail(
149 book.cover, "139x193").url if book.cover else ''
152 def simple_thumb(cls, book):
153 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
156 def simple_cover(cls, book):
157 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
160 class BookDetailHandler(BaseHandler, BookDetails):
161 """ Main handler for Book objects.
163 Responsible for single Book details.
165 allowed_methods = ['GET']
166 fields = ['title', 'parent', 'children'] + Book.formats + [
167 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
169 category_plural[c] for c in book_tag_categories]
172 def read(self, request, book):
173 """ Returns details of a book, identified by a slug and lang. """
175 return Book.objects.get(slug=book)
176 except Book.DoesNotExist:
180 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
181 """ Main handler for Book objects.
183 Responsible for lists of Book objects.
185 allowed_methods = ('GET',)
187 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio']
190 def genres(cls, book):
191 """ Returns all media for a book. """
192 return book.tags.filter(category='genre')
195 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
196 recommended=False, newest=False, books=None,
197 after=None, before=None, count=None):
198 """ Lists all books with given tags.
200 :param tags: filtering tags; should be a path of categories
201 and slugs, i.e.: authors/an-author/epoch/an-epoch/
202 :param top_level: if True and a book is included in the results,
203 it's children are aren't. By default all books matching the tags
208 return Book.objects.get(pk=pk)
209 except Book.DoesNotExist:
213 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
217 if 'after' in request.GET:
218 after = request.GET['after']
219 if 'before' in request.GET:
220 before = request.GET['before']
221 if 'count' in request.GET:
222 count = request.GET['count']
226 books = Book.tagged_top_level(tags)
227 return books if books else rc.NOT_FOUND
229 books = Book.tagged.with_all(tags)
231 books = books if books is not None else Book.objects.all()
232 books = books.order_by('slug')
235 books = books.filter(parent=None)
237 books = books.filter(media__type='mp3').distinct()
239 books = books.filter(media__type='daisy').distinct()
241 books = books.filter(recommended=True)
243 books = books.order_by('-created_at')
246 books = books.filter(slug__gt=after)
248 books = books.filter(slug__lt=before)
250 books = books.only('slug', 'title', 'cover', 'cover_thumb')
251 for category in book_tag_categories:
252 books = prefetch_relations(books, category)
256 books = list(reversed(books.order_by('-slug')[:count]))
258 books = books[:count]
262 def create(self, request, *args, **kwargs):
266 class BooksHandler(BookDetailHandler):
267 allowed_methods = ('GET', 'POST')
269 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
270 anonymous = AnonymousBooksHandler
272 def create(self, request, *args, **kwargs):
273 if not request.user.has_perm('catalogue.add_book'):
276 data = json.loads(request.POST.get('data'))
277 form = BookImportForm(data)
285 class EpubHandler(BookDetailHandler):
286 def read(self, request, slug):
287 if not is_subscribed(request.user):
290 book = Book.objects.get(slug=slug)
291 except Book.DoesNotExist:
293 response = HttpResponse(book.get_media('epub'))
297 class EBooksHandler(AnonymousBooksHandler):
298 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
301 class BookProxy(models.Model):
305 def __init__(self, book, key):
309 def __getattr__(self, item):
310 if item not in ('book', 'key'):
311 return self.book.__getattribute__(item)
313 return self.__getattribute__(item)
316 class QuerySetProxy(models.QuerySet):
317 def __init__(self, l):
321 return iter(self.list)
324 class FilterBooksHandler(AnonymousBooksHandler):
325 fields = book_tag_categories + [
326 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'has_audio', 'slug', 'key']
328 def parse_bool(self, s):
329 if s in ('true', 'false'):
334 def read(self, request):
336 search_string = request.GET.get('search')
337 is_lektura = self.parse_bool(request.GET.get('lektura'))
338 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
339 preview = self.parse_bool(request.GET.get('preview'))
341 after = request.GET.get('after')
342 count = int(request.GET.get('count', 50))
343 books = Book.objects.distinct().order_by('slug')
344 if is_lektura is not None:
345 books = books.filter(has_audience=is_lektura)
346 if is_audiobook is not None:
348 books = books.filter(media__type='mp3')
350 books = books.exclude(media__type='mp3')
351 if preview is not None:
352 books = books.filter(preview=preview)
353 for key in request.GET:
354 if key in category_singular:
355 category = category_singular[key]
356 if category in book_tag_categories:
357 slugs = request.GET[key].split(',')
358 tags = Tag.objects.filter(category=category, slug__in=slugs)
359 books = Book.tagged.with_any(tags, books)
360 if (search_string is not None) and len(search_string) < 3:
363 search_string = re_escape(search_string)
364 books_author = books.filter(cached_author__iregex='\m' + search_string)
365 books_title = books.filter(title__iregex='\m' + search_string)
366 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
367 if after and (key_sep in after):
368 which, slug = after.split(key_sep, 1)
370 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
371 else: # which == 'author'
372 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
374 book_lists = [(books_author, 'author'), (books_title, 'title')]
376 if after and key_sep in after:
377 which, slug = after.split(key_sep, 1)
378 books = books.filter(slug__gt=slug)
379 book_lists = [(books, 'book')]
382 for book_list, label in book_lists:
383 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
384 for category in book_tag_categories:
385 book_list = prefetch_relations(book_list, category)
386 remaining_count = count - len(filtered_books)
387 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
388 for book in book_list[:remaining_count]]
389 filtered_books += new_books
390 if len(filtered_books) == count:
393 return QuerySetProxy(filtered_books)
396 # add categorized tags fields for Book
397 def _tags_getter(category):
399 def get_tags(cls, book):
400 return book.tags.filter(category=category)
404 def _tag_getter(category):
406 def get_tag(cls, book):
407 return book.tag_unicode(category)
411 def add_tag_getters():
412 for plural, singular in category_singular.items():
413 setattr(BookDetails, plural, _tags_getter(singular))
414 setattr(BookDetails, singular, _tag_getter(singular))
420 # add fields for files in Book
421 def _file_getter(book_format):
425 f_url = book.media_url(book_format)
427 return MEDIA_BASE + f_url
433 def add_file_getters():
434 for book_format in Book.formats:
435 setattr(BookDetails, book_format, _file_getter(book_format))
441 class CollectionDetails(object):
442 """Custom Collection fields."""
445 def href(cls, collection):
446 """ Returns URI in the API for the collection. """
448 return API_BASE + reverse("api_collection", args=[collection.slug])
451 def url(cls, collection):
452 """ Returns URL on the site. """
454 return WL_BASE + collection.get_absolute_url()
457 def books(cls, collection):
458 return Book.objects.filter(collection.get_query())
461 class CollectionDetailHandler(BaseHandler, CollectionDetails):
462 allowed_methods = ('GET',)
463 fields = ['url', 'title', 'description', 'books']
466 def read(self, request, slug):
467 """ Returns details of a collection, identified by slug. """
469 return Collection.objects.get(slug=slug)
470 except Collection.DoesNotExist:
474 class CollectionsHandler(BaseHandler, CollectionDetails):
475 allowed_methods = ('GET',)
477 fields = ['url', 'href', 'title']
480 def read(self, request):
481 """ Returns all collections. """
482 return Collection.objects.all()
485 class TagDetails(object):
486 """Custom Tag fields."""
490 """ Returns URI in the API for the tag. """
492 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
496 """ Returns URL on the site. """
498 return WL_BASE + tag.get_absolute_url()
501 class TagDetailHandler(BaseHandler, TagDetails):
502 """ Responsible for details of a single Tag object. """
504 fields = ['name', 'url', 'sort_key', 'description']
507 def read(self, request, category, slug):
508 """ Returns details of a tag, identified by category and slug. """
511 category_sng = category_singular[category]
516 return Tag.objects.get(category=category_sng, slug=slug)
517 except Tag.DoesNotExist:
521 class TagsHandler(BaseHandler, TagDetails):
522 """ Main handler for Tag objects.
524 Responsible for lists of Tag objects
525 and fields used for representing Tags.
528 allowed_methods = ('GET',)
530 fields = ['name', 'href', 'url', 'slug']
533 def read(self, request, category=None, pk=None):
534 """ Lists all tags in the category (eg. all themes). """
537 return Tag.objects.exclude(category='set').get(pk=pk)
538 except Book.DoesNotExist:
542 category_sng = category_singular[category]
546 after = request.GET.get('after')
547 before = request.GET.get('before')
548 count = request.GET.get('count')
550 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
552 book_only = request.GET.get('book_only') == 'true'
553 picture_only = request.GET.get('picture_only') == 'true'
555 tags = tags.filter(for_books=True)
557 tags = tags.filter(for_pictures=True)
560 tags = tags.filter(slug__gt=after)
562 tags = tags.filter(slug__lt=before)
566 tags = list(reversed(tags.order_by('-slug')[:count]))
573 class FragmentDetails(object):
574 """Custom Fragment fields."""
577 def href(cls, fragment):
578 """ Returns URI in the API for the fragment. """
580 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
583 def url(cls, fragment):
584 """ Returns URL on the site for the fragment. """
586 return WL_BASE + fragment.get_absolute_url()
589 def themes(cls, fragment):
590 """ Returns a list of theme tags for the fragment. """
592 return fragment.tags.filter(category='theme')
595 class FragmentDetailHandler(BaseHandler, FragmentDetails):
596 fields = ['book', 'anchor', 'text', 'url', 'themes']
599 def read(self, request, book, anchor):
600 """ Returns details of a fragment, identified by book slug and anchor. """
602 return Fragment.objects.get(book__slug=book, anchor=anchor)
603 except Fragment.DoesNotExist:
607 class FragmentsHandler(BaseHandler, FragmentDetails):
608 """ Main handler for Fragments.
610 Responsible for lists of Fragment objects
611 and fields used for representing Fragments.
615 fields = ['book', 'url', 'anchor', 'href']
616 allowed_methods = ('GET',)
618 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
621 def read(self, request, tags):
622 """ Lists all fragments with given book, tags, themes.
624 :param tags: should be a path of categories and slugs, i.e.:
625 books/book-slug/authors/an-author/themes/a-theme/
629 tags, ancestors = read_tags(tags, request, allowed=self.categories)
632 fragments = Fragment.tagged.with_all(tags).select_related('book')
633 if fragments.exists():
639 class PictureHandler(BaseHandler):
641 fields = ('slug', 'title')
642 allowed_methods = ('POST',)
644 def create(self, request):
645 if not request.user.has_perm('picture.add_picture'):
648 data = json.loads(request.POST.get('data'))
649 form = PictureImportForm(data)
657 class UserDataHandler(BaseHandler):
660 allowed_methods = ('GET', 'POST')
662 def read(self, request, slug):
664 book = Book.objects.get(slug=slug)
665 except Book.DoesNotExist:
667 if not request.user.is_authenticated():
670 data = BookUserData.objects.get(book=book, user=request.user)
671 except BookUserData.DoesNotExist:
672 return {'state': 'not_started'}
675 def create(self, request, slug, state):
677 book = Book.objects.get(slug=slug)
678 except Book.DoesNotExist:
680 if not request.user.is_authenticated():
682 if state not in ('reading', 'complete'):
684 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
690 class UserShelfHandler(BookDetailHandler):
691 fields = book_tag_categories + [
692 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
694 def parse_bool(self, s):
695 if s in ('true', 'false'):
700 def read(self, request, state):
701 if not request.user.is_authenticated():
703 if state not in ('reading', 'complete'):
705 after = request.GET.get('after')
706 count = int(request.GET.get('count', 50))
707 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
708 .values_list('book_id', flat=True)
709 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
711 books = books.filter(slug__gt=after)
713 books = books[:count]