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. """
125 return WL_BASE + book.get_absolute_url()
128 def children(cls, book):
129 """ Returns all children for a book. """
130 return book.children.order_by('parent_number', 'sort_key')
133 def media(cls, book):
134 """ Returns all media for a book. """
135 return book.media.all()
138 def cover(cls, book):
139 return MEDIA_BASE + book.cover.url if book.cover else ''
142 def cover_thumb(cls, book):
143 return MEDIA_BASE + default.backend.get_thumbnail(
144 book.cover, "139x193").url if book.cover else ''
147 def cover_source_image(cls, book):
148 url = book.cover_source()
149 return url.rstrip('/') + '/file/'
152 class BookDetailHandler(BaseHandler, BookDetails):
153 """ Main handler for Book objects.
155 Responsible for single Book details.
157 allowed_methods = ['GET']
158 fields = ['title', 'parent', 'children'] + Book.formats + [
159 'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [
160 category_plural[c] for c in book_tag_categories]
163 def read(self, request, book):
164 """ Returns details of a book, identified by a slug and lang. """
166 return Book.objects.get(slug=book)
167 except Book.DoesNotExist:
171 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
172 """ Main handler for Book objects.
174 Responsible for lists of Book objects.
176 allowed_methods = ('GET',)
178 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
181 def genres(cls, book):
182 """ Returns all media for a book. """
183 return book.tags.filter(category='genre')
186 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
187 recommended=False, newest=False, books=None,
188 after=None, before=None, count=None):
189 """ Lists all books with given tags.
191 :param tags: filtering tags; should be a path of categories
192 and slugs, i.e.: authors/an-author/epoch/an-epoch/
193 :param top_level: if True and a book is included in the results,
194 it's children are aren't. By default all books matching the tags
199 return Book.objects.get(pk=pk)
200 except Book.DoesNotExist:
204 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
208 if 'after' in request.GET:
209 after = request.GET['after']
210 if 'before' in request.GET:
211 before = request.GET['before']
212 if 'count' in request.GET:
213 count = request.GET['count']
217 books = Book.tagged_top_level(tags)
218 return books if books else rc.NOT_FOUND
220 books = Book.tagged.with_all(tags)
222 books = books if books is not None else Book.objects.all()
223 books = books.order_by('slug')
226 books = books.filter(parent=None)
228 books = books.filter(media__type='mp3').distinct()
230 books = books.filter(media__type='daisy').distinct()
232 books = books.filter(recommended=True)
234 books = books.order_by('-created_at')
237 books = books.filter(slug__gt=after)
239 books = books.filter(slug__lt=before)
241 books = books.only('slug', 'title', 'cover', 'cover_thumb')
242 for category in book_tag_categories:
243 books = prefetch_relations(books, category)
247 books = list(reversed(books.order_by('-slug')[:count]))
249 books = books[:count]
253 def create(self, request, *args, **kwargs):
257 class BooksHandler(BookDetailHandler):
258 allowed_methods = ('GET', 'POST')
260 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
261 anonymous = AnonymousBooksHandler
263 def create(self, request, *args, **kwargs):
264 if not request.user.has_perm('catalogue.add_book'):
267 data = json.loads(request.POST.get('data'))
268 form = BookImportForm(data)
276 class EBooksHandler(AnonymousBooksHandler):
277 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
280 class FilterBooksHandler(AnonymousBooksHandler):
281 fields = book_tag_categories + [
282 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'cover_source_image']
284 def read(self, request, title_part=None, author_part=None, is_lektura=None, is_audiobook=None,
285 after=None, before=None, count=None):
286 if 'title_part' in request.GET:
287 title_part = request.GET['title_part']
288 if 'author_part' in request.GET:
289 author_part = request.GET['author_part']
290 if 'is_lektura' in request.GET:
291 is_lektura = request.GET['is_lektura']
292 if 'is_audiobook' in request.GET:
293 is_audiobook = request.GET['is_audiobook']
297 if is_lektura in ('true', 'false'):
298 is_lektura = is_lektura == 'true'
301 if is_audiobook in ('true', 'false'):
302 is_audiobook = is_audiobook == 'true'
303 books = Book.objects.distinct()
305 books = books.filter(title__iregex='\m' + title_part)
306 if author_part is not None:
307 books = books.filter(cached_author__iregex='\m' + author_part)
308 if is_lektura is not None:
309 books = books.filter(has_audience=is_lektura)
310 if is_audiobook is not None:
312 books = books.filter(media__type='mp3')
314 books = books.exclude(media__type='mp3')
315 for key in request.GET:
316 if key in category_singular:
317 category = category_singular[key]
318 if category in book_tag_categories:
319 slugs = request.GET[key].split(',')
320 tags = Tag.objects.filter(category=category, slug__in=slugs)
321 books = Book.tagged.with_any(tags, books)
322 return super(FilterBooksHandler, self).read(request, books=books, after=after, before=before, count=count)
325 # add categorized tags fields for Book
326 def _tags_getter(category):
328 def get_tags(cls, book):
329 return book.tags.filter(category=category)
333 def _tag_getter(category):
335 def get_tag(cls, book):
336 return book.tag_unicode(category)
340 def add_tag_getters():
341 for plural, singular in category_singular.items():
342 setattr(BookDetails, plural, _tags_getter(singular))
343 setattr(BookDetails, singular, _tag_getter(singular))
348 # add fields for files in Book
349 def _file_getter(book_format):
350 field = "%s_file" % book_format
353 def get_file(cls, book):
354 f = getattr(book, field)
356 return MEDIA_BASE + f.url
362 def add_file_getters():
363 for book_format in Book.formats:
364 setattr(BookDetails, book_format, _file_getter(book_format))
369 class CollectionDetails(object):
370 """Custom Collection fields."""
373 def href(cls, collection):
374 """ Returns URI in the API for the collection. """
376 return API_BASE + reverse("api_collection", args=[collection.slug])
379 def url(cls, collection):
380 """ Returns URL on the site. """
382 return WL_BASE + collection.get_absolute_url()
385 def books(cls, collection):
386 return Book.objects.filter(collection.get_query())
389 class CollectionDetailHandler(BaseHandler, CollectionDetails):
390 allowed_methods = ('GET',)
391 fields = ['url', 'title', 'description', 'books']
394 def read(self, request, slug):
395 """ Returns details of a collection, identified by slug. """
397 return Collection.objects.get(slug=slug)
398 except Collection.DoesNotExist:
402 class CollectionsHandler(BaseHandler, CollectionDetails):
403 allowed_methods = ('GET',)
405 fields = ['url', 'href', 'title']
408 def read(self, request):
409 """ Returns all collections. """
410 return Collection.objects.all()
413 class TagDetails(object):
414 """Custom Tag fields."""
418 """ Returns URI in the API for the tag. """
420 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
424 """ Returns URL on the site. """
426 return WL_BASE + tag.get_absolute_url()
429 class TagDetailHandler(BaseHandler, TagDetails):
430 """ Responsible for details of a single Tag object. """
432 fields = ['name', 'url', 'sort_key', 'description']
435 def read(self, request, category, slug):
436 """ Returns details of a tag, identified by category and slug. """
439 category_sng = category_singular[category]
444 return Tag.objects.get(category=category_sng, slug=slug)
445 except Tag.DoesNotExist:
449 class TagsHandler(BaseHandler, TagDetails):
450 """ Main handler for Tag objects.
452 Responsible for lists of Tag objects
453 and fields used for representing Tags.
456 allowed_methods = ('GET',)
458 fields = ['name', 'href', 'url', 'slug']
461 def read(self, request, category=None, pk=None):
462 """ Lists all tags in the category (eg. all themes). """
465 return Tag.objects.exclude(category='set').get(pk=pk)
466 except Book.DoesNotExist:
470 category_sng = category_singular[category]
474 after = request.GET.get('after')
475 before = request.GET.get('before')
476 count = request.GET.get('count')
478 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
481 tags = tags.filter(slug__gt=after)
483 tags = tags.filter(slug__lt=before)
487 tags = list(reversed(tags.order_by('-slug')[:count]))
494 class FragmentDetails(object):
495 """Custom Fragment fields."""
498 def href(cls, fragment):
499 """ Returns URI in the API for the fragment. """
501 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
504 def url(cls, fragment):
505 """ Returns URL on the site for the fragment. """
507 return WL_BASE + fragment.get_absolute_url()
510 def themes(cls, fragment):
511 """ Returns a list of theme tags for the fragment. """
513 return fragment.tags.filter(category='theme')
516 class FragmentDetailHandler(BaseHandler, FragmentDetails):
517 fields = ['book', 'anchor', 'text', 'url', 'themes']
520 def read(self, request, book, anchor):
521 """ Returns details of a fragment, identified by book slug and anchor. """
523 return Fragment.objects.get(book__slug=book, anchor=anchor)
524 except Fragment.DoesNotExist:
528 class FragmentsHandler(BaseHandler, FragmentDetails):
529 """ Main handler for Fragments.
531 Responsible for lists of Fragment objects
532 and fields used for representing Fragments.
536 fields = ['book', 'url', 'anchor', 'href']
537 allowed_methods = ('GET',)
539 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
542 def read(self, request, tags):
543 """ Lists all fragments with given book, tags, themes.
545 :param tags: should be a path of categories and slugs, i.e.:
546 books/book-slug/authors/an-author/themes/a-theme/
550 tags, ancestors = read_tags(tags, allowed=self.categories)
553 fragments = Fragment.tagged.with_all(tags).select_related('book')
554 if fragments.exists():
560 class PictureHandler(BaseHandler):
562 fields = ('slug', 'title')
563 allowed_methods = ('POST',)
565 def create(self, request):
566 if not request.user.has_perm('picture.add_picture'):
569 data = json.loads(request.POST.get('data'))
570 form = PictureImportForm(data)