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 librarian.cover import WLCover
23 from paypal.rest import user_is_subscribed
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':
68 books.append(Book.objects.get(slug=slug))
69 except Book.DoesNotExist:
70 raise ValueError('Unknown book.')
72 real_tags.append(Tag.objects.get(category=category, slug=slug))
73 except Tag.DoesNotExist:
74 raise ValueError('Tag not found')
79 tags = tags.strip('/').split('/')
83 category = tags.pop(0)
87 category = category_singular[category]
89 raise ValueError('Unknown category.')
91 if category not in allowed:
92 raise ValueError('Category not allowed.')
93 process(category, slug)
95 for key in request.GET:
96 if key in category_singular:
97 category = category_singular[key]
98 if category in allowed:
99 for slug in request.GET.getlist(key):
100 process(category, slug)
101 return real_tags, books
107 class BookMediaHandler(BaseHandler):
108 """ Responsible for representing media in Books. """
111 fields = ['name', 'type', 'url', 'artist', 'director']
115 """ Link to media on site. """
117 return MEDIA_BASE + media.file.url
120 def artist(cls, media):
121 return media.extra_info.get('artist_name', '')
124 def director(cls, media):
125 return media.extra_info.get('director_name', '')
128 class BookDetails(object):
129 """Custom fields used for representing Books."""
133 """ Returns an URI for a Book in the API. """
134 return API_BASE + reverse("api_book", args=[book.slug])
138 """ Returns Book's URL on the site. """
139 return WL_BASE + book.get_absolute_url()
142 def children(cls, book):
143 """ Returns all children for a book. """
144 return book.children.order_by('parent_number', 'sort_key')
147 def media(cls, book):
148 """ Returns all media for a book. """
149 return book.media.all()
152 def cover(cls, book):
153 return MEDIA_BASE + book.cover.url if book.cover else ''
156 def cover_thumb(cls, book):
157 return MEDIA_BASE + default.backend.get_thumbnail(
158 book.cover, "139x193").url if book.cover else ''
161 def simple_thumb(cls, book):
162 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
165 def simple_cover(cls, book):
166 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
169 def cover_color(cls, book):
170 return WLCover.epoch_colors.get(book.extra_info.get('epoch'), '#000000')
173 def full_sort_key(cls, book):
174 return '%s%s%s%s%s' % (book.sort_key_author, SORT_KEY_SEP, book.sort_key, SORT_KEY_SEP, book.id)
177 def books_after(books, after, new_api):
179 return books.filter(slug__gt=after)
181 author, title, book_id = after.split(SORT_KEY_SEP)
183 return Book.objects.none()
184 return books.filter(Q(sort_key_author__gt=author)
185 | (Q(sort_key_author=author) & Q(sort_key__gt=title))
186 | (Q(sort_key_author=author) & Q(sort_key=title) & Q(id__gt=int(book_id))))
189 def order_books(books, new_api):
191 return books.order_by('sort_key_author', 'sort_key', 'id')
193 return books.order_by('slug')
196 class BookDetailHandler(BaseHandler, BookDetails):
197 """ Main handler for Book objects.
199 Responsible for single Book details.
201 allowed_methods = ['GET']
202 fields = ['title', 'parent', 'children'] + Book.formats + [
203 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
204 'preview', 'cover_color'] + [
205 category_plural[c] for c in book_tag_categories]
208 def read(self, request, book):
209 """ Returns details of a book, identified by a slug and lang. """
211 return Book.objects.get(slug=book)
212 except Book.DoesNotExist:
216 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
217 """ Main handler for Book objects.
219 Responsible for lists of Book objects.
221 allowed_methods = ('GET',)
223 fields = book_list_fields
227 def genres(cls, book):
228 """ Returns all media for a book. """
229 return book.tags.filter(category='genre')
232 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
233 recommended=False, newest=False, books=None,
234 after=None, count=None):
235 """ Lists all books with given tags.
237 :param tags: filtering tags; should be a path of categories
238 and slugs, i.e.: authors/an-author/epoch/an-epoch/
239 :param top_level: if True and a book is included in the results,
240 it's children are aren't. By default all books matching the tags
246 return Book.objects.get(pk=pk)
247 except Book.DoesNotExist:
251 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
255 new_api = request.GET.get('new_api')
256 if 'after' in request.GET:
257 after = request.GET['after']
258 if 'count' in request.GET:
259 count = request.GET['count']
263 books = Book.tagged_top_level(tags)
264 return books if books else rc.NOT_FOUND
266 books = Book.tagged.with_all(tags)
268 books = books if books is not None else Book.objects.all()
269 books = self.order_books(books, new_api)
272 books = books.filter(parent=None)
274 books = books.filter(media__type='mp3').distinct()
276 books = books.filter(media__type='daisy').distinct()
278 books = books.filter(recommended=True)
280 books = books.order_by('-created_at')
283 books = self.books_after(books, after, new_api)
286 books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author')
288 books = books.only('slug', 'title', 'cover', 'cover_thumb')
289 for category in book_tag_categories:
290 books = prefetch_relations(books, category)
293 books = books[:count]
297 def create(self, request, *args, **kwargs):
301 class BooksHandler(BookDetailHandler):
302 allowed_methods = ('GET', 'POST')
304 fields = book_list_fields + ['liked']
305 anonymous = AnonymousBooksHandler
307 # hack, because piston is stupid
309 def liked(cls, book):
310 return getattr(book, 'liked', None)
312 def read(self, request, **kwargs):
313 books = AnonymousBooksHandler().read(request, **kwargs)
314 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
317 BookProxy(book).set('liked', book.id in likes)
319 return QuerySetProxy(new_books)
321 def create(self, request, *args, **kwargs):
322 if not request.user.has_perm('catalogue.add_book'):
325 data = json.loads(request.POST.get('data'))
326 form = BookImportForm(data)
334 class EpubHandler(BookDetailHandler):
335 def read(self, request, slug):
336 if not user_is_subscribed(request.user):
339 book = Book.objects.get(slug=slug)
340 except Book.DoesNotExist:
342 response = HttpResponse(book.get_media('epub'))
346 class EBooksHandler(AnonymousBooksHandler):
347 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
350 class BookProxy(models.Model):
354 def __init__(self, book, key=None):
358 def set(self, attr, value):
359 self.__setattr__(attr, value)
362 def __getattr__(self, item):
363 return self.book.__getattribute__(item)
366 class QuerySetProxy(models.QuerySet):
367 def __init__(self, l):
371 return iter(self.list)
374 class AnonFilterBooksHandler(AnonymousBooksHandler):
375 fields = book_list_fields + ['key']
377 def parse_bool(self, s):
378 if s in ('true', 'false'):
383 def read(self, request):
385 search_string = request.GET.get('search')
386 is_lektura = self.parse_bool(request.GET.get('lektura'))
387 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
388 preview = self.parse_bool(request.GET.get('preview'))
390 new_api = request.GET.get('new_api')
391 after = request.GET.get('after')
392 count = int(request.GET.get('count', 50))
393 books = self.order_books(Book.objects.distinct(), new_api)
394 if is_lektura is not None:
395 books = books.filter(has_audience=is_lektura)
396 if is_audiobook is not None:
398 books = books.filter(media__type='mp3')
400 books = books.exclude(media__type='mp3')
401 if preview is not None:
402 books = books.filter(preview=preview)
403 for key in request.GET:
404 if key in category_singular:
405 category = category_singular[key]
406 if category in book_tag_categories:
407 slugs = request.GET[key].split(',')
408 tags = Tag.objects.filter(category=category, slug__in=slugs)
409 books = Book.tagged.with_any(tags, books)
410 if (search_string is not None) and len(search_string) < 3:
413 search_string = re_escape(search_string)
414 books_author = books.filter(cached_author__iregex='\m' + search_string)
415 books_title = books.filter(title__iregex='\m' + search_string)
416 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
417 if after and (key_sep in after):
418 which, key = after.split(key_sep, 1)
420 book_lists = [(self.books_after(books_title, key, new_api), 'title')]
421 else: # which == 'author'
422 book_lists = [(self.books_after(books_author, key, new_api), 'author'), (books_title, 'title')]
424 book_lists = [(books_author, 'author'), (books_title, 'title')]
426 if after and key_sep in after:
427 which, key = after.split(key_sep, 1)
428 books = self.books_after(books, key, new_api)
429 book_lists = [(books, 'book')]
432 for book_list, label in book_lists:
433 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key_author', 'sort_key')
434 for category in book_tag_categories:
435 book_list = prefetch_relations(book_list, category)
436 remaining_count = count - len(filtered_books)
438 BookProxy(book, '%s%s%s' % (
439 label, key_sep, book.slug if not new_api else self.full_sort_key(book)))
440 for book in book_list[:remaining_count]]
441 filtered_books += new_books
442 if len(filtered_books) == count:
445 return QuerySetProxy(filtered_books)
448 class FilterBooksHandler(BooksHandler):
449 anonymous = AnonFilterBooksHandler
450 fields = book_list_fields + ['key', 'liked']
452 # hack, because piston is stupid
454 def liked(cls, book):
455 return getattr(book, 'liked', None)
457 def read(self, request):
458 qsp = AnonFilterBooksHandler().read(request)
459 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
460 for book in qsp.list:
461 book.set('liked', book.id in likes)
465 class BookPreviewHandler(BookDetailHandler):
466 fields = BookDetailHandler.fields + ['slug']
468 def read(self, request):
469 return Book.objects.filter(preview=True)
472 # add categorized tags fields for Book
473 def _tags_getter(category):
475 def get_tags(cls, book):
476 return book.tags.filter(category=category)
480 def _tag_getter(category):
482 def get_tag(cls, book):
483 return book.tag_unicode(category)
487 def add_tag_getters():
488 for plural, singular in category_singular.items():
489 setattr(BookDetails, plural, _tags_getter(singular))
490 setattr(BookDetails, singular, _tag_getter(singular))
496 # add fields for files in Book
497 def _file_getter(book_format):
501 f_url = book.media_url(book_format)
503 return MEDIA_BASE + f_url
509 def add_file_getters():
510 for book_format in Book.formats:
511 setattr(BookDetails, book_format, _file_getter(book_format))
517 class CollectionDetails(object):
518 """Custom Collection fields."""
521 def href(cls, collection):
522 """ Returns URI in the API for the collection. """
524 return API_BASE + reverse("api_collection", args=[collection.slug])
527 def url(cls, collection):
528 """ Returns URL on the site. """
530 return WL_BASE + collection.get_absolute_url()
533 def books(cls, collection):
534 return Book.objects.filter(collection.get_query())
537 class CollectionDetailHandler(BaseHandler, CollectionDetails):
538 allowed_methods = ('GET',)
539 fields = ['url', 'title', 'description', 'books']
542 def read(self, request, slug):
543 """ Returns details of a collection, identified by slug. """
545 return Collection.objects.get(slug=slug)
546 except Collection.DoesNotExist:
550 class CollectionsHandler(BaseHandler, CollectionDetails):
551 allowed_methods = ('GET',)
553 fields = ['url', 'href', 'title']
556 def read(self, request):
557 """ Returns all collections. """
558 return Collection.objects.all()
561 class TagDetails(object):
562 """Custom Tag fields."""
566 """ Returns URI in the API for the tag. """
568 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
572 """ Returns URL on the site. """
574 return WL_BASE + tag.get_absolute_url()
577 class TagDetailHandler(BaseHandler, TagDetails):
578 """ Responsible for details of a single Tag object. """
580 fields = ['name', 'url', 'sort_key', 'description']
583 def read(self, request, category, slug):
584 """ Returns details of a tag, identified by category and slug. """
587 category_sng = category_singular[category]
592 return Tag.objects.get(category=category_sng, slug=slug)
593 except Tag.DoesNotExist:
597 class TagsHandler(BaseHandler, TagDetails):
598 """ Main handler for Tag objects.
600 Responsible for lists of Tag objects
601 and fields used for representing Tags.
604 allowed_methods = ('GET',)
606 fields = ['name', 'href', 'url', 'slug']
609 def read(self, request, category=None, pk=None):
610 """ Lists all tags in the category (eg. all themes). """
614 return Tag.objects.exclude(category='set').get(pk=pk)
615 except Book.DoesNotExist:
619 category_sng = category_singular[category]
623 after = request.GET.get('after')
624 count = request.GET.get('count')
626 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
628 book_only = request.GET.get('book_only') == 'true'
629 picture_only = request.GET.get('picture_only') == 'true'
631 tags = tags.filter(for_books=True)
633 tags = tags.filter(for_pictures=True)
636 tags = tags.filter(slug__gt=after)
644 class FragmentDetails(object):
645 """Custom Fragment fields."""
648 def href(cls, fragment):
649 """ Returns URI in the API for the fragment. """
651 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
654 def url(cls, fragment):
655 """ Returns URL on the site for the fragment. """
657 return WL_BASE + fragment.get_absolute_url()
660 def themes(cls, fragment):
661 """ Returns a list of theme tags for the fragment. """
663 return fragment.tags.filter(category='theme')
666 class FragmentDetailHandler(BaseHandler, FragmentDetails):
667 fields = ['book', 'anchor', 'text', 'url', 'themes']
670 def read(self, request, book, anchor):
671 """ Returns details of a fragment, identified by book slug and anchor. """
673 return Fragment.objects.get(book__slug=book, anchor=anchor)
674 except Fragment.DoesNotExist:
678 class FragmentsHandler(BaseHandler, FragmentDetails):
679 """ Main handler for Fragments.
681 Responsible for lists of Fragment objects
682 and fields used for representing Fragments.
686 fields = ['book', 'url', 'anchor', 'href']
687 allowed_methods = ('GET',)
689 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
692 def read(self, request, tags):
693 """ Lists all fragments with given book, tags, themes.
695 :param tags: should be a path of categories and slugs, i.e.:
696 books/book-slug/authors/an-author/themes/a-theme/
700 tags, ancestors = read_tags(tags, request, allowed=self.categories)
703 fragments = Fragment.tagged.with_all(tags).select_related('book')
704 if fragments.exists():
710 class PictureHandler(BaseHandler):
712 fields = ('slug', 'title')
713 allowed_methods = ('POST',)
715 def create(self, request):
716 if not request.user.has_perm('picture.add_picture'):
719 data = json.loads(request.POST.get('data'))
720 form = PictureImportForm(data)
728 class UserDataHandler(BaseHandler):
730 fields = ('state', 'username', 'premium')
731 allowed_methods = ('GET', 'POST')
733 def read(self, request, slug=None):
734 if not request.user.is_authenticated():
737 return {'username': request.user.username, 'premium': user_is_subscribed(request.user)}
739 book = Book.objects.get(slug=slug)
740 except Book.DoesNotExist:
743 data = BookUserData.objects.get(book=book, user=request.user)
744 except BookUserData.DoesNotExist:
745 return {'state': 'not_started'}
748 def create(self, request, slug, state):
750 book = Book.objects.get(slug=slug)
751 except Book.DoesNotExist:
753 if not request.user.is_authenticated():
755 if state not in ('reading', 'complete'):
757 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
763 class UserShelfHandler(BookDetailHandler):
764 fields = book_list_fields + ['liked']
767 def parse_bool(self, s):
768 if s in ('true', 'false'):
773 # hack, because piston is stupid
775 def liked(cls, book):
776 return getattr(book, 'liked', None)
778 def read(self, request, state):
779 if not request.user.is_authenticated():
781 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
782 if state not in ('reading', 'complete', 'likes'):
784 new_api = request.GET.get('new_api')
785 after = request.GET.get('after')
786 count = int(request.GET.get('count', 50))
788 books = Book.tagged.with_any(request.user.tag_set.all())
790 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
791 .values_list('book_id', flat=True)
792 books = Book.objects.filter(id__in=list(ids)).distinct()
793 books = self.order_books(books, new_api)
795 books = self.books_after(books, after, new_api)
797 books = books[:count]
800 new_books.append(BookProxy(book).set('liked', book.id in likes))
801 return QuerySetProxy(new_books)
804 class UserLikeHandler(BaseHandler):
806 allowed_methods = ('GET', 'POST')
808 def read(self, request, slug):
809 if not request.user.is_authenticated():
812 book = Book.objects.get(slug=slug)
813 except Book.DoesNotExist:
815 return {'likes': likes(request.user, book)}
817 def create(self, request, slug):
818 if not request.user.is_authenticated():
821 book = Book.objects.get(slug=slug)
822 except Book.DoesNotExist:
824 action = request.GET.get('action', 'like')
826 book.like(request.user)
827 elif action == 'unlike':
828 book.unlike(request.user)
832 class BlogEntryHandler(BaseHandler):
835 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
837 def read(self, request):
838 after = request.GET.get('after')
839 count = int(request.GET.get('count', 20))
840 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
842 entries = entries.filter(first_published_at__lt=after)
844 entries = entries[:count]
848 def image_url(cls, entry):
849 return (WL_BASE + entry.image.url) if entry.image else None
852 def image_thumb(cls, entry):
853 return MEDIA_BASE + default.backend.get_thumbnail(
854 entry.image, "193x193").url if entry.image else ''
857 def gallery_urls(cls, entry):
858 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
862 return entry.first_published_at
866 return WL_BASE + entry.get_absolute_url()