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 catalogue.forms import BookImportForm
17 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
18 from catalogue.models.tag import prefetch_relations
19 from catalogue.utils import is_subscribed
20 from picture.models import Picture
21 from picture.forms import PictureImportForm
23 from stats.utils import piwik_track
24 from wolnelektury.utils import re_escape
26 from . import emitters # Register our emitters
28 API_BASE = WL_BASE = MEDIA_BASE = lazy(
29 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
41 for k, v in category_singular.items():
42 category_plural[v] = k
44 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
47 def read_tags(tags, request, allowed):
48 """ Reads a path of filtering tags.
50 :param str tags: a path of category and slug pairs, like: authors/an-author/...
51 :returns: list of Tag objects
52 :raises: ValueError when tags can't be found
55 def process(category, slug):
56 if category == 'book':
58 books.append(Book.objects.get(slug=slug))
59 except Book.DoesNotExist:
60 raise ValueError('Unknown book.')
62 real_tags.append(Tag.objects.get(category=category, slug=slug))
63 except Tag.DoesNotExist:
64 raise ValueError('Tag not found')
69 tags = tags.strip('/').split('/')
73 category = tags.pop(0)
77 category = category_singular[category]
79 raise ValueError('Unknown category.')
81 if category not in allowed:
82 raise ValueError('Category not allowed.')
83 process(category, slug)
85 for key in request.GET:
86 if key in category_singular:
87 category = category_singular[key]
88 if category in allowed:
89 for slug in request.GET.getlist(key):
90 process(category, slug)
91 return real_tags, books
97 class BookMediaHandler(BaseHandler):
98 """ Responsible for representing media in Books. """
101 fields = ['name', 'type', 'url', 'artist', 'director']
105 """ Link to media on site. """
107 return MEDIA_BASE + media.file.url
110 def artist(cls, media):
111 return media.extra_info.get('artist_name', '')
114 def director(cls, media):
115 return media.extra_info.get('director_name', '')
118 class BookDetails(object):
119 """Custom fields used for representing Books."""
123 """ Returns an URI for a Book in the API. """
124 return API_BASE + reverse("api_book", args=[book.slug])
128 """ Returns Book's URL on the site. """
129 return WL_BASE + book.get_absolute_url()
132 def children(cls, book):
133 """ Returns all children for a book. """
134 return book.children.order_by('parent_number', 'sort_key')
137 def media(cls, book):
138 """ Returns all media for a book. """
139 return book.media.all()
142 def cover(cls, book):
143 return MEDIA_BASE + book.cover.url if book.cover else ''
146 def cover_thumb(cls, book):
147 return MEDIA_BASE + default.backend.get_thumbnail(
148 book.cover, "139x193").url if book.cover else ''
151 def simple_thumb(cls, book):
152 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
155 def simple_cover(cls, book):
156 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
159 class BookDetailHandler(BaseHandler, BookDetails):
160 """ Main handler for Book objects.
162 Responsible for single Book details.
164 allowed_methods = ['GET']
165 fields = ['title', 'parent', 'children'] + Book.formats + [
166 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'preview'] + [
167 category_plural[c] for c in book_tag_categories]
170 def read(self, request, book):
171 """ Returns details of a book, identified by a slug and lang. """
173 return Book.objects.get(slug=book)
174 except Book.DoesNotExist:
178 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
179 """ Main handler for Book objects.
181 Responsible for lists of Book objects.
183 allowed_methods = ('GET',)
185 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb']
188 def genres(cls, book):
189 """ Returns all media for a book. """
190 return book.tags.filter(category='genre')
193 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
194 recommended=False, newest=False, books=None,
195 after=None, before=None, count=None):
196 """ Lists all books with given tags.
198 :param tags: filtering tags; should be a path of categories
199 and slugs, i.e.: authors/an-author/epoch/an-epoch/
200 :param top_level: if True and a book is included in the results,
201 it's children are aren't. By default all books matching the tags
206 return Book.objects.get(pk=pk)
207 except Book.DoesNotExist:
211 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
215 if 'after' in request.GET:
216 after = request.GET['after']
217 if 'before' in request.GET:
218 before = request.GET['before']
219 if 'count' in request.GET:
220 count = request.GET['count']
224 books = Book.tagged_top_level(tags)
225 return books if books else rc.NOT_FOUND
227 books = Book.tagged.with_all(tags)
229 books = books if books is not None else Book.objects.all()
230 books = books.order_by('slug')
233 books = books.filter(parent=None)
235 books = books.filter(media__type='mp3').distinct()
237 books = books.filter(media__type='daisy').distinct()
239 books = books.filter(recommended=True)
241 books = books.order_by('-created_at')
244 books = books.filter(slug__gt=after)
246 books = books.filter(slug__lt=before)
248 books = books.only('slug', 'title', 'cover', 'cover_thumb')
249 for category in book_tag_categories:
250 books = prefetch_relations(books, category)
254 books = list(reversed(books.order_by('-slug')[:count]))
256 books = books[:count]
260 def create(self, request, *args, **kwargs):
264 class BooksHandler(BookDetailHandler):
265 allowed_methods = ('GET', 'POST')
267 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
268 anonymous = AnonymousBooksHandler
270 def create(self, request, *args, **kwargs):
271 if not request.user.has_perm('catalogue.add_book'):
274 data = json.loads(request.POST.get('data'))
275 form = BookImportForm(data)
283 class EpubHandler(BookDetailHandler):
284 def read(self, request, slug):
285 if not is_subscribed(request.user):
288 book = Book.objects.get(slug=slug)
289 except Book.DoesNotExist:
291 response = HttpResponse(book.get_media('epub'))
295 class EBooksHandler(AnonymousBooksHandler):
296 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
299 class BookProxy(models.Model):
303 def __init__(self, book, key):
307 def __getattr__(self, item):
308 if item not in ('book', 'key'):
309 return self.book.__getattribute__(item)
311 return self.__getattribute__(item)
314 class QuerySetProxy(models.QuerySet):
315 def __init__(self, l):
319 return iter(self.list)
322 class FilterBooksHandler(AnonymousBooksHandler):
323 fields = book_tag_categories + [
324 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
326 def parse_bool(self, s):
327 if s in ('true', 'false'):
332 def read(self, request):
334 search_string = request.GET.get('search')
335 is_lektura = self.parse_bool(request.GET.get('lektura'))
336 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
337 preview = self.parse_bool(request.GET.get('preview'))
339 after = request.GET.get('after')
340 count = int(request.GET.get('count', 50))
341 books = Book.objects.distinct().order_by('slug')
342 if is_lektura is not None:
343 books = books.filter(has_audience=is_lektura)
344 if is_audiobook is not None:
346 books = books.filter(media__type='mp3')
348 books = books.exclude(media__type='mp3')
349 if preview is not None:
350 books = books.filter(preview=preview)
351 for key in request.GET:
352 if key in category_singular:
353 category = category_singular[key]
354 if category in book_tag_categories:
355 slugs = request.GET[key].split(',')
356 tags = Tag.objects.filter(category=category, slug__in=slugs)
357 books = Book.tagged.with_any(tags, books)
358 if (search_string is not None) and len(search_string) < 3:
361 search_string = re_escape(search_string)
362 books_author = books.filter(cached_author__iregex='\m' + search_string)
363 books_title = books.filter(title__iregex='\m' + search_string)
364 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
365 if after and (key_sep in after):
366 which, slug = after.split(key_sep, 1)
368 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
369 else: # which == 'author'
370 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
372 book_lists = [(books_author, 'author'), (books_title, 'title')]
374 if after and key_sep in after:
375 which, slug = after.split(key_sep, 1)
376 books = books.filter(slug__gt=slug)
377 book_lists = [(books, 'book')]
380 for book_list, label in book_lists:
381 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
382 for category in book_tag_categories:
383 book_list = prefetch_relations(book_list, category)
384 remaining_count = count - len(filtered_books)
385 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
386 for book in book_list[:remaining_count]]
387 filtered_books += new_books
388 if len(filtered_books) == count:
391 return QuerySetProxy(filtered_books)
394 # add categorized tags fields for Book
395 def _tags_getter(category):
397 def get_tags(cls, book):
398 return book.tags.filter(category=category)
402 def _tag_getter(category):
404 def get_tag(cls, book):
405 return book.tag_unicode(category)
409 def add_tag_getters():
410 for plural, singular in category_singular.items():
411 setattr(BookDetails, plural, _tags_getter(singular))
412 setattr(BookDetails, singular, _tag_getter(singular))
418 # add fields for files in Book
419 def _file_getter(book_format):
423 f_url = book.media_url(book_format)
425 return MEDIA_BASE + f_url
431 def add_file_getters():
432 for book_format in Book.formats:
433 setattr(BookDetails, book_format, _file_getter(book_format))
438 class CollectionDetails(object):
439 """Custom Collection fields."""
442 def href(cls, collection):
443 """ Returns URI in the API for the collection. """
445 return API_BASE + reverse("api_collection", args=[collection.slug])
448 def url(cls, collection):
449 """ Returns URL on the site. """
451 return WL_BASE + collection.get_absolute_url()
454 def books(cls, collection):
455 return Book.objects.filter(collection.get_query())
458 class CollectionDetailHandler(BaseHandler, CollectionDetails):
459 allowed_methods = ('GET',)
460 fields = ['url', 'title', 'description', 'books']
463 def read(self, request, slug):
464 """ Returns details of a collection, identified by slug. """
466 return Collection.objects.get(slug=slug)
467 except Collection.DoesNotExist:
471 class CollectionsHandler(BaseHandler, CollectionDetails):
472 allowed_methods = ('GET',)
474 fields = ['url', 'href', 'title']
477 def read(self, request):
478 """ Returns all collections. """
479 return Collection.objects.all()
482 class TagDetails(object):
483 """Custom Tag fields."""
487 """ Returns URI in the API for the tag. """
489 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
493 """ Returns URL on the site. """
495 return WL_BASE + tag.get_absolute_url()
498 class TagDetailHandler(BaseHandler, TagDetails):
499 """ Responsible for details of a single Tag object. """
501 fields = ['name', 'url', 'sort_key', 'description']
504 def read(self, request, category, slug):
505 """ Returns details of a tag, identified by category and slug. """
508 category_sng = category_singular[category]
513 return Tag.objects.get(category=category_sng, slug=slug)
514 except Tag.DoesNotExist:
518 class TagsHandler(BaseHandler, TagDetails):
519 """ Main handler for Tag objects.
521 Responsible for lists of Tag objects
522 and fields used for representing Tags.
525 allowed_methods = ('GET',)
527 fields = ['name', 'href', 'url', 'slug']
530 def read(self, request, category=None, pk=None):
531 """ Lists all tags in the category (eg. all themes). """
534 return Tag.objects.exclude(category='set').get(pk=pk)
535 except Book.DoesNotExist:
539 category_sng = category_singular[category]
543 after = request.GET.get('after')
544 before = request.GET.get('before')
545 count = request.GET.get('count')
547 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
549 book_only = request.GET.get('book_only') == 'true'
550 picture_only = request.GET.get('picture_only') == 'true'
552 tags = tags.filter(for_books=True)
554 tags = tags.filter(for_pictures=True)
557 tags = tags.filter(slug__gt=after)
559 tags = tags.filter(slug__lt=before)
563 tags = list(reversed(tags.order_by('-slug')[:count]))
570 class FragmentDetails(object):
571 """Custom Fragment fields."""
574 def href(cls, fragment):
575 """ Returns URI in the API for the fragment. """
577 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
580 def url(cls, fragment):
581 """ Returns URL on the site for the fragment. """
583 return WL_BASE + fragment.get_absolute_url()
586 def themes(cls, fragment):
587 """ Returns a list of theme tags for the fragment. """
589 return fragment.tags.filter(category='theme')
592 class FragmentDetailHandler(BaseHandler, FragmentDetails):
593 fields = ['book', 'anchor', 'text', 'url', 'themes']
596 def read(self, request, book, anchor):
597 """ Returns details of a fragment, identified by book slug and anchor. """
599 return Fragment.objects.get(book__slug=book, anchor=anchor)
600 except Fragment.DoesNotExist:
604 class FragmentsHandler(BaseHandler, FragmentDetails):
605 """ Main handler for Fragments.
607 Responsible for lists of Fragment objects
608 and fields used for representing Fragments.
612 fields = ['book', 'url', 'anchor', 'href']
613 allowed_methods = ('GET',)
615 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
618 def read(self, request, tags):
619 """ Lists all fragments with given book, tags, themes.
621 :param tags: should be a path of categories and slugs, i.e.:
622 books/book-slug/authors/an-author/themes/a-theme/
626 tags, ancestors = read_tags(tags, allowed=self.categories)
629 fragments = Fragment.tagged.with_all(tags).select_related('book')
630 if fragments.exists():
636 class PictureHandler(BaseHandler):
638 fields = ('slug', 'title')
639 allowed_methods = ('POST',)
641 def create(self, request):
642 if not request.user.has_perm('picture.add_picture'):
645 data = json.loads(request.POST.get('data'))
646 form = PictureImportForm(data)