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 cover_source_image(cls, book):
154 url = book.cover_source()
155 return url.rstrip('/') + '/file/'
158 class BookDetailHandler(BaseHandler, BookDetails):
159 """ Main handler for Book objects.
161 Responsible for single Book details.
163 allowed_methods = ['GET']
164 fields = ['title', 'parent', 'children'] + Book.formats + [
165 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'fragment_data'] + [
166 category_plural[c] for c in book_tag_categories]
169 def read(self, request, book):
170 """ Returns details of a book, identified by a slug and lang. """
172 return Book.objects.get(slug=book)
173 except Book.DoesNotExist:
177 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
178 """ Main handler for Book objects.
180 Responsible for lists of Book objects.
182 allowed_methods = ('GET',)
184 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb']
187 def genres(cls, book):
188 """ Returns all media for a book. """
189 return book.tags.filter(category='genre')
192 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
193 recommended=False, newest=False, books=None,
194 after=None, before=None, count=None):
195 """ Lists all books with given tags.
197 :param tags: filtering tags; should be a path of categories
198 and slugs, i.e.: authors/an-author/epoch/an-epoch/
199 :param top_level: if True and a book is included in the results,
200 it's children are aren't. By default all books matching the tags
205 return Book.objects.get(pk=pk)
206 except Book.DoesNotExist:
210 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
214 if 'after' in request.GET:
215 after = request.GET['after']
216 if 'before' in request.GET:
217 before = request.GET['before']
218 if 'count' in request.GET:
219 count = request.GET['count']
223 books = Book.tagged_top_level(tags)
224 return books if books else rc.NOT_FOUND
226 books = Book.tagged.with_all(tags)
228 books = books if books is not None else Book.objects.all()
229 books = books.order_by('slug')
232 books = books.filter(parent=None)
234 books = books.filter(media__type='mp3').distinct()
236 books = books.filter(media__type='daisy').distinct()
238 books = books.filter(recommended=True)
240 books = books.order_by('-created_at')
243 books = books.filter(slug__gt=after)
245 books = books.filter(slug__lt=before)
247 books = books.only('slug', 'title', 'cover', 'cover_thumb')
248 for category in book_tag_categories:
249 books = prefetch_relations(books, category)
253 books = list(reversed(books.order_by('-slug')[:count]))
255 books = books[:count]
259 def create(self, request, *args, **kwargs):
263 class BooksHandler(BookDetailHandler):
264 allowed_methods = ('GET', 'POST')
266 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
267 anonymous = AnonymousBooksHandler
269 def create(self, request, *args, **kwargs):
270 if not request.user.has_perm('catalogue.add_book'):
273 data = json.loads(request.POST.get('data'))
274 form = BookImportForm(data)
282 class EBooksHandler(AnonymousBooksHandler):
283 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
286 class BookProxy(models.Model):
287 def __init__(self, book, key):
291 def __getattr__(self, item):
292 if item not in ('book', 'key'):
293 return self.book.__getattribute__(item)
295 return self.__getattribute__(item)
298 class QuerySetProxy(models.QuerySet):
299 def __init__(self, l):
303 return iter(self.list)
306 class FilterBooksHandler(AnonymousBooksHandler):
307 fields = book_tag_categories + [
308 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key', 'cover_source_image']
310 def read(self, request):
312 search_string = request.GET.get('search')
313 is_lektura = request.GET.get('lektura')
314 is_audiobook = request.GET.get('audiobook')
316 after = request.GET.get('after')
317 count = int(request.GET.get('count', 50))
318 if is_lektura in ('true', 'false'):
319 is_lektura = is_lektura == 'true'
322 if is_audiobook in ('true', 'false'):
323 is_audiobook = is_audiobook == 'true'
324 books = Book.objects.distinct().order_by('slug')
325 if is_lektura is not None:
326 books = books.filter(has_audience=is_lektura)
327 if is_audiobook is not None:
329 books = books.filter(media__type='mp3')
331 books = books.exclude(media__type='mp3')
332 for key in request.GET:
333 if key in category_singular:
334 category = category_singular[key]
335 if category in book_tag_categories:
336 slugs = request.GET[key].split(',')
337 tags = Tag.objects.filter(category=category, slug__in=slugs)
338 books = Book.tagged.with_any(tags, books)
339 if (search_string is not None) and len(search_string) < 3:
342 search_string = re_escape(search_string)
343 books_author = books.filter(cached_author__iregex='\m' + search_string)
344 books_title = books.filter(title__iregex='\m' + search_string)
345 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
346 if after and (key_sep in after):
347 which, slug = after.split(key_sep, 1)
349 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
350 else: # which == 'author'
351 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
353 book_lists = [(books_author, 'author'), (books_title, 'title')]
355 if after and key_sep in after:
356 which, slug = after.split(key_sep, 1)
357 books = books.filter(slug__gt=slug)
358 book_lists = [(books, 'book')]
361 for book_list, label in book_lists:
362 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
363 for category in book_tag_categories:
364 book_list = prefetch_relations(book_list, category)
365 remaining_count = count - len(filtered_books)
366 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
367 for book in book_list[:remaining_count]]
368 filtered_books += new_books
369 if len(filtered_books) == count:
372 return QuerySetProxy(filtered_books)
375 # add categorized tags fields for Book
376 def _tags_getter(category):
378 def get_tags(cls, book):
379 return book.tags.filter(category=category)
383 def _tag_getter(category):
385 def get_tag(cls, book):
386 return book.tag_unicode(category)
390 def add_tag_getters():
391 for plural, singular in category_singular.items():
392 setattr(BookDetails, plural, _tags_getter(singular))
393 setattr(BookDetails, singular, _tag_getter(singular))
398 # add fields for files in Book
399 def _file_getter(book_format):
400 field = "%s_file" % book_format
403 def get_file(cls, book):
404 f = getattr(book, field)
406 return MEDIA_BASE + f.url
412 def add_file_getters():
413 for book_format in Book.formats:
414 setattr(BookDetails, book_format, _file_getter(book_format))
419 class CollectionDetails(object):
420 """Custom Collection fields."""
423 def href(cls, collection):
424 """ Returns URI in the API for the collection. """
426 return API_BASE + reverse("api_collection", args=[collection.slug])
429 def url(cls, collection):
430 """ Returns URL on the site. """
432 return WL_BASE + collection.get_absolute_url()
435 def books(cls, collection):
436 return Book.objects.filter(collection.get_query())
439 class CollectionDetailHandler(BaseHandler, CollectionDetails):
440 allowed_methods = ('GET',)
441 fields = ['url', 'title', 'description', 'books']
444 def read(self, request, slug):
445 """ Returns details of a collection, identified by slug. """
447 return Collection.objects.get(slug=slug)
448 except Collection.DoesNotExist:
452 class CollectionsHandler(BaseHandler, CollectionDetails):
453 allowed_methods = ('GET',)
455 fields = ['url', 'href', 'title']
458 def read(self, request):
459 """ Returns all collections. """
460 return Collection.objects.all()
463 class TagDetails(object):
464 """Custom Tag fields."""
468 """ Returns URI in the API for the tag. """
470 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
474 """ Returns URL on the site. """
476 return WL_BASE + tag.get_absolute_url()
479 class TagDetailHandler(BaseHandler, TagDetails):
480 """ Responsible for details of a single Tag object. """
482 fields = ['name', 'url', 'sort_key', 'description']
485 def read(self, request, category, slug):
486 """ Returns details of a tag, identified by category and slug. """
489 category_sng = category_singular[category]
494 return Tag.objects.get(category=category_sng, slug=slug)
495 except Tag.DoesNotExist:
499 class TagsHandler(BaseHandler, TagDetails):
500 """ Main handler for Tag objects.
502 Responsible for lists of Tag objects
503 and fields used for representing Tags.
506 allowed_methods = ('GET',)
508 fields = ['name', 'href', 'url', 'slug']
511 def read(self, request, category=None, pk=None):
512 """ Lists all tags in the category (eg. all themes). """
515 return Tag.objects.exclude(category='set').get(pk=pk)
516 except Book.DoesNotExist:
520 category_sng = category_singular[category]
524 after = request.GET.get('after')
525 before = request.GET.get('before')
526 count = request.GET.get('count')
528 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
530 book_only = request.GET.get('book_only') == 'true'
531 picture_only = request.GET.get('picture_only') == 'true'
533 tags = tags.filter(for_books=True)
535 tags = tags.filter(for_pictures=True)
538 tags = tags.filter(slug__gt=after)
540 tags = tags.filter(slug__lt=before)
544 tags = list(reversed(tags.order_by('-slug')[:count]))
551 class FragmentDetails(object):
552 """Custom Fragment fields."""
555 def href(cls, fragment):
556 """ Returns URI in the API for the fragment. """
558 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
561 def url(cls, fragment):
562 """ Returns URL on the site for the fragment. """
564 return WL_BASE + fragment.get_absolute_url()
567 def themes(cls, fragment):
568 """ Returns a list of theme tags for the fragment. """
570 return fragment.tags.filter(category='theme')
573 class FragmentDetailHandler(BaseHandler, FragmentDetails):
574 fields = ['book', 'anchor', 'text', 'url', 'themes']
577 def read(self, request, book, anchor):
578 """ Returns details of a fragment, identified by book slug and anchor. """
580 return Fragment.objects.get(book__slug=book, anchor=anchor)
581 except Fragment.DoesNotExist:
585 class FragmentsHandler(BaseHandler, FragmentDetails):
586 """ Main handler for Fragments.
588 Responsible for lists of Fragment objects
589 and fields used for representing Fragments.
593 fields = ['book', 'url', 'anchor', 'href']
594 allowed_methods = ('GET',)
596 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
599 def read(self, request, tags):
600 """ Lists all fragments with given book, tags, themes.
602 :param tags: should be a path of categories and slugs, i.e.:
603 books/book-slug/authors/an-author/themes/a-theme/
607 tags, ancestors = read_tags(tags, allowed=self.categories)
610 fragments = Fragment.tagged.with_all(tags).select_related('book')
611 if fragments.exists():
617 class PictureHandler(BaseHandler):
619 fields = ('slug', 'title')
620 allowed_methods = ('POST',)
622 def create(self, request):
623 if not request.user.has_perm('picture.add_picture'):
626 data = json.loads(request.POST.get('data'))
627 form = PictureImportForm(data)