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 django.db import models
 
  11 from piston.handler import AnonymousBaseHandler, BaseHandler
 
  12 from piston.utils import rc
 
  13 from sorl.thumbnail import default
 
  15 from catalogue.forms import BookImportForm
 
  16 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 
  17 from catalogue.models.tag import prefetch_relations
 
  18 from picture.models import Picture
 
  19 from picture.forms import PictureImportForm
 
  21 from stats.utils import piwik_track
 
  22 from wolnelektury.utils import re_escape
 
  24 from . import emitters  # Register our emitters
 
  26 API_BASE = WL_BASE = MEDIA_BASE = lazy(
 
  27     lambda: u'http://' + Site.objects.get_current().domain, unicode)()
 
  39 for k, v in category_singular.items():
 
  40     category_plural[v] = k
 
  42 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
 
  45 def read_tags(tags, request, allowed):
 
  46     """ Reads a path of filtering tags.
 
  48     :param str tags: a path of category and slug pairs, like: authors/an-author/...
 
  49     :returns: list of Tag objects
 
  50     :raises: ValueError when tags can't be found
 
  53     def process(category, slug):
 
  54         if category == 'book':
 
  56                 books.append(Book.objects.get(slug=slug))
 
  57             except Book.DoesNotExist:
 
  58                 raise ValueError('Unknown book.')
 
  60             real_tags.append(Tag.objects.get(category=category, slug=slug))
 
  61         except Tag.DoesNotExist:
 
  62             raise ValueError('Tag not found')
 
  67     tags = tags.strip('/').split('/')
 
  71         category = tags.pop(0)
 
  75             category = category_singular[category]
 
  77             raise ValueError('Unknown category.')
 
  79         if category not in allowed:
 
  80             raise ValueError('Category not allowed.')
 
  81         process(category, slug)
 
  83     for key in request.GET:
 
  84         if key in category_singular:
 
  85             category = category_singular[key]
 
  86             if category in allowed:
 
  87                 for slug in request.GET.getlist(key):
 
  88                     process(category, slug)
 
  89     return real_tags, books
 
  95 class BookMediaHandler(BaseHandler):
 
  96     """ Responsible for representing media in Books. """
 
  99     fields = ['name', 'type', 'url', 'artist', 'director']
 
 103         """ Link to media on site. """
 
 105         return MEDIA_BASE + media.file.url
 
 108     def artist(cls, media):
 
 109         return media.extra_info.get('artist_name', '')
 
 112     def director(cls, media):
 
 113         return media.extra_info.get('director_name', '')
 
 116 class BookDetails(object):
 
 117     """Custom fields used for representing Books."""
 
 121         """ Returns an URI for a Book in the API. """
 
 122         return API_BASE + reverse("api_book", args=[book.slug])
 
 126         """ Returns Book's URL on the site. """
 
 127         return WL_BASE + book.get_absolute_url()
 
 130     def children(cls, book):
 
 131         """ Returns all children for a book. """
 
 132         return book.children.order_by('parent_number', 'sort_key')
 
 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 simple_thumb(cls, book):
 
 150         return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else ''
 
 153     def simple_cover(cls, book):
 
 154         return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
 
 157 class BookDetailHandler(BaseHandler, BookDetails):
 
 158     """ Main handler for Book objects.
 
 160     Responsible for single Book details.
 
 162     allowed_methods = ['GET']
 
 163     fields = ['title', 'parent', 'children'] + Book.formats + [
 
 164         'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length'] + [
 
 165             category_plural[c] for c in book_tag_categories]
 
 168     def read(self, request, book):
 
 169         """ Returns details of a book, identified by a slug and lang. """
 
 171             return Book.objects.get(slug=book)
 
 172         except Book.DoesNotExist:
 
 176 class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
 
 177     """ Main handler for Book objects.
 
 179     Responsible for lists of Book objects.
 
 181     allowed_methods = ('GET',)
 
 183     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio']
 
 186     def genres(cls, book):
 
 187         """ Returns all media for a book. """
 
 188         return book.tags.filter(category='genre')
 
 191     def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
 
 192              recommended=False, newest=False, books=None,
 
 193              after=None, before=None, count=None):
 
 194         """ Lists all books with given tags.
 
 196         :param tags: filtering tags; should be a path of categories
 
 197              and slugs, i.e.: authors/an-author/epoch/an-epoch/
 
 198         :param top_level: if True and a book is included in the results,
 
 199              it's children are aren't. By default all books matching the tags
 
 204                 return Book.objects.get(pk=pk)
 
 205             except Book.DoesNotExist:
 
 209             tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
 
 213         if 'after' in request.GET:
 
 214             after = request.GET['after']
 
 215         if 'before' in request.GET:
 
 216             before = request.GET['before']
 
 217         if 'count' in request.GET:
 
 218             count = request.GET['count']
 
 222                 books = Book.tagged_top_level(tags)
 
 223                 return books if books else rc.NOT_FOUND
 
 225                 books = Book.tagged.with_all(tags)
 
 227             books = books if books is not None else Book.objects.all()
 
 228         books = books.order_by('slug')
 
 231             books = books.filter(parent=None)
 
 233             books = books.filter(media__type='mp3').distinct()
 
 235             books = books.filter(media__type='daisy').distinct()
 
 237             books = books.filter(recommended=True)
 
 239             books = books.order_by('-created_at')
 
 242             books = books.filter(slug__gt=after)
 
 244             books = books.filter(slug__lt=before)
 
 246         books = books.only('slug', 'title', 'cover', 'cover_thumb')
 
 247         for category in book_tag_categories:
 
 248             books = prefetch_relations(books, category)
 
 252                 books = list(reversed(books.order_by('-slug')[:count]))
 
 254                 books = books[:count]
 
 258     def create(self, request, *args, **kwargs):
 
 262 class BooksHandler(BookDetailHandler):
 
 263     allowed_methods = ('GET', 'POST')
 
 265     fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
 
 266     anonymous = AnonymousBooksHandler
 
 268     def create(self, request, *args, **kwargs):
 
 269         if not request.user.has_perm('catalogue.add_book'):
 
 272         data = json.loads(request.POST.get('data'))
 
 273         form = BookImportForm(data)
 
 281 class EBooksHandler(AnonymousBooksHandler):
 
 282     fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
 
 285 class BookProxy(models.Model):
 
 289     def __init__(self, book, key):
 
 293     def __getattr__(self, item):
 
 294         if item not in ('book', 'key'):
 
 295             return self.book.__getattribute__(item)
 
 297             return self.__getattribute__(item)
 
 300 class QuerySetProxy(models.QuerySet):
 
 301     def __init__(self, l):
 
 305         return iter(self.list)
 
 308 class FilterBooksHandler(AnonymousBooksHandler):
 
 309     fields = book_tag_categories + [
 
 310         'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'has_audio', 'slug', 'key']
 
 312     def read(self, request):
 
 314         search_string = request.GET.get('search')
 
 315         is_lektura = request.GET.get('lektura')
 
 316         is_audiobook = request.GET.get('audiobook')
 
 318         after = request.GET.get('after')
 
 319         count = int(request.GET.get('count', 50))
 
 320         if is_lektura in ('true', 'false'):
 
 321             is_lektura = is_lektura == 'true'
 
 324         if is_audiobook in ('true', 'false'):
 
 325             is_audiobook = is_audiobook == 'true'
 
 326         books = Book.objects.distinct().order_by('slug')
 
 327         if is_lektura is not None:
 
 328             books = books.filter(has_audience=is_lektura)
 
 329         if is_audiobook is not None:
 
 331                 books = books.filter(media__type='mp3')
 
 333                 books = books.exclude(media__type='mp3')
 
 334         for key in request.GET:
 
 335             if key in category_singular:
 
 336                 category = category_singular[key]
 
 337                 if category in book_tag_categories:
 
 338                     slugs = request.GET[key].split(',')
 
 339                     tags = Tag.objects.filter(category=category, slug__in=slugs)
 
 340                     books = Book.tagged.with_any(tags, books)
 
 341         if (search_string is not None) and len(search_string) < 3:
 
 344             search_string = re_escape(search_string)
 
 345             books_author = books.filter(cached_author__iregex='\m' + search_string)
 
 346             books_title = books.filter(title__iregex='\m' + search_string)
 
 347             books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
 
 348             if after and (key_sep in after):
 
 349                 which, slug = after.split(key_sep, 1)
 
 351                     book_lists = [(books_title.filter(slug__gt=slug), 'title')]
 
 352                 else:  # which == 'author'
 
 353                     book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
 
 355                 book_lists = [(books_author, 'author'), (books_title, 'title')]
 
 357             if after and key_sep in after:
 
 358                 which, slug = after.split(key_sep, 1)
 
 359                 books = books.filter(slug__gt=slug)
 
 360             book_lists = [(books, 'book')]
 
 363         for book_list, label in book_lists:
 
 364             book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
 
 365             for category in book_tag_categories:
 
 366                 book_list = prefetch_relations(book_list, category)
 
 367             remaining_count = count - len(filtered_books)
 
 368             new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
 
 369                          for book in book_list[:remaining_count]]
 
 370             filtered_books += new_books
 
 371             if len(filtered_books) == count:
 
 374         return QuerySetProxy(filtered_books)
 
 377 # add categorized tags fields for Book
 
 378 def _tags_getter(category):
 
 380     def get_tags(cls, book):
 
 381         return book.tags.filter(category=category)
 
 385 def _tag_getter(category):
 
 387     def get_tag(cls, book):
 
 388         return book.tag_unicode(category)
 
 392 def add_tag_getters():
 
 393     for plural, singular in category_singular.items():
 
 394         setattr(BookDetails, plural, _tags_getter(singular))
 
 395         setattr(BookDetails, singular, _tag_getter(singular))
 
 400 # add fields for files in Book
 
 401 def _file_getter(book_format):
 
 402     field = "%s_file" % book_format
 
 405     def get_file(cls, book):
 
 406         f = getattr(book, field)
 
 408             return MEDIA_BASE + f.url
 
 414 def add_file_getters():
 
 415     for book_format in Book.formats:
 
 416         setattr(BookDetails, book_format, _file_getter(book_format))
 
 421 class CollectionDetails(object):
 
 422     """Custom Collection fields."""
 
 425     def href(cls, collection):
 
 426         """ Returns URI in the API for the collection. """
 
 428         return API_BASE + reverse("api_collection", args=[collection.slug])
 
 431     def url(cls, collection):
 
 432         """ Returns URL on the site. """
 
 434         return WL_BASE + collection.get_absolute_url()
 
 437     def books(cls, collection):
 
 438         return Book.objects.filter(collection.get_query())
 
 441 class CollectionDetailHandler(BaseHandler, CollectionDetails):
 
 442     allowed_methods = ('GET',)
 
 443     fields = ['url', 'title', 'description', 'books']
 
 446     def read(self, request, slug):
 
 447         """ Returns details of a collection, identified by slug. """
 
 449             return Collection.objects.get(slug=slug)
 
 450         except Collection.DoesNotExist:
 
 454 class CollectionsHandler(BaseHandler, CollectionDetails):
 
 455     allowed_methods = ('GET',)
 
 457     fields = ['url', 'href', 'title']
 
 460     def read(self, request):
 
 461         """ Returns all collections. """
 
 462         return Collection.objects.all()
 
 465 class TagDetails(object):
 
 466     """Custom Tag fields."""
 
 470         """ Returns URI in the API for the tag. """
 
 472         return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
 
 476         """ Returns URL on the site. """
 
 478         return WL_BASE + tag.get_absolute_url()
 
 481 class TagDetailHandler(BaseHandler, TagDetails):
 
 482     """ Responsible for details of a single Tag object. """
 
 484     fields = ['name', 'url', 'sort_key', 'description']
 
 487     def read(self, request, category, slug):
 
 488         """ Returns details of a tag, identified by category and slug. """
 
 491             category_sng = category_singular[category]
 
 496             return Tag.objects.get(category=category_sng, slug=slug)
 
 497         except Tag.DoesNotExist:
 
 501 class TagsHandler(BaseHandler, TagDetails):
 
 502     """ Main handler for Tag objects.
 
 504     Responsible for lists of Tag objects
 
 505     and fields used for representing Tags.
 
 508     allowed_methods = ('GET',)
 
 510     fields = ['name', 'href', 'url', 'slug']
 
 513     def read(self, request, category=None, pk=None):
 
 514         """ Lists all tags in the category (eg. all themes). """
 
 517                 return Tag.objects.exclude(category='set').get(pk=pk)
 
 518             except Book.DoesNotExist:
 
 522             category_sng = category_singular[category]
 
 526         after = request.GET.get('after')
 
 527         before = request.GET.get('before')
 
 528         count = request.GET.get('count')
 
 530         tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
 
 532         book_only = request.GET.get('book_only') == 'true'
 
 533         picture_only = request.GET.get('picture_only') == 'true'
 
 535             tags = tags.filter(for_books=True)
 
 537             tags = tags.filter(for_pictures=True)
 
 540             tags = tags.filter(slug__gt=after)
 
 542             tags = tags.filter(slug__lt=before)
 
 546                 tags = list(reversed(tags.order_by('-slug')[:count]))
 
 553 class FragmentDetails(object):
 
 554     """Custom Fragment fields."""
 
 557     def href(cls, fragment):
 
 558         """ Returns URI in the API for the fragment. """
 
 560         return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
 
 563     def url(cls, fragment):
 
 564         """ Returns URL on the site for the fragment. """
 
 566         return WL_BASE + fragment.get_absolute_url()
 
 569     def themes(cls, fragment):
 
 570         """ Returns a list of theme tags for the fragment. """
 
 572         return fragment.tags.filter(category='theme')
 
 575 class FragmentDetailHandler(BaseHandler, FragmentDetails):
 
 576     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
 579     def read(self, request, book, anchor):
 
 580         """ Returns details of a fragment, identified by book slug and anchor. """
 
 582             return Fragment.objects.get(book__slug=book, anchor=anchor)
 
 583         except Fragment.DoesNotExist:
 
 587 class FragmentsHandler(BaseHandler, FragmentDetails):
 
 588     """ Main handler for Fragments.
 
 590     Responsible for lists of Fragment objects
 
 591     and fields used for representing Fragments.
 
 595     fields = ['book', 'url', 'anchor', 'href']
 
 596     allowed_methods = ('GET',)
 
 598     categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
 
 601     def read(self, request, tags):
 
 602         """ Lists all fragments with given book, tags, themes.
 
 604         :param tags: should be a path of categories and slugs, i.e.:
 
 605              books/book-slug/authors/an-author/themes/a-theme/
 
 609             tags, ancestors = read_tags(tags, allowed=self.categories)
 
 612         fragments = Fragment.tagged.with_all(tags).select_related('book')
 
 613         if fragments.exists():
 
 619 class PictureHandler(BaseHandler):
 
 621     fields = ('slug', 'title')
 
 622     allowed_methods = ('POST',)
 
 624     def create(self, request):
 
 625         if not request.user.has_perm('picture.add_picture'):
 
 628         data = json.loads(request.POST.get('data'))
 
 629         form = PictureImportForm(data)