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 picture.models import Picture
23 from picture.forms import PictureImportForm
24 from social.utils import likes
26 from stats.utils import piwik_track
27 from wolnelektury.utils import re_escape
29 from . import emitters # Register our emitters
31 API_BASE = WL_BASE = MEDIA_BASE = lazy(
32 lambda: u'http://' + 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']
50 def read_tags(tags, request, allowed):
51 """ Reads a path of filtering tags.
53 :param str tags: a path of category and slug pairs, like: authors/an-author/...
54 :returns: list of Tag objects
55 :raises: ValueError when tags can't be found
58 def process(category, slug):
59 if category == 'book':
61 books.append(Book.objects.get(slug=slug))
62 except Book.DoesNotExist:
63 raise ValueError('Unknown book.')
65 real_tags.append(Tag.objects.get(category=category, slug=slug))
66 except Tag.DoesNotExist:
67 raise ValueError('Tag not found')
72 tags = tags.strip('/').split('/')
76 category = tags.pop(0)
80 category = category_singular[category]
82 raise ValueError('Unknown category.')
84 if category not in allowed:
85 raise ValueError('Category not allowed.')
86 process(category, slug)
88 for key in request.GET:
89 if key in category_singular:
90 category = category_singular[key]
91 if category in allowed:
92 for slug in request.GET.getlist(key):
93 process(category, slug)
94 return real_tags, books
100 class BookMediaHandler(BaseHandler):
101 """ Responsible for representing media in Books. """
104 fields = ['name', 'type', 'url', 'artist', 'director']
108 """ Link to media on site. """
110 return MEDIA_BASE + media.file.url
113 def artist(cls, media):
114 return media.extra_info.get('artist_name', '')
117 def director(cls, media):
118 return media.extra_info.get('director_name', '')
121 class BookDetails(object):
122 """Custom fields used for representing Books."""
126 """ Returns an URI for a Book in the API. """
127 return API_BASE + reverse("api_book", args=[book.slug])
131 """ Returns Book's URL on the site. """
132 return WL_BASE + book.get_absolute_url()
135 def children(cls, book):
136 """ Returns all children for a book. """
137 return book.children.order_by('parent_number', 'sort_key')
140 def media(cls, book):
141 """ Returns all media for a book. """
142 return book.media.all()
145 def cover(cls, book):
146 return MEDIA_BASE + book.cover.url if book.cover else ''
149 def cover_thumb(cls, book):
150 return MEDIA_BASE + default.backend.get_thumbnail(
151 book.cover, "139x193").url if book.cover else ''
154 def simple_thumb(cls, book):
155 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
158 def simple_cover(cls, book):
159 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
162 class BookDetailHandler(BaseHandler, BookDetails):
163 """ Main handler for Book objects.
165 Responsible for single Book details.
167 allowed_methods = ['GET']
168 fields = ['title', 'parent', 'children'] + Book.formats + [
169 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
171 category_plural[c] for c in book_tag_categories]
174 def read(self, request, book):
175 """ Returns details of a book, identified by a slug and lang. """
177 return Book.objects.get(slug=book)
178 except Book.DoesNotExist:
182 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
183 """ Main handler for Book objects.
185 Responsible for lists of Book objects.
187 allowed_methods = ('GET',)
189 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio']
192 def genres(cls, book):
193 """ Returns all media for a book. """
194 return book.tags.filter(category='genre')
197 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
198 recommended=False, newest=False, books=None,
199 after=None, before=None, count=None):
200 """ Lists all books with given tags.
202 :param tags: filtering tags; should be a path of categories
203 and slugs, i.e.: authors/an-author/epoch/an-epoch/
204 :param top_level: if True and a book is included in the results,
205 it's children are aren't. By default all books matching the tags
210 return Book.objects.get(pk=pk)
211 except Book.DoesNotExist:
215 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
219 if 'after' in request.GET:
220 after = request.GET['after']
221 if 'before' in request.GET:
222 before = request.GET['before']
223 if 'count' in request.GET:
224 count = request.GET['count']
228 books = Book.tagged_top_level(tags)
229 return books if books else rc.NOT_FOUND
231 books = Book.tagged.with_all(tags)
233 books = books if books is not None else Book.objects.all()
234 books = books.order_by('slug')
237 books = books.filter(parent=None)
239 books = books.filter(media__type='mp3').distinct()
241 books = books.filter(media__type='daisy').distinct()
243 books = books.filter(recommended=True)
245 books = books.order_by('-created_at')
248 books = books.filter(slug__gt=after)
250 books = books.filter(slug__lt=before)
252 books = books.only('slug', 'title', 'cover', 'cover_thumb')
253 for category in book_tag_categories:
254 books = prefetch_relations(books, category)
258 books = list(reversed(books.order_by('-slug')[:count]))
260 books = books[:count]
264 def create(self, request, *args, **kwargs):
268 class BooksHandler(BookDetailHandler):
269 allowed_methods = ('GET', 'POST')
271 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
272 anonymous = AnonymousBooksHandler
274 def create(self, request, *args, **kwargs):
275 if not request.user.has_perm('catalogue.add_book'):
278 data = json.loads(request.POST.get('data'))
279 form = BookImportForm(data)
287 class EpubHandler(BookDetailHandler):
288 def read(self, request, slug):
289 if not is_subscribed(request.user):
292 book = Book.objects.get(slug=slug)
293 except Book.DoesNotExist:
295 response = HttpResponse(book.get_media('epub'))
299 class EBooksHandler(AnonymousBooksHandler):
300 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
303 class BookProxy(models.Model):
307 def __init__(self, book, key):
311 def __getattr__(self, item):
312 if item not in ('book', 'key'):
313 return self.book.__getattribute__(item)
315 return self.__getattribute__(item)
318 class QuerySetProxy(models.QuerySet):
319 def __init__(self, l):
323 return iter(self.list)
326 class FilterBooksHandler(AnonymousBooksHandler):
327 fields = book_tag_categories + [
328 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'has_audio', 'slug', 'key']
330 def parse_bool(self, s):
331 if s in ('true', 'false'):
336 def read(self, request):
338 search_string = request.GET.get('search')
339 is_lektura = self.parse_bool(request.GET.get('lektura'))
340 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
341 preview = self.parse_bool(request.GET.get('preview'))
343 after = request.GET.get('after')
344 count = int(request.GET.get('count', 50))
345 books = Book.objects.distinct().order_by('slug')
346 if is_lektura is not None:
347 books = books.filter(has_audience=is_lektura)
348 if is_audiobook is not None:
350 books = books.filter(media__type='mp3')
352 books = books.exclude(media__type='mp3')
353 if preview is not None:
354 books = books.filter(preview=preview)
355 for key in request.GET:
356 if key in category_singular:
357 category = category_singular[key]
358 if category in book_tag_categories:
359 slugs = request.GET[key].split(',')
360 tags = Tag.objects.filter(category=category, slug__in=slugs)
361 books = Book.tagged.with_any(tags, books)
362 if (search_string is not None) and len(search_string) < 3:
365 search_string = re_escape(search_string)
366 books_author = books.filter(cached_author__iregex='\m' + search_string)
367 books_title = books.filter(title__iregex='\m' + search_string)
368 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
369 if after and (key_sep in after):
370 which, slug = after.split(key_sep, 1)
372 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
373 else: # which == 'author'
374 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
376 book_lists = [(books_author, 'author'), (books_title, 'title')]
378 if after and key_sep in after:
379 which, slug = after.split(key_sep, 1)
380 books = books.filter(slug__gt=slug)
381 book_lists = [(books, 'book')]
384 for book_list, label in book_lists:
385 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
386 for category in book_tag_categories:
387 book_list = prefetch_relations(book_list, category)
388 remaining_count = count - len(filtered_books)
389 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
390 for book in book_list[:remaining_count]]
391 filtered_books += new_books
392 if len(filtered_books) == count:
395 return QuerySetProxy(filtered_books)
398 # add categorized tags fields for Book
399 def _tags_getter(category):
401 def get_tags(cls, book):
402 return book.tags.filter(category=category)
406 def _tag_getter(category):
408 def get_tag(cls, book):
409 return book.tag_unicode(category)
413 def add_tag_getters():
414 for plural, singular in category_singular.items():
415 setattr(BookDetails, plural, _tags_getter(singular))
416 setattr(BookDetails, singular, _tag_getter(singular))
422 # add fields for files in Book
423 def _file_getter(book_format):
427 f_url = book.media_url(book_format)
429 return MEDIA_BASE + f_url
435 def add_file_getters():
436 for book_format in Book.formats:
437 setattr(BookDetails, book_format, _file_getter(book_format))
443 class CollectionDetails(object):
444 """Custom Collection fields."""
447 def href(cls, collection):
448 """ Returns URI in the API for the collection. """
450 return API_BASE + reverse("api_collection", args=[collection.slug])
453 def url(cls, collection):
454 """ Returns URL on the site. """
456 return WL_BASE + collection.get_absolute_url()
459 def books(cls, collection):
460 return Book.objects.filter(collection.get_query())
463 class CollectionDetailHandler(BaseHandler, CollectionDetails):
464 allowed_methods = ('GET',)
465 fields = ['url', 'title', 'description', 'books']
468 def read(self, request, slug):
469 """ Returns details of a collection, identified by slug. """
471 return Collection.objects.get(slug=slug)
472 except Collection.DoesNotExist:
476 class CollectionsHandler(BaseHandler, CollectionDetails):
477 allowed_methods = ('GET',)
479 fields = ['url', 'href', 'title']
482 def read(self, request):
483 """ Returns all collections. """
484 return Collection.objects.all()
487 class TagDetails(object):
488 """Custom Tag fields."""
492 """ Returns URI in the API for the tag. """
494 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
498 """ Returns URL on the site. """
500 return WL_BASE + tag.get_absolute_url()
503 class TagDetailHandler(BaseHandler, TagDetails):
504 """ Responsible for details of a single Tag object. """
506 fields = ['name', 'url', 'sort_key', 'description']
509 def read(self, request, category, slug):
510 """ Returns details of a tag, identified by category and slug. """
513 category_sng = category_singular[category]
518 return Tag.objects.get(category=category_sng, slug=slug)
519 except Tag.DoesNotExist:
523 class TagsHandler(BaseHandler, TagDetails):
524 """ Main handler for Tag objects.
526 Responsible for lists of Tag objects
527 and fields used for representing Tags.
530 allowed_methods = ('GET',)
532 fields = ['name', 'href', 'url', 'slug']
535 def read(self, request, category=None, pk=None):
536 """ Lists all tags in the category (eg. all themes). """
539 return Tag.objects.exclude(category='set').get(pk=pk)
540 except Book.DoesNotExist:
544 category_sng = category_singular[category]
548 after = request.GET.get('after')
549 before = request.GET.get('before')
550 count = request.GET.get('count')
552 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
554 book_only = request.GET.get('book_only') == 'true'
555 picture_only = request.GET.get('picture_only') == 'true'
557 tags = tags.filter(for_books=True)
559 tags = tags.filter(for_pictures=True)
562 tags = tags.filter(slug__gt=after)
564 tags = tags.filter(slug__lt=before)
568 tags = list(reversed(tags.order_by('-slug')[:count]))
575 class FragmentDetails(object):
576 """Custom Fragment fields."""
579 def href(cls, fragment):
580 """ Returns URI in the API for the fragment. """
582 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
585 def url(cls, fragment):
586 """ Returns URL on the site for the fragment. """
588 return WL_BASE + fragment.get_absolute_url()
591 def themes(cls, fragment):
592 """ Returns a list of theme tags for the fragment. """
594 return fragment.tags.filter(category='theme')
597 class FragmentDetailHandler(BaseHandler, FragmentDetails):
598 fields = ['book', 'anchor', 'text', 'url', 'themes']
601 def read(self, request, book, anchor):
602 """ Returns details of a fragment, identified by book slug and anchor. """
604 return Fragment.objects.get(book__slug=book, anchor=anchor)
605 except Fragment.DoesNotExist:
609 class FragmentsHandler(BaseHandler, FragmentDetails):
610 """ Main handler for Fragments.
612 Responsible for lists of Fragment objects
613 and fields used for representing Fragments.
617 fields = ['book', 'url', 'anchor', 'href']
618 allowed_methods = ('GET',)
620 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
623 def read(self, request, tags):
624 """ Lists all fragments with given book, tags, themes.
626 :param tags: should be a path of categories and slugs, i.e.:
627 books/book-slug/authors/an-author/themes/a-theme/
631 tags, ancestors = read_tags(tags, request, allowed=self.categories)
634 fragments = Fragment.tagged.with_all(tags).select_related('book')
635 if fragments.exists():
641 class PictureHandler(BaseHandler):
643 fields = ('slug', 'title')
644 allowed_methods = ('POST',)
646 def create(self, request):
647 if not request.user.has_perm('picture.add_picture'):
650 data = json.loads(request.POST.get('data'))
651 form = PictureImportForm(data)
659 class UserDataHandler(BaseHandler):
661 fields = ('state', 'username')
662 allowed_methods = ('GET', 'POST')
664 def read(self, request, slug=None):
665 if not request.user.is_authenticated():
668 return {'username': request.user.username}
670 book = Book.objects.get(slug=slug)
671 except Book.DoesNotExist:
674 data = BookUserData.objects.get(book=book, user=request.user)
675 except BookUserData.DoesNotExist:
676 return {'state': 'not_started'}
679 def create(self, request, slug, state):
681 book = Book.objects.get(slug=slug)
682 except Book.DoesNotExist:
684 if not request.user.is_authenticated():
686 if state not in ('reading', 'complete'):
688 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
694 class UserShelfHandler(BookDetailHandler):
695 fields = book_tag_categories + [
696 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
698 def parse_bool(self, s):
699 if s in ('true', 'false'):
704 def read(self, request, state):
705 if not request.user.is_authenticated():
707 if state not in ('reading', 'complete', 'likes'):
709 after = request.GET.get('after')
710 count = int(request.GET.get('count', 50))
712 books = Book.tagged.with_any(request.user.tag_set.all())
714 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
715 .values_list('book_id', flat=True)
716 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
718 books = books.filter(slug__gt=after)
720 books = books[:count]
724 class UserLikeHandler(BaseHandler):
726 allowed_methods = ('GET', 'POST')
728 def read(self, request, slug):
729 if not request.user.is_authenticated():
732 book = Book.objects.get(slug=slug)
733 except Book.DoesNotExist:
735 return {'likes': likes(request.user, book)}
737 def create(self, request, slug, action='like'):
738 if not request.user.is_authenticated():
741 book = Book.objects.get(slug=slug)
742 except Book.DoesNotExist:
745 book.like(request.user)
746 elif action == 'unlike':
747 book.unlike(request.user)
751 class BlogEntryHandler(BaseHandler):
753 fields = ('title', 'lead', 'body', 'place', 'time', 'image_url', 'gallery_urls', 'type', 'key')
755 def read(self, request):
756 after = request.GET.get('after')
757 count = int(request.GET.get('count', 20))
758 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
760 entries = entries.filter(first_published_at__lt=after)
762 entries = entries[:count]
766 def image_url(cls, entry):
767 return entry.image.url if entry.image else None
770 def gallery_urls(cls, entry):
771 return [photo.url() for photo in entry.photo_set.all()]
775 return entry.first_published_at