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'https://' + 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']
50 book_list_fields = book_tag_categories + [
51 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color']
54 def read_tags(tags, request, allowed):
55 """ Reads a path of filtering tags.
57 :param str tags: a path of category and slug pairs, like: authors/an-author/...
58 :returns: list of Tag objects
59 :raises: ValueError when tags can't be found
62 def process(category, slug):
63 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("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 cover_color(cls, book):
167 return WLCover.epoch_colors.get(book.extra_info.get('epoch'), '#000000')
170 class BookDetailHandler(BaseHandler, BookDetails):
171 """ Main handler for Book objects.
173 Responsible for single Book details.
175 allowed_methods = ['GET']
176 fields = ['title', 'parent', 'children'] + Book.formats + [
177 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
178 'preview', 'cover_color'] + [
179 category_plural[c] for c in book_tag_categories]
182 def read(self, request, book):
183 """ Returns details of a book, identified by a slug and lang. """
185 return Book.objects.get(slug=book)
186 except Book.DoesNotExist:
190 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
191 """ Main handler for Book objects.
193 Responsible for lists of Book objects.
195 allowed_methods = ('GET',)
197 fields = book_list_fields
200 def genres(cls, book):
201 """ Returns all media for a book. """
202 return book.tags.filter(category='genre')
205 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
206 recommended=False, newest=False, books=None,
207 after=None, count=None):
208 """ Lists all books with given tags.
210 :param tags: filtering tags; should be a path of categories
211 and slugs, i.e.: authors/an-author/epoch/an-epoch/
212 :param top_level: if True and a book is included in the results,
213 it's children are aren't. By default all books matching the tags
218 return Book.objects.get(pk=pk)
219 except Book.DoesNotExist:
223 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
227 if 'after' in request.GET:
228 after = request.GET['after']
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')
255 books = books.filter(slug__gt=after)
257 books = books.only('slug', 'title', 'cover', 'cover_thumb')
258 for category in book_tag_categories:
259 books = prefetch_relations(books, category)
262 books = books[:count]
266 def create(self, request, *args, **kwargs):
270 class BooksHandler(BookDetailHandler):
271 allowed_methods = ('GET', 'POST')
273 fields = book_list_fields + ['liked']
274 anonymous = AnonymousBooksHandler
276 # hack, because piston is stupid
278 def liked(cls, book):
279 return getattr(book, 'liked', None)
281 def read(self, request, **kwargs):
282 books = AnonymousBooksHandler().read(request, **kwargs)
283 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
286 BookProxy(book).set('liked', book.id in likes)
288 return QuerySetProxy(new_books)
290 def create(self, request, *args, **kwargs):
291 if not request.user.has_perm('catalogue.add_book'):
294 data = json.loads(request.POST.get('data'))
295 form = BookImportForm(data)
303 class EpubHandler(BookDetailHandler):
304 def read(self, request, slug):
305 if not is_subscribed(request.user):
308 book = Book.objects.get(slug=slug)
309 except Book.DoesNotExist:
311 response = HttpResponse(book.get_media('epub'))
315 class EBooksHandler(AnonymousBooksHandler):
316 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
319 class BookProxy(models.Model):
323 def __init__(self, book, key=None):
327 def set(self, attr, value):
328 self.__setattr__(attr, value)
331 def __getattr__(self, item):
332 return self.book.__getattribute__(item)
335 class QuerySetProxy(models.QuerySet):
336 def __init__(self, l):
340 return iter(self.list)
343 class AnonFilterBooksHandler(AnonymousBooksHandler):
344 fields = book_list_fields + ['key']
346 def parse_bool(self, s):
347 if s in ('true', 'false'):
352 def read(self, request):
354 search_string = request.GET.get('search')
355 is_lektura = self.parse_bool(request.GET.get('lektura'))
356 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
357 preview = self.parse_bool(request.GET.get('preview'))
359 after = request.GET.get('after')
360 count = int(request.GET.get('count', 50))
361 books = Book.objects.distinct().order_by('slug')
362 if is_lektura is not None:
363 books = books.filter(has_audience=is_lektura)
364 if is_audiobook is not None:
366 books = books.filter(media__type='mp3')
368 books = books.exclude(media__type='mp3')
369 if preview is not None:
370 books = books.filter(preview=preview)
371 for key in request.GET:
372 if key in category_singular:
373 category = category_singular[key]
374 if category in book_tag_categories:
375 slugs = request.GET[key].split(',')
376 tags = Tag.objects.filter(category=category, slug__in=slugs)
377 books = Book.tagged.with_any(tags, books)
378 if (search_string is not None) and len(search_string) < 3:
381 search_string = re_escape(search_string)
382 books_author = books.filter(cached_author__iregex='\m' + search_string)
383 books_title = books.filter(title__iregex='\m' + search_string)
384 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
385 if after and (key_sep in after):
386 which, slug = after.split(key_sep, 1)
388 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
389 else: # which == 'author'
390 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
392 book_lists = [(books_author, 'author'), (books_title, 'title')]
394 if after and key_sep in after:
395 which, slug = after.split(key_sep, 1)
396 books = books.filter(slug__gt=slug)
397 book_lists = [(books, 'book')]
400 for book_list, label in book_lists:
401 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
402 for category in book_tag_categories:
403 book_list = prefetch_relations(book_list, category)
404 remaining_count = count - len(filtered_books)
405 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
406 for book in book_list[:remaining_count]]
407 filtered_books += new_books
408 if len(filtered_books) == count:
411 return QuerySetProxy(filtered_books)
414 class FilterBooksHandler(BooksHandler):
415 anonymous = AnonFilterBooksHandler
416 fields = book_list_fields + ['key', 'liked']
418 # hack, because piston is stupid
420 def liked(cls, book):
421 return getattr(book, 'liked', None)
423 def read(self, request):
424 qsp = AnonFilterBooksHandler().read(request)
425 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
426 for book in qsp.list:
427 book.set('liked', book.id in likes)
431 class BookPreviewHandler(BookDetailHandler):
432 fields = BookDetailHandler.fields + ['slug']
434 def read(self, request):
435 return Book.objects.filter(preview=True)
438 # add categorized tags fields for Book
439 def _tags_getter(category):
441 def get_tags(cls, book):
442 return book.tags.filter(category=category)
446 def _tag_getter(category):
448 def get_tag(cls, book):
449 return book.tag_unicode(category)
453 def add_tag_getters():
454 for plural, singular in category_singular.items():
455 setattr(BookDetails, plural, _tags_getter(singular))
456 setattr(BookDetails, singular, _tag_getter(singular))
462 # add fields for files in Book
463 def _file_getter(book_format):
467 f_url = book.media_url(book_format)
469 return MEDIA_BASE + f_url
475 def add_file_getters():
476 for book_format in Book.formats:
477 setattr(BookDetails, book_format, _file_getter(book_format))
483 class CollectionDetails(object):
484 """Custom Collection fields."""
487 def href(cls, collection):
488 """ Returns URI in the API for the collection. """
490 return API_BASE + reverse("api_collection", args=[collection.slug])
493 def url(cls, collection):
494 """ Returns URL on the site. """
496 return WL_BASE + collection.get_absolute_url()
499 def books(cls, collection):
500 return Book.objects.filter(collection.get_query())
503 class CollectionDetailHandler(BaseHandler, CollectionDetails):
504 allowed_methods = ('GET',)
505 fields = ['url', 'title', 'description', 'books']
508 def read(self, request, slug):
509 """ Returns details of a collection, identified by slug. """
511 return Collection.objects.get(slug=slug)
512 except Collection.DoesNotExist:
516 class CollectionsHandler(BaseHandler, CollectionDetails):
517 allowed_methods = ('GET',)
519 fields = ['url', 'href', 'title']
522 def read(self, request):
523 """ Returns all collections. """
524 return Collection.objects.all()
527 class TagDetails(object):
528 """Custom Tag fields."""
532 """ Returns URI in the API for the tag. """
534 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
538 """ Returns URL on the site. """
540 return WL_BASE + tag.get_absolute_url()
543 class TagDetailHandler(BaseHandler, TagDetails):
544 """ Responsible for details of a single Tag object. """
546 fields = ['name', 'url', 'sort_key', 'description']
549 def read(self, request, category, slug):
550 """ Returns details of a tag, identified by category and slug. """
553 category_sng = category_singular[category]
558 return Tag.objects.get(category=category_sng, slug=slug)
559 except Tag.DoesNotExist:
563 class TagsHandler(BaseHandler, TagDetails):
564 """ Main handler for Tag objects.
566 Responsible for lists of Tag objects
567 and fields used for representing Tags.
570 allowed_methods = ('GET',)
572 fields = ['name', 'href', 'url', 'slug']
575 def read(self, request, category=None, pk=None):
576 """ Lists all tags in the category (eg. all themes). """
579 return Tag.objects.exclude(category='set').get(pk=pk)
580 except Book.DoesNotExist:
584 category_sng = category_singular[category]
588 after = request.GET.get('after')
589 count = request.GET.get('count')
591 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
593 book_only = request.GET.get('book_only') == 'true'
594 picture_only = request.GET.get('picture_only') == 'true'
596 tags = tags.filter(for_books=True)
598 tags = tags.filter(for_pictures=True)
601 tags = tags.filter(slug__gt=after)
609 class FragmentDetails(object):
610 """Custom Fragment fields."""
613 def href(cls, fragment):
614 """ Returns URI in the API for the fragment. """
616 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
619 def url(cls, fragment):
620 """ Returns URL on the site for the fragment. """
622 return WL_BASE + fragment.get_absolute_url()
625 def themes(cls, fragment):
626 """ Returns a list of theme tags for the fragment. """
628 return fragment.tags.filter(category='theme')
631 class FragmentDetailHandler(BaseHandler, FragmentDetails):
632 fields = ['book', 'anchor', 'text', 'url', 'themes']
635 def read(self, request, book, anchor):
636 """ Returns details of a fragment, identified by book slug and anchor. """
638 return Fragment.objects.get(book__slug=book, anchor=anchor)
639 except Fragment.DoesNotExist:
643 class FragmentsHandler(BaseHandler, FragmentDetails):
644 """ Main handler for Fragments.
646 Responsible for lists of Fragment objects
647 and fields used for representing Fragments.
651 fields = ['book', 'url', 'anchor', 'href']
652 allowed_methods = ('GET',)
654 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
657 def read(self, request, tags):
658 """ Lists all fragments with given book, tags, themes.
660 :param tags: should be a path of categories and slugs, i.e.:
661 books/book-slug/authors/an-author/themes/a-theme/
665 tags, ancestors = read_tags(tags, request, allowed=self.categories)
668 fragments = Fragment.tagged.with_all(tags).select_related('book')
669 if fragments.exists():
675 class PictureHandler(BaseHandler):
677 fields = ('slug', 'title')
678 allowed_methods = ('POST',)
680 def create(self, request):
681 if not request.user.has_perm('picture.add_picture'):
684 data = json.loads(request.POST.get('data'))
685 form = PictureImportForm(data)
693 class UserDataHandler(BaseHandler):
695 fields = ('state', 'username', 'premium')
696 allowed_methods = ('GET', 'POST')
698 def read(self, request, slug=None):
699 if not request.user.is_authenticated():
702 return {'username': request.user.username, 'premium': is_subscribed(request.user)}
704 book = Book.objects.get(slug=slug)
705 except Book.DoesNotExist:
708 data = BookUserData.objects.get(book=book, user=request.user)
709 except BookUserData.DoesNotExist:
710 return {'state': 'not_started'}
713 def create(self, request, slug, state):
715 book = Book.objects.get(slug=slug)
716 except Book.DoesNotExist:
718 if not request.user.is_authenticated():
720 if state not in ('reading', 'complete'):
722 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
728 class UserShelfHandler(BookDetailHandler):
729 fields = book_list_fields + ['liked']
731 def parse_bool(self, s):
732 if s in ('true', 'false'):
737 # hack, because piston is stupid
739 def liked(cls, book):
740 return getattr(book, 'liked', None)
742 def read(self, request, state):
743 if not request.user.is_authenticated():
745 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
746 if state not in ('reading', 'complete', 'likes'):
748 after = request.GET.get('after')
749 count = int(request.GET.get('count', 50))
751 books = Book.tagged.with_any(request.user.tag_set.all())
753 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
754 .values_list('book_id', flat=True)
755 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
757 books = books.filter(slug__gt=after)
759 books = books[:count]
762 new_books.append(BookProxy(book).set('liked', book.id in likes))
763 return QuerySetProxy(new_books)
766 class UserLikeHandler(BaseHandler):
768 allowed_methods = ('GET', 'POST')
770 def read(self, request, slug):
771 if not request.user.is_authenticated():
774 book = Book.objects.get(slug=slug)
775 except Book.DoesNotExist:
777 return {'likes': likes(request.user, book)}
779 def create(self, request, slug):
780 if not request.user.is_authenticated():
783 book = Book.objects.get(slug=slug)
784 except Book.DoesNotExist:
786 action = request.GET.get('action', 'like')
788 book.like(request.user)
789 elif action == 'unlike':
790 book.unlike(request.user)
794 class BlogEntryHandler(BaseHandler):
797 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
799 def read(self, request):
800 after = request.GET.get('after')
801 count = int(request.GET.get('count', 20))
802 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
804 entries = entries.filter(first_published_at__lt=after)
806 entries = entries[:count]
810 def image_url(cls, entry):
811 return (WL_BASE + entry.image.url) if entry.image else None
814 def image_thumb(cls, entry):
815 return MEDIA_BASE + default.backend.get_thumbnail(
816 entry.image, "193x193").url if entry.image else ''
819 def gallery_urls(cls, entry):
820 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
824 return entry.first_published_at
828 return WL_BASE + entry.get_absolute_url()