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'http://' + 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']
51 def read_tags(tags, request, allowed):
52 """ Reads a path of filtering tags.
54 :param str tags: a path of category and slug pairs, like: authors/an-author/...
55 :returns: list of Tag objects
56 :raises: ValueError when tags can't be found
59 def process(category, slug):
60 if category == 'book':
62 books.append(Book.objects.get(slug=slug))
63 except Book.DoesNotExist:
64 raise ValueError('Unknown book.')
66 real_tags.append(Tag.objects.get(category=category, slug=slug))
67 except Tag.DoesNotExist:
68 raise ValueError('Tag not found')
73 tags = tags.strip('/').split('/')
77 category = tags.pop(0)
81 category = category_singular[category]
83 raise ValueError('Unknown category.')
85 if category not in allowed:
86 raise ValueError('Category not allowed.')
87 process(category, slug)
89 for key in request.GET:
90 if key in category_singular:
91 category = category_singular[key]
92 if category in allowed:
93 for slug in request.GET.getlist(key):
94 process(category, slug)
95 return real_tags, books
101 class BookMediaHandler(BaseHandler):
102 """ Responsible for representing media in Books. """
105 fields = ['name', 'type', 'url', 'artist', 'director']
109 """ Link to media on site. """
111 return MEDIA_BASE + media.file.url
114 def artist(cls, media):
115 return media.extra_info.get('artist_name', '')
118 def director(cls, media):
119 return media.extra_info.get('director_name', '')
122 class BookDetails(object):
123 """Custom fields used for representing Books."""
127 """ Returns an URI for a Book in the API. """
128 return API_BASE + reverse("api_book", args=[book.slug])
132 """ Returns Book's URL on the site. """
133 return WL_BASE + book.get_absolute_url()
136 def children(cls, book):
137 """ Returns all children for a book. """
138 return book.children.order_by('parent_number', 'sort_key')
141 def media(cls, book):
142 """ Returns all media for a book. """
143 return book.media.all()
146 def cover(cls, book):
147 return MEDIA_BASE + book.cover.url if book.cover else ''
150 def cover_thumb(cls, book):
151 return MEDIA_BASE + default.backend.get_thumbnail(
152 book.cover, "139x193").url if book.cover else ''
155 def simple_thumb(cls, book):
156 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
159 def simple_cover(cls, book):
160 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
163 def cover_color(cls, book):
164 return WLCover.epoch_colors.get(book.extra_info['epoch'], '#000000')
167 class BookDetailHandler(BaseHandler, BookDetails):
168 """ Main handler for Book objects.
170 Responsible for single Book details.
172 allowed_methods = ['GET']
173 fields = ['title', 'parent', 'children'] + Book.formats + [
174 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
175 'preview', 'cover_color'] + [
176 category_plural[c] for c in book_tag_categories]
179 def read(self, request, book):
180 """ Returns details of a book, identified by a slug and lang. """
182 return Book.objects.get(slug=book)
183 except Book.DoesNotExist:
187 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
188 """ Main handler for Book objects.
190 Responsible for lists of Book objects.
192 allowed_methods = ('GET',)
194 fields = book_tag_categories + [
195 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color']
198 def genres(cls, book):
199 """ Returns all media for a book. """
200 return book.tags.filter(category='genre')
203 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
204 recommended=False, newest=False, books=None,
205 after=None, before=None, count=None):
206 """ Lists all books with given tags.
208 :param tags: filtering tags; should be a path of categories
209 and slugs, i.e.: authors/an-author/epoch/an-epoch/
210 :param top_level: if True and a book is included in the results,
211 it's children are aren't. By default all books matching the tags
216 return Book.objects.get(pk=pk)
217 except Book.DoesNotExist:
221 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
225 if 'after' in request.GET:
226 after = request.GET['after']
227 if 'before' in request.GET:
228 before = request.GET['before']
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')
254 books = books.filter(slug__gt=after)
256 books = books.filter(slug__lt=before)
258 books = books.only('slug', 'title', 'cover', 'cover_thumb')
259 for category in book_tag_categories:
260 books = prefetch_relations(books, category)
264 books = list(reversed(books.order_by('-slug')[:count]))
266 books = books[:count]
270 def create(self, request, *args, **kwargs):
274 class BooksHandler(BookDetailHandler):
275 allowed_methods = ('GET', 'POST')
277 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'cover_color', 'slug']
278 anonymous = AnonymousBooksHandler
280 def create(self, request, *args, **kwargs):
281 if not request.user.has_perm('catalogue.add_book'):
284 data = json.loads(request.POST.get('data'))
285 form = BookImportForm(data)
293 class EpubHandler(BookDetailHandler):
294 def read(self, request, slug):
295 if not is_subscribed(request.user):
298 book = Book.objects.get(slug=slug)
299 except Book.DoesNotExist:
301 response = HttpResponse(book.get_media('epub'))
305 class EBooksHandler(AnonymousBooksHandler):
306 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
309 class BookProxy(models.Model):
313 def __init__(self, book, key):
317 def __getattr__(self, item):
318 if item not in ('book', 'key'):
319 return self.book.__getattribute__(item)
321 return self.__getattribute__(item)
324 class QuerySetProxy(models.QuerySet):
325 def __init__(self, l):
329 return iter(self.list)
332 class FilterBooksHandler(AnonymousBooksHandler):
333 fields = book_tag_categories + [
334 'href', 'title', 'url', 'cover', 'cover_thumb', 'cover_color', 'simple_thumb', 'has_audio', 'slug', 'key']
336 def parse_bool(self, s):
337 if s in ('true', 'false'):
342 def read(self, request):
344 search_string = request.GET.get('search')
345 is_lektura = self.parse_bool(request.GET.get('lektura'))
346 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
347 preview = self.parse_bool(request.GET.get('preview'))
349 after = request.GET.get('after')
350 count = int(request.GET.get('count', 50))
351 books = Book.objects.distinct().order_by('slug')
352 if is_lektura is not None:
353 books = books.filter(has_audience=is_lektura)
354 if is_audiobook is not None:
356 books = books.filter(media__type='mp3')
358 books = books.exclude(media__type='mp3')
359 if preview is not None:
360 books = books.filter(preview=preview)
361 for key in request.GET:
362 if key in category_singular:
363 category = category_singular[key]
364 if category in book_tag_categories:
365 slugs = request.GET[key].split(',')
366 tags = Tag.objects.filter(category=category, slug__in=slugs)
367 books = Book.tagged.with_any(tags, books)
368 if (search_string is not None) and len(search_string) < 3:
371 search_string = re_escape(search_string)
372 books_author = books.filter(cached_author__iregex='\m' + search_string)
373 books_title = books.filter(title__iregex='\m' + search_string)
374 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
375 if after and (key_sep in after):
376 which, slug = after.split(key_sep, 1)
378 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
379 else: # which == 'author'
380 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
382 book_lists = [(books_author, 'author'), (books_title, 'title')]
384 if after and key_sep in after:
385 which, slug = after.split(key_sep, 1)
386 books = books.filter(slug__gt=slug)
387 book_lists = [(books, 'book')]
390 for book_list, label in book_lists:
391 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
392 for category in book_tag_categories:
393 book_list = prefetch_relations(book_list, category)
394 remaining_count = count - len(filtered_books)
395 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
396 for book in book_list[:remaining_count]]
397 filtered_books += new_books
398 if len(filtered_books) == count:
401 return QuerySetProxy(filtered_books)
404 # add categorized tags fields for Book
405 def _tags_getter(category):
407 def get_tags(cls, book):
408 return book.tags.filter(category=category)
412 def _tag_getter(category):
414 def get_tag(cls, book):
415 return book.tag_unicode(category)
419 def add_tag_getters():
420 for plural, singular in category_singular.items():
421 setattr(BookDetails, plural, _tags_getter(singular))
422 setattr(BookDetails, singular, _tag_getter(singular))
428 # add fields for files in Book
429 def _file_getter(book_format):
433 f_url = book.media_url(book_format)
435 return MEDIA_BASE + f_url
441 def add_file_getters():
442 for book_format in Book.formats:
443 setattr(BookDetails, book_format, _file_getter(book_format))
449 class CollectionDetails(object):
450 """Custom Collection fields."""
453 def href(cls, collection):
454 """ Returns URI in the API for the collection. """
456 return API_BASE + reverse("api_collection", args=[collection.slug])
459 def url(cls, collection):
460 """ Returns URL on the site. """
462 return WL_BASE + collection.get_absolute_url()
465 def books(cls, collection):
466 return Book.objects.filter(collection.get_query())
469 class CollectionDetailHandler(BaseHandler, CollectionDetails):
470 allowed_methods = ('GET',)
471 fields = ['url', 'title', 'description', 'books']
474 def read(self, request, slug):
475 """ Returns details of a collection, identified by slug. """
477 return Collection.objects.get(slug=slug)
478 except Collection.DoesNotExist:
482 class CollectionsHandler(BaseHandler, CollectionDetails):
483 allowed_methods = ('GET',)
485 fields = ['url', 'href', 'title']
488 def read(self, request):
489 """ Returns all collections. """
490 return Collection.objects.all()
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). """
545 return Tag.objects.exclude(category='set').get(pk=pk)
546 except Book.DoesNotExist:
550 category_sng = category_singular[category]
554 after = request.GET.get('after')
555 before = request.GET.get('before')
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)
570 tags = tags.filter(slug__lt=before)
574 tags = list(reversed(tags.order_by('-slug')[:count]))
581 class FragmentDetails(object):
582 """Custom Fragment fields."""
585 def href(cls, fragment):
586 """ Returns URI in the API for the fragment. """
588 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
591 def url(cls, fragment):
592 """ Returns URL on the site for the fragment. """
594 return WL_BASE + fragment.get_absolute_url()
597 def themes(cls, fragment):
598 """ Returns a list of theme tags for the fragment. """
600 return fragment.tags.filter(category='theme')
603 class FragmentDetailHandler(BaseHandler, FragmentDetails):
604 fields = ['book', 'anchor', 'text', 'url', 'themes']
607 def read(self, request, book, anchor):
608 """ Returns details of a fragment, identified by book slug and anchor. """
610 return Fragment.objects.get(book__slug=book, anchor=anchor)
611 except Fragment.DoesNotExist:
615 class FragmentsHandler(BaseHandler, FragmentDetails):
616 """ Main handler for Fragments.
618 Responsible for lists of Fragment objects
619 and fields used for representing Fragments.
623 fields = ['book', 'url', 'anchor', 'href']
624 allowed_methods = ('GET',)
626 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
629 def read(self, request, tags):
630 """ Lists all fragments with given book, tags, themes.
632 :param tags: should be a path of categories and slugs, i.e.:
633 books/book-slug/authors/an-author/themes/a-theme/
637 tags, ancestors = read_tags(tags, request, allowed=self.categories)
640 fragments = Fragment.tagged.with_all(tags).select_related('book')
641 if fragments.exists():
647 class PictureHandler(BaseHandler):
649 fields = ('slug', 'title')
650 allowed_methods = ('POST',)
652 def create(self, request):
653 if not request.user.has_perm('picture.add_picture'):
656 data = json.loads(request.POST.get('data'))
657 form = PictureImportForm(data)
665 class UserDataHandler(BaseHandler):
667 fields = ('state', 'username')
668 allowed_methods = ('GET', 'POST')
670 def read(self, request, slug=None):
671 if not request.user.is_authenticated():
674 return {'username': request.user.username}
676 book = Book.objects.get(slug=slug)
677 except Book.DoesNotExist:
680 data = BookUserData.objects.get(book=book, user=request.user)
681 except BookUserData.DoesNotExist:
682 return {'state': 'not_started'}
685 def create(self, request, slug, state):
687 book = Book.objects.get(slug=slug)
688 except Book.DoesNotExist:
690 if not request.user.is_authenticated():
692 if state not in ('reading', 'complete'):
694 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
700 class UserShelfHandler(BookDetailHandler):
701 fields = book_tag_categories + [
702 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
704 def parse_bool(self, s):
705 if s in ('true', 'false'):
710 def read(self, request, state):
711 if not request.user.is_authenticated():
713 if state not in ('reading', 'complete', 'likes'):
715 after = request.GET.get('after')
716 count = int(request.GET.get('count', 50))
718 books = Book.tagged.with_any(request.user.tag_set.all())
720 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
721 .values_list('book_id', flat=True)
722 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
724 books = books.filter(slug__gt=after)
726 books = books[:count]
730 class UserLikeHandler(BaseHandler):
732 allowed_methods = ('GET', 'POST')
734 def read(self, request, slug):
735 if not request.user.is_authenticated():
738 book = Book.objects.get(slug=slug)
739 except Book.DoesNotExist:
741 return {'likes': likes(request.user, book)}
743 def create(self, request, slug, action='like'):
744 if not request.user.is_authenticated():
747 book = Book.objects.get(slug=slug)
748 except Book.DoesNotExist:
751 book.like(request.user)
752 elif action == 'unlike':
753 book.unlike(request.user)
757 class BlogEntryHandler(BaseHandler):
759 fields = ('title', 'lead', 'body', 'place', 'time', 'image_url', 'gallery_urls', 'type', 'key')
761 def read(self, request):
762 after = request.GET.get('after')
763 count = int(request.GET.get('count', 20))
764 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
766 entries = entries.filter(first_published_at__lt=after)
768 entries = entries[:count]
772 def image_url(cls, entry):
773 return entry.image.url if entry.image else None
776 def gallery_urls(cls, entry):
777 return [photo.url() for photo in entry.photo_set.all()]
781 return entry.first_published_at