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. """
126 return WL_BASE + book.get_absolute_url()
129 def children(cls, book):
130 """ Returns all children for a book. """
132 return book.children.all()
135 def media(cls, book):
136 """ Returns all media for a book. """
137 return book.media.all()
140 def cover(cls, book):
141 return MEDIA_BASE + book.cover.url if book.cover else ''
144 def cover_thumb(cls, book):
145 return MEDIA_BASE + default.backend.get_thumbnail(
146 book.cover, "139x193").url if book.cover else ''
149 def cover_source_image(cls, book):
150 url = book.cover_source()
151 return url.rstrip('/') + '/file/'
154 class BookDetailHandler(BaseHandler, BookDetails):
155 """ Main handler for Book objects.
157 Responsible for single Book details.
159 allowed_methods = ['GET']
160 fields = ['title', 'parent', 'children'] + Book.formats + [
161 'media', 'url', 'cover', 'cover_thumb'] + [
162 category_plural[c] for c in book_tag_categories]
165 def read(self, request, book):
166 """ Returns details of a book, identified by a slug and lang. """
168 return Book.objects.get(slug=book)
169 except Book.DoesNotExist:
173 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
174 """ Main handler for Book objects.
176 Responsible for lists of Book objects.
178 allowed_methods = ('GET',)
180 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
183 def genres(cls, book):
184 """ Returns all media for a book. """
185 return book.tags.filter(category='genre')
188 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
189 recommended=False, newest=False, books=None,
190 after=None, before=None, count=None):
191 """ Lists all books with given tags.
193 :param tags: filtering tags; should be a path of categories
194 and slugs, i.e.: authors/an-author/epoch/an-epoch/
195 :param top_level: if True and a book is included in the results,
196 it's children are aren't. By default all books matching the tags
201 return Book.objects.get(pk=pk)
202 except Book.DoesNotExist:
206 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
210 if 'after' in request.GET:
211 after = request.GET['after']
212 if 'before' in request.GET:
213 before = request.GET['before']
214 if 'count' in request.GET:
215 count = request.GET['count']
219 books = Book.tagged_top_level(tags)
220 return books if books else rc.NOT_FOUND
222 books = Book.tagged.with_all(tags)
224 books = books if books is not None else Book.objects.all()
225 books = books.order_by('slug')
228 books = books.filter(parent=None)
230 books = books.filter(media__type='mp3').distinct()
232 books = books.filter(media__type='daisy').distinct()
234 books = books.filter(recommended=True)
236 books = books.order_by('-created_at')
239 books = books.filter(slug__gt=after)
241 books = books.filter(slug__lt=before)
243 books = books.only('slug', 'title', 'cover', 'cover_thumb')
244 for category in book_tag_categories:
245 books = prefetch_relations(books, category)
249 books = list(reversed(books.order_by('-slug')[:count]))
251 books = books[:count]
255 def create(self, request, *args, **kwargs):
259 class BooksHandler(BookDetailHandler):
260 allowed_methods = ('GET', 'POST')
262 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
263 anonymous = AnonymousBooksHandler
265 def create(self, request, *args, **kwargs):
266 if not request.user.has_perm('catalogue.add_book'):
269 data = json.loads(request.POST.get('data'))
270 form = BookImportForm(data)
278 class EBooksHandler(AnonymousBooksHandler):
279 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
282 class FilterBooksHandler(AnonymousBooksHandler):
283 fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'cover_source_image']
285 def read(self, request, title_part=None, author_part=None, is_lektura=None, is_audiobook=None,
286 after=None, before=None, count=None):
287 if 'title_part' in request.GET:
288 title_part = request.GET['title_part']
289 if 'author_part' in request.GET:
290 author_part = request.GET['author_part']
291 if 'is_lektura' in request.GET:
292 is_lektura = request.GET['is_lektura']
293 if 'is_audiobook' in request.GET:
294 is_audiobook = request.GET['is_audiobook']
298 if is_lektura in ('true', 'false'):
299 is_lektura = is_lektura == 'true'
302 if is_audiobook in ('true', 'false'):
303 is_audiobook = is_audiobook == 'true'
304 books = Book.objects.distinct()
306 books = books.filter(title__iregex='\m' + title_part)
307 if author_part is not None:
308 books = books.filter(cached_author__iregex='\m' + author_part)
309 if is_lektura is not None:
310 books = books.filter(has_audience=is_lektura)
311 if is_audiobook is not None:
313 books = books.filter(media__type='mp3')
315 books = books.exclude(media__type='mp3')
316 return super(FilterBooksHandler, self).read(request, books=books, after=after, before=before, count=count)
319 # add categorized tags fields for Book
320 def _tags_getter(category):
322 def get_tags(cls, book):
323 return book.tags.filter(category=category)
327 def _tag_getter(category):
329 def get_tag(cls, book):
330 return book.tag_unicode(category)
334 def add_tag_getters():
335 for plural, singular in category_singular.items():
336 setattr(BookDetails, plural, _tags_getter(singular))
337 setattr(BookDetails, singular, _tag_getter(singular))
342 # add fields for files in Book
343 def _file_getter(book_format):
344 field = "%s_file" % book_format
347 def get_file(cls, book):
348 f = getattr(book, field)
350 return MEDIA_BASE + f.url
356 def add_file_getters():
357 for book_format in Book.formats:
358 setattr(BookDetails, book_format, _file_getter(book_format))
363 class CollectionDetails(object):
364 """Custom Collection fields."""
367 def href(cls, collection):
368 """ Returns URI in the API for the collection. """
370 return API_BASE + reverse("api_collection", args=[collection.slug])
373 def url(cls, collection):
374 """ Returns URL on the site. """
376 return WL_BASE + collection.get_absolute_url()
379 def books(cls, collection):
380 return Book.objects.filter(collection.get_query())
383 class CollectionDetailHandler(BaseHandler, CollectionDetails):
384 allowed_methods = ('GET',)
385 fields = ['url', 'title', 'description', 'books']
388 def read(self, request, slug):
389 """ Returns details of a collection, identified by slug. """
391 return Collection.objects.get(slug=slug)
392 except Collection.DoesNotExist:
396 class CollectionsHandler(BaseHandler, CollectionDetails):
397 allowed_methods = ('GET',)
399 fields = ['url', 'href', 'title']
402 def read(self, request):
403 """ Returns all collections. """
404 return Collection.objects.all()
407 class TagDetails(object):
408 """Custom Tag fields."""
412 """ Returns URI in the API for the tag. """
414 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
418 """ Returns URL on the site. """
420 return WL_BASE + tag.get_absolute_url()
423 class TagDetailHandler(BaseHandler, TagDetails):
424 """ Responsible for details of a single Tag object. """
426 fields = ['name', 'url', 'sort_key', 'description']
429 def read(self, request, category, slug):
430 """ Returns details of a tag, identified by category and slug. """
433 category_sng = category_singular[category]
438 return Tag.objects.get(category=category_sng, slug=slug)
439 except Tag.DoesNotExist:
443 class TagsHandler(BaseHandler, TagDetails):
444 """ Main handler for Tag objects.
446 Responsible for lists of Tag objects
447 and fields used for representing Tags.
450 allowed_methods = ('GET',)
452 fields = ['name', 'href', 'url', 'slug']
455 def read(self, request, category=None, pk=None):
456 """ Lists all tags in the category (eg. all themes). """
459 return Tag.objects.exclude(category='set').get(pk=pk)
460 except Book.DoesNotExist:
464 category_sng = category_singular[category]
468 after = request.GET.get('after')
469 before = request.GET.get('before')
470 count = request.GET.get('count')
472 tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
475 tags = tags.filter(slug__gt=after)
477 tags = tags.filter(slug__lt=before)
481 tags = list(reversed(tags.order_by('-slug')[:count]))
488 class FragmentDetails(object):
489 """Custom Fragment fields."""
492 def href(cls, fragment):
493 """ Returns URI in the API for the fragment. """
495 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
498 def url(cls, fragment):
499 """ Returns URL on the site for the fragment. """
501 return WL_BASE + fragment.get_absolute_url()
504 def themes(cls, fragment):
505 """ Returns a list of theme tags for the fragment. """
507 return fragment.tags.filter(category='theme')
510 class FragmentDetailHandler(BaseHandler, FragmentDetails):
511 fields = ['book', 'anchor', 'text', 'url', 'themes']
514 def read(self, request, book, anchor):
515 """ Returns details of a fragment, identified by book slug and anchor. """
517 return Fragment.objects.get(book__slug=book, anchor=anchor)
518 except Fragment.DoesNotExist:
522 class FragmentsHandler(BaseHandler, FragmentDetails):
523 """ Main handler for Fragments.
525 Responsible for lists of Fragment objects
526 and fields used for representing Fragments.
530 fields = ['book', 'url', 'anchor', 'href']
531 allowed_methods = ('GET',)
533 categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
536 def read(self, request, tags):
537 """ Lists all fragments with given book, tags, themes.
539 :param tags: should be a path of categories and slugs, i.e.:
540 books/book-slug/authors/an-author/themes/a-theme/
544 tags, ancestors = read_tags(tags, allowed=self.categories)
547 fragments = Fragment.tagged.with_all(tags).select_related('book')
548 if fragments.exists():
554 class PictureHandler(BaseHandler):
556 fields = ('slug', 'title')
557 allowed_methods = ('POST',)
559 def create(self, request):
560 if not request.user.has_perm('picture.add_picture'):
563 data = json.loads(request.POST.get('data'))
564 form = PictureImportForm(data)