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, books=None,
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 = books if books is not None else 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 class FilterBooksHandler(AnonymousBooksHandler):
265 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
267 def read(self, request, title_part=None, author_part=None, is_lektura=None, is_audiobook=None,
268 after=None, before=None, count=None):
271 if is_lektura in ('true', 'false'):
272 is_lektura = is_lektura == 'true'
275 if is_audiobook in ('true', 'false'):
276 is_audiobook = is_audiobook == 'true'
277 books = Book.objects.distinct()
279 books = books.filter(title__icontains=title_part)
280 if author_part is not None:
281 books = books.filter(cached_author__icontains=author_part)
282 if is_lektura is not None:
283 books = books.filter(has_audience=is_lektura)
284 if is_audiobook is not None:
286 books = books.filter(media__type='mp3')
288 books = books.exclude(media__type='mp3')
289 return super(FilterBooksHandler, self).read(request, books=books, after=after, before=before, count=count)
292 # add categorized tags fields for Book
293 def _tags_getter(category):
295 def get_tags(cls, book):
296 return book.tags.filter(category=category)
300 def _tag_getter(category):
302 def get_tag(cls, book):
303 return book.tag_unicode(category)
307 def add_tag_getters():
308 for plural, singular in category_singular.items():
309 setattr(BookDetails, plural, _tags_getter(singular))
310 setattr(BookDetails, singular, _tag_getter(singular))
315 # add fields for files in Book
316 def _file_getter(book_format):
317 field = "%s_file" % book_format
320 def get_file(cls, book):
321 f = getattr(book, field)
323 return MEDIA_BASE + f.url
329 def add_file_getters():
330 for book_format in Book.formats:
331 setattr(BookDetails, book_format, _file_getter(book_format))
336 class CollectionDetails(object):
337 """Custom Collection fields."""
340 def href(cls, collection):
341 """ Returns URI in the API for the collection. """
343 return API_BASE + reverse("api_collection", args=[collection.slug])
346 def url(cls, collection):
347 """ Returns URL on the site. """
349 return WL_BASE + collection.get_absolute_url()
352 def books(cls, collection):
353 return Book.objects.filter(collection.get_query())
356 class CollectionDetailHandler(BaseHandler, CollectionDetails):
357 allowed_methods = ('GET',)
358 fields = ['url', 'title', 'description', 'books']
361 def read(self, request, slug):
362 """ Returns details of a collection, identified by slug. """
364 return Collection.objects.get(slug=slug)
365 except Collection.DoesNotExist:
369 class CollectionsHandler(BaseHandler, CollectionDetails):
370 allowed_methods = ('GET',)
372 fields = ['url', 'href', 'title']
375 def read(self, request):
376 """ Returns all collections. """
377 return Collection.objects.all()
380 class TagDetails(object):
381 """Custom Tag fields."""
385 """ Returns URI in the API for the tag. """
387 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
391 """ Returns URL on the site. """
393 return WL_BASE + tag.get_absolute_url()
396 class TagDetailHandler(BaseHandler, TagDetails):
397 """ Responsible for details of a single Tag object. """
399 fields = ['name', 'url', 'sort_key', 'description']
402 def read(self, request, category, slug):
403 """ Returns details of a tag, identified by category and slug. """
406 category_sng = category_singular[category]
411 return Tag.objects.get(category=category_sng, slug=slug)
412 except Tag.DoesNotExist:
416 class TagsHandler(BaseHandler, TagDetails):
417 """ Main handler for Tag objects.
419 Responsible for lists of Tag objects
420 and fields used for representing Tags.
423 allowed_methods = ('GET',)
425 fields = ['name', 'href', 'url']
428 def read(self, request, category=None, pk=None):
429 """ Lists all tags in the category (eg. all themes). """
432 return Tag.objects.exclude(category='set').get(pk=pk)
433 except Book.DoesNotExist:
437 category_sng = category_singular[category]
441 tags = Tag.objects.filter(category=category_sng).exclude(items=None)
448 class FragmentDetails(object):
449 """Custom Fragment fields."""
452 def href(cls, fragment):
453 """ Returns URI in the API for the fragment. """
455 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
458 def url(cls, fragment):
459 """ Returns URL on the site for the fragment. """
461 return WL_BASE + fragment.get_absolute_url()
464 def themes(cls, fragment):
465 """ Returns a list of theme tags for the fragment. """
467 return fragment.tags.filter(category='theme')
470 class FragmentDetailHandler(BaseHandler, FragmentDetails):
471 fields = ['book', 'anchor', 'text', 'url', 'themes']
474 def read(self, request, book, anchor):
475 """ Returns details of a fragment, identified by book slug and anchor. """
477 return Fragment.objects.get(book__slug=book, anchor=anchor)
478 except Fragment.DoesNotExist:
482 class FragmentsHandler(BaseHandler, FragmentDetails):
483 """ Main handler for Fragments.
485 Responsible for lists of Fragment objects
486 and fields used for representing Fragments.
490 fields = ['book', 'url', 'anchor', 'href']
491 allowed_methods = ('GET',)
493 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
496 def read(self, request, tags):
497 """ Lists all fragments with given book, tags, themes.
499 :param tags: should be a path of categories and slugs, i.e.:
500 books/book-slug/authors/an-author/themes/a-theme/
504 tags, ancestors = read_tags(tags, allowed=self.categories)
507 fragments = Fragment.tagged.with_all(tags).select_related('book')
508 if fragments.exists():
514 class PictureHandler(BaseHandler):
516 fields = ('slug', 'title')
517 allowed_methods = ('POST',)
519 def create(self, request):
520 if not request.user.has_perm('picture.add_picture'):
523 data = json.loads(request.POST.get('data'))
524 form = PictureImportForm(data)