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 read(self, request):
328 search_string = request.GET.get('search')
329 is_lektura = request.GET.get('lektura')
330 is_audiobook = request.GET.get('audiobook')
332 after = request.GET.get('after')
333 count = int(request.GET.get('count', 50))
334 if is_lektura in ('true', 'false'):
335 is_lektura = is_lektura == 'true'
338 if is_audiobook in ('true', 'false'):
339 is_audiobook = is_audiobook == 'true'
340 books = Book.objects.distinct().order_by('slug')
341 if is_lektura is not None:
342 books = books.filter(has_audience=is_lektura)
343 if is_audiobook is not None:
345 books = books.filter(media__type='mp3')
347 books = books.exclude(media__type='mp3')
348 for key in request.GET:
349 if key in category_singular:
350 category = category_singular[key]
351 if category in book_tag_categories:
352 slugs = request.GET[key].split(',')
353 tags = Tag.objects.filter(category=category, slug__in=slugs)
354 books = Book.tagged.with_any(tags, books)
355 if (search_string is not None) and len(search_string) < 3:
358 search_string = re_escape(search_string)
359 books_author = books.filter(cached_author__iregex='\m' + search_string)
360 books_title = books.filter(title__iregex='\m' + search_string)
361 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
362 if after and (key_sep in after):
363 which, slug = after.split(key_sep, 1)
365 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
366 else: # which == 'author'
367 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
369 book_lists = [(books_author, 'author'), (books_title, 'title')]
371 if after and key_sep in after:
372 which, slug = after.split(key_sep, 1)
373 books = books.filter(slug__gt=slug)
374 book_lists = [(books, 'book')]
377 for book_list, label in book_lists:
378 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
379 for category in book_tag_categories:
380 book_list = prefetch_relations(book_list, category)
381 remaining_count = count - len(filtered_books)
382 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
383 for book in book_list[:remaining_count]]
384 filtered_books += new_books
385 if len(filtered_books) == count:
388 return QuerySetProxy(filtered_books)
391 # add categorized tags fields for Book
392 def _tags_getter(category):
394 def get_tags(cls, book):
395 return book.tags.filter(category=category)
399 def _tag_getter(category):
401 def get_tag(cls, book):
402 return book.tag_unicode(category)
406 def add_tag_getters():
407 for plural, singular in category_singular.items():
408 setattr(BookDetails, plural, _tags_getter(singular))
409 setattr(BookDetails, singular, _tag_getter(singular))
415 # add fields for files in Book
416 def _file_getter(book_format):
420 f_url = book.media_url(book_format)
422 return MEDIA_BASE + f_url
428 def add_file_getters():
429 for book_format in Book.formats:
430 setattr(BookDetails, book_format, _file_getter(book_format))
435 class CollectionDetails(object):
436 """Custom Collection fields."""
439 def href(cls, collection):
440 """ Returns URI in the API for the collection. """
442 return API_BASE + reverse("api_collection", args=[collection.slug])
445 def url(cls, collection):
446 """ Returns URL on the site. """
448 return WL_BASE + collection.get_absolute_url()
451 def books(cls, collection):
452 return Book.objects.filter(collection.get_query())
455 class CollectionDetailHandler(BaseHandler, CollectionDetails):
456 allowed_methods = ('GET',)
457 fields = ['url', 'title', 'description', 'books']
460 def read(self, request, slug):
461 """ Returns details of a collection, identified by slug. """
463 return Collection.objects.get(slug=slug)
464 except Collection.DoesNotExist:
468 class CollectionsHandler(BaseHandler, CollectionDetails):
469 allowed_methods = ('GET',)
471 fields = ['url', 'href', 'title']
474 def read(self, request):
475 """ Returns all collections. """
476 return Collection.objects.all()
479 class TagDetails(object):
480 """Custom Tag fields."""
484 """ Returns URI in the API for the tag. """
486 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
490 """ Returns URL on the site. """
492 return WL_BASE + tag.get_absolute_url()
495 class TagDetailHandler(BaseHandler, TagDetails):
496 """ Responsible for details of a single Tag object. """
498 fields = ['name', 'url', 'sort_key', 'description']
501 def read(self, request, category, slug):
502 """ Returns details of a tag, identified by category and slug. """
505 category_sng = category_singular[category]
510 return Tag.objects.get(category=category_sng, slug=slug)
511 except Tag.DoesNotExist:
515 class TagsHandler(BaseHandler, TagDetails):
516 """ Main handler for Tag objects.
518 Responsible for lists of Tag objects
519 and fields used for representing Tags.
522 allowed_methods = ('GET',)
524 fields = ['name', 'href', 'url', 'slug']
527 def read(self, request, category=None, pk=None):
528 """ Lists all tags in the category (eg. all themes). """
531 return Tag.objects.exclude(category='set').get(pk=pk)
532 except Book.DoesNotExist:
536 category_sng = category_singular[category]
540 after = request.GET.get('after')
541 before = request.GET.get('before')
542 count = request.GET.get('count')
544 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
546 book_only = request.GET.get('book_only') == 'true'
547 picture_only = request.GET.get('picture_only') == 'true'
549 tags = tags.filter(for_books=True)
551 tags = tags.filter(for_pictures=True)
554 tags = tags.filter(slug__gt=after)
556 tags = tags.filter(slug__lt=before)
560 tags = list(reversed(tags.order_by('-slug')[:count]))
567 class FragmentDetails(object):
568 """Custom Fragment fields."""
571 def href(cls, fragment):
572 """ Returns URI in the API for the fragment. """
574 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
577 def url(cls, fragment):
578 """ Returns URL on the site for the fragment. """
580 return WL_BASE + fragment.get_absolute_url()
583 def themes(cls, fragment):
584 """ Returns a list of theme tags for the fragment. """
586 return fragment.tags.filter(category='theme')
589 class FragmentDetailHandler(BaseHandler, FragmentDetails):
590 fields = ['book', 'anchor', 'text', 'url', 'themes']
593 def read(self, request, book, anchor):
594 """ Returns details of a fragment, identified by book slug and anchor. """
596 return Fragment.objects.get(book__slug=book, anchor=anchor)
597 except Fragment.DoesNotExist:
601 class FragmentsHandler(BaseHandler, FragmentDetails):
602 """ Main handler for Fragments.
604 Responsible for lists of Fragment objects
605 and fields used for representing Fragments.
609 fields = ['book', 'url', 'anchor', 'href']
610 allowed_methods = ('GET',)
612 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
615 def read(self, request, tags):
616 """ Lists all fragments with given book, tags, themes.
618 :param tags: should be a path of categories and slugs, i.e.:
619 books/book-slug/authors/an-author/themes/a-theme/
623 tags, ancestors = read_tags(tags, allowed=self.categories)
626 fragments = Fragment.tagged.with_all(tags).select_related('book')
627 if fragments.exists():
633 class PictureHandler(BaseHandler):
635 fields = ('slug', 'title')
636 allowed_methods = ('POST',)
638 def create(self, request):
639 if not request.user.has_perm('picture.add_picture'):
642 data = json.loads(request.POST.get('data'))
643 form = PictureImportForm(data)