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 Q
10 from django.http.response import HttpResponse
11 from django.utils.functional import lazy
12 from django.db import models
13 from migdal.models import Entry
14 from piston.handler import AnonymousBaseHandler, BaseHandler
15 from piston.utils import rc
16 from sorl.thumbnail import default
18 from api.models import BookUserData
19 from catalogue.api.helpers import books_after, order_books
20 from catalogue.forms import BookImportForm
21 from catalogue.models import Book, Tag, BookMedia
22 from catalogue.models.tag import prefetch_relations
23 from paypal.rest import user_is_subscribed
24 from picture.models import Picture
25 from picture.forms import PictureImportForm
26 from social.utils import likes
28 from stats.utils import piwik_track
29 from wolnelektury.utils import re_escape
32 API_BASE = WL_BASE = MEDIA_BASE = lazy(
33 lambda: u'https://' + Site.objects.get_current().domain, unicode)()
44 for k, v in category_singular.items():
45 category_plural[v] = k
47 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
49 book_list_fields = book_tag_categories + [
50 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color', 'full_sort_key']
53 def read_tags(tags, request, allowed):
54 """ Reads a path of filtering tags.
56 :param str tags: a path of category and slug pairs, like: authors/an-author/...
57 :returns: list of Tag objects
58 :raises: ValueError when tags can't be found
61 def process(category, slug):
62 if category == 'book':
65 books.append(Book.objects.get(slug=slug))
66 except Book.DoesNotExist:
67 raise ValueError('Unknown book.')
69 real_tags.append(Tag.objects.get(category=category, slug=slug))
70 except Tag.DoesNotExist:
71 raise ValueError('Tag not found')
76 tags = tags.strip('/').split('/')
80 category = tags.pop(0)
84 category = category_singular[category]
86 raise ValueError('Unknown category.')
88 if category not in allowed:
89 raise ValueError('Category not allowed.')
90 process(category, slug)
92 for key in request.GET:
93 if key in category_singular:
94 category = category_singular[key]
95 if category in allowed:
96 for slug in request.GET.getlist(key):
97 process(category, slug)
98 return real_tags, books
107 class BookDetails(object):
108 """Custom fields used for representing Books."""
112 """ Returns an URI for a Book in the API. """
113 return API_BASE + reverse("catalogue_api_book", args=[book.slug])
117 """ 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. """
123 return book.children.order_by('parent_number', 'sort_key')
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 def simple_thumb(cls, book):
141 return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
144 def simple_cover(cls, book):
145 return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
148 class BookDetailHandler(BaseHandler, BookDetails):
149 """ Main handler for Book objects.
151 Responsible for single Book details.
153 allowed_methods = ['GET']
154 fields = ['title', 'parent', 'children'] + Book.formats + [
155 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
156 'preview', 'cover_color'] + [
157 category_plural[c] for c in book_tag_categories]
160 def read(self, request, book):
161 """ Returns details of a book, identified by a slug and lang. """
163 return Book.objects.get(slug=book)
164 except Book.DoesNotExist:
168 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
169 """ Main handler for Book objects.
171 Responsible for lists of Book objects.
173 allowed_methods = ('GET',)
175 fields = book_list_fields
179 def genres(cls, book):
180 """ Returns all media for a book. """
181 return book.tags.filter(category='genre')
184 def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
185 recommended=False, newest=False, books=None,
186 after=None, count=None):
187 """ Lists all books with given tags.
189 :param tags: filtering tags; should be a path of categories
190 and slugs, i.e.: authors/an-author/epoch/an-epoch/
191 :param top_level: if True and a book is included in the results,
192 it's children are aren't. By default all books matching the tags
198 return Book.objects.get(pk=pk)
199 except Book.DoesNotExist:
203 tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
207 new_api = request.GET.get('new_api')
208 if 'after' in request.GET:
209 after = request.GET['after']
210 if 'count' in request.GET:
211 count = request.GET['count']
215 books = Book.tagged_top_level(tags)
216 return books if books else rc.NOT_FOUND
218 books = Book.tagged.with_all(tags)
220 books = books if books is not None else Book.objects.all()
221 books = order_books(books, new_api)
224 books = books.filter(parent=None)
226 books = books.filter(media__type='mp3').distinct()
228 books = books.filter(media__type='daisy').distinct()
230 books = books.filter(recommended=True)
232 books = books.order_by('-created_at')
235 books = books_after(books, after, new_api)
238 books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author')
240 books = books.only('slug', 'title', 'cover', 'cover_thumb')
241 for category in book_tag_categories:
242 books = prefetch_relations(books, category)
245 books = books[:count]
249 def create(self, request, *args, **kwargs):
253 class BooksHandler(BookDetailHandler):
254 allowed_methods = ('GET', 'POST')
256 fields = book_list_fields + ['liked']
257 anonymous = AnonymousBooksHandler
259 # hack, because piston is stupid
261 def liked(cls, book):
262 return getattr(book, 'liked', None)
264 def read(self, request, **kwargs):
265 books = AnonymousBooksHandler().read(request, **kwargs)
266 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
269 BookProxy(book).set('liked', book.id in likes)
271 return QuerySetProxy(new_books)
273 def create(self, request, *args, **kwargs):
274 if not request.user.has_perm('catalogue.add_book'):
277 data = json.loads(request.POST.get('data'))
278 form = BookImportForm(data)
286 class EBooksHandler(AnonymousBooksHandler):
287 fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
290 class BookProxy(models.Model):
294 def __init__(self, book, key=None):
298 def set(self, attr, value):
299 self.__setattr__(attr, value)
302 def __getattr__(self, item):
303 return self.book.__getattribute__(item)
306 class QuerySetProxy(models.QuerySet):
307 def __init__(self, l):
311 return iter(self.list)
314 class AnonFilterBooksHandler(AnonymousBooksHandler):
315 fields = book_list_fields + ['key']
317 def parse_bool(self, s):
318 if s in ('true', 'false'):
323 def read(self, request):
325 search_string = request.GET.get('search')
326 is_lektura = self.parse_bool(request.GET.get('lektura'))
327 is_audiobook = self.parse_bool(request.GET.get('audiobook'))
328 preview = self.parse_bool(request.GET.get('preview'))
330 new_api = request.GET.get('new_api')
331 after = request.GET.get('after')
332 count = int(request.GET.get('count', 50))
333 books = order_books(Book.objects.distinct(), new_api)
334 if is_lektura is not None:
335 books = books.filter(has_audience=is_lektura)
336 if is_audiobook is not None:
338 books = books.filter(media__type='mp3')
340 books = books.exclude(media__type='mp3')
341 if preview is not None:
342 books = books.filter(preview=preview)
343 for key in request.GET:
344 if key in category_singular:
345 category = category_singular[key]
346 if category in book_tag_categories:
347 slugs = request.GET[key].split(',')
348 tags = Tag.objects.filter(category=category, slug__in=slugs)
349 books = Book.tagged.with_any(tags, books)
350 if (search_string is not None) and len(search_string) < 3:
353 search_string = re_escape(search_string)
354 books_author = books.filter(cached_author__iregex='\m' + search_string)
355 books_title = books.filter(title__iregex='\m' + search_string)
356 books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
357 if after and (key_sep in after):
358 which, key = after.split(key_sep, 1)
360 book_lists = [(books_after(books_title, key, new_api), 'title')]
361 else: # which == 'author'
362 book_lists = [(books_after(books_author, key, new_api), 'author'), (books_title, 'title')]
364 book_lists = [(books_author, 'author'), (books_title, 'title')]
366 if after and key_sep in after:
367 which, key = after.split(key_sep, 1)
368 books = books_after(books, key, new_api)
369 book_lists = [(books, 'book')]
372 for book_list, label in book_lists:
373 book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key_author', 'sort_key')
374 for category in book_tag_categories:
375 book_list = prefetch_relations(book_list, category)
376 remaining_count = count - len(filtered_books)
378 BookProxy(book, '%s%s%s' % (
379 label, key_sep, book.slug if not new_api else book.full_sort_key()))
380 for book in book_list[:remaining_count]]
381 filtered_books += new_books
382 if len(filtered_books) == count:
385 return QuerySetProxy(filtered_books)
388 class FilterBooksHandler(BooksHandler):
389 anonymous = AnonFilterBooksHandler
390 fields = book_list_fields + ['key', 'liked']
392 # hack, because piston is stupid
394 def liked(cls, book):
395 return getattr(book, 'liked', None)
397 def read(self, request):
398 qsp = AnonFilterBooksHandler().read(request)
399 likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
400 for book in qsp.list:
401 book.set('liked', book.id in likes)
405 # add categorized tags fields for Book
406 def _tags_getter(category):
408 def get_tags(cls, book):
409 return book.tags.filter(category=category)
413 def _tag_getter(category):
415 def get_tag(cls, book):
416 return book.tag_unicode(category)
420 def add_tag_getters():
421 for plural, singular in category_singular.items():
422 setattr(BookDetails, plural, _tags_getter(singular))
423 setattr(BookDetails, singular, _tag_getter(singular))
429 # add fields for files in Book
430 def _file_getter(book_format):
434 f_url = book.media_url(book_format)
436 return MEDIA_BASE + f_url
442 def add_file_getters():
443 for book_format in Book.formats:
444 setattr(BookDetails, book_format, _file_getter(book_format))
450 class PictureHandler(BaseHandler):
452 fields = ('slug', 'title')
453 allowed_methods = ('POST',)
455 def create(self, request):
456 if not request.user.has_perm('picture.add_picture'):
459 data = json.loads(request.POST.get('data'))
460 form = PictureImportForm(data)
468 class BlogEntryHandler(BaseHandler):
471 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
473 def read(self, request):
474 after = request.GET.get('after')
475 count = int(request.GET.get('count', 20))
476 entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
478 entries = entries.filter(first_published_at__lt=after)
480 entries = entries[:count]
484 def image_url(cls, entry):
485 return (WL_BASE + entry.image.url) if entry.image else None
488 def image_thumb(cls, entry):
489 return MEDIA_BASE + default.backend.get_thumbnail(
490 entry.image, "193x193").url if entry.image else ''
493 def gallery_urls(cls, entry):
494 return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
498 return entry.first_published_at
502 return WL_BASE + entry.get_absolute_url()