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. """
126 return WL_BASE + book.get_absolute_url()
129 def children(cls, book):
130 """ Returns all children for a book. """
132 return book.children.all()
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'] + [
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 FilterBooksHandler(AnonymousBooksHandler):
283 fields = book_tag_categories + [
284 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'cover_source_image', 'fragment_data']
286 def read(self, request, title_part=None, author_part=None, is_lektura=None, is_audiobook=None,
287 after=None, before=None, count=None):
288 if 'title_part' in request.GET:
289 title_part = request.GET['title_part']
290 if 'author_part' in request.GET:
291 author_part = request.GET['author_part']
292 if 'is_lektura' in request.GET:
293 is_lektura = request.GET['is_lektura']
294 if 'is_audiobook' in request.GET:
295 is_audiobook = request.GET['is_audiobook']
299 if is_lektura in ('true', 'false'):
300 is_lektura = is_lektura == 'true'
303 if is_audiobook in ('true', 'false'):
304 is_audiobook = is_audiobook == 'true'
305 books = Book.objects.distinct()
307 books = books.filter(title__iregex='\m' + title_part)
308 if author_part is not None:
309 books = books.filter(cached_author__iregex='\m' + author_part)
310 if is_lektura is not None:
311 books = books.filter(has_audience=is_lektura)
312 if is_audiobook is not None:
314 books = books.filter(media__type='mp3')
316 books = books.exclude(media__type='mp3')
317 return super(FilterBooksHandler, self).read(request, books=books, after=after, before=before, count=count)
320 # add categorized tags fields for Book
321 def _tags_getter(category):
323 def get_tags(cls, book):
324 return book.tags.filter(category=category)
328 def _tag_getter(category):
330 def get_tag(cls, book):
331 return book.tag_unicode(category)
335 def add_tag_getters():
336 for plural, singular in category_singular.items():
337 setattr(BookDetails, plural, _tags_getter(singular))
338 setattr(BookDetails, singular, _tag_getter(singular))
343 # add fields for files in Book
344 def _file_getter(book_format):
345 field = "%s_file" % book_format
348 def get_file(cls, book):
349 f = getattr(book, field)
351 return MEDIA_BASE + f.url
357 def add_file_getters():
358 for book_format in Book.formats:
359 setattr(BookDetails, book_format, _file_getter(book_format))
364 class CollectionDetails(object):
365 """Custom Collection fields."""
368 def href(cls, collection):
369 """ Returns URI in the API for the collection. """
371 return API_BASE + reverse("api_collection", args=[collection.slug])
374 def url(cls, collection):
375 """ Returns URL on the site. """
377 return WL_BASE + collection.get_absolute_url()
380 def books(cls, collection):
381 return Book.objects.filter(collection.get_query())
384 class CollectionDetailHandler(BaseHandler, CollectionDetails):
385 allowed_methods = ('GET',)
386 fields = ['url', 'title', 'description', 'books']
389 def read(self, request, slug):
390 """ Returns details of a collection, identified by slug. """
392 return Collection.objects.get(slug=slug)
393 except Collection.DoesNotExist:
397 class CollectionsHandler(BaseHandler, CollectionDetails):
398 allowed_methods = ('GET',)
400 fields = ['url', 'href', 'title']
403 def read(self, request):
404 """ Returns all collections. """
405 return Collection.objects.all()
408 class TagDetails(object):
409 """Custom Tag fields."""
413 """ Returns URI in the API for the tag. """
415 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
419 """ Returns URL on the site. """
421 return WL_BASE + tag.get_absolute_url()
424 class TagDetailHandler(BaseHandler, TagDetails):
425 """ Responsible for details of a single Tag object. """
427 fields = ['name', 'url', 'sort_key', 'description']
430 def read(self, request, category, slug):
431 """ Returns details of a tag, identified by category and slug. """
434 category_sng = category_singular[category]
439 return Tag.objects.get(category=category_sng, slug=slug)
440 except Tag.DoesNotExist:
444 class TagsHandler(BaseHandler, TagDetails):
445 """ Main handler for Tag objects.
447 Responsible for lists of Tag objects
448 and fields used for representing Tags.
451 allowed_methods = ('GET',)
453 fields = ['name', 'href', 'url', 'slug']
456 def read(self, request, category=None, pk=None):
457 """ Lists all tags in the category (eg. all themes). """
460 return Tag.objects.exclude(category='set').get(pk=pk)
461 except Book.DoesNotExist:
465 category_sng = category_singular[category]
469 after = request.GET.get('after')
470 before = request.GET.get('before')
471 count = request.GET.get('count')
473 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
476 tags = tags.filter(slug__gt=after)
478 tags = tags.filter(slug__lt=before)
482 tags = list(reversed(tags.order_by('-slug')[:count]))
489 class FragmentDetails(object):
490 """Custom Fragment fields."""
493 def href(cls, fragment):
494 """ Returns URI in the API for the fragment. """
496 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
499 def url(cls, fragment):
500 """ Returns URL on the site for the fragment. """
502 return WL_BASE + fragment.get_absolute_url()
505 def themes(cls, fragment):
506 """ Returns a list of theme tags for the fragment. """
508 return fragment.tags.filter(category='theme')
511 class FragmentDetailHandler(BaseHandler, FragmentDetails):
512 fields = ['book', 'anchor', 'text', 'url', 'themes']
515 def read(self, request, book, anchor):
516 """ Returns details of a fragment, identified by book slug and anchor. """
518 return Fragment.objects.get(book__slug=book, anchor=anchor)
519 except Fragment.DoesNotExist:
523 class FragmentsHandler(BaseHandler, FragmentDetails):
524 """ Main handler for Fragments.
526 Responsible for lists of Fragment objects
527 and fields used for representing Fragments.
531 fields = ['book', 'url', 'anchor', 'href']
532 allowed_methods = ('GET',)
534 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
537 def read(self, request, tags):
538 """ Lists all fragments with given book, tags, themes.
540 :param tags: should be a path of categories and slugs, i.e.:
541 books/book-slug/authors/an-author/themes/a-theme/
545 tags, ancestors = read_tags(tags, allowed=self.categories)
548 fragments = Fragment.tagged.with_all(tags).select_related('book')
549 if fragments.exists():
555 class PictureHandler(BaseHandler):
557 fields = ('slug', 'title')
558 allowed_methods = ('POST',)
560 def create(self, request):
561 if not request.user.has_perm('picture.add_picture'):
564 data = json.loads(request.POST.get('data'))
565 form = PictureImportForm(data)