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']
42 def read_tags(tags, allowed):
43 """ Reads a path of filtering tags.
45 :param str tags: a path of category and slug pairs, like: authors/an-author/...
46 :returns: list of Tag objects
47 :raises: ValueError when tags can't be found
52 tags = tags.strip('/').split('/')
56 category = tags.pop(0)
60 category = category_singular[category]
62 raise ValueError('Unknown category.')
64 if category not in allowed:
65 raise ValueError('Category not allowed.')
67 if category == 'book':
69 books.append(Book.objects.get(slug=slug))
70 except Book.DoesNotExist:
71 raise ValueError('Unknown book.')
74 real_tags.append(Tag.objects.get(category=category, slug=slug))
75 except Tag.DoesNotExist:
76 raise ValueError('Tag not found')
77 return real_tags, books
83 class BookMediaHandler(BaseHandler):
84 """ Responsible for representing media in Books. """
87 fields = ['name', 'type', 'url', 'artist', 'director']
91 """ Link to media on site. """
93 return MEDIA_BASE + media.file.url
96 def artist(cls, media):
97 return media.extra_info.get('artist_name', '')
100 def director(cls, media):
101 return media.extra_info.get('director_name', '')
104 class BookDetails(object):
105 """Custom fields used for representing Books."""
109 """ Returns an URI for a Book in the API. """
110 return API_BASE + reverse("api_book", args=[book.slug])
114 """ Returns Book's URL on the site. """
116 return WL_BASE + book.get_absolute_url()
119 def children(cls, book):
120 """ Returns all children for a book. """
122 return book.children.all()
125 def media(cls, book):
126 """ Returns all media for a book. """
127 return book.media.all()
130 def cover(cls, book):
131 return MEDIA_BASE + book.cover.url if book.cover else ''
134 def cover_thumb(cls, book):
135 return MEDIA_BASE + default.backend.get_thumbnail(
136 book.cover, "139x193").url if book.cover else ''
139 class BookDetailHandler(BaseHandler, BookDetails):
140 """ Main handler for Book objects.
142 Responsible for single Book details.
144 allowed_methods = ['GET']
145 fields = ['title', 'parent', 'children'] + Book.formats + [
146 'media', 'url', 'cover', 'cover_thumb'] + [
147 category_plural[c] for c in book_tag_categories]
150 def read(self, request, book):
151 """ Returns details of a book, identified by a slug and lang. """
153 return Book.objects.get(slug=book)
154 except Book.DoesNotExist:
158 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
159 """ Main handler for Book objects.
161 Responsible for lists of Book objects.
163 allowed_methods = ('GET',)
165 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
168 def genres(cls, book):
169 """ Returns all media for a book. """
170 return book.tags.filter(category='genre')
173 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None):
174 """ Lists all books with given tags.
176 :param tags: filtering tags; should be a path of categories
177 and slugs, i.e.: authors/an-author/epoch/an-epoch/
178 :param top_level: if True and a book is included in the results,
179 it's children are aren't. By default all books matching the tags
184 return Book.objects.get(pk=pk)
185 except Book.DoesNotExist:
189 tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
195 books = Book.tagged_top_level(tags)
196 return books if books else rc.NOT_FOUND
198 books = Book.tagged.with_all(tags)
200 books = Book.objects.all()
203 books = books.filter(parent=None)
205 books = books.filter(media__type='mp3').distinct()
207 books = books.filter(media__type='daisy').distinct()
209 books = books.only('slug', 'title', 'cover')
216 def create(self, request, *args, **kwargs):
220 class BooksHandler(BookDetailHandler):
221 allowed_methods = ('GET', 'POST')
223 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
224 anonymous = AnonymousBooksHandler
226 def create(self, request, *args, **kwargs):
227 if not request.user.has_perm('catalogue.add_book'):
230 data = json.loads(request.POST.get('data'))
231 form = BookImportForm(data)
239 class EBooksHandler(AnonymousBooksHandler):
240 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
243 # add categorized tags fields for Book
244 def _tags_getter(category):
246 def get_tags(cls, book):
247 return book.tags.filter(category=category)
251 def _tag_getter(category):
253 def get_tag(cls, book):
254 return ', '.join(book.tags.filter(category=category).values_list('name', flat=True))
258 def add_tag_getters():
259 for plural, singular in category_singular.items():
260 setattr(BookDetails, plural, _tags_getter(singular))
261 setattr(BookDetails, singular, _tag_getter(singular))
266 # add fields for files in Book
267 def _file_getter(book_format):
268 field = "%s_file" % book_format
271 def get_file(cls, book):
272 f = getattr(book, field)
274 return MEDIA_BASE + f.url
280 def add_file_getters():
281 for book_format in Book.formats:
282 setattr(BookDetails, book_format, _file_getter(book_format))
287 class CollectionDetails(object):
288 """Custom Collection fields."""
291 def href(cls, collection):
292 """ Returns URI in the API for the collection. """
294 return API_BASE + reverse("api_collection", args=[collection.slug])
297 def url(cls, collection):
298 """ Returns URL on the site. """
300 return WL_BASE + collection.get_absolute_url()
303 def books(cls, collection):
304 return Book.objects.filter(collection.get_query())
307 class CollectionDetailHandler(BaseHandler, CollectionDetails):
308 allowed_methods = ('GET',)
309 fields = ['url', 'title', 'description', 'books']
312 def read(self, request, slug):
313 """ Returns details of a collection, identified by slug. """
315 return Collection.objects.get(slug=slug)
316 except Collection.DoesNotExist:
320 class CollectionsHandler(BaseHandler, CollectionDetails):
321 allowed_methods = ('GET',)
323 fields = ['url', 'href', 'title']
326 def read(self, request):
327 """ Returns all collections. """
328 return Collection.objects.all()
331 class TagDetails(object):
332 """Custom Tag fields."""
336 """ Returns URI in the API for the tag. """
338 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
342 """ Returns URL on the site. """
344 return WL_BASE + tag.get_absolute_url()
347 class TagDetailHandler(BaseHandler, TagDetails):
348 """ Responsible for details of a single Tag object. """
350 fields = ['name', 'url', 'sort_key', 'description']
353 def read(self, request, category, slug):
354 """ Returns details of a tag, identified by category and slug. """
357 category_sng = category_singular[category]
362 return Tag.objects.get(category=category_sng, slug=slug)
363 except Tag.DoesNotExist:
367 class TagsHandler(BaseHandler, TagDetails):
368 """ Main handler for Tag objects.
370 Responsible for lists of Tag objects
371 and fields used for representing Tags.
374 allowed_methods = ('GET',)
376 fields = ['name', 'href', 'url']
379 def read(self, request, category=None, pk=None):
380 """ Lists all tags in the category (eg. all themes). """
383 return Tag.objects.exclude(category='set').get(pk=pk)
384 except Book.DoesNotExist:
388 category_sng = category_singular[category]
392 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
399 class FragmentDetails(object):
400 """Custom Fragment fields."""
403 def href(cls, fragment):
404 """ Returns URI in the API for the fragment. """
406 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
409 def url(cls, fragment):
410 """ Returns URL on the site for the fragment. """
412 return WL_BASE + fragment.get_absolute_url()
415 def themes(cls, fragment):
416 """ Returns a list of theme tags for the fragment. """
418 return fragment.tags.filter(category='theme')
421 class FragmentDetailHandler(BaseHandler, FragmentDetails):
422 fields = ['book', 'anchor', 'text', 'url', 'themes']
425 def read(self, request, book, anchor):
426 """ Returns details of a fragment, identified by book slug and anchor. """
428 return Fragment.objects.get(book__slug=book, anchor=anchor)
429 except Fragment.DoesNotExist:
433 class FragmentsHandler(BaseHandler, FragmentDetails):
434 """ Main handler for Fragments.
436 Responsible for lists of Fragment objects
437 and fields used for representing Fragments.
441 fields = ['book', 'url', 'anchor', 'href']
442 allowed_methods = ('GET',)
444 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
447 def read(self, request, tags):
448 """ Lists all fragments with given book, tags, themes.
450 :param tags: should be a path of categories and slugs, i.e.:
451 books/book-slug/authors/an-author/themes/a-theme/
455 tags, ancestors = read_tags(tags, allowed=self.categories)
458 fragments = Fragment.tagged.with_all(tags).select_related('book')
459 if fragments.exists():
465 class PictureHandler(BaseHandler):
467 fields = ('slug', 'title')
468 allowed_methods = ('POST',)
470 def create(self, request):
471 if not request.user.has_perm('picture.add_picture'):
474 data = json.loads(request.POST.get('data'))
475 form = PictureImportForm(data)