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
23 from . import emitters # Register our emitters
25 API_BASE = WL_BASE = MEDIA_BASE = lazy(
26 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
38 for k, v in category_singular.items():
39 category_plural[v] = k
41 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
44 def read_tags(tags, request, allowed):
45 """ Reads a path of filtering tags.
47 :param str tags: a path of category and slug pairs, like: authors/an-author/...
48 :returns: list of Tag objects
49 :raises: ValueError when tags can't be found
52 def process(category, slug):
53 if category == 'book':
55 books.append(Book.objects.get(slug=slug))
56 except Book.DoesNotExist:
57 raise ValueError('Unknown book.')
59 real_tags.append(Tag.objects.get(category=category, slug=slug))
60 except Tag.DoesNotExist:
61 raise ValueError('Tag not found')
66 tags = tags.strip('/').split('/')
70 category = tags.pop(0)
74 category = category_singular[category]
76 raise ValueError('Unknown category.')
78 if category not in allowed:
79 raise ValueError('Category not allowed.')
80 process(category, slug)
82 for key in request.GET:
83 if key in category_singular:
84 category = category_singular[key]
85 if category in allowed:
86 for slug in request.GET.getlist(key):
87 process(category, slug)
88 return real_tags, books
94 class BookMediaHandler(BaseHandler):
95 """ Responsible for representing media in Books. """
98 fields = ['name', 'type', 'url', 'artist', 'director']
102 """ Link to media on site. """
104 return MEDIA_BASE + media.file.url
107 def artist(cls, media):
108 return media.extra_info.get('artist_name', '')
111 def director(cls, media):
112 return media.extra_info.get('director_name', '')
115 class BookDetails(object):
116 """Custom fields used for representing Books."""
120 """ Returns an URI for a Book in the API. """
121 return API_BASE + reverse("api_book", args=[book.slug])
125 """ Returns Book's URL on the site. """
126 return WL_BASE + book.get_absolute_url()
129 def children(cls, book):
130 """ Returns all children for a book. """
131 return book.children.order_by('parent_number', 'sort_key')
134 def media(cls, book):
135 """ Returns all media for a book. """
136 return book.media.all()
139 def cover(cls, book):
140 return MEDIA_BASE + book.cover.url if book.cover else ''
143 def cover_thumb(cls, book):
144 return MEDIA_BASE + default.backend.get_thumbnail(
145 book.cover, "139x193").url if book.cover else ''
148 def cover_source_image(cls, book):
149 url = book.cover_source()
150 return url.rstrip('/') + '/file/'
153 class BookDetailHandler(BaseHandler, BookDetails):
154 """ Main handler for Book objects.
156 Responsible for single Book details.
158 allowed_methods = ['GET']
159 fields = ['title', 'parent', 'children'] + Book.formats + [
160 'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [
161 category_plural[c] for c in book_tag_categories]
164 def read(self, request, book):
165 """ Returns details of a book, identified by a slug and lang. """
167 return Book.objects.get(slug=book)
168 except Book.DoesNotExist:
172 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
173 """ Main handler for Book objects.
175 Responsible for lists of Book objects.
177 allowed_methods = ('GET',)
179 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
182 def genres(cls, book):
183 """ Returns all media for a book. """
184 return book.tags.filter(category='genre')
187 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
188 recommended=False, newest=False, books=None,
189 after=None, before=None, count=None):
190 """ Lists all books with given tags.
192 :param tags: filtering tags; should be a path of categories
193 and slugs, i.e.: authors/an-author/epoch/an-epoch/
194 :param top_level: if True and a book is included in the results,
195 it's children are aren't. By default all books matching the tags
200 return Book.objects.get(pk=pk)
201 except Book.DoesNotExist:
205 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
209 if 'after' in request.GET:
210 after = request.GET['after']
211 if 'before' in request.GET:
212 before = request.GET['before']
213 if 'count' in request.GET:
214 count = request.GET['count']
218 books = Book.tagged_top_level(tags)
219 return books if books else rc.NOT_FOUND
221 books = Book.tagged.with_all(tags)
223 books = books if books is not None else Book.objects.all()
224 books = books.order_by('slug')
227 books = books.filter(parent=None)
229 books = books.filter(media__type='mp3').distinct()
231 books = books.filter(media__type='daisy').distinct()
233 books = books.filter(recommended=True)
235 books = books.order_by('-created_at')
238 books = books.filter(slug__gt=after)
240 books = books.filter(slug__lt=before)
242 books = books.only('slug', 'title', 'cover', 'cover_thumb')
243 for category in book_tag_categories:
244 books = prefetch_relations(books, category)
248 books = list(reversed(books.order_by('-slug')[:count]))
250 books = books[:count]
254 def create(self, request, *args, **kwargs):
258 class BooksHandler(BookDetailHandler):
259 allowed_methods = ('GET', 'POST')
261 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
262 anonymous = AnonymousBooksHandler
264 def create(self, request, *args, **kwargs):
265 if not request.user.has_perm('catalogue.add_book'):
268 data = json.loads(request.POST.get('data'))
269 form = BookImportForm(data)
277 class EBooksHandler(AnonymousBooksHandler):
278 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
281 class BookProxy(models.Model):
282 def __init__(self, book, key):
286 def __getattr__(self, item):
287 if item not in ('book', 'key'):
288 return self.book.__getattribute__(item)
290 return self.__getattribute__(item)
293 class QuerySetProxy(models.QuerySet):
294 def __init__(self, l):
298 return iter(self.list)
301 class FilterBooksHandler(AnonymousBooksHandler):
302 fields = book_tag_categories + [
303 'href', 'title', 'url', 'cover', 'cover_thumb', 'key', 'cover_source_image']
305 def read(self, request):
307 search_string = request.GET.get('search')
308 is_lektura = request.GET.get('lektura')
309 is_audiobook = request.GET.get('audiobook')
311 after = request.GET.get('after')
312 count = int(request.GET.get('count', 50))
313 if is_lektura in ('true', 'false'):
314 is_lektura = is_lektura == 'true'
317 if is_audiobook in ('true', 'false'):
318 is_audiobook = is_audiobook == 'true'
319 books = Book.objects.distinct().order_by('slug')
320 if is_lektura is not None:
321 books = books.filter(has_audience=is_lektura)
322 if is_audiobook is not None:
324 books = books.filter(media__type='mp3')
326 books = books.exclude(media__type='mp3')
327 for key in request.GET:
328 if key in category_singular:
329 category = category_singular[key]
330 if category in book_tag_categories:
331 slugs = request.GET[key].split(',')
332 tags = Tag.objects.filter(category=category, slug__in=slugs)
333 books = Book.tagged.with_any(tags, books)
334 if (search_string is not None) and len(search_string) < 3:
337 books_author = books.filter(cached_author__iregex='\m' + search_string)
338 books_title = books.filter(title__iregex='\m' + search_string)
339 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
340 if after and (key_sep in after):
341 which, slug = after.split(key_sep, 1)
343 book_lists = [(books_title.filter(slug__gt=slug), 'title')]
344 else: # which == 'author'
345 book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
347 book_lists = [(books_author, 'author'), (books_title, 'title')]
349 if after and key_sep in after:
350 which, slug = after.split(key_sep, 1)
351 books = books.filter(slug__gt=slug)
352 book_lists = [(books, 'book')]
355 for book_list, label in book_lists:
356 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
357 for category in book_tag_categories:
358 book_list = prefetch_relations(book_list, category)
359 remaining_count = count - len(filtered_books)
360 new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
361 for book in book_list[:remaining_count]]
362 filtered_books += new_books
363 if len(filtered_books) == count:
366 return QuerySetProxy(filtered_books)
369 # add categorized tags fields for Book
370 def _tags_getter(category):
372 def get_tags(cls, book):
373 return book.tags.filter(category=category)
377 def _tag_getter(category):
379 def get_tag(cls, book):
380 return book.tag_unicode(category)
384 def add_tag_getters():
385 for plural, singular in category_singular.items():
386 setattr(BookDetails, plural, _tags_getter(singular))
387 setattr(BookDetails, singular, _tag_getter(singular))
392 # add fields for files in Book
393 def _file_getter(book_format):
394 field = "%s_file" % book_format
397 def get_file(cls, book):
398 f = getattr(book, field)
400 return MEDIA_BASE + f.url
406 def add_file_getters():
407 for book_format in Book.formats:
408 setattr(BookDetails, book_format, _file_getter(book_format))
413 class CollectionDetails(object):
414 """Custom Collection fields."""
417 def href(cls, collection):
418 """ Returns URI in the API for the collection. """
420 return API_BASE + reverse("api_collection", args=[collection.slug])
423 def url(cls, collection):
424 """ Returns URL on the site. """
426 return WL_BASE + collection.get_absolute_url()
429 def books(cls, collection):
430 return Book.objects.filter(collection.get_query())
433 class CollectionDetailHandler(BaseHandler, CollectionDetails):
434 allowed_methods = ('GET',)
435 fields = ['url', 'title', 'description', 'books']
438 def read(self, request, slug):
439 """ Returns details of a collection, identified by slug. """
441 return Collection.objects.get(slug=slug)
442 except Collection.DoesNotExist:
446 class CollectionsHandler(BaseHandler, CollectionDetails):
447 allowed_methods = ('GET',)
449 fields = ['url', 'href', 'title']
452 def read(self, request):
453 """ Returns all collections. """
454 return Collection.objects.all()
457 class TagDetails(object):
458 """Custom Tag fields."""
462 """ Returns URI in the API for the tag. """
464 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
468 """ Returns URL on the site. """
470 return WL_BASE + tag.get_absolute_url()
473 class TagDetailHandler(BaseHandler, TagDetails):
474 """ Responsible for details of a single Tag object. """
476 fields = ['name', 'url', 'sort_key', 'description']
479 def read(self, request, category, slug):
480 """ Returns details of a tag, identified by category and slug. """
483 category_sng = category_singular[category]
488 return Tag.objects.get(category=category_sng, slug=slug)
489 except Tag.DoesNotExist:
493 class TagsHandler(BaseHandler, TagDetails):
494 """ Main handler for Tag objects.
496 Responsible for lists of Tag objects
497 and fields used for representing Tags.
500 allowed_methods = ('GET',)
502 fields = ['name', 'href', 'url', 'slug']
505 def read(self, request, category=None, pk=None):
506 """ Lists all tags in the category (eg. all themes). """
509 return Tag.objects.exclude(category='set').get(pk=pk)
510 except Book.DoesNotExist:
514 category_sng = category_singular[category]
518 after = request.GET.get('after')
519 before = request.GET.get('before')
520 count = request.GET.get('count')
522 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
525 tags = tags.filter(slug__gt=after)
527 tags = tags.filter(slug__lt=before)
531 tags = list(reversed(tags.order_by('-slug')[:count]))
538 class FragmentDetails(object):
539 """Custom Fragment fields."""
542 def href(cls, fragment):
543 """ Returns URI in the API for the fragment. """
545 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
548 def url(cls, fragment):
549 """ Returns URL on the site for the fragment. """
551 return WL_BASE + fragment.get_absolute_url()
554 def themes(cls, fragment):
555 """ Returns a list of theme tags for the fragment. """
557 return fragment.tags.filter(category='theme')
560 class FragmentDetailHandler(BaseHandler, FragmentDetails):
561 fields = ['book', 'anchor', 'text', 'url', 'themes']
564 def read(self, request, book, anchor):
565 """ Returns details of a fragment, identified by book slug and anchor. """
567 return Fragment.objects.get(book__slug=book, anchor=anchor)
568 except Fragment.DoesNotExist:
572 class FragmentsHandler(BaseHandler, FragmentDetails):
573 """ Main handler for Fragments.
575 Responsible for lists of Fragment objects
576 and fields used for representing Fragments.
580 fields = ['book', 'url', 'anchor', 'href']
581 allowed_methods = ('GET',)
583 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
586 def read(self, request, tags):
587 """ Lists all fragments with given book, tags, themes.
589 :param tags: should be a path of categories and slugs, i.e.:
590 books/book-slug/authors/an-author/themes/a-theme/
594 tags, ancestors = read_tags(tags, allowed=self.categories)
597 fragments = Fragment.tagged.with_all(tags).select_related('book')
598 if fragments.exists():
604 class PictureHandler(BaseHandler):
606 fields = ('slug', 'title')
607 allowed_methods = ('POST',)
609 def create(self, request):
610 if not request.user.has_perm('picture.add_picture'):
613 data = json.loads(request.POST.get('data'))
614 form = PictureImportForm(data)