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 piston.handler import AnonymousBaseHandler, BaseHandler
 
  13 from piston.utils import rc
 
  14 from sorl.thumbnail import default
 
  16 from api.models import BookUserData
 
  17 from catalogue.forms import BookImportForm
 
  18 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 
  19 from catalogue.models.tag import prefetch_relations
 
  20 from catalogue.utils import is_subscribed
 
  21 from picture.models import Picture
 
  22 from picture.forms import PictureImportForm
 
  24 from stats.utils import piwik_track
 
  25 from wolnelektury.utils import re_escape
 
  27 from . import emitters  # Register our emitters
 
  29 API_BASE = WL_BASE = MEDIA_BASE = lazy(
 
  30     lambda: u'http://' + Site.objects.get_current().domain, unicode)()
 
  42 for k, v in category_singular.items():
 
  43     category_plural[v] = k
 
  45 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
 
  48 def read_tags(tags, request, allowed):
 
  49     """ Reads a path of filtering tags.
 
  51     :param str tags: a path of category and slug pairs, like: authors/an-author/...
 
  52     :returns: list of Tag objects
 
  53     :raises: ValueError when tags can't be found
 
  56     def process(category, slug):
 
  57         if category == 'book':
 
  59                 books.append(Book.objects.get(slug=slug))
 
  60             except Book.DoesNotExist:
 
  61                 raise ValueError('Unknown book.')
 
  63             real_tags.append(Tag.objects.get(category=category, slug=slug))
 
  64         except Tag.DoesNotExist:
 
  65             raise ValueError('Tag not found')
 
  70     tags = tags.strip('/').split('/')
 
  74         category = tags.pop(0)
 
  78             category = category_singular[category]
 
  80             raise ValueError('Unknown category.')
 
  82         if category not in allowed:
 
  83             raise ValueError('Category not allowed.')
 
  84         process(category, slug)
 
  86     for key in request.GET:
 
  87         if key in category_singular:
 
  88             category = category_singular[key]
 
  89             if category in allowed:
 
  90                 for slug in request.GET.getlist(key):
 
  91                     process(category, slug)
 
  92     return real_tags, books
 
  98 class BookMediaHandler(BaseHandler):
 
  99     """ Responsible for representing media in Books. """
 
 102     fields = ['name', 'type', 'url', 'artist', 'director']
 
 106         """ Link to media on site. """
 
 108         return MEDIA_BASE + media.file.url
 
 111     def artist(cls, media):
 
 112         return media.extra_info.get('artist_name', '')
 
 115     def director(cls, media):
 
 116         return media.extra_info.get('director_name', '')
 
 119 class BookDetails(object):
 
 120     """Custom fields used for representing Books."""
 
 124         """ Returns an URI for a Book in the API. """
 
 125         return API_BASE + reverse("api_book", args=[book.slug])
 
 129         """ Returns Book's URL on the site. """
 
 130         return WL_BASE + book.get_absolute_url()
 
 133     def children(cls, book):
 
 134         """ Returns all children for a book. """
 
 135         return book.children.order_by('parent_number', 'sort_key')
 
 138     def media(cls, book):
 
 139         """ Returns all media for a book. """
 
 140         return book.media.all()
 
 143     def cover(cls, book):
 
 144         return MEDIA_BASE + book.cover.url if book.cover else ''
 
 147     def cover_thumb(cls, book):
 
 148         return MEDIA_BASE + default.backend.get_thumbnail(
 
 149                     book.cover, "139x193").url if book.cover else ''
 
 152     def simple_thumb(cls, book):
 
 153         return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
 
 156     def simple_cover(cls, book):
 
 157         return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
 
 160 class BookDetailHandler(BaseHandler, BookDetails):
 
 161     """ Main handler for Book objects.
 
 163     Responsible for single Book details.
 
 165     allowed_methods = ['GET']
 
 166     fields = ['title', 'parent', 'children'] + Book.formats + [
 
 167         'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
 
 169             category_plural[c] for c in book_tag_categories]
 
 172     def read(self, request, book):
 
 173         """ Returns details of a book, identified by a slug and lang. """
 
 175             return Book.objects.get(slug=book)
 
 176         except Book.DoesNotExist:
 
 180 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
 
 181     """ Main handler for Book objects.
 
 183     Responsible for lists of Book objects.
 
 185     allowed_methods = ('GET',)
 
 187     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio']
 
 190     def genres(cls, book):
 
 191         """ Returns all media for a book. """
 
 192         return book.tags.filter(category='genre')
 
 195     def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
 
 196              recommended=False, newest=False, books=None,
 
 197              after=None, before=None, count=None):
 
 198         """ Lists all books with given tags.
 
 200         :param tags: filtering tags; should be a path of categories
 
 201              and slugs, i.e.: authors/an-author/epoch/an-epoch/
 
 202         :param top_level: if True and a book is included in the results,
 
 203              it's children are aren't. By default all books matching the tags
 
 208                 return Book.objects.get(pk=pk)
 
 209             except Book.DoesNotExist:
 
 213             tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
 
 217         if 'after' in request.GET:
 
 218             after = request.GET['after']
 
 219         if 'before' in request.GET:
 
 220             before = request.GET['before']
 
 221         if 'count' in request.GET:
 
 222             count = request.GET['count']
 
 226                 books = Book.tagged_top_level(tags)
 
 227                 return books if books else rc.NOT_FOUND
 
 229                 books = Book.tagged.with_all(tags)
 
 231             books = books if books is not None else Book.objects.all()
 
 232         books = books.order_by('slug')
 
 235             books = books.filter(parent=None)
 
 237             books = books.filter(media__type='mp3').distinct()
 
 239             books = books.filter(media__type='daisy').distinct()
 
 241             books = books.filter(recommended=True)
 
 243             books = books.order_by('-created_at')
 
 246             books = books.filter(slug__gt=after)
 
 248             books = books.filter(slug__lt=before)
 
 250         books = books.only('slug', 'title', 'cover', 'cover_thumb')
 
 251         for category in book_tag_categories:
 
 252             books = prefetch_relations(books, category)
 
 256                 books = list(reversed(books.order_by('-slug')[:count]))
 
 258                 books = books[:count]
 
 262     def create(self, request, *args, **kwargs):
 
 266 class BooksHandler(BookDetailHandler):
 
 267     allowed_methods = ('GET', 'POST')
 
 269     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
 
 270     anonymous = AnonymousBooksHandler
 
 272     def create(self, request, *args, **kwargs):
 
 273         if not request.user.has_perm('catalogue.add_book'):
 
 276         data = json.loads(request.POST.get('data'))
 
 277         form = BookImportForm(data)
 
 285 class EpubHandler(BookDetailHandler):
 
 286     def read(self, request, slug):
 
 287         if not is_subscribed(request.user):
 
 290             book = Book.objects.get(slug=slug)
 
 291         except Book.DoesNotExist:
 
 293         response = HttpResponse(book.get_media('epub'))
 
 297 class EBooksHandler(AnonymousBooksHandler):
 
 298     fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
 
 301 class BookProxy(models.Model):
 
 305     def __init__(self, book, key):
 
 309     def __getattr__(self, item):
 
 310         if item not in ('book', 'key'):
 
 311             return self.book.__getattribute__(item)
 
 313             return self.__getattribute__(item)
 
 316 class QuerySetProxy(models.QuerySet):
 
 317     def __init__(self, l):
 
 321         return iter(self.list)
 
 324 class FilterBooksHandler(AnonymousBooksHandler):
 
 325     fields = book_tag_categories + [
 
 326         'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'has_audio', 'slug', 'key']
 
 328     def parse_bool(self, s):
 
 329         if s in ('true', 'false'):
 
 334     def read(self, request):
 
 336         search_string = request.GET.get('search')
 
 337         is_lektura = self.parse_bool(request.GET.get('lektura'))
 
 338         is_audiobook = self.parse_bool(request.GET.get('audiobook'))
 
 339         preview = self.parse_bool(request.GET.get('preview'))
 
 341         after = request.GET.get('after')
 
 342         count = int(request.GET.get('count', 50))
 
 343         books = Book.objects.distinct().order_by('slug')
 
 344         if is_lektura is not None:
 
 345             books = books.filter(has_audience=is_lektura)
 
 346         if is_audiobook is not None:
 
 348                 books = books.filter(media__type='mp3')
 
 350                 books = books.exclude(media__type='mp3')
 
 351         if preview is not None:
 
 352             books = books.filter(preview=preview)
 
 353         for key in request.GET:
 
 354             if key in category_singular:
 
 355                 category = category_singular[key]
 
 356                 if category in book_tag_categories:
 
 357                     slugs = request.GET[key].split(',')
 
 358                     tags = Tag.objects.filter(category=category, slug__in=slugs)
 
 359                     books = Book.tagged.with_any(tags, books)
 
 360         if (search_string is not None) and len(search_string) < 3:
 
 363             search_string = re_escape(search_string)
 
 364             books_author = books.filter(cached_author__iregex='\m' + search_string)
 
 365             books_title = books.filter(title__iregex='\m' + search_string)
 
 366             books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
 
 367             if after and (key_sep in after):
 
 368                 which, slug = after.split(key_sep, 1)
 
 370                     book_lists = [(books_title.filter(slug__gt=slug), 'title')]
 
 371                 else:  # which == 'author'
 
 372                     book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
 
 374                 book_lists = [(books_author, 'author'), (books_title, 'title')]
 
 376             if after and key_sep in after:
 
 377                 which, slug = after.split(key_sep, 1)
 
 378                 books = books.filter(slug__gt=slug)
 
 379             book_lists = [(books, 'book')]
 
 382         for book_list, label in book_lists:
 
 383             book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
 
 384             for category in book_tag_categories:
 
 385                 book_list = prefetch_relations(book_list, category)
 
 386             remaining_count = count - len(filtered_books)
 
 387             new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
 
 388                          for book in book_list[:remaining_count]]
 
 389             filtered_books += new_books
 
 390             if len(filtered_books) == count:
 
 393         return QuerySetProxy(filtered_books)
 
 396 # add categorized tags fields for Book
 
 397 def _tags_getter(category):
 
 399     def get_tags(cls, book):
 
 400         return book.tags.filter(category=category)
 
 404 def _tag_getter(category):
 
 406     def get_tag(cls, book):
 
 407         return book.tag_unicode(category)
 
 411 def add_tag_getters():
 
 412     for plural, singular in category_singular.items():
 
 413         setattr(BookDetails, plural, _tags_getter(singular))
 
 414         setattr(BookDetails, singular, _tag_getter(singular))
 
 420 # add fields for files in Book
 
 421 def _file_getter(book_format):
 
 425         f_url = book.media_url(book_format)
 
 427             return MEDIA_BASE + f_url
 
 433 def add_file_getters():
 
 434     for book_format in Book.formats:
 
 435         setattr(BookDetails, book_format, _file_getter(book_format))
 
 441 class CollectionDetails(object):
 
 442     """Custom Collection fields."""
 
 445     def href(cls, collection):
 
 446         """ Returns URI in the API for the collection. """
 
 448         return API_BASE + reverse("api_collection", args=[collection.slug])
 
 451     def url(cls, collection):
 
 452         """ Returns URL on the site. """
 
 454         return WL_BASE + collection.get_absolute_url()
 
 457     def books(cls, collection):
 
 458         return Book.objects.filter(collection.get_query())
 
 461 class CollectionDetailHandler(BaseHandler, CollectionDetails):
 
 462     allowed_methods = ('GET',)
 
 463     fields = ['url', 'title', 'description', 'books']
 
 466     def read(self, request, slug):
 
 467         """ Returns details of a collection, identified by slug. """
 
 469             return Collection.objects.get(slug=slug)
 
 470         except Collection.DoesNotExist:
 
 474 class CollectionsHandler(BaseHandler, CollectionDetails):
 
 475     allowed_methods = ('GET',)
 
 477     fields = ['url', 'href', 'title']
 
 480     def read(self, request):
 
 481         """ Returns all collections. """
 
 482         return Collection.objects.all()
 
 485 class TagDetails(object):
 
 486     """Custom Tag fields."""
 
 490         """ Returns URI in the API for the tag. """
 
 492         return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
 
 496         """ Returns URL on the site. """
 
 498         return WL_BASE + tag.get_absolute_url()
 
 501 class TagDetailHandler(BaseHandler, TagDetails):
 
 502     """ Responsible for details of a single Tag object. """
 
 504     fields = ['name', 'url', 'sort_key', 'description']
 
 507     def read(self, request, category, slug):
 
 508         """ Returns details of a tag, identified by category and slug. """
 
 511             category_sng = category_singular[category]
 
 516             return Tag.objects.get(category=category_sng, slug=slug)
 
 517         except Tag.DoesNotExist:
 
 521 class TagsHandler(BaseHandler, TagDetails):
 
 522     """ Main handler for Tag objects.
 
 524     Responsible for lists of Tag objects
 
 525     and fields used for representing Tags.
 
 528     allowed_methods = ('GET',)
 
 530     fields = ['name', 'href', 'url', 'slug']
 
 533     def read(self, request, category=None, pk=None):
 
 534         """ Lists all tags in the category (eg. all themes). """
 
 537                 return Tag.objects.exclude(category='set').get(pk=pk)
 
 538             except Book.DoesNotExist:
 
 542             category_sng = category_singular[category]
 
 546         after = request.GET.get('after')
 
 547         before = request.GET.get('before')
 
 548         count = request.GET.get('count')
 
 550         tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
 
 552         book_only = request.GET.get('book_only') == 'true'
 
 553         picture_only = request.GET.get('picture_only') == 'true'
 
 555             tags = tags.filter(for_books=True)
 
 557             tags = tags.filter(for_pictures=True)
 
 560             tags = tags.filter(slug__gt=after)
 
 562             tags = tags.filter(slug__lt=before)
 
 566                 tags = list(reversed(tags.order_by('-slug')[:count]))
 
 573 class FragmentDetails(object):
 
 574     """Custom Fragment fields."""
 
 577     def href(cls, fragment):
 
 578         """ Returns URI in the API for the fragment. """
 
 580         return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
 
 583     def url(cls, fragment):
 
 584         """ Returns URL on the site for the fragment. """
 
 586         return WL_BASE + fragment.get_absolute_url()
 
 589     def themes(cls, fragment):
 
 590         """ Returns a list of theme tags for the fragment. """
 
 592         return fragment.tags.filter(category='theme')
 
 595 class FragmentDetailHandler(BaseHandler, FragmentDetails):
 
 596     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
 599     def read(self, request, book, anchor):
 
 600         """ Returns details of a fragment, identified by book slug and anchor. """
 
 602             return Fragment.objects.get(book__slug=book, anchor=anchor)
 
 603         except Fragment.DoesNotExist:
 
 607 class FragmentsHandler(BaseHandler, FragmentDetails):
 
 608     """ Main handler for Fragments.
 
 610     Responsible for lists of Fragment objects
 
 611     and fields used for representing Fragments.
 
 615     fields = ['book', 'url', 'anchor', 'href']
 
 616     allowed_methods = ('GET',)
 
 618     categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
 
 621     def read(self, request, tags):
 
 622         """ Lists all fragments with given book, tags, themes.
 
 624         :param tags: should be a path of categories and slugs, i.e.:
 
 625              books/book-slug/authors/an-author/themes/a-theme/
 
 629             tags, ancestors = read_tags(tags, request, allowed=self.categories)
 
 632         fragments = Fragment.tagged.with_all(tags).select_related('book')
 
 633         if fragments.exists():
 
 639 class PictureHandler(BaseHandler):
 
 641     fields = ('slug', 'title')
 
 642     allowed_methods = ('POST',)
 
 644     def create(self, request):
 
 645         if not request.user.has_perm('picture.add_picture'):
 
 648         data = json.loads(request.POST.get('data'))
 
 649         form = PictureImportForm(data)
 
 657 class UserDataHandler(BaseHandler):
 
 659     fields = ('state', 'username')
 
 660     allowed_methods = ('GET', 'POST')
 
 662     def read(self, request, slug=None):
 
 663         if not request.user.is_authenticated():
 
 666             return {'username': request.user.username}
 
 668             book = Book.objects.get(slug=slug)
 
 669         except Book.DoesNotExist:
 
 672             data = BookUserData.objects.get(book=book, user=request.user)
 
 673         except BookUserData.DoesNotExist:
 
 674             return {'state': 'not_started'}
 
 677     def create(self, request, slug, state):
 
 679             book = Book.objects.get(slug=slug)
 
 680         except Book.DoesNotExist:
 
 682         if not request.user.is_authenticated():
 
 684         if state not in ('reading', 'complete'):
 
 686         data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
 
 692 class UserShelfHandler(BookDetailHandler):
 
 693     fields = book_tag_categories + [
 
 694         'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
 
 696     def parse_bool(self, s):
 
 697         if s in ('true', 'false'):
 
 702     def read(self, request, state):
 
 703         if not request.user.is_authenticated():
 
 705         if state not in ('reading', 'complete'):
 
 707         after = request.GET.get('after')
 
 708         count = int(request.GET.get('count', 50))
 
 709         ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
 
 710             .values_list('book_id', flat=True)
 
 711         books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
 
 713             books = books.filter(slug__gt=after)
 
 715             books = books[:count]