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 if 'after' in request.GET:
214 after = request.GET['after']
215 if 'before' in request.GET:
216 before = request.GET['before']
217 if 'count' in request.GET:
218 count = request.GET['count']
222 books = Book.tagged_top_level(tags)
223 return books if books else rc.NOT_FOUND
225 books = Book.tagged.with_all(tags)
227 books = books if books is not None else Book.objects.all()
228 books = books.order_by('slug')
231 books = books.filter(parent=None)
233 books = books.filter(media__type='mp3').distinct()
235 books = books.filter(media__type='daisy').distinct()
237 books = books.filter(recommended=True)
239 books = books.order_by('-created_at')
242 books = books.filter(slug__gt=after)
244 books = books.filter(slug__lt=before)
246 books = books.only('slug', 'title', 'cover', 'cover_thumb')
247 for category in book_tag_categories:
248 books = prefetch_relations(books, category)
252 books = list(reversed(books.order_by('-slug')[:count]))
254 books = books[:count]
258 def create(self, request, *args, **kwargs):
262 class BooksHandler(BookDetailHandler):
263 allowed_methods = ('GET', 'POST')
265 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
266 anonymous = AnonymousBooksHandler
268 def create(self, request, *args, **kwargs):
269 if not request.user.has_perm('catalogue.add_book'):
272 data = json.loads(request.POST.get('data'))
273 form = BookImportForm(data)
281 class EBooksHandler(AnonymousBooksHandler):
282 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
285 class BookProxy(models.Model):
289 def __init__(self, book, key):
293 def __getattr__(self, item):
294 if item not in ('book', 'key'):
295 return self.book.__getattribute__(item)
297 return self.__getattribute__(item)
300 class QuerySetProxy(models.QuerySet):
301 def __init__(self, l):
305 return iter(self.list)
308 class FilterBooksHandler(AnonymousBooksHandler):
309 fields = book_tag_categories + [
310 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
312 def read(self, request):
314 search_string = request.GET.get('search')
315 is_lektura = request.GET.get('lektura')
316 is_audiobook = request.GET.get('audiobook')
318 after = request.GET.get('after')
319 count = int(request.GET.get('count', 50))
320 if is_lektura in ('true', 'false'):
321 is_lektura = is_lektura == 'true'
324 if is_audiobook in ('true', 'false'):
325 is_audiobook = is_audiobook == 'true'
326 books = Book.objects.distinct().order_by('slug')
327 if is_lektura is not None:
328 books = books.filter(has_audience=is_lektura)
329 if is_audiobook is not None:
331 books = books.filter(media__type='mp3')
333 books = books.exclude(media__type='mp3')
334 for key in request.GET:
335 if key in category_singular:
336 category = category_singular[key]
337 if category in book_tag_categories:
338 slugs = request.GET[key].split(',')
339 tags = Tag.objects.filter(category=category, slug__in=slugs)
340 books = Book.tagged.with_any(tags, books)
341 if (search_string is not None) and len(search_string) < 3:
344 search_string = re_escape(search_string)
345 books_author = books.filter(cached_author__iregex='\m' + search_string)
346 books_title = books.filter(title__iregex='\m' + search_string)
347 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
348 if after and (key_sep in after):
349 which, slug = after.split(key_sep, 1)
351 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
352 else: # which == 'author'
353 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
355 book_lists = [(books_author, 'author'), (books_title, 'title')]
357 if after and key_sep in after:
358 which, slug = after.split(key_sep, 1)
359 books = books.filter(slug__gt=slug)
360 book_lists = [(books, 'book')]
363 for book_list, label in book_lists:
364 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
365 for category in book_tag_categories:
366 book_list = prefetch_relations(book_list, category)
367 remaining_count = count - len(filtered_books)
368 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
369 for book in book_list[:remaining_count]]
370 filtered_books += new_books
371 if len(filtered_books) == count:
374 return QuerySetProxy(filtered_books)
377 # add categorized tags fields for Book
378 def _tags_getter(category):
380 def get_tags(cls, book):
381 return book.tags.filter(category=category)
385 def _tag_getter(category):
387 def get_tag(cls, book):
388 return book.tag_unicode(category)
392 def add_tag_getters():
393 for plural, singular in category_singular.items():
394 setattr(BookDetails, plural, _tags_getter(singular))
395 setattr(BookDetails, singular, _tag_getter(singular))
400 # add fields for files in Book
401 def _file_getter(book_format):
402 field = "%s_file" % book_format
405 def get_file(cls, book):
406 f = getattr(book, field)
408 return MEDIA_BASE + f.url
414 def add_file_getters():
415 for book_format in Book.formats:
416 setattr(BookDetails, book_format, _file_getter(book_format))
421 class CollectionDetails(object):
422 """Custom Collection fields."""
425 def href(cls, collection):
426 """ Returns URI in the API for the collection. """
428 return API_BASE + reverse("api_collection", args=[collection.slug])
431 def url(cls, collection):
432 """ Returns URL on the site. """
434 return WL_BASE + collection.get_absolute_url()
437 def books(cls, collection):
438 return Book.objects.filter(collection.get_query())
441 class CollectionDetailHandler(BaseHandler, CollectionDetails):
442 allowed_methods = ('GET',)
443 fields = ['url', 'title', 'description', 'books']
446 def read(self, request, slug):
447 """ Returns details of a collection, identified by slug. """
449 return Collection.objects.get(slug=slug)
450 except Collection.DoesNotExist:
454 class CollectionsHandler(BaseHandler, CollectionDetails):
455 allowed_methods = ('GET',)
457 fields = ['url', 'href', 'title']
460 def read(self, request):
461 """ Returns all collections. """
462 return Collection.objects.all()
465 class TagDetails(object):
466 """Custom Tag fields."""
470 """ Returns URI in the API for the tag. """
472 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
476 """ Returns URL on the site. """
478 return WL_BASE + tag.get_absolute_url()
481 class TagDetailHandler(BaseHandler, TagDetails):
482 """ Responsible for details of a single Tag object. """
484 fields = ['name', 'url', 'sort_key', 'description']
487 def read(self, request, category, slug):
488 """ Returns details of a tag, identified by category and slug. """
491 category_sng = category_singular[category]
496 return Tag.objects.get(category=category_sng, slug=slug)
497 except Tag.DoesNotExist:
501 class TagsHandler(BaseHandler, TagDetails):
502 """ Main handler for Tag objects.
504 Responsible for lists of Tag objects
505 and fields used for representing Tags.
508 allowed_methods = ('GET',)
510 fields = ['name', 'href', 'url', 'slug']
513 def read(self, request, category=None, pk=None):
514 """ Lists all tags in the category (eg. all themes). """
517 return Tag.objects.exclude(category='set').get(pk=pk)
518 except Book.DoesNotExist:
522 category_sng = category_singular[category]
526 after = request.GET.get('after')
527 before = request.GET.get('before')
528 count = request.GET.get('count')
530 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
532 book_only = request.GET.get('book_only') == 'true'
533 picture_only = request.GET.get('picture_only') == 'true'
535 tags = tags.filter(for_books=True)
537 tags = tags.filter(for_pictures=True)
540 tags = tags.filter(slug__gt=after)
542 tags = tags.filter(slug__lt=before)
546 tags = list(reversed(tags.order_by('-slug')[:count]))
553 class FragmentDetails(object):
554 """Custom Fragment fields."""
557 def href(cls, fragment):
558 """ Returns URI in the API for the fragment. """
560 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
563 def url(cls, fragment):
564 """ Returns URL on the site for the fragment. """
566 return WL_BASE + fragment.get_absolute_url()
569 def themes(cls, fragment):
570 """ Returns a list of theme tags for the fragment. """
572 return fragment.tags.filter(category='theme')
575 class FragmentDetailHandler(BaseHandler, FragmentDetails):
576 fields = ['book', 'anchor', 'text', 'url', 'themes']
579 def read(self, request, book, anchor):
580 """ Returns details of a fragment, identified by book slug and anchor. """
582 return Fragment.objects.get(book__slug=book, anchor=anchor)
583 except Fragment.DoesNotExist:
587 class FragmentsHandler(BaseHandler, FragmentDetails):
588 """ Main handler for Fragments.
590 Responsible for lists of Fragment objects
591 and fields used for representing Fragments.
595 fields = ['book', 'url', 'anchor', 'href']
596 allowed_methods = ('GET',)
598 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
601 def read(self, request, tags):
602 """ Lists all fragments with given book, tags, themes.
604 :param tags: should be a path of categories and slugs, i.e.:
605 books/book-slug/authors/an-author/themes/a-theme/
609 tags, ancestors = read_tags(tags, allowed=self.categories)
612 fragments = Fragment.tagged.with_all(tags).select_related('book')
613 if fragments.exists():
619 class PictureHandler(BaseHandler):
621 fields = ('slug', 'title')
622 allowed_methods = ('POST',)
624 def create(self, request):
625 if not request.user.has_perm('picture.add_picture'):
628 data = json.loads(request.POST.get('data'))
629 form = PictureImportForm(data)