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.http.response import HttpResponse
 
  10 from django.utils.functional import lazy
 
  11 from django.db import models
 
  12 from migdal.models import Entry
 
  13 from piston.handler import AnonymousBaseHandler, BaseHandler
 
  14 from piston.utils import rc
 
  15 from sorl.thumbnail import default
 
  17 from api.models import BookUserData
 
  18 from catalogue.forms import BookImportForm
 
  19 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 
  20 from catalogue.models.tag import prefetch_relations
 
  21 from catalogue.utils import is_subscribed
 
  22 from librarian.cover import WLCover
 
  23 from picture.models import Picture
 
  24 from picture.forms import PictureImportForm
 
  25 from social.utils import likes
 
  27 from stats.utils import piwik_track
 
  28 from wolnelektury.utils import re_escape
 
  30 from . import emitters  # Register our emitters
 
  32 API_BASE = WL_BASE = MEDIA_BASE = lazy(
 
  33     lambda: u'https://' + Site.objects.get_current().domain, unicode)()
 
  45 for k, v in category_singular.items():
 
  46     category_plural[v] = k
 
  48 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
 
  50 book_list_fields = book_tag_categories + [
 
  51     'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color']
 
  54 def read_tags(tags, request, allowed):
 
  55     """ Reads a path of filtering tags.
 
  57     :param str tags: a path of category and slug pairs, like: authors/an-author/...
 
  58     :returns: list of Tag objects
 
  59     :raises: ValueError when tags can't be found
 
  62     def process(category, slug):
 
  63         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
 
 104 class BookMediaHandler(BaseHandler):
 
 105     """ Responsible for representing media in Books. """
 
 108     fields = ['name', 'type', 'url', 'artist', 'director']
 
 112         """ Link to media on site. """
 
 114         return MEDIA_BASE + media.file.url
 
 117     def artist(cls, media):
 
 118         return media.extra_info.get('artist_name', '')
 
 121     def director(cls, media):
 
 122         return media.extra_info.get('director_name', '')
 
 125 class BookDetails(object):
 
 126     """Custom fields used for representing Books."""
 
 130         """ Returns an URI for a Book in the API. """
 
 131         return API_BASE + reverse("api_book", args=[book.slug])
 
 135         """ Returns Book's URL on the site. """
 
 136         return WL_BASE + book.get_absolute_url()
 
 139     def children(cls, book):
 
 140         """ Returns all children for a book. """
 
 141         return book.children.order_by('parent_number', 'sort_key')
 
 144     def media(cls, book):
 
 145         """ Returns all media for a book. """
 
 146         return book.media.all()
 
 149     def cover(cls, book):
 
 150         return MEDIA_BASE + book.cover.url if book.cover else ''
 
 153     def cover_thumb(cls, book):
 
 154         return MEDIA_BASE + default.backend.get_thumbnail(
 
 155                     book.cover, "139x193").url if book.cover else ''
 
 158     def simple_thumb(cls, book):
 
 159         return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
 
 162     def simple_cover(cls, book):
 
 163         return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
 
 166     def cover_color(cls, book):
 
 167         return WLCover.epoch_colors.get(book.extra_info.get('epoch'), '#000000')
 
 170 class BookDetailHandler(BaseHandler, BookDetails):
 
 171     """ Main handler for Book objects.
 
 173     Responsible for single Book details.
 
 175     allowed_methods = ['GET']
 
 176     fields = ['title', 'parent', 'children'] + Book.formats + [
 
 177         'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
 
 178         'preview', 'cover_color'] + [
 
 179             category_plural[c] for c in book_tag_categories]
 
 182     def read(self, request, book):
 
 183         """ Returns details of a book, identified by a slug and lang. """
 
 185             return Book.objects.get(slug=book)
 
 186         except Book.DoesNotExist:
 
 190 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
 
 191     """ Main handler for Book objects.
 
 193     Responsible for lists of Book objects.
 
 195     allowed_methods = ('GET',)
 
 197     fields = book_list_fields
 
 200     def genres(cls, book):
 
 201         """ Returns all media for a book. """
 
 202         return book.tags.filter(category='genre')
 
 205     def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
 
 206              recommended=False, newest=False, books=None,
 
 207              after=None, count=None):
 
 208         """ Lists all books with given tags.
 
 210         :param tags: filtering tags; should be a path of categories
 
 211              and slugs, i.e.: authors/an-author/epoch/an-epoch/
 
 212         :param top_level: if True and a book is included in the results,
 
 213              it's children are aren't. By default all books matching the tags
 
 218                 return Book.objects.get(pk=pk)
 
 219             except Book.DoesNotExist:
 
 223             tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
 
 227         if 'after' in request.GET:
 
 228             after = request.GET['after']
 
 229         if 'count' in request.GET:
 
 230             count = request.GET['count']
 
 234                 books = Book.tagged_top_level(tags)
 
 235                 return books if books else rc.NOT_FOUND
 
 237                 books = Book.tagged.with_all(tags)
 
 239             books = books if books is not None else Book.objects.all()
 
 240         books = books.order_by('slug')
 
 243             books = books.filter(parent=None)
 
 245             books = books.filter(media__type='mp3').distinct()
 
 247             books = books.filter(media__type='daisy').distinct()
 
 249             books = books.filter(recommended=True)
 
 251             books = books.order_by('-created_at')
 
 255             books = books.filter(slug__gt=after)
 
 257         books = books.only('slug', 'title', 'cover', 'cover_thumb')
 
 258         for category in book_tag_categories:
 
 259             books = prefetch_relations(books, category)
 
 262             books = books[:count]
 
 266     def create(self, request, *args, **kwargs):
 
 270 class BooksHandler(BookDetailHandler):
 
 271     allowed_methods = ('GET', 'POST')
 
 273     fields = book_list_fields + ['liked']
 
 274     anonymous = AnonymousBooksHandler
 
 276     # hack, because piston is stupid
 
 278     def liked(cls, book):
 
 279         return getattr(book, 'liked', None)
 
 281     def read(self, request, **kwargs):
 
 282         books = AnonymousBooksHandler().read(request, **kwargs)
 
 283         likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
 
 286             BookProxy(book).set('liked', book.id in likes)
 
 288         return QuerySetProxy(new_books)
 
 290     def create(self, request, *args, **kwargs):
 
 291         if not request.user.has_perm('catalogue.add_book'):
 
 294         data = json.loads(request.POST.get('data'))
 
 295         form = BookImportForm(data)
 
 303 class EpubHandler(BookDetailHandler):
 
 304     def read(self, request, slug):
 
 305         if not is_subscribed(request.user):
 
 308             book = Book.objects.get(slug=slug)
 
 309         except Book.DoesNotExist:
 
 311         response = HttpResponse(book.get_media('epub'))
 
 315 class EBooksHandler(AnonymousBooksHandler):
 
 316     fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
 
 319 class BookProxy(models.Model):
 
 323     def __init__(self, book, key=None):
 
 327     def set(self, attr, value):
 
 328         self.__setattr__(attr, value)
 
 331     def __getattr__(self, item):
 
 332         return self.book.__getattribute__(item)
 
 335 class QuerySetProxy(models.QuerySet):
 
 336     def __init__(self, l):
 
 340         return iter(self.list)
 
 343 class AnonFilterBooksHandler(AnonymousBooksHandler):
 
 344     fields = book_list_fields + ['key']
 
 346     def parse_bool(self, s):
 
 347         if s in ('true', 'false'):
 
 352     def read(self, request):
 
 354         search_string = request.GET.get('search')
 
 355         is_lektura = self.parse_bool(request.GET.get('lektura'))
 
 356         is_audiobook = self.parse_bool(request.GET.get('audiobook'))
 
 357         preview = self.parse_bool(request.GET.get('preview'))
 
 359         after = request.GET.get('after')
 
 360         count = int(request.GET.get('count', 50))
 
 361         books = Book.objects.distinct().order_by('slug')
 
 362         if is_lektura is not None:
 
 363             books = books.filter(has_audience=is_lektura)
 
 364         if is_audiobook is not None:
 
 366                 books = books.filter(media__type='mp3')
 
 368                 books = books.exclude(media__type='mp3')
 
 369         if preview is not None:
 
 370             books = books.filter(preview=preview)
 
 371         for key in request.GET:
 
 372             if key in category_singular:
 
 373                 category = category_singular[key]
 
 374                 if category in book_tag_categories:
 
 375                     slugs = request.GET[key].split(',')
 
 376                     tags = Tag.objects.filter(category=category, slug__in=slugs)
 
 377                     books = Book.tagged.with_any(tags, books)
 
 378         if (search_string is not None) and len(search_string) < 3:
 
 381             search_string = re_escape(search_string)
 
 382             books_author = books.filter(cached_author__iregex='\m' + search_string)
 
 383             books_title = books.filter(title__iregex='\m' + search_string)
 
 384             books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
 
 385             if after and (key_sep in after):
 
 386                 which, slug = after.split(key_sep, 1)
 
 388                     book_lists = [(books_title.filter(slug__gt=slug), 'title')]
 
 389                 else:  # which == 'author'
 
 390                     book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
 
 392                 book_lists = [(books_author, 'author'), (books_title, 'title')]
 
 394             if after and key_sep in after:
 
 395                 which, slug = after.split(key_sep, 1)
 
 396                 books = books.filter(slug__gt=slug)
 
 397             book_lists = [(books, 'book')]
 
 400         for book_list, label in book_lists:
 
 401             book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
 
 402             for category in book_tag_categories:
 
 403                 book_list = prefetch_relations(book_list, category)
 
 404             remaining_count = count - len(filtered_books)
 
 405             new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
 
 406                          for book in book_list[:remaining_count]]
 
 407             filtered_books += new_books
 
 408             if len(filtered_books) == count:
 
 411         return QuerySetProxy(filtered_books)
 
 414 class FilterBooksHandler(BooksHandler):
 
 415     anonymous = AnonFilterBooksHandler
 
 416     fields = book_list_fields + ['key', 'liked']
 
 418     # hack, because piston is stupid
 
 420     def liked(cls, book):
 
 421         return getattr(book, 'liked', None)
 
 423     def read(self, request):
 
 424         qsp = AnonFilterBooksHandler().read(request)
 
 425         likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
 
 426         for book in qsp.list:
 
 427             book.set('liked', book.id in likes)
 
 431 class BookPreviewHandler(BookDetailHandler):
 
 432     fields = BookDetailHandler.fields + ['slug']
 
 434     def read(self, request):
 
 435         return Book.objects.filter(preview=True)
 
 438 # add categorized tags fields for Book
 
 439 def _tags_getter(category):
 
 441     def get_tags(cls, book):
 
 442         return book.tags.filter(category=category)
 
 446 def _tag_getter(category):
 
 448     def get_tag(cls, book):
 
 449         return book.tag_unicode(category)
 
 453 def add_tag_getters():
 
 454     for plural, singular in category_singular.items():
 
 455         setattr(BookDetails, plural, _tags_getter(singular))
 
 456         setattr(BookDetails, singular, _tag_getter(singular))
 
 462 # add fields for files in Book
 
 463 def _file_getter(book_format):
 
 467         f_url = book.media_url(book_format)
 
 469             return MEDIA_BASE + f_url
 
 475 def add_file_getters():
 
 476     for book_format in Book.formats:
 
 477         setattr(BookDetails, book_format, _file_getter(book_format))
 
 483 class CollectionDetails(object):
 
 484     """Custom Collection fields."""
 
 487     def href(cls, collection):
 
 488         """ Returns URI in the API for the collection. """
 
 490         return API_BASE + reverse("api_collection", args=[collection.slug])
 
 493     def url(cls, collection):
 
 494         """ Returns URL on the site. """
 
 496         return WL_BASE + collection.get_absolute_url()
 
 499     def books(cls, collection):
 
 500         return Book.objects.filter(collection.get_query())
 
 503 class CollectionDetailHandler(BaseHandler, CollectionDetails):
 
 504     allowed_methods = ('GET',)
 
 505     fields = ['url', 'title', 'description', 'books']
 
 508     def read(self, request, slug):
 
 509         """ Returns details of a collection, identified by slug. """
 
 511             return Collection.objects.get(slug=slug)
 
 512         except Collection.DoesNotExist:
 
 516 class CollectionsHandler(BaseHandler, CollectionDetails):
 
 517     allowed_methods = ('GET',)
 
 519     fields = ['url', 'href', 'title']
 
 522     def read(self, request):
 
 523         """ Returns all collections. """
 
 524         return Collection.objects.all()
 
 527 class TagDetails(object):
 
 528     """Custom Tag fields."""
 
 532         """ Returns URI in the API for the tag. """
 
 534         return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
 
 538         """ Returns URL on the site. """
 
 540         return WL_BASE + tag.get_absolute_url()
 
 543 class TagDetailHandler(BaseHandler, TagDetails):
 
 544     """ Responsible for details of a single Tag object. """
 
 546     fields = ['name', 'url', 'sort_key', 'description']
 
 549     def read(self, request, category, slug):
 
 550         """ Returns details of a tag, identified by category and slug. """
 
 553             category_sng = category_singular[category]
 
 558             return Tag.objects.get(category=category_sng, slug=slug)
 
 559         except Tag.DoesNotExist:
 
 563 class TagsHandler(BaseHandler, TagDetails):
 
 564     """ Main handler for Tag objects.
 
 566     Responsible for lists of Tag objects
 
 567     and fields used for representing Tags.
 
 570     allowed_methods = ('GET',)
 
 572     fields = ['name', 'href', 'url', 'slug']
 
 575     def read(self, request, category=None, pk=None):
 
 576         """ Lists all tags in the category (eg. all themes). """
 
 579                 return Tag.objects.exclude(category='set').get(pk=pk)
 
 580             except Book.DoesNotExist:
 
 584             category_sng = category_singular[category]
 
 588         after = request.GET.get('after')
 
 589         count = request.GET.get('count')
 
 591         tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
 
 593         book_only = request.GET.get('book_only') == 'true'
 
 594         picture_only = request.GET.get('picture_only') == 'true'
 
 596             tags = tags.filter(for_books=True)
 
 598             tags = tags.filter(for_pictures=True)
 
 601             tags = tags.filter(slug__gt=after)
 
 609 class FragmentDetails(object):
 
 610     """Custom Fragment fields."""
 
 613     def href(cls, fragment):
 
 614         """ Returns URI in the API for the fragment. """
 
 616         return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
 
 619     def url(cls, fragment):
 
 620         """ Returns URL on the site for the fragment. """
 
 622         return WL_BASE + fragment.get_absolute_url()
 
 625     def themes(cls, fragment):
 
 626         """ Returns a list of theme tags for the fragment. """
 
 628         return fragment.tags.filter(category='theme')
 
 631 class FragmentDetailHandler(BaseHandler, FragmentDetails):
 
 632     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
 635     def read(self, request, book, anchor):
 
 636         """ Returns details of a fragment, identified by book slug and anchor. """
 
 638             return Fragment.objects.get(book__slug=book, anchor=anchor)
 
 639         except Fragment.DoesNotExist:
 
 643 class FragmentsHandler(BaseHandler, FragmentDetails):
 
 644     """ Main handler for Fragments.
 
 646     Responsible for lists of Fragment objects
 
 647     and fields used for representing Fragments.
 
 651     fields = ['book', 'url', 'anchor', 'href']
 
 652     allowed_methods = ('GET',)
 
 654     categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
 
 657     def read(self, request, tags):
 
 658         """ Lists all fragments with given book, tags, themes.
 
 660         :param tags: should be a path of categories and slugs, i.e.:
 
 661              books/book-slug/authors/an-author/themes/a-theme/
 
 665             tags, ancestors = read_tags(tags, request, allowed=self.categories)
 
 668         fragments = Fragment.tagged.with_all(tags).select_related('book')
 
 669         if fragments.exists():
 
 675 class PictureHandler(BaseHandler):
 
 677     fields = ('slug', 'title')
 
 678     allowed_methods = ('POST',)
 
 680     def create(self, request):
 
 681         if not request.user.has_perm('picture.add_picture'):
 
 684         data = json.loads(request.POST.get('data'))
 
 685         form = PictureImportForm(data)
 
 693 class UserDataHandler(BaseHandler):
 
 695     fields = ('state', 'username', 'premium')
 
 696     allowed_methods = ('GET', 'POST')
 
 698     def read(self, request, slug=None):
 
 699         if not request.user.is_authenticated():
 
 702             return {'username': request.user.username, 'premium': is_subscribed(request.user)}
 
 704             book = Book.objects.get(slug=slug)
 
 705         except Book.DoesNotExist:
 
 708             data = BookUserData.objects.get(book=book, user=request.user)
 
 709         except BookUserData.DoesNotExist:
 
 710             return {'state': 'not_started'}
 
 713     def create(self, request, slug, state):
 
 715             book = Book.objects.get(slug=slug)
 
 716         except Book.DoesNotExist:
 
 718         if not request.user.is_authenticated():
 
 720         if state not in ('reading', 'complete'):
 
 722         data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
 
 728 class UserShelfHandler(BookDetailHandler):
 
 729     fields = book_list_fields + ['liked']
 
 731     def parse_bool(self, s):
 
 732         if s in ('true', 'false'):
 
 737     # hack, because piston is stupid
 
 739     def liked(cls, book):
 
 740         return getattr(book, 'liked', None)
 
 742     def read(self, request, state):
 
 743         if not request.user.is_authenticated():
 
 745         likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
 
 746         if state not in ('reading', 'complete', 'likes'):
 
 748         after = request.GET.get('after')
 
 749         count = int(request.GET.get('count', 50))
 
 751             books = Book.tagged.with_any(request.user.tag_set.all())
 
 753             ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
 
 754                 .values_list('book_id', flat=True)
 
 755             books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
 
 757             books = books.filter(slug__gt=after)
 
 759             books = books[:count]
 
 762             new_books.append(BookProxy(book).set('liked', book.id in likes))
 
 763         return QuerySetProxy(new_books)
 
 766 class UserLikeHandler(BaseHandler):
 
 768     allowed_methods = ('GET', 'POST')
 
 770     def read(self, request, slug):
 
 771         if not request.user.is_authenticated():
 
 774             book = Book.objects.get(slug=slug)
 
 775         except Book.DoesNotExist:
 
 777         return {'likes': likes(request.user, book)}
 
 779     def create(self, request, slug):
 
 780         if not request.user.is_authenticated():
 
 783             book = Book.objects.get(slug=slug)
 
 784         except Book.DoesNotExist:
 
 786         action = request.GET.get('action', 'like')
 
 788             book.like(request.user)
 
 789         elif action == 'unlike':
 
 790             book.unlike(request.user)
 
 794 class BlogEntryHandler(BaseHandler):
 
 797         'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
 
 799     def read(self, request):
 
 800         after = request.GET.get('after')
 
 801         count = int(request.GET.get('count', 20))
 
 802         entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
 
 804             entries = entries.filter(first_published_at__lt=after)
 
 806             entries = entries[:count]
 
 810     def image_url(cls, entry):
 
 811         return (WL_BASE + entry.image.url) if entry.image else None
 
 814     def image_thumb(cls, entry):
 
 815         return MEDIA_BASE + default.backend.get_thumbnail(
 
 816             entry.image, "193x193").url if entry.image else ''
 
 819     def gallery_urls(cls, entry):
 
 820         return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
 
 824         return entry.first_published_at
 
 828         return WL_BASE + entry.get_absolute_url()