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 cover_source_image(cls, book):
150 url = book.cover_source()
151 return url.rstrip('/') + '/file/'
154 class BookDetailHandler(BaseHandler, BookDetails):
155 """ Main handler for Book objects.
157 Responsible for single Book details.
159 allowed_methods = ['GET']
160 fields = ['title', 'parent', 'children'] + Book.formats + [
161 'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [
162 category_plural[c] for c in book_tag_categories]
165 def read(self, request, book):
166 """ Returns details of a book, identified by a slug and lang. """
168 return Book.objects.get(slug=book)
169 except Book.DoesNotExist:
173 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
174 """ Main handler for Book objects.
176 Responsible for lists of Book objects.
178 allowed_methods = ('GET',)
180 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
183 def genres(cls, book):
184 """ Returns all media for a book. """
185 return book.tags.filter(category='genre')
188 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
189 recommended=False, newest=False, books=None,
190 after=None, before=None, count=None):
191 """ Lists all books with given tags.
193 :param tags: filtering tags; should be a path of categories
194 and slugs, i.e.: authors/an-author/epoch/an-epoch/
195 :param top_level: if True and a book is included in the results,
196 it's children are aren't. By default all books matching the tags
201 return Book.objects.get(pk=pk)
202 except Book.DoesNotExist:
206 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
210 if 'after' in request.GET:
211 after = request.GET['after']
212 if 'before' in request.GET:
213 before = request.GET['before']
214 if 'count' in request.GET:
215 count = request.GET['count']
219 books = Book.tagged_top_level(tags)
220 return books if books else rc.NOT_FOUND
222 books = Book.tagged.with_all(tags)
224 books = books if books is not None else Book.objects.all()
225 books = books.order_by('slug')
228 books = books.filter(parent=None)
230 books = books.filter(media__type='mp3').distinct()
232 books = books.filter(media__type='daisy').distinct()
234 books = books.filter(recommended=True)
236 books = books.order_by('-created_at')
239 books = books.filter(slug__gt=after)
241 books = books.filter(slug__lt=before)
243 books = books.only('slug', 'title', 'cover', 'cover_thumb')
244 for category in book_tag_categories:
245 books = prefetch_relations(books, category)
249 books = list(reversed(books.order_by('-slug')[:count]))
251 books = books[:count]
255 def create(self, request, *args, **kwargs):
259 class BooksHandler(BookDetailHandler):
260 allowed_methods = ('GET', 'POST')
262 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
263 anonymous = AnonymousBooksHandler
265 def create(self, request, *args, **kwargs):
266 if not request.user.has_perm('catalogue.add_book'):
269 data = json.loads(request.POST.get('data'))
270 form = BookImportForm(data)
278 class EBooksHandler(AnonymousBooksHandler):
279 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
282 class BookProxy(models.Model):
283 def __init__(self, book, key):
287 def __getattr__(self, item):
288 if item not in ('book', 'key'):
289 return self.book.__getattribute__(item)
291 return self.__getattribute__(item)
294 class QuerySetProxy(models.QuerySet):
295 def __init__(self, l):
299 return iter(self.list)
302 class FilterBooksHandler(AnonymousBooksHandler):
303 fields = book_tag_categories + [
304 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'key', 'cover_source_image']
306 def read(self, request):
308 search_string = request.GET.get('search')
309 is_lektura = request.GET.get('lektura')
310 is_audiobook = request.GET.get('audiobook')
312 after = request.GET.get('after')
313 count = int(request.GET.get('count', 50))
314 if is_lektura in ('true', 'false'):
315 is_lektura = is_lektura == 'true'
318 if is_audiobook in ('true', 'false'):
319 is_audiobook = is_audiobook == 'true'
320 books = Book.objects.distinct().order_by('slug')
321 if is_lektura is not None:
322 books = books.filter(has_audience=is_lektura)
323 if is_audiobook is not None:
325 books = books.filter(media__type='mp3')
327 books = books.exclude(media__type='mp3')
328 for key in request.GET:
329 if key in category_singular:
330 category = category_singular[key]
331 if category in book_tag_categories:
332 slugs = request.GET[key].split(',')
333 tags = Tag.objects.filter(category=category, slug__in=slugs)
334 books = Book.tagged.with_any(tags, books)
335 if (search_string is not None) and len(search_string) < 3:
338 search_string = re_escape(search_string)
339 books_author = books.filter(cached_author__iregex='\m' + search_string)
340 books_title = books.filter(title__iregex='\m' + search_string)
341 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
342 if after and (key_sep in after):
343 which, slug = after.split(key_sep, 1)
345 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
346 else: # which == 'author'
347 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
349 book_lists = [(books_author, 'author'), (books_title, 'title')]
351 if after and key_sep in after:
352 which, slug = after.split(key_sep, 1)
353 books = books.filter(slug__gt=slug)
354 book_lists = [(books, 'book')]
357 for book_list, label in book_lists:
358 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
359 for category in book_tag_categories:
360 book_list = prefetch_relations(book_list, category)
361 remaining_count = count - len(filtered_books)
362 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
363 for book in book_list[:remaining_count]]
364 filtered_books += new_books
365 if len(filtered_books) == count:
368 return QuerySetProxy(filtered_books)
371 # add categorized tags fields for Book
372 def _tags_getter(category):
374 def get_tags(cls, book):
375 return book.tags.filter(category=category)
379 def _tag_getter(category):
381 def get_tag(cls, book):
382 return book.tag_unicode(category)
386 def add_tag_getters():
387 for plural, singular in category_singular.items():
388 setattr(BookDetails, plural, _tags_getter(singular))
389 setattr(BookDetails, singular, _tag_getter(singular))
394 # add fields for files in Book
395 def _file_getter(book_format):
396 field = "%s_file" % book_format
399 def get_file(cls, book):
400 f = getattr(book, field)
402 return MEDIA_BASE + f.url
408 def add_file_getters():
409 for book_format in Book.formats:
410 setattr(BookDetails, book_format, _file_getter(book_format))
415 class CollectionDetails(object):
416 """Custom Collection fields."""
419 def href(cls, collection):
420 """ Returns URI in the API for the collection. """
422 return API_BASE + reverse("api_collection", args=[collection.slug])
425 def url(cls, collection):
426 """ Returns URL on the site. """
428 return WL_BASE + collection.get_absolute_url()
431 def books(cls, collection):
432 return Book.objects.filter(collection.get_query())
435 class CollectionDetailHandler(BaseHandler, CollectionDetails):
436 allowed_methods = ('GET',)
437 fields = ['url', 'title', 'description', 'books']
440 def read(self, request, slug):
441 """ Returns details of a collection, identified by slug. """
443 return Collection.objects.get(slug=slug)
444 except Collection.DoesNotExist:
448 class CollectionsHandler(BaseHandler, CollectionDetails):
449 allowed_methods = ('GET',)
451 fields = ['url', 'href', 'title']
454 def read(self, request):
455 """ Returns all collections. """
456 return Collection.objects.all()
459 class TagDetails(object):
460 """Custom Tag fields."""
464 """ Returns URI in the API for the tag. """
466 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
470 """ Returns URL on the site. """
472 return WL_BASE + tag.get_absolute_url()
475 class TagDetailHandler(BaseHandler, TagDetails):
476 """ Responsible for details of a single Tag object. """
478 fields = ['name', 'url', 'sort_key', 'description']
481 def read(self, request, category, slug):
482 """ Returns details of a tag, identified by category and slug. """
485 category_sng = category_singular[category]
490 return Tag.objects.get(category=category_sng, slug=slug)
491 except Tag.DoesNotExist:
495 class TagsHandler(BaseHandler, TagDetails):
496 """ Main handler for Tag objects.
498 Responsible for lists of Tag objects
499 and fields used for representing Tags.
502 allowed_methods = ('GET',)
504 fields = ['name', 'href', 'url', 'slug']
507 def read(self, request, category=None, pk=None):
508 """ Lists all tags in the category (eg. all themes). """
511 return Tag.objects.exclude(category='set').get(pk=pk)
512 except Book.DoesNotExist:
516 category_sng = category_singular[category]
520 after = request.GET.get('after')
521 before = request.GET.get('before')
522 count = request.GET.get('count')
524 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
527 tags = tags.filter(slug__gt=after)
529 tags = tags.filter(slug__lt=before)
533 tags = list(reversed(tags.order_by('-slug')[:count]))
540 class FragmentDetails(object):
541 """Custom Fragment fields."""
544 def href(cls, fragment):
545 """ Returns URI in the API for the fragment. """
547 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
550 def url(cls, fragment):
551 """ Returns URL on the site for the fragment. """
553 return WL_BASE + fragment.get_absolute_url()
556 def themes(cls, fragment):
557 """ Returns a list of theme tags for the fragment. """
559 return fragment.tags.filter(category='theme')
562 class FragmentDetailHandler(BaseHandler, FragmentDetails):
563 fields = ['book', 'anchor', 'text', 'url', 'themes']
566 def read(self, request, book, anchor):
567 """ Returns details of a fragment, identified by book slug and anchor. """
569 return Fragment.objects.get(book__slug=book, anchor=anchor)
570 except Fragment.DoesNotExist:
574 class FragmentsHandler(BaseHandler, FragmentDetails):
575 """ Main handler for Fragments.
577 Responsible for lists of Fragment objects
578 and fields used for representing Fragments.
582 fields = ['book', 'url', 'anchor', 'href']
583 allowed_methods = ('GET',)
585 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
588 def read(self, request, tags):
589 """ Lists all fragments with given book, tags, themes.
591 :param tags: should be a path of categories and slugs, i.e.:
592 books/book-slug/authors/an-author/themes/a-theme/
596 tags, ancestors = read_tags(tags, allowed=self.categories)
599 fragments = Fragment.tagged.with_all(tags).select_related('book')
600 if fragments.exists():
606 class PictureHandler(BaseHandler):
608 fields = ('slug', 'title')
609 allowed_methods = ('POST',)
611 def create(self, request):
612 if not request.user.has_perm('picture.add_picture'):
615 data = json.loads(request.POST.get('data'))
616 form = PictureImportForm(data)