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 piston.handler import AnonymousBaseHandler, BaseHandler
11 from piston.utils import rc
12 from sorl.thumbnail import default
14 from catalogue.forms import BookImportForm
15 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
16 from catalogue.models.tag import prefetch_relations
17 from picture.models import Picture
18 from picture.forms import PictureImportForm
20 from stats.utils import piwik_track
22 from . import emitters # Register our emitters
24 API_BASE = WL_BASE = MEDIA_BASE = lazy(
25 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
37 for k, v in category_singular.items():
38 category_plural[v] = k
40 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
43 def read_tags(tags, request, allowed):
44 """ Reads a path of filtering tags.
46 :param str tags: a path of category and slug pairs, like: authors/an-author/...
47 :returns: list of Tag objects
48 :raises: ValueError when tags can't be found
51 def process(category, slug):
52 if category == 'book':
54 books.append(Book.objects.get(slug=slug))
55 except Book.DoesNotExist:
56 raise ValueError('Unknown book.')
58 real_tags.append(Tag.objects.get(category=category, slug=slug))
59 except Tag.DoesNotExist:
60 raise ValueError('Tag not found')
65 tags = tags.strip('/').split('/')
69 category = tags.pop(0)
73 category = category_singular[category]
75 raise ValueError('Unknown category.')
77 if category not in allowed:
78 raise ValueError('Category not allowed.')
79 process(category, slug)
81 for key in request.GET:
82 if key in category_singular:
83 category = category_singular[key]
84 if category in allowed:
85 for slug in request.GET.getlist(key):
86 process(category, slug)
87 return real_tags, books
93 class BookMediaHandler(BaseHandler):
94 """ Responsible for representing media in Books. """
97 fields = ['name', 'type', 'url', 'artist', 'director']
101 """ Link to media on site. """
103 return MEDIA_BASE + media.file.url
106 def artist(cls, media):
107 return media.extra_info.get('artist_name', '')
110 def director(cls, media):
111 return media.extra_info.get('director_name', '')
114 class BookDetails(object):
115 """Custom fields used for representing Books."""
119 """ Returns an URI for a Book in the API. """
120 return API_BASE + reverse("api_book", args=[book.slug])
124 """ Returns Book's URL on the site. """
125 return WL_BASE + book.get_absolute_url()
128 def children(cls, book):
129 """ Returns all children for a book. """
130 return book.children.order_by('parent_number', 'sort_key')
133 def media(cls, book):
134 """ Returns all media for a book. """
135 return book.media.all()
138 def cover(cls, book):
139 return MEDIA_BASE + book.cover.url if book.cover else ''
142 def cover_thumb(cls, book):
143 return MEDIA_BASE + default.backend.get_thumbnail(
144 book.cover, "139x193").url if book.cover else ''
147 def cover_source_image(cls, book):
148 url = book.cover_source()
149 return url.rstrip('/') + '/file/'
152 class BookDetailHandler(BaseHandler, BookDetails):
153 """ Main handler for Book objects.
155 Responsible for single Book details.
157 allowed_methods = ['GET']
158 fields = ['title', 'parent', 'children'] + Book.formats + [
159 'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [
160 category_plural[c] for c in book_tag_categories]
163 def read(self, request, book):
164 """ Returns details of a book, identified by a slug and lang. """
166 return Book.objects.get(slug=book)
167 except Book.DoesNotExist:
171 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
172 """ Main handler for Book objects.
174 Responsible for lists of Book objects.
176 allowed_methods = ('GET',)
178 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
181 def genres(cls, book):
182 """ Returns all media for a book. """
183 return book.tags.filter(category='genre')
186 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
187 recommended=False, newest=False, books=None,
188 after=None, before=None, count=None):
189 """ Lists all books with given tags.
191 :param tags: filtering tags; should be a path of categories
192 and slugs, i.e.: authors/an-author/epoch/an-epoch/
193 :param top_level: if True and a book is included in the results,
194 it's children are aren't. By default all books matching the tags
199 return Book.objects.get(pk=pk)
200 except Book.DoesNotExist:
204 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
208 if 'after' in request.GET:
209 after = request.GET['after']
210 if 'before' in request.GET:
211 before = request.GET['before']
212 if 'count' in request.GET:
213 count = request.GET['count']
217 books = Book.tagged_top_level(tags)
218 return books if books else rc.NOT_FOUND
220 books = Book.tagged.with_all(tags)
222 books = books if books is not None else Book.objects.all()
223 books = books.order_by('slug')
226 books = books.filter(parent=None)
228 books = books.filter(media__type='mp3').distinct()
230 books = books.filter(media__type='daisy').distinct()
232 books = books.filter(recommended=True)
234 books = books.order_by('-created_at')
237 books = books.filter(slug__gt=after)
239 books = books.filter(slug__lt=before)
241 books = books.only('slug', 'title', 'cover', 'cover_thumb')
242 for category in book_tag_categories:
243 books = prefetch_relations(books, category)
247 books = list(reversed(books.order_by('-slug')[:count]))
249 books = books[:count]
253 def create(self, request, *args, **kwargs):
257 class BooksHandler(BookDetailHandler):
258 allowed_methods = ('GET', 'POST')
260 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
261 anonymous = AnonymousBooksHandler
263 def create(self, request, *args, **kwargs):
264 if not request.user.has_perm('catalogue.add_book'):
267 data = json.loads(request.POST.get('data'))
268 form = BookImportForm(data)
276 class EBooksHandler(AnonymousBooksHandler):
277 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
280 class FilterBooksHandler(AnonymousBooksHandler):
281 fields = book_tag_categories + [
282 'href', 'title', 'url', 'cover', 'cover_thumb', 'key', 'cover_source_image']
284 def read(self, request):
286 search_string = request.GET.get('search')
287 is_lektura = request.GET.get('lektura')
288 is_audiobook = request.GET.get('audiobook')
290 after = request.GET.get('after')
291 count = request.GET.get('count', 50)
292 if is_lektura in ('true', 'false'):
293 is_lektura = is_lektura == 'true'
296 if is_audiobook in ('true', 'false'):
297 is_audiobook = is_audiobook == 'true'
298 books = Book.objects.distinct().order_by('slug')
299 if is_lektura is not None:
300 books = books.filter(has_audience=is_lektura)
301 if is_audiobook is not None:
303 books = books.filter(media__type='mp3')
305 books = books.exclude(media__type='mp3')
306 for key in request.GET:
307 if key in category_singular:
308 category = category_singular[key]
309 if category in book_tag_categories:
310 slugs = request.GET[key].split(',')
311 tags = Tag.objects.filter(category=category, slug__in=slugs)
312 books = Book.tagged.with_any(tags, books)
313 if (search_string is not None) and len(search_string) < 3:
316 books_author = books.filter(cached_author__iregex='\m' + search_string)
317 books_title = books.filter(title__iregex='\m' + search_string)
318 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
319 if after and (key_sep in after):
320 which, slug = after.split(key_sep, 1)
322 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
323 else: # which == 'author'
324 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
326 book_lists = [(books_author, 'author'), (books_title, 'title')]
328 if after and key_sep in after:
329 which, slug = after.split(key_sep, 1)
330 books = books.filter(slug__gt=slug)
331 book_lists = [(books, 'book')]
334 for book_list, label in book_lists:
335 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
336 for category in book_tag_categories:
337 book_list = prefetch_relations(book_list, category)
338 remaining_count = count - len(filtered_books)
339 new_books = list(book_list[:remaining_count])
340 for book in new_books:
341 book.key = '%s%s%s' % (label, key_sep, book.slug)
342 filtered_books += new_books
343 if len(filtered_books) == count:
346 return filtered_books
349 # add categorized tags fields for Book
350 def _tags_getter(category):
352 def get_tags(cls, book):
353 return book.tags.filter(category=category)
357 def _tag_getter(category):
359 def get_tag(cls, book):
360 return book.tag_unicode(category)
364 def add_tag_getters():
365 for plural, singular in category_singular.items():
366 setattr(BookDetails, plural, _tags_getter(singular))
367 setattr(BookDetails, singular, _tag_getter(singular))
372 # add fields for files in Book
373 def _file_getter(book_format):
374 field = "%s_file" % book_format
377 def get_file(cls, book):
378 f = getattr(book, field)
380 return MEDIA_BASE + f.url
386 def add_file_getters():
387 for book_format in Book.formats:
388 setattr(BookDetails, book_format, _file_getter(book_format))
393 class CollectionDetails(object):
394 """Custom Collection fields."""
397 def href(cls, collection):
398 """ Returns URI in the API for the collection. """
400 return API_BASE + reverse("api_collection", args=[collection.slug])
403 def url(cls, collection):
404 """ Returns URL on the site. """
406 return WL_BASE + collection.get_absolute_url()
409 def books(cls, collection):
410 return Book.objects.filter(collection.get_query())
413 class CollectionDetailHandler(BaseHandler, CollectionDetails):
414 allowed_methods = ('GET',)
415 fields = ['url', 'title', 'description', 'books']
418 def read(self, request, slug):
419 """ Returns details of a collection, identified by slug. """
421 return Collection.objects.get(slug=slug)
422 except Collection.DoesNotExist:
426 class CollectionsHandler(BaseHandler, CollectionDetails):
427 allowed_methods = ('GET',)
429 fields = ['url', 'href', 'title']
432 def read(self, request):
433 """ Returns all collections. """
434 return Collection.objects.all()
437 class TagDetails(object):
438 """Custom Tag fields."""
442 """ Returns URI in the API for the tag. """
444 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
448 """ Returns URL on the site. """
450 return WL_BASE + tag.get_absolute_url()
453 class TagDetailHandler(BaseHandler, TagDetails):
454 """ Responsible for details of a single Tag object. """
456 fields = ['name', 'url', 'sort_key', 'description']
459 def read(self, request, category, slug):
460 """ Returns details of a tag, identified by category and slug. """
463 category_sng = category_singular[category]
468 return Tag.objects.get(category=category_sng, slug=slug)
469 except Tag.DoesNotExist:
473 class TagsHandler(BaseHandler, TagDetails):
474 """ Main handler for Tag objects.
476 Responsible for lists of Tag objects
477 and fields used for representing Tags.
480 allowed_methods = ('GET',)
482 fields = ['name', 'href', 'url', 'slug']
485 def read(self, request, category=None, pk=None):
486 """ Lists all tags in the category (eg. all themes). """
489 return Tag.objects.exclude(category='set').get(pk=pk)
490 except Book.DoesNotExist:
494 category_sng = category_singular[category]
498 after = request.GET.get('after')
499 before = request.GET.get('before')
500 count = request.GET.get('count')
502 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
505 tags = tags.filter(slug__gt=after)
507 tags = tags.filter(slug__lt=before)
511 tags = list(reversed(tags.order_by('-slug')[:count]))
518 class FragmentDetails(object):
519 """Custom Fragment fields."""
522 def href(cls, fragment):
523 """ Returns URI in the API for the fragment. """
525 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
528 def url(cls, fragment):
529 """ Returns URL on the site for the fragment. """
531 return WL_BASE + fragment.get_absolute_url()
534 def themes(cls, fragment):
535 """ Returns a list of theme tags for the fragment. """
537 return fragment.tags.filter(category='theme')
540 class FragmentDetailHandler(BaseHandler, FragmentDetails):
541 fields = ['book', 'anchor', 'text', 'url', 'themes']
544 def read(self, request, book, anchor):
545 """ Returns details of a fragment, identified by book slug and anchor. """
547 return Fragment.objects.get(book__slug=book, anchor=anchor)
548 except Fragment.DoesNotExist:
552 class FragmentsHandler(BaseHandler, FragmentDetails):
553 """ Main handler for Fragments.
555 Responsible for lists of Fragment objects
556 and fields used for representing Fragments.
560 fields = ['book', 'url', 'anchor', 'href']
561 allowed_methods = ('GET',)
563 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
566 def read(self, request, tags):
567 """ Lists all fragments with given book, tags, themes.
569 :param tags: should be a path of categories and slugs, i.e.:
570 books/book-slug/authors/an-author/themes/a-theme/
574 tags, ancestors = read_tags(tags, allowed=self.categories)
577 fragments = Fragment.tagged.with_all(tags).select_related('book')
578 if fragments.exists():
584 class PictureHandler(BaseHandler):
586 fields = ('slug', 'title')
587 allowed_methods = ('POST',)
589 def create(self, request):
590 if not request.user.has_perm('picture.add_picture'):
593 data = json.loads(request.POST.get('data'))
594 form = PictureImportForm(data)