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 paypal.rest import user_is_subscribed
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
31 API_BASE = WL_BASE = MEDIA_BASE = lazy(
32 lambda: u'https://' + Site.objects.get_current().domain, unicode)()
43 for k, v in category_singular.items():
44 category_plural[v] = k
46 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
48 book_list_fields = book_tag_categories + [
49 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color', 'full_sort_key']
52 def read_tags(tags, request, allowed):
53 """ Reads a path of filtering tags.
55 :param str tags: a path of category and slug pairs, like: authors/an-author/...
56 :returns: list of Tag objects
57 :raises: ValueError when tags can't be found
60 def process(category, slug):
61 if category == 'book':
64 books.append(Book.objects.get(slug=slug))
65 except Book.DoesNotExist:
66 raise ValueError('Unknown book.')
68 real_tags.append(Tag.objects.get(category=category, slug=slug))
69 except Tag.DoesNotExist:
70 raise ValueError('Tag not found')
75 tags = tags.strip('/').split('/')
79 category = tags.pop(0)
83 category = category_singular[category]
85 raise ValueError('Unknown category.')
87 if category not in allowed:
88 raise ValueError('Category not allowed.')
89 process(category, slug)
91 for key in request.GET:
92 if key in category_singular:
93 category = category_singular[key]
94 if category in allowed:
95 for slug in request.GET.getlist(key):
96 process(category, slug)
97 return real_tags, books
103 class BookMediaHandler(BaseHandler):
104 """ Responsible for representing media in Books. """
107 fields = ['name', 'type', 'url', 'artist', 'director']
111 """ Link to media on site. """
113 return MEDIA_BASE + media.file.url
116 def artist(cls, media):
117 return media.extra_info.get('artist_name', '')
120 def director(cls, media):
121 return media.extra_info.get('director_name', '')
124 class BookDetails(object):
125 """Custom fields used for representing Books."""
129 """ Returns an URI for a Book in the API. """
130 return API_BASE + reverse("catalogue_api_book", args=[book.slug])
134 """ Returns Book's URL on the site. """
135 return WL_BASE + book.get_absolute_url()
138 def children(cls, book):
139 """ Returns all children for a book. """
140 return book.children.order_by('parent_number', 'sort_key')
143 def media(cls, book):
144 """ Returns all media for a book. """
145 return book.media.all()
148 def cover(cls, book):
149 return MEDIA_BASE + book.cover.url if book.cover else ''
152 def cover_thumb(cls, book):
153 return MEDIA_BASE + default.backend.get_thumbnail(
154 book.cover, "139x193").url if book.cover else ''
157 def simple_thumb(cls, book):
158 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
161 def simple_cover(cls, book):
162 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
165 def books_after(books, after, new_api):
167 return books.filter(slug__gt=after)
169 author, title, book_id = after.split(Book.SORT_KEY_SEP)
171 return Book.objects.none()
172 return books.filter(Q(sort_key_author__gt=author)
173 | (Q(sort_key_author=author) & Q(sort_key__gt=title))
174 | (Q(sort_key_author=author) & Q(sort_key=title) & Q(id__gt=int(book_id))))
177 def order_books(books, new_api):
179 return books.order_by('sort_key_author', 'sort_key', 'id')
181 return books.order_by('slug')
184 class BookDetailHandler(BaseHandler, BookDetails):
185 """ Main handler for Book objects.
187 Responsible for single Book details.
189 allowed_methods = ['GET']
190 fields = ['title', 'parent', 'children'] + Book.formats + [
191 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
192 'preview', 'cover_color'] + [
193 category_plural[c] for c in book_tag_categories]
196 def read(self, request, book):
197 """ Returns details of a book, identified by a slug and lang. """
199 return Book.objects.get(slug=book)
200 except Book.DoesNotExist:
204 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
205 """ Main handler for Book objects.
207 Responsible for lists of Book objects.
209 allowed_methods = ('GET',)
211 fields = book_list_fields
215 def genres(cls, book):
216 """ Returns all media for a book. """
217 return book.tags.filter(category='genre')
220 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
221 recommended=False, newest=False, books=None,
222 after=None, count=None):
223 """ Lists all books with given tags.
225 :param tags: filtering tags; should be a path of categories
226 and slugs, i.e.: authors/an-author/epoch/an-epoch/
227 :param top_level: if True and a book is included in the results,
228 it's children are aren't. By default all books matching the tags
234 return Book.objects.get(pk=pk)
235 except Book.DoesNotExist:
239 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
243 new_api = request.GET.get('new_api')
244 if 'after' in request.GET:
245 after = request.GET['after']
246 if 'count' in request.GET:
247 count = request.GET['count']
251 books = Book.tagged_top_level(tags)
252 return books if books else rc.NOT_FOUND
254 books = Book.tagged.with_all(tags)
256 books = books if books is not None else Book.objects.all()
257 books = self.order_books(books, new_api)
260 books = books.filter(parent=None)
262 books = books.filter(media__type='mp3').distinct()
264 books = books.filter(media__type='daisy').distinct()
266 books = books.filter(recommended=True)
268 books = books.order_by('-created_at')
271 books = self.books_after(books, after, new_api)
274 books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author')
276 books = books.only('slug', 'title', 'cover', 'cover_thumb')
277 for category in book_tag_categories:
278 books = prefetch_relations(books, category)
281 books = books[:count]
285 def create(self, request, *args, **kwargs):
289 class BooksHandler(BookDetailHandler):
290 allowed_methods = ('GET', 'POST')
292 fields = book_list_fields + ['liked']
293 anonymous = AnonymousBooksHandler
295 # hack, because piston is stupid
297 def liked(cls, book):
298 return getattr(book, 'liked', None)
300 def read(self, request, **kwargs):
301 books = AnonymousBooksHandler().read(request, **kwargs)
302 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
305 BookProxy(book).set('liked', book.id in likes)
307 return QuerySetProxy(new_books)
309 def create(self, request, *args, **kwargs):
310 if not request.user.has_perm('catalogue.add_book'):
313 data = json.loads(request.POST.get('data'))
314 form = BookImportForm(data)
322 class EBooksHandler(AnonymousBooksHandler):
323 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
326 class BookProxy(models.Model):
330 def __init__(self, book, key=None):
334 def set(self, attr, value):
335 self.__setattr__(attr, value)
338 def __getattr__(self, item):
339 return self.book.__getattribute__(item)
342 class QuerySetProxy(models.QuerySet):
343 def __init__(self, l):
347 return iter(self.list)
350 class AnonFilterBooksHandler(AnonymousBooksHandler):
351 fields = book_list_fields + ['key']
353 def parse_bool(self, s):
354 if s in ('true', 'false'):
359 def read(self, request):
361 search_string = request.GET.get('search')
362 is_lektura = self.parse_bool(request.GET.get('lektura'))
363 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
364 preview = self.parse_bool(request.GET.get('preview'))
366 new_api = request.GET.get('new_api')
367 after = request.GET.get('after')
368 count = int(request.GET.get('count', 50))
369 books = self.order_books(Book.objects.distinct(), new_api)
370 if is_lektura is not None:
371 books = books.filter(has_audience=is_lektura)
372 if is_audiobook is not None:
374 books = books.filter(media__type='mp3')
376 books = books.exclude(media__type='mp3')
377 if preview is not None:
378 books = books.filter(preview=preview)
379 for key in request.GET:
380 if key in category_singular:
381 category = category_singular[key]
382 if category in book_tag_categories:
383 slugs = request.GET[key].split(',')
384 tags = Tag.objects.filter(category=category, slug__in=slugs)
385 books = Book.tagged.with_any(tags, books)
386 if (search_string is not None) and len(search_string) < 3:
389 search_string = re_escape(search_string)
390 books_author = books.filter(cached_author__iregex='\m' + search_string)
391 books_title = books.filter(title__iregex='\m' + search_string)
392 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
393 if after and (key_sep in after):
394 which, key = after.split(key_sep, 1)
396 book_lists = [(self.books_after(books_title, key, new_api), 'title')]
397 else: # which == 'author'
398 book_lists = [(self.books_after(books_author, key, new_api), 'author'), (books_title, 'title')]
400 book_lists = [(books_author, 'author'), (books_title, 'title')]
402 if after and key_sep in after:
403 which, key = after.split(key_sep, 1)
404 books = self.books_after(books, key, new_api)
405 book_lists = [(books, 'book')]
408 for book_list, label in book_lists:
409 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key_author', 'sort_key')
410 for category in book_tag_categories:
411 book_list = prefetch_relations(book_list, category)
412 remaining_count = count - len(filtered_books)
414 BookProxy(book, '%s%s%s' % (
415 label, key_sep, book.slug if not new_api else book.full_sort_key()))
416 for book in book_list[:remaining_count]]
417 filtered_books += new_books
418 if len(filtered_books) == count:
421 return QuerySetProxy(filtered_books)
424 class FilterBooksHandler(BooksHandler):
425 anonymous = AnonFilterBooksHandler
426 fields = book_list_fields + ['key', 'liked']
428 # hack, because piston is stupid
430 def liked(cls, book):
431 return getattr(book, 'liked', None)
433 def read(self, request):
434 qsp = AnonFilterBooksHandler().read(request)
435 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
436 for book in qsp.list:
437 book.set('liked', book.id in likes)
441 class BookPreviewHandler(BookDetailHandler):
442 fields = BookDetailHandler.fields + ['slug']
444 def read(self, request):
445 return Book.objects.filter(preview=True)
448 # add categorized tags fields for Book
449 def _tags_getter(category):
451 def get_tags(cls, book):
452 return book.tags.filter(category=category)
456 def _tag_getter(category):
458 def get_tag(cls, book):
459 return book.tag_unicode(category)
463 def add_tag_getters():
464 for plural, singular in category_singular.items():
465 setattr(BookDetails, plural, _tags_getter(singular))
466 setattr(BookDetails, singular, _tag_getter(singular))
472 # add fields for files in Book
473 def _file_getter(book_format):
477 f_url = book.media_url(book_format)
479 return MEDIA_BASE + f_url
485 def add_file_getters():
486 for book_format in Book.formats:
487 setattr(BookDetails, book_format, _file_getter(book_format))
493 class TagDetails(object):
494 """Custom Tag fields."""
498 """ Returns URI in the API for the tag. """
500 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
504 """ Returns URL on the site. """
506 return WL_BASE + tag.get_absolute_url()
509 class TagDetailHandler(BaseHandler, TagDetails):
510 """ Responsible for details of a single Tag object. """
512 fields = ['name', 'url', 'sort_key', 'description']
515 def read(self, request, category, slug):
516 """ Returns details of a tag, identified by category and slug. """
519 category_sng = category_singular[category]
524 return Tag.objects.get(category=category_sng, slug=slug)
525 except Tag.DoesNotExist:
529 class TagsHandler(BaseHandler, TagDetails):
530 """ Main handler for Tag objects.
532 Responsible for lists of Tag objects
533 and fields used for representing Tags.
536 allowed_methods = ('GET',)
538 fields = ['name', 'href', 'url', 'slug']
541 def read(self, request, category=None, pk=None):
542 """ Lists all tags in the category (eg. all themes). """
546 return Tag.objects.exclude(category='set').get(pk=pk)
547 except Book.DoesNotExist:
551 category_sng = category_singular[category]
555 after = request.GET.get('after')
556 count = request.GET.get('count')
558 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
560 book_only = request.GET.get('book_only') == 'true'
561 picture_only = request.GET.get('picture_only') == 'true'
563 tags = tags.filter(for_books=True)
565 tags = tags.filter(for_pictures=True)
568 tags = tags.filter(slug__gt=after)
576 class FragmentDetails(object):
577 """Custom Fragment fields."""
580 def href(cls, fragment):
581 """ Returns URI in the API for the fragment. """
583 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
586 def url(cls, fragment):
587 """ Returns URL on the site for the fragment. """
589 return WL_BASE + fragment.get_absolute_url()
592 def themes(cls, fragment):
593 """ Returns a list of theme tags for the fragment. """
595 return fragment.tags.filter(category='theme')
598 class FragmentDetailHandler(BaseHandler, FragmentDetails):
599 fields = ['book', 'anchor', 'text', 'url', 'themes']
602 def read(self, request, book, anchor):
603 """ Returns details of a fragment, identified by book slug and anchor. """
605 return Fragment.objects.get(book__slug=book, anchor=anchor)
606 except Fragment.DoesNotExist:
610 class FragmentsHandler(BaseHandler, FragmentDetails):
611 """ Main handler for Fragments.
613 Responsible for lists of Fragment objects
614 and fields used for representing Fragments.
618 fields = ['book', 'url', 'anchor', 'href']
619 allowed_methods = ('GET',)
621 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
624 def read(self, request, tags):
625 """ Lists all fragments with given book, tags, themes.
627 :param tags: should be a path of categories and slugs, i.e.:
628 books/book-slug/authors/an-author/themes/a-theme/
632 tags, ancestors = read_tags(tags, request, allowed=self.categories)
635 fragments = Fragment.tagged.with_all(tags).select_related('book')
636 if fragments.exists():
642 class PictureHandler(BaseHandler):
644 fields = ('slug', 'title')
645 allowed_methods = ('POST',)
647 def create(self, request):
648 if not request.user.has_perm('picture.add_picture'):
651 data = json.loads(request.POST.get('data'))
652 form = PictureImportForm(data)
660 class UserShelfHandler(BookDetailHandler):
661 fields = book_list_fields + ['liked']
664 def parse_bool(self, s):
665 if s in ('true', 'false'):
670 # hack, because piston is stupid
672 def liked(cls, book):
673 return getattr(book, 'liked', None)
675 def read(self, request, state):
676 if not request.user.is_authenticated():
678 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
679 if state not in ('reading', 'complete', 'likes'):
681 new_api = request.GET.get('new_api')
682 after = request.GET.get('after')
683 count = int(request.GET.get('count', 50))
685 books = Book.tagged.with_any(request.user.tag_set.all())
687 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
688 .values_list('book_id', flat=True)
689 books = Book.objects.filter(id__in=list(ids)).distinct()
690 books = self.order_books(books, new_api)
692 books = self.books_after(books, after, new_api)
694 books = books[:count]
697 new_books.append(BookProxy(book).set('liked', book.id in likes))
698 return QuerySetProxy(new_books)
701 class UserLikeHandler(BaseHandler):
703 allowed_methods = ('GET', 'POST')
705 def read(self, request, slug):
706 if not request.user.is_authenticated():
709 book = Book.objects.get(slug=slug)
710 except Book.DoesNotExist:
712 return {'likes': likes(request.user, book)}
714 def create(self, request, slug):
715 if not request.user.is_authenticated():
718 book = Book.objects.get(slug=slug)
719 except Book.DoesNotExist:
721 action = request.GET.get('action', 'like')
723 book.like(request.user)
724 elif action == 'unlike':
725 book.unlike(request.user)
729 class BlogEntryHandler(BaseHandler):
732 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
734 def read(self, request):
735 after = request.GET.get('after')
736 count = int(request.GET.get('count', 20))
737 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
739 entries = entries.filter(first_published_at__lt=after)
741 entries = entries[:count]
745 def image_url(cls, entry):
746 return (WL_BASE + entry.image.url) if entry.image else None
749 def image_thumb(cls, entry):
750 return MEDIA_BASE + default.backend.get_thumbnail(
751 entry.image, "193x193").url if entry.image else ''
754 def gallery_urls(cls, entry):
755 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
759 return entry.first_published_at
763 return WL_BASE + entry.get_absolute_url()