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.db.models import Prefetch
10 from django.utils.functional import lazy
11 from piston.handler import AnonymousBaseHandler, BaseHandler
12 from piston.utils import rc
13 from sorl.thumbnail import default
15 from catalogue.forms import BookImportForm
16 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
17 from catalogue.models.tag import TagRelation
18 from picture.models import Picture
19 from picture.forms import PictureImportForm
21 from stats.utils import piwik_track
23 from . import emitters # Register our emitters
25 API_BASE = WL_BASE = MEDIA_BASE = lazy(
26 lambda: u'http://' + Site.objects.get_current().domain, unicode)()
38 for k, v in category_singular.items():
39 category_plural[v] = k
41 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
44 def read_tags(tags, allowed):
45 """ Reads a path of filtering tags.
47 :param str tags: a path of category and slug pairs, like: authors/an-author/...
48 :returns: list of Tag objects
49 :raises: ValueError when tags can't be found
54 tags = tags.strip('/').split('/')
58 category = tags.pop(0)
62 category = category_singular[category]
64 raise ValueError('Unknown category.')
66 if category not in allowed:
67 raise ValueError('Category not allowed.')
69 if category == 'book':
71 books.append(Book.objects.get(slug=slug))
72 except Book.DoesNotExist:
73 raise ValueError('Unknown book.')
76 real_tags.append(Tag.objects.get(category=category, slug=slug))
77 except Tag.DoesNotExist:
78 raise ValueError('Tag not found')
79 return real_tags, books
85 class BookMediaHandler(BaseHandler):
86 """ Responsible for representing media in Books. """
89 fields = ['name', 'type', 'url', 'artist', 'director']
93 """ Link to media on site. """
95 return MEDIA_BASE + media.file.url
98 def artist(cls, media):
99 return media.extra_info.get('artist_name', '')
102 def director(cls, media):
103 return media.extra_info.get('director_name', '')
106 class BookDetails(object):
107 """Custom fields used for representing Books."""
111 """ Returns an URI for a Book in the API. """
112 return API_BASE + reverse("api_book", args=[book.slug])
116 """ Returns Book's URL on the site. """
118 return WL_BASE + book.get_absolute_url()
121 def children(cls, book):
122 """ Returns all children for a book. """
124 return book.children.all()
127 def media(cls, book):
128 """ Returns all media for a book. """
129 return book.media.all()
132 def cover(cls, book):
133 return MEDIA_BASE + book.cover.url if book.cover else ''
136 def cover_thumb(cls, book):
137 return MEDIA_BASE + default.backend.get_thumbnail(
138 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, audiobooks=False, daisy=False, pk=None):
176 """ Lists all books with given tags.
178 :param tags: filtering tags; should be a path of categories
179 and slugs, i.e.: authors/an-author/epoch/an-epoch/
180 :param top_level: if True and a book is included in the results,
181 it's children are aren't. By default all books matching the tags
186 return Book.objects.get(pk=pk)
187 except Book.DoesNotExist:
191 tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
197 books = Book.tagged_top_level(tags)
198 return books if books else rc.NOT_FOUND
200 books = Book.tagged.with_all(tags)
202 books = Book.objects.all()
205 books = books.filter(parent=None)
207 books = books.filter(media__type='mp3').distinct()
209 books = books.filter(media__type='daisy').distinct()
211 books = books.only('slug', 'title', 'cover', 'cover_thumb')
212 for category in book_tag_categories:
213 books = books.prefetch_related(
216 queryset=TagRelation.objects.filter(tag__category=category)
217 .select_related('tag').only('tag__name_pl', 'object_id'),
218 to_attr='%s_relations' % category))
225 def create(self, request, *args, **kwargs):
229 class BooksHandler(BookDetailHandler):
230 allowed_methods = ('GET', 'POST')
232 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
233 anonymous = AnonymousBooksHandler
235 def create(self, request, *args, **kwargs):
236 if not request.user.has_perm('catalogue.add_book'):
239 data = json.loads(request.POST.get('data'))
240 form = BookImportForm(data)
248 class EBooksHandler(AnonymousBooksHandler):
249 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
252 # add categorized tags fields for Book
253 def _tags_getter(category):
255 def get_tags(cls, book):
256 return book.tags.filter(category=category)
260 def _tag_getter(category):
262 def get_tag(cls, book):
263 if hasattr(book, '%s_relations' % category):
264 return ', '.join(rel.tag.name for rel in getattr(book, '%s_relations' % category))
265 return ', '.join(book.tags.filter(category=category).values_list('name', flat=True))
269 def add_tag_getters():
270 for plural, singular in category_singular.items():
271 setattr(BookDetails, plural, _tags_getter(singular))
272 setattr(BookDetails, singular, _tag_getter(singular))
277 # add fields for files in Book
278 def _file_getter(book_format):
279 field = "%s_file" % book_format
282 def get_file(cls, book):
283 f = getattr(book, field)
285 return MEDIA_BASE + f.url
291 def add_file_getters():
292 for book_format in Book.formats:
293 setattr(BookDetails, book_format, _file_getter(book_format))
298 class CollectionDetails(object):
299 """Custom Collection fields."""
302 def href(cls, collection):
303 """ Returns URI in the API for the collection. """
305 return API_BASE + reverse("api_collection", args=[collection.slug])
308 def url(cls, collection):
309 """ Returns URL on the site. """
311 return WL_BASE + collection.get_absolute_url()
314 def books(cls, collection):
315 return Book.objects.filter(collection.get_query())
318 class CollectionDetailHandler(BaseHandler, CollectionDetails):
319 allowed_methods = ('GET',)
320 fields = ['url', 'title', 'description', 'books']
323 def read(self, request, slug):
324 """ Returns details of a collection, identified by slug. """
326 return Collection.objects.get(slug=slug)
327 except Collection.DoesNotExist:
331 class CollectionsHandler(BaseHandler, CollectionDetails):
332 allowed_methods = ('GET',)
334 fields = ['url', 'href', 'title']
337 def read(self, request):
338 """ Returns all collections. """
339 return Collection.objects.all()
342 class TagDetails(object):
343 """Custom Tag fields."""
347 """ Returns URI in the API for the tag. """
349 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
353 """ Returns URL on the site. """
355 return WL_BASE + tag.get_absolute_url()
358 class TagDetailHandler(BaseHandler, TagDetails):
359 """ Responsible for details of a single Tag object. """
361 fields = ['name', 'url', 'sort_key', 'description']
364 def read(self, request, category, slug):
365 """ Returns details of a tag, identified by category and slug. """
368 category_sng = category_singular[category]
373 return Tag.objects.get(category=category_sng, slug=slug)
374 except Tag.DoesNotExist:
378 class TagsHandler(BaseHandler, TagDetails):
379 """ Main handler for Tag objects.
381 Responsible for lists of Tag objects
382 and fields used for representing Tags.
385 allowed_methods = ('GET',)
387 fields = ['name', 'href', 'url']
390 def read(self, request, category=None, pk=None):
391 """ Lists all tags in the category (eg. all themes). """
394 return Tag.objects.exclude(category='set').get(pk=pk)
395 except Book.DoesNotExist:
399 category_sng = category_singular[category]
403 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
410 class FragmentDetails(object):
411 """Custom Fragment fields."""
414 def href(cls, fragment):
415 """ Returns URI in the API for the fragment. """
417 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
420 def url(cls, fragment):
421 """ Returns URL on the site for the fragment. """
423 return WL_BASE + fragment.get_absolute_url()
426 def themes(cls, fragment):
427 """ Returns a list of theme tags for the fragment. """
429 return fragment.tags.filter(category='theme')
432 class FragmentDetailHandler(BaseHandler, FragmentDetails):
433 fields = ['book', 'anchor', 'text', 'url', 'themes']
436 def read(self, request, book, anchor):
437 """ Returns details of a fragment, identified by book slug and anchor. """
439 return Fragment.objects.get(book__slug=book, anchor=anchor)
440 except Fragment.DoesNotExist:
444 class FragmentsHandler(BaseHandler, FragmentDetails):
445 """ Main handler for Fragments.
447 Responsible for lists of Fragment objects
448 and fields used for representing Fragments.
452 fields = ['book', 'url', 'anchor', 'href']
453 allowed_methods = ('GET',)
455 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
458 def read(self, request, tags):
459 """ Lists all fragments with given book, tags, themes.
461 :param tags: should be a path of categories and slugs, i.e.:
462 books/book-slug/authors/an-author/themes/a-theme/
466 tags, ancestors = read_tags(tags, allowed=self.categories)
469 fragments = Fragment.tagged.with_all(tags).select_related('book')
470 if fragments.exists():
476 class PictureHandler(BaseHandler):
478 fields = ('slug', 'title')
479 allowed_methods = ('POST',)
481 def create(self, request):
482 if not request.user.has_perm('picture.add_picture'):
485 data = json.loads(request.POST.get('data'))
486 form = PictureImportForm(data)