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
25 from stats.utils import piwik_track
26 from wolnelektury.utils import re_escape
28 from . import emitters # Register our emitters
30 API_BASE = WL_BASE = MEDIA_BASE = lazy(
31 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
43 for k, v in category_singular.items():
44 category_plural[v] = k
46 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
49 def read_tags(tags, request, allowed):
50 """ Reads a path of filtering tags.
52 :param str tags: a path of category and slug pairs, like: authors/an-author/...
53 :returns: list of Tag objects
54 :raises: ValueError when tags can't be found
57 def process(category, slug):
58 if category == 'book':
60 books.append(Book.objects.get(slug=slug))
61 except Book.DoesNotExist:
62 raise ValueError('Unknown book.')
64 real_tags.append(Tag.objects.get(category=category, slug=slug))
65 except Tag.DoesNotExist:
66 raise ValueError('Tag not found')
71 tags = tags.strip('/').split('/')
75 category = tags.pop(0)
79 category = category_singular[category]
81 raise ValueError('Unknown category.')
83 if category not in allowed:
84 raise ValueError('Category not allowed.')
85 process(category, slug)
87 for key in request.GET:
88 if key in category_singular:
89 category = category_singular[key]
90 if category in allowed:
91 for slug in request.GET.getlist(key):
92 process(category, slug)
93 return real_tags, books
99 class BookMediaHandler(BaseHandler):
100 """ Responsible for representing media in Books. """
103 fields = ['name', 'type', 'url', 'artist', 'director']
107 """ Link to media on site. """
109 return MEDIA_BASE + media.file.url
112 def artist(cls, media):
113 return media.extra_info.get('artist_name', '')
116 def director(cls, media):
117 return media.extra_info.get('director_name', '')
120 class BookDetails(object):
121 """Custom fields used for representing Books."""
125 """ Returns an URI for a Book in the API. """
126 return API_BASE + reverse("api_book", args=[book.slug])
130 """ Returns Book's URL on the site. """
131 return WL_BASE + book.get_absolute_url()
134 def children(cls, book):
135 """ Returns all children for a book. """
136 return book.children.order_by('parent_number', 'sort_key')
139 def media(cls, book):
140 """ Returns all media for a book. """
141 return book.media.all()
144 def cover(cls, book):
145 return MEDIA_BASE + book.cover.url if book.cover else ''
148 def cover_thumb(cls, book):
149 return MEDIA_BASE + default.backend.get_thumbnail(
150 book.cover, "139x193").url if book.cover else ''
153 def simple_thumb(cls, book):
154 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
157 def simple_cover(cls, book):
158 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
161 class BookDetailHandler(BaseHandler, BookDetails):
162 """ Main handler for Book objects.
164 Responsible for single Book details.
166 allowed_methods = ['GET']
167 fields = ['title', 'parent', 'children'] + Book.formats + [
168 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
170 category_plural[c] for c in book_tag_categories]
173 def read(self, request, book):
174 """ Returns details of a book, identified by a slug and lang. """
176 return Book.objects.get(slug=book)
177 except Book.DoesNotExist:
181 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
182 """ Main handler for Book objects.
184 Responsible for lists of Book objects.
186 allowed_methods = ('GET',)
188 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio']
191 def genres(cls, book):
192 """ Returns all media for a book. """
193 return book.tags.filter(category='genre')
196 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
197 recommended=False, newest=False, books=None,
198 after=None, before=None, count=None):
199 """ Lists all books with given tags.
201 :param tags: filtering tags; should be a path of categories
202 and slugs, i.e.: authors/an-author/epoch/an-epoch/
203 :param top_level: if True and a book is included in the results,
204 it's children are aren't. By default all books matching the tags
209 return Book.objects.get(pk=pk)
210 except Book.DoesNotExist:
214 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
218 if 'after' in request.GET:
219 after = request.GET['after']
220 if 'before' in request.GET:
221 before = request.GET['before']
222 if 'count' in request.GET:
223 count = request.GET['count']
227 books = Book.tagged_top_level(tags)
228 return books if books else rc.NOT_FOUND
230 books = Book.tagged.with_all(tags)
232 books = books if books is not None else Book.objects.all()
233 books = books.order_by('slug')
236 books = books.filter(parent=None)
238 books = books.filter(media__type='mp3').distinct()
240 books = books.filter(media__type='daisy').distinct()
242 books = books.filter(recommended=True)
244 books = books.order_by('-created_at')
247 books = books.filter(slug__gt=after)
249 books = books.filter(slug__lt=before)
251 books = books.only('slug', 'title', 'cover', 'cover_thumb')
252 for category in book_tag_categories:
253 books = prefetch_relations(books, category)
257 books = list(reversed(books.order_by('-slug')[:count]))
259 books = books[:count]
263 def create(self, request, *args, **kwargs):
267 class BooksHandler(BookDetailHandler):
268 allowed_methods = ('GET', 'POST')
270 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
271 anonymous = AnonymousBooksHandler
273 def create(self, request, *args, **kwargs):
274 if not request.user.has_perm('catalogue.add_book'):
277 data = json.loads(request.POST.get('data'))
278 form = BookImportForm(data)
286 class EpubHandler(BookDetailHandler):
287 def read(self, request, slug):
288 if not is_subscribed(request.user):
291 book = Book.objects.get(slug=slug)
292 except Book.DoesNotExist:
294 response = HttpResponse(book.get_media('epub'))
298 class EBooksHandler(AnonymousBooksHandler):
299 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
302 class BookProxy(models.Model):
306 def __init__(self, book, key):
310 def __getattr__(self, item):
311 if item not in ('book', 'key'):
312 return self.book.__getattribute__(item)
314 return self.__getattribute__(item)
317 class QuerySetProxy(models.QuerySet):
318 def __init__(self, l):
322 return iter(self.list)
325 class FilterBooksHandler(AnonymousBooksHandler):
326 fields = book_tag_categories + [
327 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'has_audio', 'slug', 'key']
329 def parse_bool(self, s):
330 if s in ('true', 'false'):
335 def read(self, request):
337 search_string = request.GET.get('search')
338 is_lektura = self.parse_bool(request.GET.get('lektura'))
339 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
340 preview = self.parse_bool(request.GET.get('preview'))
342 after = request.GET.get('after')
343 count = int(request.GET.get('count', 50))
344 books = Book.objects.distinct().order_by('slug')
345 if is_lektura is not None:
346 books = books.filter(has_audience=is_lektura)
347 if is_audiobook is not None:
349 books = books.filter(media__type='mp3')
351 books = books.exclude(media__type='mp3')
352 if preview is not None:
353 books = books.filter(preview=preview)
354 for key in request.GET:
355 if key in category_singular:
356 category = category_singular[key]
357 if category in book_tag_categories:
358 slugs = request.GET[key].split(',')
359 tags = Tag.objects.filter(category=category, slug__in=slugs)
360 books = Book.tagged.with_any(tags, books)
361 if (search_string is not None) and len(search_string) < 3:
364 search_string = re_escape(search_string)
365 books_author = books.filter(cached_author__iregex='\m' + search_string)
366 books_title = books.filter(title__iregex='\m' + search_string)
367 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
368 if after and (key_sep in after):
369 which, slug = after.split(key_sep, 1)
371 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
372 else: # which == 'author'
373 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
375 book_lists = [(books_author, 'author'), (books_title, 'title')]
377 if after and key_sep in after:
378 which, slug = after.split(key_sep, 1)
379 books = books.filter(slug__gt=slug)
380 book_lists = [(books, 'book')]
383 for book_list, label in book_lists:
384 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
385 for category in book_tag_categories:
386 book_list = prefetch_relations(book_list, category)
387 remaining_count = count - len(filtered_books)
388 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
389 for book in book_list[:remaining_count]]
390 filtered_books += new_books
391 if len(filtered_books) == count:
394 return QuerySetProxy(filtered_books)
397 # add categorized tags fields for Book
398 def _tags_getter(category):
400 def get_tags(cls, book):
401 return book.tags.filter(category=category)
405 def _tag_getter(category):
407 def get_tag(cls, book):
408 return book.tag_unicode(category)
412 def add_tag_getters():
413 for plural, singular in category_singular.items():
414 setattr(BookDetails, plural, _tags_getter(singular))
415 setattr(BookDetails, singular, _tag_getter(singular))
421 # add fields for files in Book
422 def _file_getter(book_format):
426 f_url = book.media_url(book_format)
428 return MEDIA_BASE + f_url
434 def add_file_getters():
435 for book_format in Book.formats:
436 setattr(BookDetails, book_format, _file_getter(book_format))
442 class CollectionDetails(object):
443 """Custom Collection fields."""
446 def href(cls, collection):
447 """ Returns URI in the API for the collection. """
449 return API_BASE + reverse("api_collection", args=[collection.slug])
452 def url(cls, collection):
453 """ Returns URL on the site. """
455 return WL_BASE + collection.get_absolute_url()
458 def books(cls, collection):
459 return Book.objects.filter(collection.get_query())
462 class CollectionDetailHandler(BaseHandler, CollectionDetails):
463 allowed_methods = ('GET',)
464 fields = ['url', 'title', 'description', 'books']
467 def read(self, request, slug):
468 """ Returns details of a collection, identified by slug. """
470 return Collection.objects.get(slug=slug)
471 except Collection.DoesNotExist:
475 class CollectionsHandler(BaseHandler, CollectionDetails):
476 allowed_methods = ('GET',)
478 fields = ['url', 'href', 'title']
481 def read(self, request):
482 """ Returns all collections. """
483 return Collection.objects.all()
486 class TagDetails(object):
487 """Custom Tag fields."""
491 """ Returns URI in the API for the tag. """
493 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
497 """ Returns URL on the site. """
499 return WL_BASE + tag.get_absolute_url()
502 class TagDetailHandler(BaseHandler, TagDetails):
503 """ Responsible for details of a single Tag object. """
505 fields = ['name', 'url', 'sort_key', 'description']
508 def read(self, request, category, slug):
509 """ Returns details of a tag, identified by category and slug. """
512 category_sng = category_singular[category]
517 return Tag.objects.get(category=category_sng, slug=slug)
518 except Tag.DoesNotExist:
522 class TagsHandler(BaseHandler, TagDetails):
523 """ Main handler for Tag objects.
525 Responsible for lists of Tag objects
526 and fields used for representing Tags.
529 allowed_methods = ('GET',)
531 fields = ['name', 'href', 'url', 'slug']
534 def read(self, request, category=None, pk=None):
535 """ Lists all tags in the category (eg. all themes). """
538 return Tag.objects.exclude(category='set').get(pk=pk)
539 except Book.DoesNotExist:
543 category_sng = category_singular[category]
547 after = request.GET.get('after')
548 before = request.GET.get('before')
549 count = request.GET.get('count')
551 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
553 book_only = request.GET.get('book_only') == 'true'
554 picture_only = request.GET.get('picture_only') == 'true'
556 tags = tags.filter(for_books=True)
558 tags = tags.filter(for_pictures=True)
561 tags = tags.filter(slug__gt=after)
563 tags = tags.filter(slug__lt=before)
567 tags = list(reversed(tags.order_by('-slug')[:count]))
574 class FragmentDetails(object):
575 """Custom Fragment fields."""
578 def href(cls, fragment):
579 """ Returns URI in the API for the fragment. """
581 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
584 def url(cls, fragment):
585 """ Returns URL on the site for the fragment. """
587 return WL_BASE + fragment.get_absolute_url()
590 def themes(cls, fragment):
591 """ Returns a list of theme tags for the fragment. """
593 return fragment.tags.filter(category='theme')
596 class FragmentDetailHandler(BaseHandler, FragmentDetails):
597 fields = ['book', 'anchor', 'text', 'url', 'themes']
600 def read(self, request, book, anchor):
601 """ Returns details of a fragment, identified by book slug and anchor. """
603 return Fragment.objects.get(book__slug=book, anchor=anchor)
604 except Fragment.DoesNotExist:
608 class FragmentsHandler(BaseHandler, FragmentDetails):
609 """ Main handler for Fragments.
611 Responsible for lists of Fragment objects
612 and fields used for representing Fragments.
616 fields = ['book', 'url', 'anchor', 'href']
617 allowed_methods = ('GET',)
619 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
622 def read(self, request, tags):
623 """ Lists all fragments with given book, tags, themes.
625 :param tags: should be a path of categories and slugs, i.e.:
626 books/book-slug/authors/an-author/themes/a-theme/
630 tags, ancestors = read_tags(tags, request, allowed=self.categories)
633 fragments = Fragment.tagged.with_all(tags).select_related('book')
634 if fragments.exists():
640 class PictureHandler(BaseHandler):
642 fields = ('slug', 'title')
643 allowed_methods = ('POST',)
645 def create(self, request):
646 if not request.user.has_perm('picture.add_picture'):
649 data = json.loads(request.POST.get('data'))
650 form = PictureImportForm(data)
658 class UserDataHandler(BaseHandler):
660 fields = ('state', 'username')
661 allowed_methods = ('GET', 'POST')
663 def read(self, request, slug=None):
664 if not request.user.is_authenticated():
667 return {'username': request.user.username}
669 book = Book.objects.get(slug=slug)
670 except Book.DoesNotExist:
673 data = BookUserData.objects.get(book=book, user=request.user)
674 except BookUserData.DoesNotExist:
675 return {'state': 'not_started'}
678 def create(self, request, slug, state):
680 book = Book.objects.get(slug=slug)
681 except Book.DoesNotExist:
683 if not request.user.is_authenticated():
685 if state not in ('reading', 'complete'):
687 data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
693 class UserShelfHandler(BookDetailHandler):
694 fields = book_tag_categories + [
695 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
697 def parse_bool(self, s):
698 if s in ('true', 'false'):
703 def read(self, request, state):
704 if not request.user.is_authenticated():
706 if state not in ('reading', 'complete'):
708 after = request.GET.get('after')
709 count = int(request.GET.get('count', 50))
710 ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
711 .values_list('book_id', flat=True)
712 books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
714 books = books.filter(slug__gt=after)
716 books = books[:count]
720 class BlogEntryHandler(BaseHandler):
722 fields = ('title', 'lead', 'body', 'place', 'time', 'image_url', 'gallery_urls', 'type', 'key')
724 def read(self, request):
725 after = request.GET.get('after')
726 count = int(request.GET.get('count', 20))
727 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
729 entries = entries.filter(first_published_at__lt=after)
731 entries = entries[:count]
735 def image_url(cls, entry):
736 return entry.image.url if entry.image else None
739 def gallery_urls(cls, entry):
740 return [photo.url() for photo in entry.photo_set.all()]
744 return entry.first_published_at