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.utils.functional import lazy
10 from django.db import models
11 from piston.handler import AnonymousBaseHandler, BaseHandler
12 from piston.utils import rc
13 from sorl.thumbnail import default
15 from catalogue.forms import BookImportForm
16 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
17 from catalogue.models.tag import prefetch_relations
18 from picture.models import Picture
19 from picture.forms import PictureImportForm
21 from stats.utils import piwik_track
22 from wolnelektury.utils import re_escape
24 from . import emitters # Register our emitters
26 API_BASE = WL_BASE = MEDIA_BASE = lazy(
27 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
39 for k, v in category_singular.items():
40 category_plural[v] = k
42 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
45 def read_tags(tags, request, allowed):
46 """ Reads a path of filtering tags.
48 :param str tags: a path of category and slug pairs, like: authors/an-author/...
49 :returns: list of Tag objects
50 :raises: ValueError when tags can't be found
53 def process(category, slug):
54 if category == 'book':
56 books.append(Book.objects.get(slug=slug))
57 except Book.DoesNotExist:
58 raise ValueError('Unknown book.')
60 real_tags.append(Tag.objects.get(category=category, slug=slug))
61 except Tag.DoesNotExist:
62 raise ValueError('Tag not found')
67 tags = tags.strip('/').split('/')
71 category = tags.pop(0)
75 category = category_singular[category]
77 raise ValueError('Unknown category.')
79 if category not in allowed:
80 raise ValueError('Category not allowed.')
81 process(category, slug)
83 for key in request.GET:
84 if key in category_singular:
85 category = category_singular[key]
86 if category in allowed:
87 for slug in request.GET.getlist(key):
88 process(category, slug)
89 return real_tags, books
95 class BookMediaHandler(BaseHandler):
96 """ Responsible for representing media in Books. """
99 fields = ['name', 'type', 'url', 'artist', 'director']
103 """ Link to media on site. """
105 return MEDIA_BASE + media.file.url
108 def artist(cls, media):
109 return media.extra_info.get('artist_name', '')
112 def director(cls, media):
113 return media.extra_info.get('director_name', '')
116 class BookDetails(object):
117 """Custom fields used for representing Books."""
121 """ Returns an URI for a Book in the API. """
122 return API_BASE + reverse("api_book", args=[book.slug])
126 """ Returns Book's URL on the site. """
127 return WL_BASE + book.get_absolute_url()
130 def children(cls, book):
131 """ Returns all children for a book. """
132 return book.children.order_by('parent_number', 'sort_key')
135 def media(cls, book):
136 """ Returns all media for a book. """
137 return book.media.all()
140 def cover(cls, book):
141 return MEDIA_BASE + book.cover.url if book.cover else ''
144 def cover_thumb(cls, book):
145 return MEDIA_BASE + default.backend.get_thumbnail(
146 book.cover, "139x193").url if book.cover else ''
149 def simple_thumb(cls, book):
150 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
153 def simple_cover(cls, book):
154 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
157 class BookDetailHandler(BaseHandler, BookDetails):
158 """ Main handler for Book objects.
160 Responsible for single Book details.
162 allowed_methods = ['GET']
163 fields = ['title', 'parent', 'children'] + Book.formats + [
164 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data'] + [
165 category_plural[c] for c in book_tag_categories]
168 def read(self, request, book):
169 """ Returns details of a book, identified by a slug and lang. """
171 return Book.objects.get(slug=book)
172 except Book.DoesNotExist:
176 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
177 """ Main handler for Book objects.
179 Responsible for lists of Book objects.
181 allowed_methods = ('GET',)
183 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb']
186 def genres(cls, book):
187 """ Returns all media for a book. """
188 return book.tags.filter(category='genre')
191 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
192 recommended=False, newest=False, books=None,
193 after=None, before=None, count=None):
194 """ Lists all books with given tags.
196 :param tags: filtering tags; should be a path of categories
197 and slugs, i.e.: authors/an-author/epoch/an-epoch/
198 :param top_level: if True and a book is included in the results,
199 it's children are aren't. By default all books matching the tags
204 return Book.objects.get(pk=pk)
205 except Book.DoesNotExist:
209 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
213 top_level = top_level or newest
215 if 'after' in request.GET:
216 after = request.GET['after']
217 if 'before' in request.GET:
218 before = request.GET['before']
219 if 'count' in request.GET:
220 count = request.GET['count']
224 books = Book.tagged_top_level(tags)
225 return books if books else rc.NOT_FOUND
227 books = Book.tagged.with_all(tags)
229 books = books if books is not None else Book.objects.all()
230 books = books.order_by('slug')
233 books = books.filter(parent=None)
235 books = books.filter(media__type='mp3').distinct()
237 books = books.filter(media__type='daisy').distinct()
239 books = books.filter(recommended=True)
241 books = books.order_by('-created_at')
244 books = books.filter(slug__gt=after)
246 books = books.filter(slug__lt=before)
248 books = books.only('slug', 'title', 'cover', 'cover_thumb')
249 for category in book_tag_categories:
250 books = prefetch_relations(books, category)
254 books = list(reversed(books.order_by('-slug')[:count]))
256 books = books[:count]
260 def create(self, request, *args, **kwargs):
264 class BooksHandler(BookDetailHandler):
265 allowed_methods = ('GET', 'POST')
267 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
268 anonymous = AnonymousBooksHandler
270 def create(self, request, *args, **kwargs):
271 if not request.user.has_perm('catalogue.add_book'):
274 data = json.loads(request.POST.get('data'))
275 form = BookImportForm(data)
283 class EBooksHandler(AnonymousBooksHandler):
284 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
287 class BookProxy(models.Model):
288 def __init__(self, book, key):
292 def __getattr__(self, item):
293 if item not in ('book', 'key'):
294 return self.book.__getattribute__(item)
296 return self.__getattribute__(item)
299 class QuerySetProxy(models.QuerySet):
300 def __init__(self, l):
304 return iter(self.list)
307 class FilterBooksHandler(AnonymousBooksHandler):
308 fields = book_tag_categories + [
309 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
311 def read(self, request):
313 search_string = request.GET.get('search')
314 is_lektura = request.GET.get('lektura')
315 is_audiobook = request.GET.get('audiobook')
317 after = request.GET.get('after')
318 count = int(request.GET.get('count', 50))
319 if is_lektura in ('true', 'false'):
320 is_lektura = is_lektura == 'true'
323 if is_audiobook in ('true', 'false'):
324 is_audiobook = is_audiobook == 'true'
325 books = Book.objects.distinct().order_by('slug')
326 if is_lektura is not None:
327 books = books.filter(has_audience=is_lektura)
328 if is_audiobook is not None:
330 books = books.filter(media__type='mp3')
332 books = books.exclude(media__type='mp3')
333 for key in request.GET:
334 if key in category_singular:
335 category = category_singular[key]
336 if category in book_tag_categories:
337 slugs = request.GET[key].split(',')
338 tags = Tag.objects.filter(category=category, slug__in=slugs)
339 books = Book.tagged.with_any(tags, books)
340 if (search_string is not None) and len(search_string) < 3:
343 search_string = re_escape(search_string)
344 books_author = books.filter(cached_author__iregex='\m' + search_string)
345 books_title = books.filter(title__iregex='\m' + search_string)
346 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
347 if after and (key_sep in after):
348 which, slug = after.split(key_sep, 1)
350 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
351 else: # which == 'author'
352 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
354 book_lists = [(books_author, 'author'), (books_title, 'title')]
356 if after and key_sep in after:
357 which, slug = after.split(key_sep, 1)
358 books = books.filter(slug__gt=slug)
359 book_lists = [(books, 'book')]
362 for book_list, label in book_lists:
363 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
364 for category in book_tag_categories:
365 book_list = prefetch_relations(book_list, category)
366 remaining_count = count - len(filtered_books)
367 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
368 for book in book_list[:remaining_count]]
369 filtered_books += new_books
370 if len(filtered_books) == count:
373 return QuerySetProxy(filtered_books)
376 # add categorized tags fields for Book
377 def _tags_getter(category):
379 def get_tags(cls, book):
380 return book.tags.filter(category=category)
384 def _tag_getter(category):
386 def get_tag(cls, book):
387 return book.tag_unicode(category)
391 def add_tag_getters():
392 for plural, singular in category_singular.items():
393 setattr(BookDetails, plural, _tags_getter(singular))
394 setattr(BookDetails, singular, _tag_getter(singular))
399 # add fields for files in Book
400 def _file_getter(book_format):
401 field = "%s_file" % book_format
404 def get_file(cls, book):
405 f = getattr(book, field)
407 return MEDIA_BASE + f.url
413 def add_file_getters():
414 for book_format in Book.formats:
415 setattr(BookDetails, book_format, _file_getter(book_format))
420 class CollectionDetails(object):
421 """Custom Collection fields."""
424 def href(cls, collection):
425 """ Returns URI in the API for the collection. """
427 return API_BASE + reverse("api_collection", args=[collection.slug])
430 def url(cls, collection):
431 """ Returns URL on the site. """
433 return WL_BASE + collection.get_absolute_url()
436 def books(cls, collection):
437 return Book.objects.filter(collection.get_query())
440 class CollectionDetailHandler(BaseHandler, CollectionDetails):
441 allowed_methods = ('GET',)
442 fields = ['url', 'title', 'description', 'books']
445 def read(self, request, slug):
446 """ Returns details of a collection, identified by slug. """
448 return Collection.objects.get(slug=slug)
449 except Collection.DoesNotExist:
453 class CollectionsHandler(BaseHandler, CollectionDetails):
454 allowed_methods = ('GET',)
456 fields = ['url', 'href', 'title']
459 def read(self, request):
460 """ Returns all collections. """
461 return Collection.objects.all()
464 class TagDetails(object):
465 """Custom Tag fields."""
469 """ Returns URI in the API for the tag. """
471 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
475 """ Returns URL on the site. """
477 return WL_BASE + tag.get_absolute_url()
480 class TagDetailHandler(BaseHandler, TagDetails):
481 """ Responsible for details of a single Tag object. """
483 fields = ['name', 'url', 'sort_key', 'description']
486 def read(self, request, category, slug):
487 """ Returns details of a tag, identified by category and slug. """
490 category_sng = category_singular[category]
495 return Tag.objects.get(category=category_sng, slug=slug)
496 except Tag.DoesNotExist:
500 class TagsHandler(BaseHandler, TagDetails):
501 """ Main handler for Tag objects.
503 Responsible for lists of Tag objects
504 and fields used for representing Tags.
507 allowed_methods = ('GET',)
509 fields = ['name', 'href', 'url', 'slug']
512 def read(self, request, category=None, pk=None):
513 """ Lists all tags in the category (eg. all themes). """
516 return Tag.objects.exclude(category='set').get(pk=pk)
517 except Book.DoesNotExist:
521 category_sng = category_singular[category]
525 after = request.GET.get('after')
526 before = request.GET.get('before')
527 count = request.GET.get('count')
529 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
531 book_only = request.GET.get('book_only') == 'true'
532 picture_only = request.GET.get('picture_only') == 'true'
534 tags = tags.filter(for_books=True)
536 tags = tags.filter(for_pictures=True)
539 tags = tags.filter(slug__gt=after)
541 tags = tags.filter(slug__lt=before)
545 tags = list(reversed(tags.order_by('-slug')[:count]))
552 class FragmentDetails(object):
553 """Custom Fragment fields."""
556 def href(cls, fragment):
557 """ Returns URI in the API for the fragment. """
559 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
562 def url(cls, fragment):
563 """ Returns URL on the site for the fragment. """
565 return WL_BASE + fragment.get_absolute_url()
568 def themes(cls, fragment):
569 """ Returns a list of theme tags for the fragment. """
571 return fragment.tags.filter(category='theme')
574 class FragmentDetailHandler(BaseHandler, FragmentDetails):
575 fields = ['book', 'anchor', 'text', 'url', 'themes']
578 def read(self, request, book, anchor):
579 """ Returns details of a fragment, identified by book slug and anchor. """
581 return Fragment.objects.get(book__slug=book, anchor=anchor)
582 except Fragment.DoesNotExist:
586 class FragmentsHandler(BaseHandler, FragmentDetails):
587 """ Main handler for Fragments.
589 Responsible for lists of Fragment objects
590 and fields used for representing Fragments.
594 fields = ['book', 'url', 'anchor', 'href']
595 allowed_methods = ('GET',)
597 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
600 def read(self, request, tags):
601 """ Lists all fragments with given book, tags, themes.
603 :param tags: should be a path of categories and slugs, i.e.:
604 books/book-slug/authors/an-author/themes/a-theme/
608 tags, ancestors = read_tags(tags, allowed=self.categories)
611 fragments = Fragment.tagged.with_all(tags).select_related('book')
612 if fragments.exists():
618 class PictureHandler(BaseHandler):
620 fields = ('slug', 'title')
621 allowed_methods = ('POST',)
623 def create(self, request):
624 if not request.user.has_perm('picture.add_picture'):
627 data = json.loads(request.POST.get('data'))
628 form = PictureImportForm(data)