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, 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
53 tags = tags.strip('/').split('/')
57 category = tags.pop(0)
61 category = category_singular[category]
63 raise ValueError('Unknown category.')
65 if category not in allowed:
66 raise ValueError('Category not allowed.')
68 if category == 'book':
70 books.append(Book.objects.get(slug=slug))
71 except Book.DoesNotExist:
72 raise ValueError('Unknown book.')
75 real_tags.append(Tag.objects.get(category=category, slug=slug))
76 except Tag.DoesNotExist:
77 raise ValueError('Tag not found')
78 return real_tags, books
84 class BookMediaHandler(BaseHandler):
85 """ Responsible for representing media in Books. """
88 fields = ['name', 'type', 'url', 'artist', 'director']
92 """ Link to media on site. """
94 return MEDIA_BASE + media.file.url
97 def artist(cls, media):
98 return media.extra_info.get('artist_name', '')
101 def director(cls, media):
102 return media.extra_info.get('director_name', '')
105 class BookDetails(object):
106 """Custom fields used for representing Books."""
110 """ Returns an URI for a Book in the API. """
111 return API_BASE + reverse("api_book", args=[book.slug])
115 """ Returns Book's URL on the site. """
117 return WL_BASE + book.get_absolute_url()
120 def children(cls, book):
121 """ Returns all children for a book. """
123 return book.children.all()
126 def media(cls, book):
127 """ Returns all media for a book. """
128 return book.media.all()
131 def cover(cls, book):
132 return MEDIA_BASE + book.cover.url if book.cover else ''
135 def cover_thumb(cls, book):
136 return MEDIA_BASE + default.backend.get_thumbnail(
137 book.cover, "139x193").url if book.cover else ''
140 class BookDetailHandler(BaseHandler, BookDetails):
141 """ Main handler for Book objects.
143 Responsible for single Book details.
145 allowed_methods = ['GET']
146 fields = ['title', 'parent', 'children'] + Book.formats + [
147 'media', 'url', 'cover', 'cover_thumb'] + [
148 category_plural[c] for c in book_tag_categories]
151 def read(self, request, book):
152 """ Returns details of a book, identified by a slug and lang. """
154 return Book.objects.get(slug=book)
155 except Book.DoesNotExist:
159 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
160 """ Main handler for Book objects.
162 Responsible for lists of Book objects.
164 allowed_methods = ('GET',)
166 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
169 def genres(cls, book):
170 """ Returns all media for a book. """
171 return book.tags.filter(category='genre')
174 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
175 recommended=False, newest=False,
176 after=None, before=None, count=None):
177 """ Lists all books with given tags.
179 :param tags: filtering tags; should be a path of categories
180 and slugs, i.e.: authors/an-author/epoch/an-epoch/
181 :param top_level: if True and a book is included in the results,
182 it's children are aren't. By default all books matching the tags
187 return Book.objects.get(pk=pk)
188 except Book.DoesNotExist:
192 tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
198 books = Book.tagged_top_level(tags)
199 return books if books else rc.NOT_FOUND
201 books = Book.tagged.with_all(tags)
203 books = Book.objects.all()
204 books = books.order_by('slug')
207 books = books.filter(parent=None)
209 books = books.filter(media__type='mp3').distinct()
211 books = books.filter(media__type='daisy').distinct()
213 books = books.filter(recommended=True)
215 books = books.order_by('-created_at')
218 books = books.filter(slug__gt=after)
220 books = books.filter(slug__lt=before)
222 books = books.only('slug', 'title', 'cover', 'cover_thumb')
223 for category in book_tag_categories:
224 books = prefetch_relations(books, category)
228 books = list(reversed(books.order_by('-slug')[:count]))
230 books = books[:count]
237 def create(self, request, *args, **kwargs):
241 class BooksHandler(BookDetailHandler):
242 allowed_methods = ('GET', 'POST')
244 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
245 anonymous = AnonymousBooksHandler
247 def create(self, request, *args, **kwargs):
248 if not request.user.has_perm('catalogue.add_book'):
251 data = json.loads(request.POST.get('data'))
252 form = BookImportForm(data)
260 class EBooksHandler(AnonymousBooksHandler):
261 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
264 # add categorized tags fields for Book
265 def _tags_getter(category):
267 def get_tags(cls, book):
268 return book.tags.filter(category=category)
272 def _tag_getter(category):
274 def get_tag(cls, book):
275 return book.tag_unicode(category)
279 def add_tag_getters():
280 for plural, singular in category_singular.items():
281 setattr(BookDetails, plural, _tags_getter(singular))
282 setattr(BookDetails, singular, _tag_getter(singular))
287 # add fields for files in Book
288 def _file_getter(book_format):
289 field = "%s_file" % book_format
292 def get_file(cls, book):
293 f = getattr(book, field)
295 return MEDIA_BASE + f.url
301 def add_file_getters():
302 for book_format in Book.formats:
303 setattr(BookDetails, book_format, _file_getter(book_format))
308 class CollectionDetails(object):
309 """Custom Collection fields."""
312 def href(cls, collection):
313 """ Returns URI in the API for the collection. """
315 return API_BASE + reverse("api_collection", args=[collection.slug])
318 def url(cls, collection):
319 """ Returns URL on the site. """
321 return WL_BASE + collection.get_absolute_url()
324 def books(cls, collection):
325 return Book.objects.filter(collection.get_query())
328 class CollectionDetailHandler(BaseHandler, CollectionDetails):
329 allowed_methods = ('GET',)
330 fields = ['url', 'title', 'description', 'books']
333 def read(self, request, slug):
334 """ Returns details of a collection, identified by slug. """
336 return Collection.objects.get(slug=slug)
337 except Collection.DoesNotExist:
341 class CollectionsHandler(BaseHandler, CollectionDetails):
342 allowed_methods = ('GET',)
344 fields = ['url', 'href', 'title']
347 def read(self, request):
348 """ Returns all collections. """
349 return Collection.objects.all()
352 class TagDetails(object):
353 """Custom Tag fields."""
357 """ Returns URI in the API for the tag. """
359 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
363 """ Returns URL on the site. """
365 return WL_BASE + tag.get_absolute_url()
368 class TagDetailHandler(BaseHandler, TagDetails):
369 """ Responsible for details of a single Tag object. """
371 fields = ['name', 'url', 'sort_key', 'description']
374 def read(self, request, category, slug):
375 """ Returns details of a tag, identified by category and slug. """
378 category_sng = category_singular[category]
383 return Tag.objects.get(category=category_sng, slug=slug)
384 except Tag.DoesNotExist:
388 class TagsHandler(BaseHandler, TagDetails):
389 """ Main handler for Tag objects.
391 Responsible for lists of Tag objects
392 and fields used for representing Tags.
395 allowed_methods = ('GET',)
397 fields = ['name', 'href', 'url']
400 def read(self, request, category=None, pk=None):
401 """ Lists all tags in the category (eg. all themes). """
404 return Tag.objects.exclude(category='set').get(pk=pk)
405 except Book.DoesNotExist:
409 category_sng = category_singular[category]
413 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
420 class FragmentDetails(object):
421 """Custom Fragment fields."""
424 def href(cls, fragment):
425 """ Returns URI in the API for the fragment. """
427 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
430 def url(cls, fragment):
431 """ Returns URL on the site for the fragment. """
433 return WL_BASE + fragment.get_absolute_url()
436 def themes(cls, fragment):
437 """ Returns a list of theme tags for the fragment. """
439 return fragment.tags.filter(category='theme')
442 class FragmentDetailHandler(BaseHandler, FragmentDetails):
443 fields = ['book', 'anchor', 'text', 'url', 'themes']
446 def read(self, request, book, anchor):
447 """ Returns details of a fragment, identified by book slug and anchor. """
449 return Fragment.objects.get(book__slug=book, anchor=anchor)
450 except Fragment.DoesNotExist:
454 class FragmentsHandler(BaseHandler, FragmentDetails):
455 """ Main handler for Fragments.
457 Responsible for lists of Fragment objects
458 and fields used for representing Fragments.
462 fields = ['book', 'url', 'anchor', 'href']
463 allowed_methods = ('GET',)
465 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
468 def read(self, request, tags):
469 """ Lists all fragments with given book, tags, themes.
471 :param tags: should be a path of categories and slugs, i.e.:
472 books/book-slug/authors/an-author/themes/a-theme/
476 tags, ancestors = read_tags(tags, allowed=self.categories)
479 fragments = Fragment.tagged.with_all(tags).select_related('book')
480 if fragments.exists():
486 class PictureHandler(BaseHandler):
488 fields = ('slug', 'title')
489 allowed_methods = ('POST',)
491 def create(self, request):
492 if not request.user.has_perm('picture.add_picture'):
495 data = json.loads(request.POST.get('data'))
496 form = PictureImportForm(data)