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, before=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 'before' in request.GET:
230 before = request.GET['before']
231 if 'count' in request.GET:
232 count = request.GET['count']
236 books = Book.tagged_top_level(tags)
237 return books if books else rc.NOT_FOUND
239 books = Book.tagged.with_all(tags)
241 books = books if books is not None else Book.objects.all()
242 books = books.order_by('slug')
245 books = books.filter(parent=None)
247 books = books.filter(media__type='mp3').distinct()
249 books = books.filter(media__type='daisy').distinct()
251 books = books.filter(recommended=True)
253 books = books.order_by('-created_at')
256 books = books.filter(slug__gt=after)
258 books = books.filter(slug__lt=before)
260 books = books.only('slug', 'title', 'cover', 'cover_thumb')
261 for category in book_tag_categories:
262 books = prefetch_relations(books, category)
266 books = list(reversed(books.order_by('-slug')[:count]))
268 books = books[:count]
272 def create(self, request, *args, **kwargs):
276 class BooksHandler(BookDetailHandler):
277 allowed_methods = ('GET', 'POST')
279 fields = book_list_fields + ['liked']
280 anonymous = AnonymousBooksHandler
282 # hack, because piston is stupid
284 def liked(cls, book):
287 def read(self, request, **kwargs):
288 books = AnonymousBooksHandler().read(request, **kwargs)
289 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
292 BookProxy(book).set('liked', book.id in likes)
294 return QuerySetProxy(new_books)
296 def create(self, request, *args, **kwargs):
297 if not request.user.has_perm('catalogue.add_book'):
300 data = json.loads(request.POST.get('data'))
301 form = BookImportForm(data)
309 class EpubHandler(BookDetailHandler):
310 def read(self, request, slug):
311 if not is_subscribed(request.user):
314 book = Book.objects.get(slug=slug)
315 except Book.DoesNotExist:
317 response = HttpResponse(book.get_media('epub'))
321 class EBooksHandler(AnonymousBooksHandler):
322 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
325 class BookProxy(models.Model):
329 def __init__(self, book, key=None):
333 def set(self, attr, value):
334 self.__setattr__(attr, value)
337 def __getattr__(self, item):
338 return self.book.__getattribute__(item)
341 class QuerySetProxy(models.QuerySet):
342 def __init__(self, l):
346 return iter(self.list)
349 class AnonFilterBooksHandler(AnonymousBooksHandler):
350 fields = book_list_fields + ['key']
352 def parse_bool(self, s):
353 if s in ('true', 'false'):
358 def read(self, request):
360 search_string = request.GET.get('search')
361 is_lektura = self.parse_bool(request.GET.get('lektura'))
362 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
363 preview = self.parse_bool(request.GET.get('preview'))
365 after = request.GET.get('after')
366 count = int(request.GET.get('count', 50))
367 books = Book.objects.distinct().order_by('slug')
368 if is_lektura is not None:
369 books = books.filter(has_audience=is_lektura)
370 if is_audiobook is not None:
372 books = books.filter(media__type='mp3')
374 books = books.exclude(media__type='mp3')
375 if preview is not None:
376 books = books.filter(preview=preview)
377 for key in request.GET:
378 if key in category_singular:
379 category = category_singular[key]
380 if category in book_tag_categories:
381 slugs = request.GET[key].split(',')
382 tags = Tag.objects.filter(category=category, slug__in=slugs)
383 books = Book.tagged.with_any(tags, books)
384 if (search_string is not None) and len(search_string) < 3:
387 search_string = re_escape(search_string)
388 books_author = books.filter(cached_author__iregex='\m' + search_string)
389 books_title = books.filter(title__iregex='\m' + search_string)
390 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
391 if after and (key_sep in after):
392 which, slug = after.split(key_sep, 1)
394 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
395 else: # which == 'author'
396 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
398 book_lists = [(books_author, 'author'), (books_title, 'title')]
400 if after and key_sep in after:
401 which, slug = after.split(key_sep, 1)
402 books = books.filter(slug__gt=slug)
403 book_lists = [(books, 'book')]
406 for book_list, label in book_lists:
407 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
408 for category in book_tag_categories:
409 book_list = prefetch_relations(book_list, category)
410 remaining_count = count - len(filtered_books)
411 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
412 for book in book_list[:remaining_count]]
413 filtered_books += new_books
414 if len(filtered_books) == count:
417 return QuerySetProxy(filtered_books)
420 class FilterBooksHandler(BooksHandler):
421 anonymous = AnonFilterBooksHandler
422 fields = book_list_fields + ['key', 'liked']
424 # hack, because piston is stupid
426 def liked(cls, book):
429 def read(self, request):
430 qsp = AnonFilterBooksHandler().read(request)
431 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
432 for book in qsp.list:
433 book.set('liked', book.id in likes)
437 class BookPreviewHandler(BookDetailHandler):
438 fields = BookDetailHandler.fields + ['slug']
440 def read(self, request):
441 return Book.objects.filter(preview=True)
444 # add categorized tags fields for Book
445 def _tags_getter(category):
447 def get_tags(cls, book):
448 return book.tags.filter(category=category)
452 def _tag_getter(category):
454 def get_tag(cls, book):
455 return book.tag_unicode(category)
459 def add_tag_getters():
460 for plural, singular in category_singular.items():
461 setattr(BookDetails, plural, _tags_getter(singular))
462 setattr(BookDetails, singular, _tag_getter(singular))
468 # add fields for files in Book
469 def _file_getter(book_format):
473 f_url = book.media_url(book_format)
475 return MEDIA_BASE + f_url
481 def add_file_getters():
482 for book_format in Book.formats:
483 setattr(BookDetails, book_format, _file_getter(book_format))
489 class CollectionDetails(object):
490 """Custom Collection fields."""
493 def href(cls, collection):
494 """ Returns URI in the API for the collection. """
496 return API_BASE + reverse("api_collection", args=[collection.slug])
499 def url(cls, collection):
500 """ Returns URL on the site. """
502 return WL_BASE + collection.get_absolute_url()
505 def books(cls, collection):
506 return Book.objects.filter(collection.get_query())
509 class CollectionDetailHandler(BaseHandler, CollectionDetails):
510 allowed_methods = ('GET',)
511 fields = ['url', 'title', 'description', 'books']
514 def read(self, request, slug):
515 """ Returns details of a collection, identified by slug. """
517 return Collection.objects.get(slug=slug)
518 except Collection.DoesNotExist:
522 class CollectionsHandler(BaseHandler, CollectionDetails):
523 allowed_methods = ('GET',)
525 fields = ['url', 'href', 'title']
528 def read(self, request):
529 """ Returns all collections. """
530 return Collection.objects.all()
533 class TagDetails(object):
534 """Custom Tag fields."""
538 """ Returns URI in the API for the tag. """
540 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
544 """ Returns URL on the site. """
546 return WL_BASE + tag.get_absolute_url()
549 class TagDetailHandler(BaseHandler, TagDetails):
550 """ Responsible for details of a single Tag object. """
552 fields = ['name', 'url', 'sort_key', 'description']
555 def read(self, request, category, slug):
556 """ Returns details of a tag, identified by category and slug. """
559 category_sng = category_singular[category]
564 return Tag.objects.get(category=category_sng, slug=slug)
565 except Tag.DoesNotExist:
569 class TagsHandler(BaseHandler, TagDetails):
570 """ Main handler for Tag objects.
572 Responsible for lists of Tag objects
573 and fields used for representing Tags.
576 allowed_methods = ('GET',)
578 fields = ['name', 'href', 'url', 'slug']
581 def read(self, request, category=None, pk=None):
582 """ Lists all tags in the category (eg. all themes). """
585 return Tag.objects.exclude(category='set').get(pk=pk)
586 except Book.DoesNotExist:
590 category_sng = category_singular[category]
594 after = request.GET.get('after')
595 before = request.GET.get('before')
596 count = request.GET.get('count')
598 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
600 book_only = request.GET.get('book_only') == 'true'
601 picture_only = request.GET.get('picture_only') == 'true'
603 tags = tags.filter(for_books=True)
605 tags = tags.filter(for_pictures=True)
608 tags = tags.filter(slug__gt=after)
610 tags = tags.filter(slug__lt=before)
614 tags = list(reversed(tags.order_by('-slug')[:count]))
621 class FragmentDetails(object):
622 """Custom Fragment fields."""
625 def href(cls, fragment):
626 """ Returns URI in the API for the fragment. """
628 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
631 def url(cls, fragment):
632 """ Returns URL on the site for the fragment. """
634 return WL_BASE + fragment.get_absolute_url()
637 def themes(cls, fragment):
638 """ Returns a list of theme tags for the fragment. """
640 return fragment.tags.filter(category='theme')
643 class FragmentDetailHandler(BaseHandler, FragmentDetails):
644 fields = ['book', 'anchor', 'text', 'url', 'themes']
647 def read(self, request, book, anchor):
648 """ Returns details of a fragment, identified by book slug and anchor. """
650 return Fragment.objects.get(book__slug=book, anchor=anchor)
651 except Fragment.DoesNotExist:
655 class FragmentsHandler(BaseHandler, FragmentDetails):
656 """ Main handler for Fragments.
658 Responsible for lists of Fragment objects
659 and fields used for representing Fragments.
663 fields = ['book', 'url', 'anchor', 'href']
664 allowed_methods = ('GET',)
666 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
669 def read(self, request, tags):
670 """ Lists all fragments with given book, tags, themes.
672 :param tags: should be a path of categories and slugs, i.e.:
673 books/book-slug/authors/an-author/themes/a-theme/
677 tags, ancestors = read_tags(tags, request, allowed=self.categories)
680 fragments = Fragment.tagged.with_all(tags).select_related('book')
681 if fragments.exists():
687 class PictureHandler(BaseHandler):
689 fields = ('slug', 'title')
690 allowed_methods = ('POST',)
692 def create(self, request):
693 if not request.user.has_perm('picture.add_picture'):
696 data = json.loads(request.POST.get('data'))
697 form = PictureImportForm(data)
705 class UserDataHandler(BaseHandler):
707 fields = ('state', 'username', 'premium')
708 allowed_methods = ('GET', 'POST')
710 def read(self, request, slug=None):
711 if not request.user.is_authenticated():
714 return {'username': request.user.username, 'premium': is_subscribed(request.user)}
716 book = Book.objects.get(slug=slug)
717 except Book.DoesNotExist:
720 data = BookUserData.objects.get(book=book, user=request.user)
721 except BookUserData.DoesNotExist:
722 return {'state': 'not_started'}
725 def create(self, request, slug, state):
727 book = Book.objects.get(slug=slug)
728 except Book.DoesNotExist:
730 if not request.user.is_authenticated():
732 if state not in ('reading', 'complete'):
734 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
740 class UserShelfHandler(BookDetailHandler):
741 fields = book_list_fields + ['liked']
743 def parse_bool(self, s):
744 if s in ('true', 'false'):
749 # hack, because piston is stupid
751 def liked(cls, book):
754 def read(self, request, state):
755 if not request.user.is_authenticated():
757 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
758 if state not in ('reading', 'complete', 'likes'):
760 after = request.GET.get('after')
761 count = int(request.GET.get('count', 50))
763 books = Book.tagged.with_any(request.user.tag_set.all())
765 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
766 .values_list('book_id', flat=True)
767 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
769 books = books.filter(slug__gt=after)
771 books = books[:count]
774 new_books.append(BookProxy(book).set('liked', book.id in likes))
775 return QuerySetProxy(new_books)
778 class UserLikeHandler(BaseHandler):
780 allowed_methods = ('GET', 'POST')
782 def read(self, request, slug):
783 if not request.user.is_authenticated():
786 book = Book.objects.get(slug=slug)
787 except Book.DoesNotExist:
789 return {'likes': likes(request.user, book)}
791 def create(self, request, slug):
792 if not request.user.is_authenticated():
795 book = Book.objects.get(slug=slug)
796 except Book.DoesNotExist:
798 action = request.GET.get('action', 'like')
800 book.like(request.user)
801 elif action == 'unlike':
802 book.unlike(request.user)
806 class BlogEntryHandler(BaseHandler):
809 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
811 def read(self, request):
812 after = request.GET.get('after')
813 count = int(request.GET.get('count', 20))
814 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
816 entries = entries.filter(first_published_at__lt=after)
818 entries = entries[:count]
822 def image_url(cls, entry):
823 return (WL_BASE + entry.image.url) if entry.image else None
826 def image_thumb(cls, entry):
827 return MEDIA_BASE + default.backend.get_thumbnail(
828 entry.image, "193x193").url if entry.image else ''
831 def gallery_urls(cls, entry):
832 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
836 return entry.first_published_at
840 return WL_BASE + entry.get_absolute_url()