add slug back to filter-books api
[wolnelektury.git] / src / api / handlers.py
index 01430cb..159e4f2 100644 (file)
@@ -6,19 +6,20 @@ import json
 
 from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
 
 from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
-from django.db.models import Prefetch
 from django.utils.functional import lazy
 from django.utils.functional import lazy
+from django.db import models
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
 from sorl.thumbnail import default
 
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
 from sorl.thumbnail import default
 
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
-from catalogue.models.tag import TagRelation
+from catalogue.models.tag import prefetch_relations
 from picture.models import Picture
 from picture.forms import PictureImportForm
 
 from stats.utils import piwik_track
 from picture.models import Picture
 from picture.forms import PictureImportForm
 
 from stats.utils import piwik_track
+from wolnelektury.utils import re_escape
 
 from . import emitters  # Register our emitters
 
 
 from . import emitters  # Register our emitters
 
@@ -41,13 +42,25 @@ for k, v in category_singular.items():
 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
 
 
 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
 
 
-def read_tags(tags, allowed):
+def read_tags(tags, request, allowed):
     """ Reads a path of filtering tags.
 
     :param str tags: a path of category and slug pairs, like: authors/an-author/...
     :returns: list of Tag objects
     :raises: ValueError when tags can't be found
     """
     """ Reads a path of filtering tags.
 
     :param str tags: a path of category and slug pairs, like: authors/an-author/...
     :returns: list of Tag objects
     :raises: ValueError when tags can't be found
     """
+
+    def process(category, slug):
+        if category == 'book':
+            try:
+                books.append(Book.objects.get(slug=slug))
+            except Book.DoesNotExist:
+                raise ValueError('Unknown book.')
+        try:
+            real_tags.append(Tag.objects.get(category=category, slug=slug))
+        except Tag.DoesNotExist:
+            raise ValueError('Tag not found')
+
     if not tags:
         return [], []
 
     if not tags:
         return [], []
 
@@ -65,17 +78,14 @@ def read_tags(tags, allowed):
 
         if category not in allowed:
             raise ValueError('Category not allowed.')
 
         if category not in allowed:
             raise ValueError('Category not allowed.')
-
-        if category == 'book':
-            try:
-                books.append(Book.objects.get(slug=slug))
-            except Book.DoesNotExist:
-                raise ValueError('Unknown book.')
-
-        try:
-            real_tags.append(Tag.objects.get(category=category, slug=slug))
-        except Tag.DoesNotExist:
-            raise ValueError('Tag not found')
+        process(category, slug)
+
+    for key in request.GET:
+        if key in category_singular:
+            category = category_singular[key]
+            if category in allowed:
+                for slug in request.GET.getlist(key):
+                    process(category, slug)
     return real_tags, books
 
 
     return real_tags, books
 
 
@@ -114,14 +124,12 @@ class BookDetails(object):
     @classmethod
     def url(cls, book):
         """ Returns Book's URL on the site. """
     @classmethod
     def url(cls, book):
         """ Returns Book's URL on the site. """
-
         return WL_BASE + book.get_absolute_url()
 
     @classmethod
     def children(cls, book):
         """ Returns all children for a book. """
         return WL_BASE + book.get_absolute_url()
 
     @classmethod
     def children(cls, book):
         """ Returns all children for a book. """
-
-        return book.children.all()
+        return book.children.order_by('parent_number', 'sort_key')
 
     @classmethod
     def media(cls, book):
 
     @classmethod
     def media(cls, book):
@@ -137,6 +145,11 @@ class BookDetails(object):
         return MEDIA_BASE + default.backend.get_thumbnail(
                     book.cover, "139x193").url if book.cover else ''
 
         return MEDIA_BASE + default.backend.get_thumbnail(
                     book.cover, "139x193").url if book.cover else ''
 
+    @classmethod
+    def cover_source_image(cls, book):
+        url = book.cover_source()
+        return url.rstrip('/') + '/file/'
+
 
 class BookDetailHandler(BaseHandler, BookDetails):
     """ Main handler for Book objects.
 
 class BookDetailHandler(BaseHandler, BookDetails):
     """ Main handler for Book objects.
@@ -145,7 +158,7 @@ class BookDetailHandler(BaseHandler, BookDetails):
     """
     allowed_methods = ['GET']
     fields = ['title', 'parent', 'children'] + Book.formats + [
     """
     allowed_methods = ['GET']
     fields = ['title', 'parent', 'children'] + Book.formats + [
-        'media', 'url', 'cover', 'cover_thumb'] + [
+        'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [
             category_plural[c] for c in book_tag_categories]
 
     @piwik_track
             category_plural[c] for c in book_tag_categories]
 
     @piwik_track
@@ -164,7 +177,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
     """
     allowed_methods = ('GET',)
     model = Book
     """
     allowed_methods = ('GET',)
     model = Book
-    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
+    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
 
     @classmethod
     def genres(cls, book):
 
     @classmethod
     def genres(cls, book):
@@ -172,7 +185,9 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
         return book.tags.filter(category='genre')
 
     @piwik_track
         return book.tags.filter(category='genre')
 
     @piwik_track
-    def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None):
+    def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
+             recommended=False, newest=False, books=None,
+             after=None, before=None, count=None):
         """ Lists all books with given tags.
 
         :param tags: filtering tags; should be a path of categories
         """ Lists all books with given tags.
 
         :param tags: filtering tags; should be a path of categories
@@ -188,10 +203,17 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
                 return rc.NOT_FOUND
 
         try:
                 return rc.NOT_FOUND
 
         try:
-            tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
+            tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
         except ValueError:
             return rc.NOT_FOUND
 
         except ValueError:
             return rc.NOT_FOUND
 
+        if 'after' in request.GET:
+            after = request.GET['after']
+        if 'before' in request.GET:
+            before = request.GET['before']
+        if 'count' in request.GET:
+            count = request.GET['count']
+
         if tags:
             if top_level:
                 books = Book.tagged_top_level(tags)
         if tags:
             if top_level:
                 books = Book.tagged_top_level(tags)
@@ -199,7 +221,8 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
             else:
                 books = Book.tagged.with_all(tags)
         else:
             else:
                 books = Book.tagged.with_all(tags)
         else:
-            books = Book.objects.all()
+            books = books if books is not None else Book.objects.all()
+        books = books.order_by('slug')
 
         if top_level:
             books = books.filter(parent=None)
 
         if top_level:
             books = books.filter(parent=None)
@@ -207,20 +230,27 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
             books = books.filter(media__type='mp3').distinct()
         if daisy:
             books = books.filter(media__type='daisy').distinct()
             books = books.filter(media__type='mp3').distinct()
         if daisy:
             books = books.filter(media__type='daisy').distinct()
+        if recommended:
+            books = books.filter(recommended=True)
+        if newest:
+            books = books.order_by('-created_at')
+
+        if after:
+            books = books.filter(slug__gt=after)
+        if before:
+            books = books.filter(slug__lt=before)
 
         books = books.only('slug', 'title', 'cover', 'cover_thumb')
         for category in book_tag_categories:
 
         books = books.only('slug', 'title', 'cover', 'cover_thumb')
         for category in book_tag_categories:
-            books = books.prefetch_related(
-                Prefetch(
-                    'tag_relations',
-                    queryset=TagRelation.objects.filter(tag__category=category)
-                        .select_related('tag').only('tag__name_pl', 'object_id'),
-                    to_attr='%s_relations' % category))
-
-        if books:
-            return books
-        else:
-            return rc.NOT_FOUND
+            books = prefetch_relations(books, category)
+
+        if count:
+            if before:
+                books = list(reversed(books.order_by('-slug')[:count]))
+            else:
+                books = books[:count]
+
+        return books
 
     def create(self, request, *args, **kwargs):
         return rc.FORBIDDEN
 
     def create(self, request, *args, **kwargs):
         return rc.FORBIDDEN
@@ -229,7 +259,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
 class BooksHandler(BookDetailHandler):
     allowed_methods = ('GET', 'POST')
     model = Book
 class BooksHandler(BookDetailHandler):
     allowed_methods = ('GET', 'POST')
     model = Book
-    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
+    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
     anonymous = AnonymousBooksHandler
 
     def create(self, request, *args, **kwargs):
     anonymous = AnonymousBooksHandler
 
     def create(self, request, *args, **kwargs):
@@ -246,7 +276,96 @@ class BooksHandler(BookDetailHandler):
 
 
 class EBooksHandler(AnonymousBooksHandler):
 
 
 class EBooksHandler(AnonymousBooksHandler):
-    fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
+    fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
+
+
+class BookProxy(models.Model):
+    def __init__(self, book, key):
+        self.book = book
+        self.key = key
+
+    def __getattr__(self, item):
+        if item not in ('book', 'key'):
+            return self.book.__getattribute__(item)
+        else:
+            return self.__getattribute__(item)
+
+
+class QuerySetProxy(models.QuerySet):
+    def __init__(self, l):
+        self.list = l
+
+    def __iter__(self):
+        return iter(self.list)
+
+
+class FilterBooksHandler(AnonymousBooksHandler):
+    fields = book_tag_categories + [
+        'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'key', 'cover_source_image']
+
+    def read(self, request):
+        key_sep = '$'
+        search_string = request.GET.get('search')
+        is_lektura = request.GET.get('lektura')
+        is_audiobook = request.GET.get('audiobook')
+
+        after = request.GET.get('after')
+        count = int(request.GET.get('count', 50))
+        if is_lektura in ('true', 'false'):
+            is_lektura = is_lektura == 'true'
+        else:
+            is_lektura = None
+        if is_audiobook in ('true', 'false'):
+            is_audiobook = is_audiobook == 'true'
+        books = Book.objects.distinct().order_by('slug')
+        if is_lektura is not None:
+            books = books.filter(has_audience=is_lektura)
+        if is_audiobook is not None:
+            if is_audiobook:
+                books = books.filter(media__type='mp3')
+            else:
+                books = books.exclude(media__type='mp3')
+        for key in request.GET:
+            if key in category_singular:
+                category = category_singular[key]
+                if category in book_tag_categories:
+                    slugs = request.GET[key].split(',')
+                    tags = Tag.objects.filter(category=category, slug__in=slugs)
+                    books = Book.tagged.with_any(tags, books)
+        if (search_string is not None) and len(search_string) < 3:
+            search_string = None
+        if search_string:
+            search_string = re_escape(search_string)
+            books_author = books.filter(cached_author__iregex='\m' + search_string)
+            books_title = books.filter(title__iregex='\m' + search_string)
+            books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
+            if after and (key_sep in after):
+                which, slug = after.split(key_sep, 1)
+                if which == 'title':
+                    book_lists = [(books_title.filter(slug__gt=slug), 'title')]
+                else:  # which == 'author'
+                    book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
+            else:
+                book_lists = [(books_author, 'author'), (books_title, 'title')]
+        else:
+            if after and key_sep in after:
+                which, slug = after.split(key_sep, 1)
+                books = books.filter(slug__gt=slug)
+            book_lists = [(books, 'book')]
+
+        filtered_books = []
+        for book_list, label in book_lists:
+            book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
+            for category in book_tag_categories:
+                book_list = prefetch_relations(book_list, category)
+            remaining_count = count - len(filtered_books)
+            new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
+                         for book in book_list[:remaining_count]]
+            filtered_books += new_books
+            if len(filtered_books) == count:
+                break
+
+        return QuerySetProxy(filtered_books)
 
 
 # add categorized tags fields for Book
 
 
 # add categorized tags fields for Book
@@ -260,9 +379,7 @@ def _tags_getter(category):
 def _tag_getter(category):
     @classmethod
     def get_tag(cls, book):
 def _tag_getter(category):
     @classmethod
     def get_tag(cls, book):
-        if hasattr(book, '%s_relations' % category):
-            return ', '.join(rel.tag.name for rel in getattr(book, '%s_relations' % category))
-        return ', '.join(book.tags.filter(category=category).values_list('name', flat=True))
+        return book.tag_unicode(category)
     return get_tag
 
 
     return get_tag
 
 
@@ -384,7 +501,7 @@ class TagsHandler(BaseHandler, TagDetails):
     """
     allowed_methods = ('GET',)
     model = Tag
     """
     allowed_methods = ('GET',)
     model = Tag
-    fields = ['name', 'href', 'url']
+    fields = ['name', 'href', 'url', 'slug']
 
     @piwik_track
     def read(self, request, category=None, pk=None):
 
     @piwik_track
     def read(self, request, category=None, pk=None):
@@ -400,11 +517,24 @@ class TagsHandler(BaseHandler, TagDetails):
         except KeyError:
             return rc.NOT_FOUND
 
         except KeyError:
             return rc.NOT_FOUND
 
-        tags = Tag.objects.filter(category=category_sng).exclude(items=None)
-        if tags.exists():
-            return tags
-        else:
-            return rc.NOT_FOUND
+        after = request.GET.get('after')
+        before = request.GET.get('before')
+        count = request.GET.get('count')
+
+        tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
+
+        if after:
+            tags = tags.filter(slug__gt=after)
+        if before:
+            tags = tags.filter(slug__lt=before)
+
+        if count:
+            if before:
+                tags = list(reversed(tags.order_by('-slug')[:count]))
+            else:
+                tags = tags[:count]
+
+        return tags
 
 
 class FragmentDetails(object):
 
 
 class FragmentDetails(object):