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['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
280 anonymous = AnonymousBooksHandler
282 def create(self, request, *args, **kwargs):
283 if not request.user.has_perm('catalogue.add_book'):
286 data = json.loads(request.POST.get('data'))
287 form = BookImportForm(data)
295 class EpubHandler(BookDetailHandler):
296 def read(self, request, slug):
297 if not is_subscribed(request.user):
300 book = Book.objects.get(slug=slug)
301 except Book.DoesNotExist:
303 response = HttpResponse(book.get_media('epub'))
307 class EBooksHandler(AnonymousBooksHandler):
308 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
311 class BookProxy(models.Model):
315 def __init__(self, book, key):
319 def __getattr__(self, item):
320 if item not in ('book', 'key'):
321 return self.book.__getattribute__(item)
323 return self.__getattribute__(item)
326 class QuerySetProxy(models.QuerySet):
327 def __init__(self, l):
331 return iter(self.list)
334 class FilterBooksHandler(AnonymousBooksHandler):
335 fields = book_list_fields + ['key']
337 def parse_bool(self, s):
338 if s in ('true', 'false'):
343 def read(self, request):
345 search_string = request.GET.get('search')
346 is_lektura = self.parse_bool(request.GET.get('lektura'))
347 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
348 preview = self.parse_bool(request.GET.get('preview'))
350 after = request.GET.get('after')
351 count = int(request.GET.get('count', 50))
352 books = Book.objects.distinct().order_by('slug')
353 if is_lektura is not None:
354 books = books.filter(has_audience=is_lektura)
355 if is_audiobook is not None:
357 books = books.filter(media__type='mp3')
359 books = books.exclude(media__type='mp3')
360 if preview is not None:
361 books = books.filter(preview=preview)
362 for key in request.GET:
363 if key in category_singular:
364 category = category_singular[key]
365 if category in book_tag_categories:
366 slugs = request.GET[key].split(',')
367 tags = Tag.objects.filter(category=category, slug__in=slugs)
368 books = Book.tagged.with_any(tags, books)
369 if (search_string is not None) and len(search_string) < 3:
372 search_string = re_escape(search_string)
373 books_author = books.filter(cached_author__iregex='\m' + search_string)
374 books_title = books.filter(title__iregex='\m' + search_string)
375 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
376 if after and (key_sep in after):
377 which, slug = after.split(key_sep, 1)
379 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
380 else: # which == 'author'
381 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
383 book_lists = [(books_author, 'author'), (books_title, 'title')]
385 if after and key_sep in after:
386 which, slug = after.split(key_sep, 1)
387 books = books.filter(slug__gt=slug)
388 book_lists = [(books, 'book')]
391 for book_list, label in book_lists:
392 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
393 for category in book_tag_categories:
394 book_list = prefetch_relations(book_list, category)
395 remaining_count = count - len(filtered_books)
396 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
397 for book in book_list[:remaining_count]]
398 filtered_books += new_books
399 if len(filtered_books) == count:
402 return QuerySetProxy(filtered_books)
405 class BookPreviewHandler(BookDetailHandler):
406 fields = BookDetailHandler.fields + ['slug']
408 def read(self, request):
409 return Book.objects.filter(preview=True)
412 # add categorized tags fields for Book
413 def _tags_getter(category):
415 def get_tags(cls, book):
416 return book.tags.filter(category=category)
420 def _tag_getter(category):
422 def get_tag(cls, book):
423 return book.tag_unicode(category)
427 def add_tag_getters():
428 for plural, singular in category_singular.items():
429 setattr(BookDetails, plural, _tags_getter(singular))
430 setattr(BookDetails, singular, _tag_getter(singular))
436 # add fields for files in Book
437 def _file_getter(book_format):
441 f_url = book.media_url(book_format)
443 return MEDIA_BASE + f_url
449 def add_file_getters():
450 for book_format in Book.formats:
451 setattr(BookDetails, book_format, _file_getter(book_format))
457 class CollectionDetails(object):
458 """Custom Collection fields."""
461 def href(cls, collection):
462 """ Returns URI in the API for the collection. """
464 return API_BASE + reverse("api_collection", args=[collection.slug])
467 def url(cls, collection):
468 """ Returns URL on the site. """
470 return WL_BASE + collection.get_absolute_url()
473 def books(cls, collection):
474 return Book.objects.filter(collection.get_query())
477 class CollectionDetailHandler(BaseHandler, CollectionDetails):
478 allowed_methods = ('GET',)
479 fields = ['url', 'title', 'description', 'books']
482 def read(self, request, slug):
483 """ Returns details of a collection, identified by slug. """
485 return Collection.objects.get(slug=slug)
486 except Collection.DoesNotExist:
490 class CollectionsHandler(BaseHandler, CollectionDetails):
491 allowed_methods = ('GET',)
493 fields = ['url', 'href', 'title']
496 def read(self, request):
497 """ Returns all collections. """
498 return Collection.objects.all()
501 class TagDetails(object):
502 """Custom Tag fields."""
506 """ Returns URI in the API for the tag. """
508 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
512 """ Returns URL on the site. """
514 return WL_BASE + tag.get_absolute_url()
517 class TagDetailHandler(BaseHandler, TagDetails):
518 """ Responsible for details of a single Tag object. """
520 fields = ['name', 'url', 'sort_key', 'description']
523 def read(self, request, category, slug):
524 """ Returns details of a tag, identified by category and slug. """
527 category_sng = category_singular[category]
532 return Tag.objects.get(category=category_sng, slug=slug)
533 except Tag.DoesNotExist:
537 class TagsHandler(BaseHandler, TagDetails):
538 """ Main handler for Tag objects.
540 Responsible for lists of Tag objects
541 and fields used for representing Tags.
544 allowed_methods = ('GET',)
546 fields = ['name', 'href', 'url', 'slug']
549 def read(self, request, category=None, pk=None):
550 """ Lists all tags in the category (eg. all themes). """
553 return Tag.objects.exclude(category='set').get(pk=pk)
554 except Book.DoesNotExist:
558 category_sng = category_singular[category]
562 after = request.GET.get('after')
563 before = request.GET.get('before')
564 count = request.GET.get('count')
566 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
568 book_only = request.GET.get('book_only') == 'true'
569 picture_only = request.GET.get('picture_only') == 'true'
571 tags = tags.filter(for_books=True)
573 tags = tags.filter(for_pictures=True)
576 tags = tags.filter(slug__gt=after)
578 tags = tags.filter(slug__lt=before)
582 tags = list(reversed(tags.order_by('-slug')[:count]))
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': 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
711 def parse_bool(self, s):
712 if s in ('true', 'false'):
717 def read(self, request, state):
718 if not request.user.is_authenticated():
720 if state not in ('reading', 'complete', 'likes'):
722 after = request.GET.get('after')
723 count = int(request.GET.get('count', 50))
725 books = Book.tagged.with_any(request.user.tag_set.all())
727 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
728 .values_list('book_id', flat=True)
729 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
731 books = books.filter(slug__gt=after)
733 books = books[:count]
737 class UserLikeHandler(BaseHandler):
739 allowed_methods = ('GET', 'POST')
741 def read(self, request, slug):
742 if not request.user.is_authenticated():
745 book = Book.objects.get(slug=slug)
746 except Book.DoesNotExist:
748 return {'likes': likes(request.user, book)}
750 def create(self, request, slug):
751 if not request.user.is_authenticated():
754 book = Book.objects.get(slug=slug)
755 except Book.DoesNotExist:
757 action = request.GET.get('action', 'like')
759 book.like(request.user)
760 elif action == 'unlike':
761 book.unlike(request.user)
765 class BlogEntryHandler(BaseHandler):
768 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
770 def read(self, request):
771 after = request.GET.get('after')
772 count = int(request.GET.get('count', 20))
773 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
775 entries = entries.filter(first_published_at__lt=after)
777 entries = entries[:count]
781 def image_url(cls, entry):
782 return (WL_BASE + entry.image.url) if entry.image else None
785 def image_thumb(cls, entry):
786 return MEDIA_BASE + default.backend.get_thumbnail(
787 entry.image, "193x193").url if entry.image else ''
790 def gallery_urls(cls, entry):
791 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
795 return entry.first_published_at
799 return WL_BASE + entry.get_absolute_url()