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):
286 def __init__(self, book, key):
290 def __getattr__(self, item):
291 if item not in ('book', 'key'):
292 return self.book.__getattribute__(item)
294 return self.__getattribute__(item)
297 class QuerySetProxy(models.QuerySet):
298 def __init__(self, l):
302 return iter(self.list)
305 class FilterBooksHandler(AnonymousBooksHandler):
306 fields = book_tag_categories + [
307 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
309 def read(self, request):
311 search_string = request.GET.get('search')
312 is_lektura = request.GET.get('lektura')
313 is_audiobook = request.GET.get('audiobook')
315 after = request.GET.get('after')
316 count = int(request.GET.get('count', 50))
317 if is_lektura in ('true', 'false'):
318 is_lektura = is_lektura == 'true'
321 if is_audiobook in ('true', 'false'):
322 is_audiobook = is_audiobook == 'true'
323 books = Book.objects.distinct().order_by('slug')
324 if is_lektura is not None:
325 books = books.filter(has_audience=is_lektura)
326 if is_audiobook is not None:
328 books = books.filter(media__type='mp3')
330 books = books.exclude(media__type='mp3')
331 for key in request.GET:
332 if key in category_singular:
333 category = category_singular[key]
334 if category in book_tag_categories:
335 slugs = request.GET[key].split(',')
336 tags = Tag.objects.filter(category=category, slug__in=slugs)
337 books = Book.tagged.with_any(tags, books)
338 if (search_string is not None) and len(search_string) < 3:
341 search_string = re_escape(search_string)
342 books_author = books.filter(cached_author__iregex='\m' + search_string)
343 books_title = books.filter(title__iregex='\m' + search_string)
344 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
345 if after and (key_sep in after):
346 which, slug = after.split(key_sep, 1)
348 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
349 else: # which == 'author'
350 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
352 book_lists = [(books_author, 'author'), (books_title, 'title')]
354 if after and key_sep in after:
355 which, slug = after.split(key_sep, 1)
356 books = books.filter(slug__gt=slug)
357 book_lists = [(books, 'book')]
360 for book_list, label in book_lists:
361 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
362 for category in book_tag_categories:
363 book_list = prefetch_relations(book_list, category)
364 remaining_count = count - len(filtered_books)
365 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
366 for book in book_list[:remaining_count]]
367 filtered_books += new_books
368 if len(filtered_books) == count:
371 return QuerySetProxy(filtered_books)
374 # add categorized tags fields for Book
375 def _tags_getter(category):
377 def get_tags(cls, book):
378 return book.tags.filter(category=category)
382 def _tag_getter(category):
384 def get_tag(cls, book):
385 return book.tag_unicode(category)
389 def add_tag_getters():
390 for plural, singular in category_singular.items():
391 setattr(BookDetails, plural, _tags_getter(singular))
392 setattr(BookDetails, singular, _tag_getter(singular))
397 # add fields for files in Book
398 def _file_getter(book_format):
399 field = "%s_file" % book_format
402 def get_file(cls, book):
403 f = getattr(book, field)
405 return MEDIA_BASE + f.url
411 def add_file_getters():
412 for book_format in Book.formats:
413 setattr(BookDetails, book_format, _file_getter(book_format))
418 class CollectionDetails(object):
419 """Custom Collection fields."""
422 def href(cls, collection):
423 """ Returns URI in the API for the collection. """
425 return API_BASE + reverse("api_collection", args=[collection.slug])
428 def url(cls, collection):
429 """ Returns URL on the site. """
431 return WL_BASE + collection.get_absolute_url()
434 def books(cls, collection):
435 return Book.objects.filter(collection.get_query())
438 class CollectionDetailHandler(BaseHandler, CollectionDetails):
439 allowed_methods = ('GET',)
440 fields = ['url', 'title', 'description', 'books']
443 def read(self, request, slug):
444 """ Returns details of a collection, identified by slug. """
446 return Collection.objects.get(slug=slug)
447 except Collection.DoesNotExist:
451 class CollectionsHandler(BaseHandler, CollectionDetails):
452 allowed_methods = ('GET',)
454 fields = ['url', 'href', 'title']
457 def read(self, request):
458 """ Returns all collections. """
459 return Collection.objects.all()
462 class TagDetails(object):
463 """Custom Tag fields."""
467 """ Returns URI in the API for the tag. """
469 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
473 """ Returns URL on the site. """
475 return WL_BASE + tag.get_absolute_url()
478 class TagDetailHandler(BaseHandler, TagDetails):
479 """ Responsible for details of a single Tag object. """
481 fields = ['name', 'url', 'sort_key', 'description']
484 def read(self, request, category, slug):
485 """ Returns details of a tag, identified by category and slug. """
488 category_sng = category_singular[category]
493 return Tag.objects.get(category=category_sng, slug=slug)
494 except Tag.DoesNotExist:
498 class TagsHandler(BaseHandler, TagDetails):
499 """ Main handler for Tag objects.
501 Responsible for lists of Tag objects
502 and fields used for representing Tags.
505 allowed_methods = ('GET',)
507 fields = ['name', 'href', 'url', 'slug']
510 def read(self, request, category=None, pk=None):
511 """ Lists all tags in the category (eg. all themes). """
514 return Tag.objects.exclude(category='set').get(pk=pk)
515 except Book.DoesNotExist:
519 category_sng = category_singular[category]
523 after = request.GET.get('after')
524 before = request.GET.get('before')
525 count = request.GET.get('count')
527 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
529 book_only = request.GET.get('book_only') == 'true'
530 picture_only = request.GET.get('picture_only') == 'true'
532 tags = tags.filter(for_books=True)
534 tags = tags.filter(for_pictures=True)
537 tags = tags.filter(slug__gt=after)
539 tags = tags.filter(slug__lt=before)
543 tags = list(reversed(tags.order_by('-slug')[:count]))
550 class FragmentDetails(object):
551 """Custom Fragment fields."""
554 def href(cls, fragment):
555 """ Returns URI in the API for the fragment. """
557 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
560 def url(cls, fragment):
561 """ Returns URL on the site for the fragment. """
563 return WL_BASE + fragment.get_absolute_url()
566 def themes(cls, fragment):
567 """ Returns a list of theme tags for the fragment. """
569 return fragment.tags.filter(category='theme')
572 class FragmentDetailHandler(BaseHandler, FragmentDetails):
573 fields = ['book', 'anchor', 'text', 'url', 'themes']
576 def read(self, request, book, anchor):
577 """ Returns details of a fragment, identified by book slug and anchor. """
579 return Fragment.objects.get(book__slug=book, anchor=anchor)
580 except Fragment.DoesNotExist:
584 class FragmentsHandler(BaseHandler, FragmentDetails):
585 """ Main handler for Fragments.
587 Responsible for lists of Fragment objects
588 and fields used for representing Fragments.
592 fields = ['book', 'url', 'anchor', 'href']
593 allowed_methods = ('GET',)
595 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
598 def read(self, request, tags):
599 """ Lists all fragments with given book, tags, themes.
601 :param tags: should be a path of categories and slugs, i.e.:
602 books/book-slug/authors/an-author/themes/a-theme/
606 tags, ancestors = read_tags(tags, allowed=self.categories)
609 fragments = Fragment.tagged.with_all(tags).select_related('book')
610 if fragments.exists():
616 class PictureHandler(BaseHandler):
618 fields = ('slug', 'title')
619 allowed_methods = ('POST',)
621 def create(self, request):
622 if not request.user.has_perm('picture.add_picture'):
625 data = json.loads(request.POST.get('data'))
626 form = PictureImportForm(data)