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.db.models import Q
10 from django.http.response import HttpResponse
11 from django.utils.functional import lazy
12 from django.db import models
13 from migdal.models import Entry
14 from piston.handler import AnonymousBaseHandler, BaseHandler
15 from piston.utils import rc
16 from sorl.thumbnail import default
18 from api.models import BookUserData
19 from catalogue.forms import BookImportForm
20 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
21 from catalogue.models.tag import prefetch_relations
22 from catalogue.utils import is_subscribed
23 from librarian.cover import WLCover
24 from picture.models import Picture
25 from picture.forms import PictureImportForm
26 from social.utils import likes
28 from stats.utils import piwik_track
29 from wolnelektury.utils import re_escape
31 from . import emitters # Register our emitters
33 API_BASE = WL_BASE = MEDIA_BASE = lazy(
34 lambda: u'https://' + Site.objects.get_current().domain, unicode)()
47 for k, v in category_singular.items():
48 category_plural[v] = k
50 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
52 book_list_fields = book_tag_categories + [
53 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color', 'full_sort_key']
56 def read_tags(tags, request, allowed):
57 """ Reads a path of filtering tags.
59 :param str tags: a path of category and slug pairs, like: authors/an-author/...
60 :returns: list of Tag objects
61 :raises: ValueError when tags can't be found
64 def process(category, slug):
65 if category == 'book':
67 books.append(Book.objects.get(slug=slug))
68 except Book.DoesNotExist:
69 raise ValueError('Unknown book.')
71 real_tags.append(Tag.objects.get(category=category, slug=slug))
72 except Tag.DoesNotExist:
73 raise ValueError('Tag not found')
78 tags = tags.strip('/').split('/')
82 category = tags.pop(0)
86 category = category_singular[category]
88 raise ValueError('Unknown category.')
90 if category not in allowed:
91 raise ValueError('Category not allowed.')
92 process(category, slug)
94 for key in request.GET:
95 if key in category_singular:
96 category = category_singular[key]
97 if category in allowed:
98 for slug in request.GET.getlist(key):
99 process(category, slug)
100 return real_tags, books
106 class BookMediaHandler(BaseHandler):
107 """ Responsible for representing media in Books. """
110 fields = ['name', 'type', 'url', 'artist', 'director']
114 """ Link to media on site. """
116 return MEDIA_BASE + media.file.url
119 def artist(cls, media):
120 return media.extra_info.get('artist_name', '')
123 def director(cls, media):
124 return media.extra_info.get('director_name', '')
127 class BookDetails(object):
128 """Custom fields used for representing Books."""
132 """ Returns an URI for a Book in the API. """
133 return API_BASE + reverse("api_book", args=[book.slug])
137 """ Returns Book's URL on the site. """
138 return WL_BASE + book.get_absolute_url()
141 def children(cls, book):
142 """ Returns all children for a book. """
143 return book.children.order_by('parent_number', 'sort_key')
146 def media(cls, book):
147 """ Returns all media for a book. """
148 return book.media.all()
151 def cover(cls, book):
152 return MEDIA_BASE + book.cover.url if book.cover else ''
155 def cover_thumb(cls, book):
156 return MEDIA_BASE + default.backend.get_thumbnail(
157 book.cover, "139x193").url if book.cover else ''
160 def simple_thumb(cls, book):
161 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
164 def simple_cover(cls, book):
165 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
168 def cover_color(cls, book):
169 return WLCover.epoch_colors.get(book.extra_info.get('epoch'), '#000000')
172 def full_sort_key(cls, book):
173 return '%s%s%s%s%s' % (book.sort_key_author, SORT_KEY_SEP, book.sort_key, SORT_KEY_SEP, book.id)
176 def books_after(books, after, new_api):
178 return books.filter(slug__gt=after)
180 author, title, book_id = after.split(SORT_KEY_SEP)
183 return books.filter(Q(sort_key_author__gt=author)
184 | (Q(sort_key_author=author) & Q(sort_key__gt=title))
185 | (Q(sort_key_author=author) & Q(sort_key=title) & Q(id__gt=int(book_id))))
188 def order_books(books, new_api):
190 return books.order_by('sort_key_author', 'sort_key', 'id')
192 return books.order_by('slug')
195 class BookDetailHandler(BaseHandler, BookDetails):
196 """ Main handler for Book objects.
198 Responsible for single Book details.
200 allowed_methods = ['GET']
201 fields = ['title', 'parent', 'children'] + Book.formats + [
202 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
203 'preview', 'cover_color'] + [
204 category_plural[c] for c in book_tag_categories]
207 def read(self, request, book):
208 """ Returns details of a book, identified by a slug and lang. """
210 return Book.objects.get(slug=book)
211 except Book.DoesNotExist:
215 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
216 """ Main handler for Book objects.
218 Responsible for lists of Book objects.
220 allowed_methods = ('GET',)
222 fields = book_list_fields
225 def genres(cls, book):
226 """ Returns all media for a book. """
227 return book.tags.filter(category='genre')
230 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
231 recommended=False, newest=False, books=None,
232 after=None, count=None):
233 """ Lists all books with given tags.
235 :param tags: filtering tags; should be a path of categories
236 and slugs, i.e.: authors/an-author/epoch/an-epoch/
237 :param top_level: if True and a book is included in the results,
238 it's children are aren't. By default all books matching the tags
243 return Book.objects.get(pk=pk)
244 except Book.DoesNotExist:
248 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
252 new_api = request.GET.get('new_api')
253 if 'after' in request.GET:
254 after = request.GET['after']
255 if 'count' in request.GET:
256 count = request.GET['count']
260 books = Book.tagged_top_level(tags)
261 return books if books else rc.NOT_FOUND
263 books = Book.tagged.with_all(tags)
265 books = books if books is not None else Book.objects.all()
266 books = self.order_books(books, new_api)
269 books = books.filter(parent=None)
271 books = books.filter(media__type='mp3').distinct()
273 books = books.filter(media__type='daisy').distinct()
275 books = books.filter(recommended=True)
277 books = books.order_by('-created_at')
280 books = self.books_after(books, after, new_api)
283 books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author')
285 books = books.only('slug', 'title', 'cover', 'cover_thumb')
286 for category in book_tag_categories:
287 books = prefetch_relations(books, category)
290 books = books[:count]
294 def create(self, request, *args, **kwargs):
298 class BooksHandler(BookDetailHandler):
299 allowed_methods = ('GET', 'POST')
301 fields = book_list_fields + ['liked']
302 anonymous = AnonymousBooksHandler
304 # hack, because piston is stupid
306 def liked(cls, book):
307 return getattr(book, 'liked', None)
309 def read(self, request, **kwargs):
310 books = AnonymousBooksHandler().read(request, **kwargs)
311 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
314 BookProxy(book).set('liked', book.id in likes)
316 return QuerySetProxy(new_books)
318 def create(self, request, *args, **kwargs):
319 if not request.user.has_perm('catalogue.add_book'):
322 data = json.loads(request.POST.get('data'))
323 form = BookImportForm(data)
331 class EpubHandler(BookDetailHandler):
332 def read(self, request, slug):
333 if not is_subscribed(request.user):
336 book = Book.objects.get(slug=slug)
337 except Book.DoesNotExist:
339 response = HttpResponse(book.get_media('epub'))
343 class EBooksHandler(AnonymousBooksHandler):
344 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
347 class BookProxy(models.Model):
351 def __init__(self, book, key=None):
355 def set(self, attr, value):
356 self.__setattr__(attr, value)
359 def __getattr__(self, item):
360 return self.book.__getattribute__(item)
363 class QuerySetProxy(models.QuerySet):
364 def __init__(self, l):
368 return iter(self.list)
371 class AnonFilterBooksHandler(AnonymousBooksHandler):
372 fields = book_list_fields + ['key']
374 def parse_bool(self, s):
375 if s in ('true', 'false'):
380 def read(self, request):
382 search_string = request.GET.get('search')
383 is_lektura = self.parse_bool(request.GET.get('lektura'))
384 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
385 preview = self.parse_bool(request.GET.get('preview'))
387 new_api = request.GET.get('new_api')
388 after = request.GET.get('after')
389 count = int(request.GET.get('count', 50))
390 books = self.order_books(Book.objects.distinct(), new_api)
391 if is_lektura is not None:
392 books = books.filter(has_audience=is_lektura)
393 if is_audiobook is not None:
395 books = books.filter(media__type='mp3')
397 books = books.exclude(media__type='mp3')
398 if preview is not None:
399 books = books.filter(preview=preview)
400 for key in request.GET:
401 if key in category_singular:
402 category = category_singular[key]
403 if category in book_tag_categories:
404 slugs = request.GET[key].split(',')
405 tags = Tag.objects.filter(category=category, slug__in=slugs)
406 books = Book.tagged.with_any(tags, books)
407 if (search_string is not None) and len(search_string) < 3:
410 search_string = re_escape(search_string)
411 books_author = books.filter(cached_author__iregex='\m' + search_string)
412 books_title = books.filter(title__iregex='\m' + search_string)
413 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
414 if after and (key_sep in after):
415 which, key = after.split(key_sep, 1)
417 book_lists = [(self.books_after(books_title, key, new_api), 'title')]
418 else: # which == 'author'
419 book_lists = [(self.books_after(books_author, key, new_api), 'author'), (books_title, 'title')]
421 book_lists = [(books_author, 'author'), (books_title, 'title')]
423 if after and key_sep in after:
424 which, key = after.split(key_sep, 1)
425 books = self.books_after(books, key, new_api)
426 book_lists = [(books, 'book')]
429 for book_list, label in book_lists:
430 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key_author', 'sort_key')
431 for category in book_tag_categories:
432 book_list = prefetch_relations(book_list, category)
433 remaining_count = count - len(filtered_books)
435 BookProxy(book, '%s%s%s' % (
436 label, key_sep, book.slug if not new_api else self.full_sort_key(book)))
437 for book in book_list[:remaining_count]]
438 filtered_books += new_books
439 if len(filtered_books) == count:
442 return QuerySetProxy(filtered_books)
445 class FilterBooksHandler(BooksHandler):
446 anonymous = AnonFilterBooksHandler
447 fields = book_list_fields + ['key', 'liked']
449 # hack, because piston is stupid
451 def liked(cls, book):
452 return getattr(book, 'liked', None)
454 def read(self, request):
455 qsp = AnonFilterBooksHandler().read(request)
456 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
457 for book in qsp.list:
458 book.set('liked', book.id in likes)
462 class BookPreviewHandler(BookDetailHandler):
463 fields = BookDetailHandler.fields + ['slug']
465 def read(self, request):
466 return Book.objects.filter(preview=True)
469 # add categorized tags fields for Book
470 def _tags_getter(category):
472 def get_tags(cls, book):
473 return book.tags.filter(category=category)
477 def _tag_getter(category):
479 def get_tag(cls, book):
480 return book.tag_unicode(category)
484 def add_tag_getters():
485 for plural, singular in category_singular.items():
486 setattr(BookDetails, plural, _tags_getter(singular))
487 setattr(BookDetails, singular, _tag_getter(singular))
493 # add fields for files in Book
494 def _file_getter(book_format):
498 f_url = book.media_url(book_format)
500 return MEDIA_BASE + f_url
506 def add_file_getters():
507 for book_format in Book.formats:
508 setattr(BookDetails, book_format, _file_getter(book_format))
514 class CollectionDetails(object):
515 """Custom Collection fields."""
518 def href(cls, collection):
519 """ Returns URI in the API for the collection. """
521 return API_BASE + reverse("api_collection", args=[collection.slug])
524 def url(cls, collection):
525 """ Returns URL on the site. """
527 return WL_BASE + collection.get_absolute_url()
530 def books(cls, collection):
531 return Book.objects.filter(collection.get_query())
534 class CollectionDetailHandler(BaseHandler, CollectionDetails):
535 allowed_methods = ('GET',)
536 fields = ['url', 'title', 'description', 'books']
539 def read(self, request, slug):
540 """ Returns details of a collection, identified by slug. """
542 return Collection.objects.get(slug=slug)
543 except Collection.DoesNotExist:
547 class CollectionsHandler(BaseHandler, CollectionDetails):
548 allowed_methods = ('GET',)
550 fields = ['url', 'href', 'title']
553 def read(self, request):
554 """ Returns all collections. """
555 return Collection.objects.all()
558 class TagDetails(object):
559 """Custom Tag fields."""
563 """ Returns URI in the API for the tag. """
565 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
569 """ Returns URL on the site. """
571 return WL_BASE + tag.get_absolute_url()
574 class TagDetailHandler(BaseHandler, TagDetails):
575 """ Responsible for details of a single Tag object. """
577 fields = ['name', 'url', 'sort_key', 'description']
580 def read(self, request, category, slug):
581 """ Returns details of a tag, identified by category and slug. """
584 category_sng = category_singular[category]
589 return Tag.objects.get(category=category_sng, slug=slug)
590 except Tag.DoesNotExist:
594 class TagsHandler(BaseHandler, TagDetails):
595 """ Main handler for Tag objects.
597 Responsible for lists of Tag objects
598 and fields used for representing Tags.
601 allowed_methods = ('GET',)
603 fields = ['name', 'href', 'url', 'slug']
606 def read(self, request, category=None, pk=None):
607 """ Lists all tags in the category (eg. all themes). """
610 return Tag.objects.exclude(category='set').get(pk=pk)
611 except Book.DoesNotExist:
615 category_sng = category_singular[category]
619 after = request.GET.get('after')
620 count = request.GET.get('count')
622 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
624 book_only = request.GET.get('book_only') == 'true'
625 picture_only = request.GET.get('picture_only') == 'true'
627 tags = tags.filter(for_books=True)
629 tags = tags.filter(for_pictures=True)
632 tags = tags.filter(slug__gt=after)
640 class FragmentDetails(object):
641 """Custom Fragment fields."""
644 def href(cls, fragment):
645 """ Returns URI in the API for the fragment. """
647 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
650 def url(cls, fragment):
651 """ Returns URL on the site for the fragment. """
653 return WL_BASE + fragment.get_absolute_url()
656 def themes(cls, fragment):
657 """ Returns a list of theme tags for the fragment. """
659 return fragment.tags.filter(category='theme')
662 class FragmentDetailHandler(BaseHandler, FragmentDetails):
663 fields = ['book', 'anchor', 'text', 'url', 'themes']
666 def read(self, request, book, anchor):
667 """ Returns details of a fragment, identified by book slug and anchor. """
669 return Fragment.objects.get(book__slug=book, anchor=anchor)
670 except Fragment.DoesNotExist:
674 class FragmentsHandler(BaseHandler, FragmentDetails):
675 """ Main handler for Fragments.
677 Responsible for lists of Fragment objects
678 and fields used for representing Fragments.
682 fields = ['book', 'url', 'anchor', 'href']
683 allowed_methods = ('GET',)
685 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
688 def read(self, request, tags):
689 """ Lists all fragments with given book, tags, themes.
691 :param tags: should be a path of categories and slugs, i.e.:
692 books/book-slug/authors/an-author/themes/a-theme/
696 tags, ancestors = read_tags(tags, request, allowed=self.categories)
699 fragments = Fragment.tagged.with_all(tags).select_related('book')
700 if fragments.exists():
706 class PictureHandler(BaseHandler):
708 fields = ('slug', 'title')
709 allowed_methods = ('POST',)
711 def create(self, request):
712 if not request.user.has_perm('picture.add_picture'):
715 data = json.loads(request.POST.get('data'))
716 form = PictureImportForm(data)
724 class UserDataHandler(BaseHandler):
726 fields = ('state', 'username', 'premium')
727 allowed_methods = ('GET', 'POST')
729 def read(self, request, slug=None):
730 if not request.user.is_authenticated():
733 return {'username': request.user.username, 'premium': is_subscribed(request.user)}
735 book = Book.objects.get(slug=slug)
736 except Book.DoesNotExist:
739 data = BookUserData.objects.get(book=book, user=request.user)
740 except BookUserData.DoesNotExist:
741 return {'state': 'not_started'}
744 def create(self, request, slug, state):
746 book = Book.objects.get(slug=slug)
747 except Book.DoesNotExist:
749 if not request.user.is_authenticated():
751 if state not in ('reading', 'complete'):
753 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
759 class UserShelfHandler(BookDetailHandler):
760 fields = book_list_fields + ['liked']
762 def parse_bool(self, s):
763 if s in ('true', 'false'):
768 # hack, because piston is stupid
770 def liked(cls, book):
771 return getattr(book, 'liked', None)
773 def read(self, request, state):
774 if not request.user.is_authenticated():
776 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
777 if state not in ('reading', 'complete', 'likes'):
779 new_api = request.GET.get('new_api')
780 after = request.GET.get('after')
781 count = int(request.GET.get('count', 50))
783 books = Book.tagged.with_any(request.user.tag_set.all())
785 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
786 .values_list('book_id', flat=True)
787 books = Book.objects.filter(id__in=list(ids)).distinct()
788 books = self.order_books(books, new_api)
790 books = self.books_after(books, after, new_api)
792 books = books[:count]
795 new_books.append(BookProxy(book).set('liked', book.id in likes))
796 return QuerySetProxy(new_books)
799 class UserLikeHandler(BaseHandler):
801 allowed_methods = ('GET', 'POST')
803 def read(self, request, slug):
804 if not request.user.is_authenticated():
807 book = Book.objects.get(slug=slug)
808 except Book.DoesNotExist:
810 return {'likes': likes(request.user, book)}
812 def create(self, request, slug):
813 if not request.user.is_authenticated():
816 book = Book.objects.get(slug=slug)
817 except Book.DoesNotExist:
819 action = request.GET.get('action', 'like')
821 book.like(request.user)
822 elif action == 'unlike':
823 book.unlike(request.user)
827 class BlogEntryHandler(BaseHandler):
830 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
832 def read(self, request):
833 after = request.GET.get('after')
834 count = int(request.GET.get('count', 20))
835 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
837 entries = entries.filter(first_published_at__lt=after)
839 entries = entries[:count]
843 def image_url(cls, entry):
844 return (WL_BASE + entry.image.url) if entry.image else None
847 def image_thumb(cls, entry):
848 return MEDIA_BASE + default.backend.get_thumbnail(
849 entry.image, "193x193").url if entry.image else ''
852 def gallery_urls(cls, entry):
853 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
857 return entry.first_published_at
861 return WL_BASE + entry.get_absolute_url()