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 picture.models import Picture
17 from picture.forms import PictureImportForm
19 from stats.utils import piwik_track
21 from . import emitters # Register our emitters
23 API_BASE = WL_BASE = MEDIA_BASE = lazy(
24 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
36 for k, v in category_singular.items():
37 category_plural[v] = k
39 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 not category 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 ''
141 class BookDetailHandler(BaseHandler, BookDetails):
142 """ Main handler for Book objects.
144 Responsible for single Book details.
146 allowed_methods = ['GET']
147 fields = ['title', 'parent', 'children'] + Book.formats + [
148 'media', 'url', 'cover', 'cover_thumb'] + [
149 category_plural[c] for c in book_tag_categories]
152 def read(self, request, book):
153 """ Returns details of a book, identified by a slug and lang. """
155 return Book.objects.get(slug=book)
156 except Book.DoesNotExist:
160 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
161 """ Main handler for Book objects.
163 Responsible for lists of Book objects.
165 allowed_methods = ('GET',)
167 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
170 def genres(cls, book):
171 """ Returns all media for a book. """
172 return book.tags.filter(category='genre')
175 def read(self, request, tags=None, top_level=False,
176 audiobooks=False, daisy=False, pk=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()
206 books = books.filter(parent=None)
208 books = books.filter(media__type='mp3').distinct()
210 books = books.filter(media__type='daisy').distinct()
217 def create(self, request, *args, **kwargs):
221 class BooksHandler(BookDetailHandler):
222 allowed_methods = ('GET', 'POST')
224 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
225 anonymous = AnonymousBooksHandler
227 def create(self, request, *args, **kwargs):
228 if not request.user.has_perm('catalogue.add_book'):
231 data = json.loads(request.POST.get('data'))
232 form = BookImportForm(data)
240 class EBooksHandler(AnonymousBooksHandler):
241 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
244 # add categorized tags fields for Book
245 def _tags_getter(category):
247 def get_tags(cls, book):
248 return book.tags.filter(category=category)
250 def _tag_getter(category):
252 def get_tag(cls, book):
253 return ', '.join(tag.name for tag in book.tags.filter(category=category))
255 for plural, singular in category_singular.items():
256 setattr(BookDetails, plural, _tags_getter(singular))
257 setattr(BookDetails, singular, _tag_getter(singular))
259 # add fields for files in Book
260 def _file_getter(format):
261 field = "%s_file" % format
263 def get_file(cls, book):
264 f = getattr(book, field)
266 return MEDIA_BASE + f.url
270 for format in Book.formats:
271 setattr(BookDetails, format, _file_getter(format))
274 class CollectionDetails(object):
275 """Custom Collection fields."""
278 def href(cls, collection):
279 """ Returns URI in the API for the collection. """
281 return API_BASE + reverse("api_collection", args=[collection.slug])
284 def url(cls, collection):
285 """ Returns URL on the site. """
287 return WL_BASE + collection.get_absolute_url()
290 def books(cls, collection):
291 return Book.objects.filter(collection.get_query())
295 class CollectionDetailHandler(BaseHandler, CollectionDetails):
296 allowed_methods = ('GET',)
297 fields = ['url', 'title', 'description', 'books']
300 def read(self, request, slug):
301 """ Returns details of a collection, identified by slug. """
303 return Collection.objects.get(slug=slug)
304 except Collection.DoesNotExist:
308 class CollectionsHandler(BaseHandler, CollectionDetails):
309 allowed_methods = ('GET',)
311 fields = ['url', 'href', 'title']
314 def read(self, request):
315 """ Returns all collections. """
316 return Collection.objects.all()
319 class TagDetails(object):
320 """Custom Tag fields."""
324 """ Returns URI in the API for the tag. """
326 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
330 """ Returns URL on the site. """
332 return WL_BASE + tag.get_absolute_url()
335 class TagDetailHandler(BaseHandler, TagDetails):
336 """ Responsible for details of a single Tag object. """
338 fields = ['name', 'url', 'sort_key', 'description']
341 def read(self, request, category, slug):
342 """ Returns details of a tag, identified by category and slug. """
345 category_sng = category_singular[category]
350 return Tag.objects.get(category=category_sng, slug=slug)
351 except Tag.DoesNotExist:
355 class TagsHandler(BaseHandler, TagDetails):
356 """ Main handler for Tag objects.
358 Responsible for lists of Tag objects
359 and fields used for representing Tags.
362 allowed_methods = ('GET',)
364 fields = ['name', 'href', 'url']
367 def read(self, request, category=None, pk=None):
368 """ Lists all tags in the category (eg. all themes). """
371 return Tag.objects.exclude(category='set').get(pk=pk)
372 except Book.DoesNotExist:
376 category_sng = category_singular[category]
380 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
387 class FragmentDetails(object):
388 """Custom Fragment fields."""
391 def href(cls, fragment):
392 """ Returns URI in the API for the fragment. """
394 return API_BASE + reverse("api_fragment",
395 args=[fragment.book.slug, fragment.anchor])
398 def url(cls, fragment):
399 """ Returns URL on the site for the fragment. """
401 return WL_BASE + fragment.get_absolute_url()
404 def themes(cls, fragment):
405 """ Returns a list of theme tags for the fragment. """
407 return fragment.tags.filter(category='theme')
410 class FragmentDetailHandler(BaseHandler, FragmentDetails):
411 fields = ['book', 'anchor', 'text', 'url', 'themes']
414 def read(self, request, book, anchor):
415 """ Returns details of a fragment, identified by book slug and anchor. """
417 return Fragment.objects.get(book__slug=book, anchor=anchor)
418 except Fragment.DoesNotExist:
422 class FragmentsHandler(BaseHandler, FragmentDetails):
423 """ Main handler for Fragments.
425 Responsible for lists of Fragment objects
426 and fields used for representing Fragments.
430 fields = ['book', 'url', 'anchor', 'href']
431 allowed_methods = ('GET',)
433 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
436 def read(self, request, tags):
437 """ Lists all fragments with given book, tags, themes.
439 :param tags: should be a path of categories and slugs, i.e.:
440 books/book-slug/authors/an-author/themes/a-theme/
444 tags, ancestors = read_tags(tags, allowed=self.categories)
447 fragments = Fragment.tagged.with_all(tags).select_related('book')
448 if fragments.exists():
454 class PictureHandler(BaseHandler):
456 fields = ('slug', 'title')
457 allowed_methods = ('POST',)
459 def create(self, request):
460 if not request.user.has_perm('picture.add_picture'):
463 data = json.loads(request.POST.get('data'))
464 form = PictureImportForm(data)