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
30 from . import emitters # Register our emitters
32 API_BASE = WL_BASE = MEDIA_BASE = lazy(
33 lambda: u'https://' + Site.objects.get_current().domain, unicode)()
44 for k, v in category_singular.items():
45 category_plural[v] = k
47 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
49 book_list_fields = book_tag_categories + [
50 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color', 'full_sort_key']
53 def read_tags(tags, request, allowed):
54 """ Reads a path of filtering tags.
56 :param str tags: a path of category and slug pairs, like: authors/an-author/...
57 :returns: list of Tag objects
58 :raises: ValueError when tags can't be found
61 def process(category, slug):
62 if category == 'book':
65 books.append(Book.objects.get(slug=slug))
66 except Book.DoesNotExist:
67 raise ValueError('Unknown book.')
69 real_tags.append(Tag.objects.get(category=category, slug=slug))
70 except Tag.DoesNotExist:
71 raise ValueError('Tag not found')
76 tags = tags.strip('/').split('/')
80 category = tags.pop(0)
84 category = category_singular[category]
86 raise ValueError('Unknown category.')
88 if category not in allowed:
89 raise ValueError('Category not allowed.')
90 process(category, slug)
92 for key in request.GET:
93 if key in category_singular:
94 category = category_singular[key]
95 if category in allowed:
96 for slug in request.GET.getlist(key):
97 process(category, slug)
98 return real_tags, books
104 class BookMediaHandler(BaseHandler):
105 """ Responsible for representing media in Books. """
108 fields = ['name', 'type', 'url', 'artist', 'director']
112 """ Link to media on site. """
114 return MEDIA_BASE + media.file.url
117 def artist(cls, media):
118 return media.extra_info.get('artist_name', '')
121 def director(cls, media):
122 return media.extra_info.get('director_name', '')
125 class BookDetails(object):
126 """Custom fields used for representing Books."""
130 """ Returns an URI for a Book in the API. """
131 return API_BASE + reverse("catalogue_api_book", args=[book.slug])
135 """ Returns Book's URL on the site. """
136 return WL_BASE + book.get_absolute_url()
139 def children(cls, book):
140 """ Returns all children for a book. """
141 return book.children.order_by('parent_number', 'sort_key')
144 def media(cls, book):
145 """ Returns all media for a book. """
146 return book.media.all()
149 def cover(cls, book):
150 return MEDIA_BASE + book.cover.url if book.cover else ''
153 def cover_thumb(cls, book):
154 return MEDIA_BASE + default.backend.get_thumbnail(
155 book.cover, "139x193").url if book.cover else ''
158 def simple_thumb(cls, book):
159 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
162 def simple_cover(cls, book):
163 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
166 def books_after(books, after, new_api):
168 return books.filter(slug__gt=after)
170 author, title, book_id = after.split(Book.SORT_KEY_SEP)
172 return Book.objects.none()
173 return books.filter(Q(sort_key_author__gt=author)
174 | (Q(sort_key_author=author) & Q(sort_key__gt=title))
175 | (Q(sort_key_author=author) & Q(sort_key=title) & Q(id__gt=int(book_id))))
178 def order_books(books, new_api):
180 return books.order_by('sort_key_author', 'sort_key', 'id')
182 return books.order_by('slug')
185 class BookDetailHandler(BaseHandler, BookDetails):
186 """ Main handler for Book objects.
188 Responsible for single Book details.
190 allowed_methods = ['GET']
191 fields = ['title', 'parent', 'children'] + Book.formats + [
192 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
193 'preview', 'cover_color'] + [
194 category_plural[c] for c in book_tag_categories]
197 def read(self, request, book):
198 """ Returns details of a book, identified by a slug and lang. """
200 return Book.objects.get(slug=book)
201 except Book.DoesNotExist:
205 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
206 """ Main handler for Book objects.
208 Responsible for lists of Book objects.
210 allowed_methods = ('GET',)
212 fields = book_list_fields
216 def genres(cls, book):
217 """ Returns all media for a book. """
218 return book.tags.filter(category='genre')
221 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
222 recommended=False, newest=False, books=None,
223 after=None, count=None):
224 """ Lists all books with given tags.
226 :param tags: filtering tags; should be a path of categories
227 and slugs, i.e.: authors/an-author/epoch/an-epoch/
228 :param top_level: if True and a book is included in the results,
229 it's children are aren't. By default all books matching the tags
235 return Book.objects.get(pk=pk)
236 except Book.DoesNotExist:
240 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
244 new_api = request.GET.get('new_api')
245 if 'after' in request.GET:
246 after = request.GET['after']
247 if 'count' in request.GET:
248 count = request.GET['count']
252 books = Book.tagged_top_level(tags)
253 return books if books else rc.NOT_FOUND
255 books = Book.tagged.with_all(tags)
257 books = books if books is not None else Book.objects.all()
258 books = self.order_books(books, new_api)
261 books = books.filter(parent=None)
263 books = books.filter(media__type='mp3').distinct()
265 books = books.filter(media__type='daisy').distinct()
267 books = books.filter(recommended=True)
269 books = books.order_by('-created_at')
272 books = self.books_after(books, after, new_api)
275 books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author')
277 books = books.only('slug', 'title', 'cover', 'cover_thumb')
278 for category in book_tag_categories:
279 books = prefetch_relations(books, category)
282 books = books[:count]
286 def create(self, request, *args, **kwargs):
290 class BooksHandler(BookDetailHandler):
291 allowed_methods = ('GET', 'POST')
293 fields = book_list_fields + ['liked']
294 anonymous = AnonymousBooksHandler
296 # hack, because piston is stupid
298 def liked(cls, book):
299 return getattr(book, 'liked', None)
301 def read(self, request, **kwargs):
302 books = AnonymousBooksHandler().read(request, **kwargs)
303 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
306 BookProxy(book).set('liked', book.id in likes)
308 return QuerySetProxy(new_books)
310 def create(self, request, *args, **kwargs):
311 if not request.user.has_perm('catalogue.add_book'):
314 data = json.loads(request.POST.get('data'))
315 form = BookImportForm(data)
323 class EpubHandler(BookDetailHandler):
324 def read(self, request, slug):
325 if not user_is_subscribed(request.user):
328 book = Book.objects.get(slug=slug)
329 except Book.DoesNotExist:
331 response = HttpResponse(book.get_media('epub'))
335 class EBooksHandler(AnonymousBooksHandler):
336 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
339 class BookProxy(models.Model):
343 def __init__(self, book, key=None):
347 def set(self, attr, value):
348 self.__setattr__(attr, value)
351 def __getattr__(self, item):
352 return self.book.__getattribute__(item)
355 class QuerySetProxy(models.QuerySet):
356 def __init__(self, l):
360 return iter(self.list)
363 class AnonFilterBooksHandler(AnonymousBooksHandler):
364 fields = book_list_fields + ['key']
366 def parse_bool(self, s):
367 if s in ('true', 'false'):
372 def read(self, request):
374 search_string = request.GET.get('search')
375 is_lektura = self.parse_bool(request.GET.get('lektura'))
376 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
377 preview = self.parse_bool(request.GET.get('preview'))
379 new_api = request.GET.get('new_api')
380 after = request.GET.get('after')
381 count = int(request.GET.get('count', 50))
382 books = self.order_books(Book.objects.distinct(), new_api)
383 if is_lektura is not None:
384 books = books.filter(has_audience=is_lektura)
385 if is_audiobook is not None:
387 books = books.filter(media__type='mp3')
389 books = books.exclude(media__type='mp3')
390 if preview is not None:
391 books = books.filter(preview=preview)
392 for key in request.GET:
393 if key in category_singular:
394 category = category_singular[key]
395 if category in book_tag_categories:
396 slugs = request.GET[key].split(',')
397 tags = Tag.objects.filter(category=category, slug__in=slugs)
398 books = Book.tagged.with_any(tags, books)
399 if (search_string is not None) and len(search_string) < 3:
402 search_string = re_escape(search_string)
403 books_author = books.filter(cached_author__iregex='\m' + search_string)
404 books_title = books.filter(title__iregex='\m' + search_string)
405 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
406 if after and (key_sep in after):
407 which, key = after.split(key_sep, 1)
409 book_lists = [(self.books_after(books_title, key, new_api), 'title')]
410 else: # which == 'author'
411 book_lists = [(self.books_after(books_author, key, new_api), 'author'), (books_title, 'title')]
413 book_lists = [(books_author, 'author'), (books_title, 'title')]
415 if after and key_sep in after:
416 which, key = after.split(key_sep, 1)
417 books = self.books_after(books, key, new_api)
418 book_lists = [(books, 'book')]
421 for book_list, label in book_lists:
422 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key_author', 'sort_key')
423 for category in book_tag_categories:
424 book_list = prefetch_relations(book_list, category)
425 remaining_count = count - len(filtered_books)
427 BookProxy(book, '%s%s%s' % (
428 label, key_sep, book.slug if not new_api else book.full_sort_key()))
429 for book in book_list[:remaining_count]]
430 filtered_books += new_books
431 if len(filtered_books) == count:
434 return QuerySetProxy(filtered_books)
437 class FilterBooksHandler(BooksHandler):
438 anonymous = AnonFilterBooksHandler
439 fields = book_list_fields + ['key', 'liked']
441 # hack, because piston is stupid
443 def liked(cls, book):
444 return getattr(book, 'liked', None)
446 def read(self, request):
447 qsp = AnonFilterBooksHandler().read(request)
448 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
449 for book in qsp.list:
450 book.set('liked', book.id in likes)
454 class BookPreviewHandler(BookDetailHandler):
455 fields = BookDetailHandler.fields + ['slug']
457 def read(self, request):
458 return Book.objects.filter(preview=True)
461 # add categorized tags fields for Book
462 def _tags_getter(category):
464 def get_tags(cls, book):
465 return book.tags.filter(category=category)
469 def _tag_getter(category):
471 def get_tag(cls, book):
472 return book.tag_unicode(category)
476 def add_tag_getters():
477 for plural, singular in category_singular.items():
478 setattr(BookDetails, plural, _tags_getter(singular))
479 setattr(BookDetails, singular, _tag_getter(singular))
485 # add fields for files in Book
486 def _file_getter(book_format):
490 f_url = book.media_url(book_format)
492 return MEDIA_BASE + f_url
498 def add_file_getters():
499 for book_format in Book.formats:
500 setattr(BookDetails, book_format, _file_getter(book_format))
506 class TagDetails(object):
507 """Custom Tag fields."""
511 """ Returns URI in the API for the tag. """
513 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
517 """ Returns URL on the site. """
519 return WL_BASE + tag.get_absolute_url()
522 class TagDetailHandler(BaseHandler, TagDetails):
523 """ Responsible for details of a single Tag object. """
525 fields = ['name', 'url', 'sort_key', 'description']
528 def read(self, request, category, slug):
529 """ Returns details of a tag, identified by category and slug. """
532 category_sng = category_singular[category]
537 return Tag.objects.get(category=category_sng, slug=slug)
538 except Tag.DoesNotExist:
542 class TagsHandler(BaseHandler, TagDetails):
543 """ Main handler for Tag objects.
545 Responsible for lists of Tag objects
546 and fields used for representing Tags.
549 allowed_methods = ('GET',)
551 fields = ['name', 'href', 'url', 'slug']
554 def read(self, request, category=None, pk=None):
555 """ Lists all tags in the category (eg. all themes). """
559 return Tag.objects.exclude(category='set').get(pk=pk)
560 except Book.DoesNotExist:
564 category_sng = category_singular[category]
568 after = request.GET.get('after')
569 count = request.GET.get('count')
571 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
573 book_only = request.GET.get('book_only') == 'true'
574 picture_only = request.GET.get('picture_only') == 'true'
576 tags = tags.filter(for_books=True)
578 tags = tags.filter(for_pictures=True)
581 tags = tags.filter(slug__gt=after)
589 class FragmentDetails(object):
590 """Custom Fragment fields."""
593 def href(cls, fragment):
594 """ Returns URI in the API for the fragment. """
596 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
599 def url(cls, fragment):
600 """ Returns URL on the site for the fragment. """
602 return WL_BASE + fragment.get_absolute_url()
605 def themes(cls, fragment):
606 """ Returns a list of theme tags for the fragment. """
608 return fragment.tags.filter(category='theme')
611 class FragmentDetailHandler(BaseHandler, FragmentDetails):
612 fields = ['book', 'anchor', 'text', 'url', 'themes']
615 def read(self, request, book, anchor):
616 """ Returns details of a fragment, identified by book slug and anchor. """
618 return Fragment.objects.get(book__slug=book, anchor=anchor)
619 except Fragment.DoesNotExist:
623 class FragmentsHandler(BaseHandler, FragmentDetails):
624 """ Main handler for Fragments.
626 Responsible for lists of Fragment objects
627 and fields used for representing Fragments.
631 fields = ['book', 'url', 'anchor', 'href']
632 allowed_methods = ('GET',)
634 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
637 def read(self, request, tags):
638 """ Lists all fragments with given book, tags, themes.
640 :param tags: should be a path of categories and slugs, i.e.:
641 books/book-slug/authors/an-author/themes/a-theme/
645 tags, ancestors = read_tags(tags, request, allowed=self.categories)
648 fragments = Fragment.tagged.with_all(tags).select_related('book')
649 if fragments.exists():
655 class PictureHandler(BaseHandler):
657 fields = ('slug', 'title')
658 allowed_methods = ('POST',)
660 def create(self, request):
661 if not request.user.has_perm('picture.add_picture'):
664 data = json.loads(request.POST.get('data'))
665 form = PictureImportForm(data)
673 class UserDataHandler(BaseHandler):
675 fields = ('state', 'username', 'premium')
676 allowed_methods = ('GET', 'POST')
678 def read(self, request, slug=None):
679 if not request.user.is_authenticated():
682 return {'username': request.user.username, 'premium': user_is_subscribed(request.user)}
684 book = Book.objects.get(slug=slug)
685 except Book.DoesNotExist:
688 data = BookUserData.objects.get(book=book, user=request.user)
689 except BookUserData.DoesNotExist:
690 return {'state': 'not_started'}
693 def create(self, request, slug, state):
695 book = Book.objects.get(slug=slug)
696 except Book.DoesNotExist:
698 if not request.user.is_authenticated():
700 if state not in ('reading', 'complete'):
702 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
708 class UserShelfHandler(BookDetailHandler):
709 fields = book_list_fields + ['liked']
712 def parse_bool(self, s):
713 if s in ('true', 'false'):
718 # hack, because piston is stupid
720 def liked(cls, book):
721 return getattr(book, 'liked', None)
723 def read(self, request, state):
724 if not request.user.is_authenticated():
726 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
727 if state not in ('reading', 'complete', 'likes'):
729 new_api = request.GET.get('new_api')
730 after = request.GET.get('after')
731 count = int(request.GET.get('count', 50))
733 books = Book.tagged.with_any(request.user.tag_set.all())
735 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
736 .values_list('book_id', flat=True)
737 books = Book.objects.filter(id__in=list(ids)).distinct()
738 books = self.order_books(books, new_api)
740 books = self.books_after(books, after, new_api)
742 books = books[:count]
745 new_books.append(BookProxy(book).set('liked', book.id in likes))
746 return QuerySetProxy(new_books)
749 class UserLikeHandler(BaseHandler):
751 allowed_methods = ('GET', 'POST')
753 def read(self, request, slug):
754 if not request.user.is_authenticated():
757 book = Book.objects.get(slug=slug)
758 except Book.DoesNotExist:
760 return {'likes': likes(request.user, book)}
762 def create(self, request, slug):
763 if not request.user.is_authenticated():
766 book = Book.objects.get(slug=slug)
767 except Book.DoesNotExist:
769 action = request.GET.get('action', 'like')
771 book.like(request.user)
772 elif action == 'unlike':
773 book.unlike(request.user)
777 class BlogEntryHandler(BaseHandler):
780 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
782 def read(self, request):
783 after = request.GET.get('after')
784 count = int(request.GET.get('count', 20))
785 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
787 entries = entries.filter(first_published_at__lt=after)
789 entries = entries[:count]
793 def image_url(cls, entry):
794 return (WL_BASE + entry.image.url) if entry.image else None
797 def image_thumb(cls, entry):
798 return MEDIA_BASE + default.backend.get_thumbnail(
799 entry.image, "193x193").url if entry.image else ''
802 def gallery_urls(cls, entry):
803 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
807 return entry.first_published_at
811 return WL_BASE + entry.get_absolute_url()