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']
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 """ Lists all books with given tags.
177 :param tags: filtering tags; should be a path of categories
178 and slugs, i.e.: authors/an-author/epoch/an-epoch/
179 :param top_level: if True and a book is included in the results,
180 it's children are aren't. By default all books matching the tags
185 return Book.objects.get(pk=pk)
186 except Book.DoesNotExist:
190 tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
196 books = Book.tagged_top_level(tags)
197 return books if books else rc.NOT_FOUND
199 books = Book.tagged.with_all(tags)
201 books = Book.objects.all()
204 books = books.filter(parent=None)
206 books = books.filter(media__type='mp3').distinct()
208 books = books.filter(media__type='daisy').distinct()
210 books = books.only('slug', 'title', 'cover', 'cover_thumb')
211 for category in book_tag_categories:
212 books = prefetch_relations(books, category)
218 def create(self, request, *args, **kwargs):
222 class BooksHandler(BookDetailHandler):
223 allowed_methods = ('GET', 'POST')
225 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
226 anonymous = AnonymousBooksHandler
228 def create(self, request, *args, **kwargs):
229 if not request.user.has_perm('catalogue.add_book'):
232 data = json.loads(request.POST.get('data'))
233 form = BookImportForm(data)
241 class EBooksHandler(AnonymousBooksHandler):
242 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
245 # add categorized tags fields for Book
246 def _tags_getter(category):
248 def get_tags(cls, book):
249 return book.tags.filter(category=category)
253 def _tag_getter(category):
255 def get_tag(cls, book):
256 return book.tag_unicode(category)
260 def add_tag_getters():
261 for plural, singular in category_singular.items():
262 setattr(BookDetails, plural, _tags_getter(singular))
263 setattr(BookDetails, singular, _tag_getter(singular))
268 # add fields for files in Book
269 def _file_getter(book_format):
270 field = "%s_file" % book_format
273 def get_file(cls, book):
274 f = getattr(book, field)
276 return MEDIA_BASE + f.url
282 def add_file_getters():
283 for book_format in Book.formats:
284 setattr(BookDetails, book_format, _file_getter(book_format))
289 class CollectionDetails(object):
290 """Custom Collection fields."""
293 def href(cls, collection):
294 """ Returns URI in the API for the collection. """
296 return API_BASE + reverse("api_collection", args=[collection.slug])
299 def url(cls, collection):
300 """ Returns URL on the site. """
302 return WL_BASE + collection.get_absolute_url()
305 def books(cls, collection):
306 return Book.objects.filter(collection.get_query())
309 class CollectionDetailHandler(BaseHandler, CollectionDetails):
310 allowed_methods = ('GET',)
311 fields = ['url', 'title', 'description', 'books']
314 def read(self, request, slug):
315 """ Returns details of a collection, identified by slug. """
317 return Collection.objects.get(slug=slug)
318 except Collection.DoesNotExist:
322 class CollectionsHandler(BaseHandler, CollectionDetails):
323 allowed_methods = ('GET',)
325 fields = ['url', 'href', 'title']
328 def read(self, request):
329 """ Returns all collections. """
330 return Collection.objects.all()
333 class TagDetails(object):
334 """Custom Tag fields."""
338 """ Returns URI in the API for the tag. """
340 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
344 """ Returns URL on the site. """
346 return WL_BASE + tag.get_absolute_url()
349 class TagDetailHandler(BaseHandler, TagDetails):
350 """ Responsible for details of a single Tag object. """
352 fields = ['name', 'url', 'sort_key', 'description']
355 def read(self, request, category, slug):
356 """ Returns details of a tag, identified by category and slug. """
359 category_sng = category_singular[category]
364 return Tag.objects.get(category=category_sng, slug=slug)
365 except Tag.DoesNotExist:
369 class TagsHandler(BaseHandler, TagDetails):
370 """ Main handler for Tag objects.
372 Responsible for lists of Tag objects
373 and fields used for representing Tags.
376 allowed_methods = ('GET',)
378 fields = ['name', 'href', 'url']
381 def read(self, request, category=None, pk=None):
382 """ Lists all tags in the category (eg. all themes). """
385 return Tag.objects.exclude(category='set').get(pk=pk)
386 except Book.DoesNotExist:
390 category_sng = category_singular[category]
394 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
401 class FragmentDetails(object):
402 """Custom Fragment fields."""
405 def href(cls, fragment):
406 """ Returns URI in the API for the fragment. """
408 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
411 def url(cls, fragment):
412 """ Returns URL on the site for the fragment. """
414 return WL_BASE + fragment.get_absolute_url()
417 def themes(cls, fragment):
418 """ Returns a list of theme tags for the fragment. """
420 return fragment.tags.filter(category='theme')
423 class FragmentDetailHandler(BaseHandler, FragmentDetails):
424 fields = ['book', 'anchor', 'text', 'url', 'themes']
427 def read(self, request, book, anchor):
428 """ Returns details of a fragment, identified by book slug and anchor. """
430 return Fragment.objects.get(book__slug=book, anchor=anchor)
431 except Fragment.DoesNotExist:
435 class FragmentsHandler(BaseHandler, FragmentDetails):
436 """ Main handler for Fragments.
438 Responsible for lists of Fragment objects
439 and fields used for representing Fragments.
443 fields = ['book', 'url', 'anchor', 'href']
444 allowed_methods = ('GET',)
446 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
449 def read(self, request, tags):
450 """ Lists all fragments with given book, tags, themes.
452 :param tags: should be a path of categories and slugs, i.e.:
453 books/book-slug/authors/an-author/themes/a-theme/
457 tags, ancestors = read_tags(tags, allowed=self.categories)
460 fragments = Fragment.tagged.with_all(tags).select_related('book')
461 if fragments.exists():
467 class PictureHandler(BaseHandler):
469 fields = ('slug', 'title')
470 allowed_methods = ('POST',)
472 def create(self, request):
473 if not request.user.has_perm('picture.add_picture'):
476 data = json.loads(request.POST.get('data'))
477 form = PictureImportForm(data)