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', 'preview'] + [
168 category_plural[c] for c in book_tag_categories]
171 def read(self, request, book):
172 """ Returns details of a book, identified by a slug and lang. """
174 return Book.objects.get(slug=book)
175 except Book.DoesNotExist:
179 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
180 """ Main handler for Book objects.
182 Responsible for lists of Book objects.
184 allowed_methods = ('GET',)
186 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb']
189 def genres(cls, book):
190 """ Returns all media for a book. """
191 return book.tags.filter(category='genre')
194 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
195 recommended=False, newest=False, books=None,
196 after=None, before=None, count=None):
197 """ Lists all books with given tags.
199 :param tags: filtering tags; should be a path of categories
200 and slugs, i.e.: authors/an-author/epoch/an-epoch/
201 :param top_level: if True and a book is included in the results,
202 it's children are aren't. By default all books matching the tags
207 return Book.objects.get(pk=pk)
208 except Book.DoesNotExist:
212 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
216 if 'after' in request.GET:
217 after = request.GET['after']
218 if 'before' in request.GET:
219 before = request.GET['before']
220 if 'count' in request.GET:
221 count = request.GET['count']
225 books = Book.tagged_top_level(tags)
226 return books if books else rc.NOT_FOUND
228 books = Book.tagged.with_all(tags)
230 books = books if books is not None else Book.objects.all()
231 books = books.order_by('slug')
234 books = books.filter(parent=None)
236 books = books.filter(media__type='mp3').distinct()
238 books = books.filter(media__type='daisy').distinct()
240 books = books.filter(recommended=True)
242 books = books.order_by('-created_at')
245 books = books.filter(slug__gt=after)
247 books = books.filter(slug__lt=before)
249 books = books.only('slug', 'title', 'cover', 'cover_thumb')
250 for category in book_tag_categories:
251 books = prefetch_relations(books, category)
255 books = list(reversed(books.order_by('-slug')[:count]))
257 books = books[:count]
261 def create(self, request, *args, **kwargs):
265 class BooksHandler(BookDetailHandler):
266 allowed_methods = ('GET', 'POST')
268 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
269 anonymous = AnonymousBooksHandler
271 def create(self, request, *args, **kwargs):
272 if not request.user.has_perm('catalogue.add_book'):
275 data = json.loads(request.POST.get('data'))
276 form = BookImportForm(data)
284 class EpubHandler(BookDetailHandler):
285 def read(self, request, slug):
286 if not is_subscribed(request.user):
289 book = Book.objects.get(slug=slug)
290 except Book.DoesNotExist:
292 response = HttpResponse(book.get_media('epub'))
296 class EBooksHandler(AnonymousBooksHandler):
297 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
300 class BookProxy(models.Model):
304 def __init__(self, book, key):
308 def __getattr__(self, item):
309 if item not in ('book', 'key'):
310 return self.book.__getattribute__(item)
312 return self.__getattribute__(item)
315 class QuerySetProxy(models.QuerySet):
316 def __init__(self, l):
320 return iter(self.list)
323 class FilterBooksHandler(AnonymousBooksHandler):
324 fields = book_tag_categories + [
325 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
327 def parse_bool(self, s):
328 if s in ('true', 'false'):
333 def read(self, request):
335 search_string = request.GET.get('search')
336 is_lektura = self.parse_bool(request.GET.get('lektura'))
337 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
338 preview = self.parse_bool(request.GET.get('preview'))
340 after = request.GET.get('after')
341 count = int(request.GET.get('count', 50))
342 books = Book.objects.distinct().order_by('slug')
343 if is_lektura is not None:
344 books = books.filter(has_audience=is_lektura)
345 if is_audiobook is not None:
347 books = books.filter(media__type='mp3')
349 books = books.exclude(media__type='mp3')
350 if preview is not None:
351 books = books.filter(preview=preview)
352 for key in request.GET:
353 if key in category_singular:
354 category = category_singular[key]
355 if category in book_tag_categories:
356 slugs = request.GET[key].split(',')
357 tags = Tag.objects.filter(category=category, slug__in=slugs)
358 books = Book.tagged.with_any(tags, books)
359 if (search_string is not None) and len(search_string) < 3:
362 search_string = re_escape(search_string)
363 books_author = books.filter(cached_author__iregex='\m' + search_string)
364 books_title = books.filter(title__iregex='\m' + search_string)
365 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
366 if after and (key_sep in after):
367 which, slug = after.split(key_sep, 1)
369 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
370 else: # which == 'author'
371 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
373 book_lists = [(books_author, 'author'), (books_title, 'title')]
375 if after and key_sep in after:
376 which, slug = after.split(key_sep, 1)
377 books = books.filter(slug__gt=slug)
378 book_lists = [(books, 'book')]
381 for book_list, label in book_lists:
382 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
383 for category in book_tag_categories:
384 book_list = prefetch_relations(book_list, category)
385 remaining_count = count - len(filtered_books)
386 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
387 for book in book_list[:remaining_count]]
388 filtered_books += new_books
389 if len(filtered_books) == count:
392 return QuerySetProxy(filtered_books)
395 # add categorized tags fields for Book
396 def _tags_getter(category):
398 def get_tags(cls, book):
399 return book.tags.filter(category=category)
403 def _tag_getter(category):
405 def get_tag(cls, book):
406 return book.tag_unicode(category)
410 def add_tag_getters():
411 for plural, singular in category_singular.items():
412 setattr(BookDetails, plural, _tags_getter(singular))
413 setattr(BookDetails, singular, _tag_getter(singular))
419 # add fields for files in Book
420 def _file_getter(book_format):
424 f_url = book.media_url(book_format)
426 return MEDIA_BASE + f_url
432 def add_file_getters():
433 for book_format in Book.formats:
434 setattr(BookDetails, book_format, _file_getter(book_format))
439 class CollectionDetails(object):
440 """Custom Collection fields."""
443 def href(cls, collection):
444 """ Returns URI in the API for the collection. """
446 return API_BASE + reverse("api_collection", args=[collection.slug])
449 def url(cls, collection):
450 """ Returns URL on the site. """
452 return WL_BASE + collection.get_absolute_url()
455 def books(cls, collection):
456 return Book.objects.filter(collection.get_query())
459 class CollectionDetailHandler(BaseHandler, CollectionDetails):
460 allowed_methods = ('GET',)
461 fields = ['url', 'title', 'description', 'books']
464 def read(self, request, slug):
465 """ Returns details of a collection, identified by slug. """
467 return Collection.objects.get(slug=slug)
468 except Collection.DoesNotExist:
472 class CollectionsHandler(BaseHandler, CollectionDetails):
473 allowed_methods = ('GET',)
475 fields = ['url', 'href', 'title']
478 def read(self, request):
479 """ Returns all collections. """
480 return Collection.objects.all()
483 class TagDetails(object):
484 """Custom Tag fields."""
488 """ Returns URI in the API for the tag. """
490 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
494 """ Returns URL on the site. """
496 return WL_BASE + tag.get_absolute_url()
499 class TagDetailHandler(BaseHandler, TagDetails):
500 """ Responsible for details of a single Tag object. """
502 fields = ['name', 'url', 'sort_key', 'description']
505 def read(self, request, category, slug):
506 """ Returns details of a tag, identified by category and slug. """
509 category_sng = category_singular[category]
514 return Tag.objects.get(category=category_sng, slug=slug)
515 except Tag.DoesNotExist:
519 class TagsHandler(BaseHandler, TagDetails):
520 """ Main handler for Tag objects.
522 Responsible for lists of Tag objects
523 and fields used for representing Tags.
526 allowed_methods = ('GET',)
528 fields = ['name', 'href', 'url', 'slug']
531 def read(self, request, category=None, pk=None):
532 """ Lists all tags in the category (eg. all themes). """
535 return Tag.objects.exclude(category='set').get(pk=pk)
536 except Book.DoesNotExist:
540 category_sng = category_singular[category]
544 after = request.GET.get('after')
545 before = request.GET.get('before')
546 count = request.GET.get('count')
548 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
550 book_only = request.GET.get('book_only') == 'true'
551 picture_only = request.GET.get('picture_only') == 'true'
553 tags = tags.filter(for_books=True)
555 tags = tags.filter(for_pictures=True)
558 tags = tags.filter(slug__gt=after)
560 tags = tags.filter(slug__lt=before)
564 tags = list(reversed(tags.order_by('-slug')[:count]))
571 class FragmentDetails(object):
572 """Custom Fragment fields."""
575 def href(cls, fragment):
576 """ Returns URI in the API for the fragment. """
578 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
581 def url(cls, fragment):
582 """ Returns URL on the site for the fragment. """
584 return WL_BASE + fragment.get_absolute_url()
587 def themes(cls, fragment):
588 """ Returns a list of theme tags for the fragment. """
590 return fragment.tags.filter(category='theme')
593 class FragmentDetailHandler(BaseHandler, FragmentDetails):
594 fields = ['book', 'anchor', 'text', 'url', 'themes']
597 def read(self, request, book, anchor):
598 """ Returns details of a fragment, identified by book slug and anchor. """
600 return Fragment.objects.get(book__slug=book, anchor=anchor)
601 except Fragment.DoesNotExist:
605 class FragmentsHandler(BaseHandler, FragmentDetails):
606 """ Main handler for Fragments.
608 Responsible for lists of Fragment objects
609 and fields used for representing Fragments.
613 fields = ['book', 'url', 'anchor', 'href']
614 allowed_methods = ('GET',)
616 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
619 def read(self, request, tags):
620 """ Lists all fragments with given book, tags, themes.
622 :param tags: should be a path of categories and slugs, i.e.:
623 books/book-slug/authors/an-author/themes/a-theme/
627 tags, ancestors = read_tags(tags, allowed=self.categories)
630 fragments = Fragment.tagged.with_all(tags).select_related('book')
631 if fragments.exists():
637 class PictureHandler(BaseHandler):
639 fields = ('slug', 'title')
640 allowed_methods = ('POST',)
642 def create(self, request):
643 if not request.user.has_perm('picture.add_picture'):
646 data = json.loads(request.POST.get('data'))
647 form = PictureImportForm(data)
655 class UserDataHandler(BaseHandler):
658 allowed_methods = ('GET', 'POST')
660 def read(self, request, slug):
662 book = Book.objects.get(slug=slug)
663 except Book.DoesNotExist:
665 if not request.user.is_authenticated():
668 data = BookUserData.objects.get(book=book, user=request.user)
669 except BookUserData.DoesNotExist:
670 return {'state': 'not_started'}
673 def create(self, request, slug, state):
675 book = Book.objects.get(slug=slug)
676 except Book.DoesNotExist:
678 if not request.user.is_authenticated():
680 if state not in ('reading', 'complete'):
682 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
688 class UserShelfHandler(BookDetailHandler):
689 fields = book_tag_categories + [
690 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
692 def parse_bool(self, s):
693 if s in ('true', 'false'):
698 def read(self, request, state):
699 if not request.user.is_authenticated():
701 if state not in ('reading', 'complete'):
703 after = request.GET.get('after')
704 count = int(request.GET.get('count', 50))
705 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete').values_list('book_id', flat=True)
706 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
708 books = books.filter(slug__gt=after)
710 books = books[:count]