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, request, 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
51 def process(category, slug):
52 if category == 'book':
54 books.append(Book.objects.get(slug=slug))
55 except Book.DoesNotExist:
56 raise ValueError('Unknown book.')
58 real_tags.append(Tag.objects.get(category=category, slug=slug))
59 except Tag.DoesNotExist:
60 raise ValueError('Tag not found')
65 tags = tags.strip('/').split('/')
69 category = tags.pop(0)
73 category = category_singular[category]
75 raise ValueError('Unknown category.')
77 if category not in allowed:
78 raise ValueError('Category not allowed.')
79 process(category, slug)
81 for key in request.GET:
82 if key in category_singular:
83 category = category_singular[key]
84 if category in allowed:
85 for slug in request.GET.getlist(key):
86 process(category, slug)
87 return real_tags, books
93 class BookMediaHandler(BaseHandler):
94 """ Responsible for representing media in Books. """
97 fields = ['name', 'type', 'url', 'artist', 'director']
101 """ Link to media on site. """
103 return MEDIA_BASE + media.file.url
106 def artist(cls, media):
107 return media.extra_info.get('artist_name', '')
110 def director(cls, media):
111 return media.extra_info.get('director_name', '')
114 class BookDetails(object):
115 """Custom fields used for representing Books."""
119 """ Returns an URI for a Book in the API. """
120 return API_BASE + reverse("api_book", args=[book.slug])
124 """ Returns Book's URL on the site. """
125 return WL_BASE + book.get_absolute_url()
128 def children(cls, book):
129 """ Returns all children for a book. """
130 return book.children.order_by('parent_number', 'sort_key')
133 def media(cls, book):
134 """ Returns all media for a book. """
135 return book.media.all()
138 def cover(cls, book):
139 return MEDIA_BASE + book.cover.url if book.cover else ''
142 def cover_thumb(cls, book):
143 return MEDIA_BASE + default.backend.get_thumbnail(
144 book.cover, "139x193").url if book.cover else ''
147 def cover_source_image(cls, book):
148 url = book.cover_source()
149 return url.rstrip('/') + '/file/'
152 class BookDetailHandler(BaseHandler, BookDetails):
153 """ Main handler for Book objects.
155 Responsible for single Book details.
157 allowed_methods = ['GET']
158 fields = ['title', 'parent', 'children'] + Book.formats + [
159 'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [
160 category_plural[c] for c in book_tag_categories]
163 def read(self, request, book):
164 """ Returns details of a book, identified by a slug and lang. """
166 return Book.objects.get(slug=book)
167 except Book.DoesNotExist:
171 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
172 """ Main handler for Book objects.
174 Responsible for lists of Book objects.
176 allowed_methods = ('GET',)
178 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
181 def genres(cls, book):
182 """ Returns all media for a book. """
183 return book.tags.filter(category='genre')
186 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
187 recommended=False, newest=False, books=None,
188 after=None, before=None, count=None):
189 """ Lists all books with given tags.
191 :param tags: filtering tags; should be a path of categories
192 and slugs, i.e.: authors/an-author/epoch/an-epoch/
193 :param top_level: if True and a book is included in the results,
194 it's children are aren't. By default all books matching the tags
199 return Book.objects.get(pk=pk)
200 except Book.DoesNotExist:
204 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
208 if 'after' in request.GET:
209 after = request.GET['after']
210 if 'before' in request.GET:
211 before = request.GET['before']
212 if 'count' in request.GET:
213 count = request.GET['count']
217 books = Book.tagged_top_level(tags)
218 return books if books else rc.NOT_FOUND
220 books = Book.tagged.with_all(tags)
222 books = books if books is not None else Book.objects.all()
223 books = books.order_by('slug')
226 books = books.filter(parent=None)
228 books = books.filter(media__type='mp3').distinct()
230 books = books.filter(media__type='daisy').distinct()
232 books = books.filter(recommended=True)
234 books = books.order_by('-created_at')
237 books = books.filter(slug__gt=after)
239 books = books.filter(slug__lt=before)
241 books = books.only('slug', 'title', 'cover', 'cover_thumb')
242 for category in book_tag_categories:
243 books = prefetch_relations(books, category)
247 books = list(reversed(books.order_by('-slug')[:count]))
249 books = books[:count]
253 def create(self, request, *args, **kwargs):
257 class BooksHandler(BookDetailHandler):
258 allowed_methods = ('GET', 'POST')
260 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
261 anonymous = AnonymousBooksHandler
263 def create(self, request, *args, **kwargs):
264 if not request.user.has_perm('catalogue.add_book'):
267 data = json.loads(request.POST.get('data'))
268 form = BookImportForm(data)
276 class EBooksHandler(AnonymousBooksHandler):
277 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
280 class FilterBooksHandler(AnonymousBooksHandler):
281 fields = book_tag_categories + [
282 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'cover_source_image']
284 def read(self, request, title_part=None, author_part=None, is_lektura=None, is_audiobook=None,
285 after=None, before=None, count=None):
286 if 'title_part' in request.GET:
287 title_part = request.GET['title_part']
288 if 'author_part' in request.GET:
289 author_part = request.GET['author_part']
290 if 'is_lektura' in request.GET:
291 is_lektura = request.GET['is_lektura']
292 if 'is_audiobook' in request.GET:
293 is_audiobook = request.GET['is_audiobook']
297 if is_lektura in ('true', 'false'):
298 is_lektura = is_lektura == 'true'
301 if is_audiobook in ('true', 'false'):
302 is_audiobook = is_audiobook == 'true'
303 books = Book.objects.distinct()
305 books = books.filter(title__iregex='\m' + title_part)
306 if author_part is not None:
307 books = books.filter(cached_author__iregex='\m' + author_part)
308 if is_lektura is not None:
309 books = books.filter(has_audience=is_lektura)
310 if is_audiobook is not None:
312 books = books.filter(media__type='mp3')
314 books = books.exclude(media__type='mp3')
315 return super(FilterBooksHandler, self).read(request, books=books, after=after, before=before, count=count)
318 # add categorized tags fields for Book
319 def _tags_getter(category):
321 def get_tags(cls, book):
322 return book.tags.filter(category=category)
326 def _tag_getter(category):
328 def get_tag(cls, book):
329 return book.tag_unicode(category)
333 def add_tag_getters():
334 for plural, singular in category_singular.items():
335 setattr(BookDetails, plural, _tags_getter(singular))
336 setattr(BookDetails, singular, _tag_getter(singular))
341 # add fields for files in Book
342 def _file_getter(book_format):
343 field = "%s_file" % book_format
346 def get_file(cls, book):
347 f = getattr(book, field)
349 return MEDIA_BASE + f.url
355 def add_file_getters():
356 for book_format in Book.formats:
357 setattr(BookDetails, book_format, _file_getter(book_format))
362 class CollectionDetails(object):
363 """Custom Collection fields."""
366 def href(cls, collection):
367 """ Returns URI in the API for the collection. """
369 return API_BASE + reverse("api_collection", args=[collection.slug])
372 def url(cls, collection):
373 """ Returns URL on the site. """
375 return WL_BASE + collection.get_absolute_url()
378 def books(cls, collection):
379 return Book.objects.filter(collection.get_query())
382 class CollectionDetailHandler(BaseHandler, CollectionDetails):
383 allowed_methods = ('GET',)
384 fields = ['url', 'title', 'description', 'books']
387 def read(self, request, slug):
388 """ Returns details of a collection, identified by slug. """
390 return Collection.objects.get(slug=slug)
391 except Collection.DoesNotExist:
395 class CollectionsHandler(BaseHandler, CollectionDetails):
396 allowed_methods = ('GET',)
398 fields = ['url', 'href', 'title']
401 def read(self, request):
402 """ Returns all collections. """
403 return Collection.objects.all()
406 class TagDetails(object):
407 """Custom Tag fields."""
411 """ Returns URI in the API for the tag. """
413 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
417 """ Returns URL on the site. """
419 return WL_BASE + tag.get_absolute_url()
422 class TagDetailHandler(BaseHandler, TagDetails):
423 """ Responsible for details of a single Tag object. """
425 fields = ['name', 'url', 'sort_key', 'description']
428 def read(self, request, category, slug):
429 """ Returns details of a tag, identified by category and slug. """
432 category_sng = category_singular[category]
437 return Tag.objects.get(category=category_sng, slug=slug)
438 except Tag.DoesNotExist:
442 class TagsHandler(BaseHandler, TagDetails):
443 """ Main handler for Tag objects.
445 Responsible for lists of Tag objects
446 and fields used for representing Tags.
449 allowed_methods = ('GET',)
451 fields = ['name', 'href', 'url', 'slug']
454 def read(self, request, category=None, pk=None):
455 """ Lists all tags in the category (eg. all themes). """
458 return Tag.objects.exclude(category='set').get(pk=pk)
459 except Book.DoesNotExist:
463 category_sng = category_singular[category]
467 after = request.GET.get('after')
468 before = request.GET.get('before')
469 count = request.GET.get('count')
471 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
474 tags = tags.filter(slug__gt=after)
476 tags = tags.filter(slug__lt=before)
480 tags = list(reversed(tags.order_by('-slug')[:count]))
487 class FragmentDetails(object):
488 """Custom Fragment fields."""
491 def href(cls, fragment):
492 """ Returns URI in the API for the fragment. """
494 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
497 def url(cls, fragment):
498 """ Returns URL on the site for the fragment. """
500 return WL_BASE + fragment.get_absolute_url()
503 def themes(cls, fragment):
504 """ Returns a list of theme tags for the fragment. """
506 return fragment.tags.filter(category='theme')
509 class FragmentDetailHandler(BaseHandler, FragmentDetails):
510 fields = ['book', 'anchor', 'text', 'url', 'themes']
513 def read(self, request, book, anchor):
514 """ Returns details of a fragment, identified by book slug and anchor. """
516 return Fragment.objects.get(book__slug=book, anchor=anchor)
517 except Fragment.DoesNotExist:
521 class FragmentsHandler(BaseHandler, FragmentDetails):
522 """ Main handler for Fragments.
524 Responsible for lists of Fragment objects
525 and fields used for representing Fragments.
529 fields = ['book', 'url', 'anchor', 'href']
530 allowed_methods = ('GET',)
532 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
535 def read(self, request, tags):
536 """ Lists all fragments with given book, tags, themes.
538 :param tags: should be a path of categories and slugs, i.e.:
539 books/book-slug/authors/an-author/themes/a-theme/
543 tags, ancestors = read_tags(tags, allowed=self.categories)
546 fragments = Fragment.tagged.with_all(tags).select_related('book')
547 if fragments.exists():
553 class PictureHandler(BaseHandler):
555 fields = ('slug', 'title')
556 allowed_methods = ('POST',)
558 def create(self, request):
559 if not request.user.has_perm('picture.add_picture'):
562 data = json.loads(request.POST.get('data'))
563 form = PictureImportForm(data)