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 migdal.models import Entry
13 from piston.handler import AnonymousBaseHandler, BaseHandler
14 from piston.utils import rc
15 from sorl.thumbnail import default
17 from api.models import BookUserData
18 from catalogue.forms import BookImportForm
19 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
20 from catalogue.models.tag import prefetch_relations
21 from catalogue.utils import is_subscribed
22 from librarian.cover import WLCover
23 from picture.models import Picture
24 from picture.forms import PictureImportForm
25 from social.utils import likes
27 from stats.utils import piwik_track
28 from wolnelektury.utils import re_escape
30 from . import emitters # Register our emitters
32 API_BASE = WL_BASE = MEDIA_BASE = lazy(
33 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
45 for k, v in category_singular.items():
46 category_plural[v] = k
48 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
51 def read_tags(tags, request, allowed):
52 """ Reads a path of filtering tags.
54 :param str tags: a path of category and slug pairs, like: authors/an-author/...
55 :returns: list of Tag objects
56 :raises: ValueError when tags can't be found
59 def process(category, slug):
60 if category == 'book':
62 books.append(Book.objects.get(slug=slug))
63 except Book.DoesNotExist:
64 raise ValueError('Unknown book.')
66 real_tags.append(Tag.objects.get(category=category, slug=slug))
67 except Tag.DoesNotExist:
68 raise ValueError('Tag not found')
73 tags = tags.strip('/').split('/')
77 category = tags.pop(0)
81 category = category_singular[category]
83 raise ValueError('Unknown category.')
85 if category not in allowed:
86 raise ValueError('Category not allowed.')
87 process(category, slug)
89 for key in request.GET:
90 if key in category_singular:
91 category = category_singular[key]
92 if category in allowed:
93 for slug in request.GET.getlist(key):
94 process(category, slug)
95 return real_tags, books
101 class BookMediaHandler(BaseHandler):
102 """ Responsible for representing media in Books. """
105 fields = ['name', 'type', 'url', 'artist', 'director']
109 """ Link to media on site. """
111 return MEDIA_BASE + media.file.url
114 def artist(cls, media):
115 return media.extra_info.get('artist_name', '')
118 def director(cls, media):
119 return media.extra_info.get('director_name', '')
122 class BookDetails(object):
123 """Custom fields used for representing Books."""
127 """ Returns an URI for a Book in the API. """
128 return API_BASE + reverse("api_book", args=[book.slug])
132 """ Returns Book's URL on the site. """
133 return WL_BASE + book.get_absolute_url()
136 def children(cls, book):
137 """ Returns all children for a book. """
138 return book.children.order_by('parent_number', 'sort_key')
141 def media(cls, book):
142 """ Returns all media for a book. """
143 return book.media.all()
146 def cover(cls, book):
147 return MEDIA_BASE + book.cover.url if book.cover else ''
150 def cover_thumb(cls, book):
151 return MEDIA_BASE + default.backend.get_thumbnail(
152 book.cover, "139x193").url if book.cover else ''
155 def simple_thumb(cls, book):
156 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
159 def simple_cover(cls, book):
160 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
163 def cover_color(cls, book):
164 return WLCover.epoch_colors.get(book.extra_info['epoch'], '#000000')
167 class BookDetailHandler(BaseHandler, BookDetails):
168 """ Main handler for Book objects.
170 Responsible for single Book details.
172 allowed_methods = ['GET']
173 fields = ['title', 'parent', 'children'] + Book.formats + [
174 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
175 'preview', 'cover_color'] + [
176 category_plural[c] for c in book_tag_categories]
179 def read(self, request, book):
180 """ Returns details of a book, identified by a slug and lang. """
182 return Book.objects.get(slug=book)
183 except Book.DoesNotExist:
187 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
188 """ Main handler for Book objects.
190 Responsible for lists of Book objects.
192 allowed_methods = ('GET',)
194 fields = book_tag_categories + [
195 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color']
198 def genres(cls, book):
199 """ Returns all media for a book. """
200 return book.tags.filter(category='genre')
203 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
204 recommended=False, newest=False, books=None,
205 after=None, before=None, count=None):
206 """ Lists all books with given tags.
208 :param tags: filtering tags; should be a path of categories
209 and slugs, i.e.: authors/an-author/epoch/an-epoch/
210 :param top_level: if True and a book is included in the results,
211 it's children are aren't. By default all books matching the tags
216 return Book.objects.get(pk=pk)
217 except Book.DoesNotExist:
221 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
225 if 'after' in request.GET:
226 after = request.GET['after']
227 if 'before' in request.GET:
228 before = request.GET['before']
229 if 'count' in request.GET:
230 count = request.GET['count']
234 books = Book.tagged_top_level(tags)
235 return books if books else rc.NOT_FOUND
237 books = Book.tagged.with_all(tags)
239 books = books if books is not None else Book.objects.all()
240 books = books.order_by('slug')
243 books = books.filter(parent=None)
245 books = books.filter(media__type='mp3').distinct()
247 books = books.filter(media__type='daisy').distinct()
249 books = books.filter(recommended=True)
251 books = books.order_by('-created_at')
254 books = books.filter(slug__gt=after)
256 books = books.filter(slug__lt=before)
258 books = books.only('slug', 'title', 'cover', 'cover_thumb')
259 for category in book_tag_categories:
260 books = prefetch_relations(books, category)
264 books = list(reversed(books.order_by('-slug')[:count]))
266 books = books[:count]
270 def create(self, request, *args, **kwargs):
274 class BooksHandler(BookDetailHandler):
275 allowed_methods = ('GET', 'POST')
277 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'cover_color', 'slug']
278 anonymous = AnonymousBooksHandler
280 def create(self, request, *args, **kwargs):
281 if not request.user.has_perm('catalogue.add_book'):
284 data = json.loads(request.POST.get('data'))
285 form = BookImportForm(data)
293 class EpubHandler(BookDetailHandler):
294 def read(self, request, slug):
295 if not is_subscribed(request.user):
298 book = Book.objects.get(slug=slug)
299 except Book.DoesNotExist:
301 response = HttpResponse(book.get_media('epub'))
305 class EBooksHandler(AnonymousBooksHandler):
306 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
309 class BookProxy(models.Model):
313 def __init__(self, book, key):
317 def __getattr__(self, item):
318 if item not in ('book', 'key'):
319 return self.book.__getattribute__(item)
321 return self.__getattribute__(item)
324 class QuerySetProxy(models.QuerySet):
325 def __init__(self, l):
329 return iter(self.list)
332 class FilterBooksHandler(AnonymousBooksHandler):
333 fields = book_tag_categories + [
334 'href', 'title', 'url', 'cover', 'cover_thumb', 'cover_color', 'simple_thumb', 'has_audio', 'slug', 'key']
336 def parse_bool(self, s):
337 if s in ('true', 'false'):
342 def read(self, request):
344 search_string = request.GET.get('search')
345 is_lektura = self.parse_bool(request.GET.get('lektura'))
346 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
347 preview = self.parse_bool(request.GET.get('preview'))
349 after = request.GET.get('after')
350 count = int(request.GET.get('count', 50))
351 books = Book.objects.distinct().order_by('slug')
352 if is_lektura is not None:
353 books = books.filter(has_audience=is_lektura)
354 if is_audiobook is not None:
356 books = books.filter(media__type='mp3')
358 books = books.exclude(media__type='mp3')
359 if preview is not None:
360 books = books.filter(preview=preview)
361 for key in request.GET:
362 if key in category_singular:
363 category = category_singular[key]
364 if category in book_tag_categories:
365 slugs = request.GET[key].split(',')
366 tags = Tag.objects.filter(category=category, slug__in=slugs)
367 books = Book.tagged.with_any(tags, books)
368 if (search_string is not None) and len(search_string) < 3:
371 search_string = re_escape(search_string)
372 books_author = books.filter(cached_author__iregex='\m' + search_string)
373 books_title = books.filter(title__iregex='\m' + search_string)
374 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
375 if after and (key_sep in after):
376 which, slug = after.split(key_sep, 1)
378 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
379 else: # which == 'author'
380 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
382 book_lists = [(books_author, 'author'), (books_title, 'title')]
384 if after and key_sep in after:
385 which, slug = after.split(key_sep, 1)
386 books = books.filter(slug__gt=slug)
387 book_lists = [(books, 'book')]
390 for book_list, label in book_lists:
391 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
392 for category in book_tag_categories:
393 book_list = prefetch_relations(book_list, category)
394 remaining_count = count - len(filtered_books)
395 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
396 for book in book_list[:remaining_count]]
397 filtered_books += new_books
398 if len(filtered_books) == count:
401 return QuerySetProxy(filtered_books)
404 class BookPreviewHandler(BookDetailHandler):
405 def read(self, request):
406 return Book.objects.filter(preview=True)
409 # add categorized tags fields for Book
410 def _tags_getter(category):
412 def get_tags(cls, book):
413 return book.tags.filter(category=category)
417 def _tag_getter(category):
419 def get_tag(cls, book):
420 return book.tag_unicode(category)
424 def add_tag_getters():
425 for plural, singular in category_singular.items():
426 setattr(BookDetails, plural, _tags_getter(singular))
427 setattr(BookDetails, singular, _tag_getter(singular))
433 # add fields for files in Book
434 def _file_getter(book_format):
438 f_url = book.media_url(book_format)
440 return MEDIA_BASE + f_url
446 def add_file_getters():
447 for book_format in Book.formats:
448 setattr(BookDetails, book_format, _file_getter(book_format))
454 class CollectionDetails(object):
455 """Custom Collection fields."""
458 def href(cls, collection):
459 """ Returns URI in the API for the collection. """
461 return API_BASE + reverse("api_collection", args=[collection.slug])
464 def url(cls, collection):
465 """ Returns URL on the site. """
467 return WL_BASE + collection.get_absolute_url()
470 def books(cls, collection):
471 return Book.objects.filter(collection.get_query())
474 class CollectionDetailHandler(BaseHandler, CollectionDetails):
475 allowed_methods = ('GET',)
476 fields = ['url', 'title', 'description', 'books']
479 def read(self, request, slug):
480 """ Returns details of a collection, identified by slug. """
482 return Collection.objects.get(slug=slug)
483 except Collection.DoesNotExist:
487 class CollectionsHandler(BaseHandler, CollectionDetails):
488 allowed_methods = ('GET',)
490 fields = ['url', 'href', 'title']
493 def read(self, request):
494 """ Returns all collections. """
495 return Collection.objects.all()
498 class TagDetails(object):
499 """Custom Tag fields."""
503 """ Returns URI in the API for the tag. """
505 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
509 """ Returns URL on the site. """
511 return WL_BASE + tag.get_absolute_url()
514 class TagDetailHandler(BaseHandler, TagDetails):
515 """ Responsible for details of a single Tag object. """
517 fields = ['name', 'url', 'sort_key', 'description']
520 def read(self, request, category, slug):
521 """ Returns details of a tag, identified by category and slug. """
524 category_sng = category_singular[category]
529 return Tag.objects.get(category=category_sng, slug=slug)
530 except Tag.DoesNotExist:
534 class TagsHandler(BaseHandler, TagDetails):
535 """ Main handler for Tag objects.
537 Responsible for lists of Tag objects
538 and fields used for representing Tags.
541 allowed_methods = ('GET',)
543 fields = ['name', 'href', 'url', 'slug']
546 def read(self, request, category=None, pk=None):
547 """ Lists all tags in the category (eg. all themes). """
550 return Tag.objects.exclude(category='set').get(pk=pk)
551 except Book.DoesNotExist:
555 category_sng = category_singular[category]
559 after = request.GET.get('after')
560 before = request.GET.get('before')
561 count = request.GET.get('count')
563 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
565 book_only = request.GET.get('book_only') == 'true'
566 picture_only = request.GET.get('picture_only') == 'true'
568 tags = tags.filter(for_books=True)
570 tags = tags.filter(for_pictures=True)
573 tags = tags.filter(slug__gt=after)
575 tags = tags.filter(slug__lt=before)
579 tags = list(reversed(tags.order_by('-slug')[:count]))
586 class FragmentDetails(object):
587 """Custom Fragment fields."""
590 def href(cls, fragment):
591 """ Returns URI in the API for the fragment. """
593 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
596 def url(cls, fragment):
597 """ Returns URL on the site for the fragment. """
599 return WL_BASE + fragment.get_absolute_url()
602 def themes(cls, fragment):
603 """ Returns a list of theme tags for the fragment. """
605 return fragment.tags.filter(category='theme')
608 class FragmentDetailHandler(BaseHandler, FragmentDetails):
609 fields = ['book', 'anchor', 'text', 'url', 'themes']
612 def read(self, request, book, anchor):
613 """ Returns details of a fragment, identified by book slug and anchor. """
615 return Fragment.objects.get(book__slug=book, anchor=anchor)
616 except Fragment.DoesNotExist:
620 class FragmentsHandler(BaseHandler, FragmentDetails):
621 """ Main handler for Fragments.
623 Responsible for lists of Fragment objects
624 and fields used for representing Fragments.
628 fields = ['book', 'url', 'anchor', 'href']
629 allowed_methods = ('GET',)
631 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
634 def read(self, request, tags):
635 """ Lists all fragments with given book, tags, themes.
637 :param tags: should be a path of categories and slugs, i.e.:
638 books/book-slug/authors/an-author/themes/a-theme/
642 tags, ancestors = read_tags(tags, request, allowed=self.categories)
645 fragments = Fragment.tagged.with_all(tags).select_related('book')
646 if fragments.exists():
652 class PictureHandler(BaseHandler):
654 fields = ('slug', 'title')
655 allowed_methods = ('POST',)
657 def create(self, request):
658 if not request.user.has_perm('picture.add_picture'):
661 data = json.loads(request.POST.get('data'))
662 form = PictureImportForm(data)
670 class UserDataHandler(BaseHandler):
672 fields = ('state', 'username')
673 allowed_methods = ('GET', 'POST')
675 def read(self, request, slug=None):
676 if not request.user.is_authenticated():
679 return {'username': request.user.username}
681 book = Book.objects.get(slug=slug)
682 except Book.DoesNotExist:
685 data = BookUserData.objects.get(book=book, user=request.user)
686 except BookUserData.DoesNotExist:
687 return {'state': 'not_started'}
690 def create(self, request, slug, state):
692 book = Book.objects.get(slug=slug)
693 except Book.DoesNotExist:
695 if not request.user.is_authenticated():
697 if state not in ('reading', 'complete'):
699 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
705 class UserShelfHandler(BookDetailHandler):
706 fields = book_tag_categories + [
707 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
709 def parse_bool(self, s):
710 if s in ('true', 'false'):
715 def read(self, request, state):
716 if not request.user.is_authenticated():
718 if state not in ('reading', 'complete', 'likes'):
720 after = request.GET.get('after')
721 count = int(request.GET.get('count', 50))
723 books = Book.tagged.with_any(request.user.tag_set.all())
725 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
726 .values_list('book_id', flat=True)
727 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
729 books = books.filter(slug__gt=after)
731 books = books[:count]
735 class UserLikeHandler(BaseHandler):
737 allowed_methods = ('GET', 'POST')
739 def read(self, request, slug):
740 if not request.user.is_authenticated():
743 book = Book.objects.get(slug=slug)
744 except Book.DoesNotExist:
746 return {'likes': likes(request.user, book)}
748 def create(self, request, slug, action='like'):
749 if not request.user.is_authenticated():
752 book = Book.objects.get(slug=slug)
753 except Book.DoesNotExist:
756 book.like(request.user)
757 elif action == 'unlike':
758 book.unlike(request.user)
762 class BlogEntryHandler(BaseHandler):
764 fields = ('title', 'lead', 'body', 'place', 'time', 'image_url', 'gallery_urls', 'type', 'key')
766 def read(self, request):
767 after = request.GET.get('after')
768 count = int(request.GET.get('count', 20))
769 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
771 entries = entries.filter(first_published_at__lt=after)
773 entries = entries[:count]
777 def image_url(cls, entry):
778 return entry.image.url if entry.image else None
781 def gallery_urls(cls, entry):
782 return [photo.url() for photo in entry.photo_set.all()]
786 return entry.first_published_at