Preliminary math and tables support.
[wolnelektury.git] / apps / api / handlers.py
index 7f74c19..5fe931e 100644 (file)
@@ -1,21 +1,16 @@
 # -*- coding: utf-8 -*-
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 # -*- coding: utf-8 -*-
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-
-from datetime import datetime, timedelta
+#
 import json
 import json
-from urlparse import urljoin
 
 
-from django.conf import settings
 from django.contrib.sites.models import Site
 from django.contrib.sites.models import Site
-from django.core.cache import get_cache
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
+from django.utils.functional import lazy
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
 from sorl.thumbnail import default
 
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
 from sorl.thumbnail import default
 
-from api.helpers import timestamp
-from api.models import Deleted
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 from picture.models import Picture
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 from picture.models import Picture
@@ -23,7 +18,10 @@ from picture.forms import PictureImportForm
 
 from stats.utils import piwik_track
 
 
 from stats.utils import piwik_track
 
-API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain
+from . import emitters # Register our emitters
+
+API_BASE = WL_BASE = MEDIA_BASE = lazy(
+    lambda: u'http://' + Site.objects.get_current().domain, unicode)()
 
 
 category_singular = {
 
 
 category_singular = {
@@ -34,7 +32,7 @@ category_singular = {
     'themes': 'theme',
     'books': 'book',
 }
     'themes': 'theme',
     'books': 'book',
 }
-category_plural={}
+category_plural = {}
 for k, v in category_singular.items():
     category_plural[v] = k
 
 for k, v in category_singular.items():
     category_plural[v] = k
 
@@ -50,10 +48,11 @@ def read_tags(tags, allowed):
     :raises: ValueError when tags can't be found
     """
     if not tags:
     :raises: ValueError when tags can't be found
     """
     if not tags:
-        return []
+        return [], []
 
     tags = tags.strip('/').split('/')
     real_tags = []
 
     tags = tags.strip('/').split('/')
     real_tags = []
+    books = []
     while tags:
         category = tags.pop(0)
         slug = tags.pop(0)
     while tags:
         category = tags.pop(0)
         slug = tags.pop(0)
@@ -66,15 +65,17 @@ def read_tags(tags, allowed):
         if not category in allowed:
             raise ValueError('Category not allowed.')
 
         if not category in allowed:
             raise ValueError('Category not allowed.')
 
-        # !^%@#$^#!
         if category == 'book':
         if category == 'book':
-            slug = 'l-' + slug
+            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')
 
         try:
             real_tags.append(Tag.objects.get(category=category, slug=slug))
         except Tag.DoesNotExist:
             raise ValueError('Tag not found')
-    return real_tags
+    return real_tags, books
 
 
 # RESTful handlers
 
 
 # RESTful handlers
@@ -99,16 +100,11 @@ class BookMediaHandler(BaseHandler):
     @classmethod
     def director(cls, media):
         return media.extra_info.get('director_name', '')
     @classmethod
     def director(cls, media):
         return media.extra_info.get('director_name', '')
-        
 
 
 class BookDetails(object):
     """Custom fields used for representing Books."""
 
 
 
 class BookDetails(object):
     """Custom fields used for representing Books."""
 
-    @classmethod
-    def author(cls, book):
-        return ",".join(t[0] for t in book.related_info()['tags'].get('author', []))
-
     @classmethod
     def href(cls, book):
         """ Returns an URI for a Book in the API. """
     @classmethod
     def href(cls, book):
         """ Returns an URI for a Book in the API. """
@@ -168,11 +164,16 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
     """
     allowed_methods = ('GET',)
     model = Book
     """
     allowed_methods = ('GET',)
     model = Book
-    fields = ['author', 'href', 'title', 'url', 'cover']
+    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
+
+    @classmethod
+    def genres(cls, book):
+        """ Returns all media for a book. """
+        return book.tags.filter(category='genre')
 
     @piwik_track
 
     @piwik_track
-    def read(self, request, tags, top_level=False,
-                audiobooks=False, daisy=False):
+    def read(self, request, tags=None, top_level=False,
+                audiobooks=False, daisy=False, pk=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
@@ -181,8 +182,14 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
              it's children are aren't. By default all books matching the tags
              are returned.
         """
              it's children are aren't. By default all books matching the tags
              are returned.
         """
+        if pk is not None:
+            try:
+                return Book.objects.get(pk=pk)
+            except Book.DoesNotExist:
+                return rc.NOT_FOUND
+
         try:
         try:
-            tags = read_tags(tags, allowed=book_tag_categories)
+            tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
         except ValueError:
             return rc.NOT_FOUND
 
         except ValueError:
             return rc.NOT_FOUND
 
@@ -194,7 +201,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
                 books = Book.tagged.with_all(tags)
         else:
             books = Book.objects.all()
                 books = Book.tagged.with_all(tags)
         else:
             books = Book.objects.all()
-            
+
         if top_level:
             books = books.filter(parent=None)
         if audiobooks:
         if top_level:
             books = books.filter(parent=None)
         if audiobooks:
@@ -214,7 +221,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
 class BooksHandler(BookDetailHandler):
     allowed_methods = ('GET', 'POST')
     model = Book
 class BooksHandler(BookDetailHandler):
     allowed_methods = ('GET', 'POST')
     model = Book
-    fields = ['author', 'href', 'title', 'url']
+    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
     anonymous = AnonymousBooksHandler
 
     def create(self, request, *args, **kwargs):
     anonymous = AnonymousBooksHandler
 
     def create(self, request, *args, **kwargs):
@@ -240,8 +247,14 @@ def _tags_getter(category):
     def get_tags(cls, book):
         return book.tags.filter(category=category)
     return get_tags
     def get_tags(cls, book):
         return book.tags.filter(category=category)
     return get_tags
+def _tag_getter(category):
+    @classmethod
+    def get_tag(cls, book):
+        return ', '.join(tag.name for tag in book.tags.filter(category=category))
+    return get_tag
 for plural, singular in category_singular.items():
     setattr(BookDetails, plural, _tags_getter(singular))
 for plural, singular in category_singular.items():
     setattr(BookDetails, plural, _tags_getter(singular))
+    setattr(BookDetails, singular, _tag_getter(singular))
 
 # add fields for files in Book
 def _file_getter(format):
 
 # add fields for files in Book
 def _file_getter(format):
@@ -285,7 +298,6 @@ class CollectionDetailHandler(BaseHandler, CollectionDetails):
 
     @piwik_track
     def read(self, request, slug):
 
     @piwik_track
     def read(self, request, slug):
-        print slug
         """ Returns details of a collection, identified by slug. """
         try:
             return Collection.objects.get(slug=slug)
         """ Returns details of a collection, identified by slug. """
         try:
             return Collection.objects.get(slug=slug)
@@ -352,15 +364,20 @@ class TagsHandler(BaseHandler, TagDetails):
     fields = ['name', 'href', 'url']
 
     @piwik_track
     fields = ['name', 'href', 'url']
 
     @piwik_track
-    def read(self, request, category):
+    def read(self, request, category=None, pk=None):
         """ Lists all tags in the category (eg. all themes). """
         """ Lists all tags in the category (eg. all themes). """
+        if pk is not None:
+            try:
+                return Tag.objects.exclude(category='set').get(pk=pk)
+            except Book.DoesNotExist:
+                return rc.NOT_FOUND
 
         try:
             category_sng = category_singular[category]
         except KeyError, e:
             return rc.NOT_FOUND
 
 
         try:
             category_sng = category_singular[category]
         except KeyError, e:
             return rc.NOT_FOUND
 
-        tags = Tag.objects.filter(category=category_sng).exclude(book_count=0)
+        tags = Tag.objects.filter(category=category_sng).exclude(items=None)
         if tags.exists():
             return tags
         else:
         if tags.exists():
             return tags
         else:
@@ -374,7 +391,7 @@ class FragmentDetails(object):
     def href(cls, fragment):
         """ Returns URI in the API for the fragment. """
 
     def href(cls, fragment):
         """ Returns URI in the API for the fragment. """
 
-        return API_BASE + reverse("api_fragment", 
+        return API_BASE + reverse("api_fragment",
             args=[fragment.book.slug, fragment.anchor])
 
     @classmethod
             args=[fragment.book.slug, fragment.anchor])
 
     @classmethod
@@ -424,7 +441,7 @@ class FragmentsHandler(BaseHandler, FragmentDetails):
 
         """
         try:
 
         """
         try:
-            tags = read_tags(tags, allowed=self.categories)
+            tags, ancestors = read_tags(tags, allowed=self.categories)
         except ValueError:
             return rc.NOT_FOUND
         fragments = Fragment.tagged.with_all(tags).select_related('book')
         except ValueError:
             return rc.NOT_FOUND
         fragments = Fragment.tagged.with_all(tags).select_related('book')
@@ -434,271 +451,6 @@ class FragmentsHandler(BaseHandler, FragmentDetails):
             return rc.NOT_FOUND
 
 
             return rc.NOT_FOUND
 
 
-
-# Changes handlers
-
-class CatalogueHandler(BaseHandler):
-
-    @staticmethod
-    def fields(request, name):
-        fields_str = request.GET.get(name) if request is not None else None
-        return fields_str.split(',') if fields_str is not None else None
-
-    @staticmethod
-    def until(t=None):
-        """ Returns time suitable for use as upper time boundary for check.
-
-            Used to avoid issues with time between setting the change stamp
-            and actually saving the model in database.
-            Cuts the microsecond part to avoid issues with DBs where time has
-            more precision.
-
-            :param datetime t: manually sets the upper boundary
-
-        """
-        # set to five minutes ago, to avoid concurrency issues
-        if t is None:
-            t = datetime.now() - timedelta(seconds=settings.API_WAIT)
-        # set to whole second in case DB supports something smaller
-        return t.replace(microsecond=0)
-
-    @staticmethod
-    def book_dict(book, fields=None):
-        all_fields = ['url', 'title', 'description',
-                      'gazeta_link', 'wiki_link',
-                      ] + Book.formats + BookMedia.formats.keys() + [
-                      'parent', 'parent_number',
-                      'tags',
-                      'license', 'license_description', 'source_name',
-                      'technical_editors', 'editors',
-                      'author', 'sort_key',
-                     ]
-        if fields:
-            fields = (f for f in fields if f in all_fields)
-        else:
-            fields = all_fields
-
-        extra_info = book.extra_info
-
-        obj = {}
-        for field in fields:
-
-            if field in Book.formats:
-                f = getattr(book, field+'_file')
-                if f:
-                    obj[field] = {
-                        'url': f.url,
-                        'size': f.size,
-                    }
-
-            elif field in BookMedia.formats:
-                media = []
-                for m in book.media.filter(type=field).iterator():
-                    media.append({
-                        'url': m.file.url,
-                        'size': m.file.size,
-                    })
-                if media:
-                    obj[field] = media
-
-            elif field == 'url':
-                obj[field] = book.get_absolute_url()
-
-            elif field == 'tags':
-                obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set')).iterator()]
-
-            elif field == 'author':
-                obj[field] = ", ".join(t.name for t in book.tags.filter(category='author').iterator())
-
-            elif field == 'parent':
-                obj[field] = book.parent_id
-
-            elif field in ('license', 'license_description', 'source_name',
-                      'technical_editors', 'editors'):
-                f = extra_info.get(field)
-                if f:
-                    obj[field] = f
-
-            else:
-                f = getattr(book, field)
-                if f:
-                    obj[field] = f
-
-        obj['id'] = book.id
-        return obj
-
-    @classmethod
-    def book_changes(cls, request=None, since=0, until=None, fields=None):
-        since = datetime.fromtimestamp(int(since))
-        until = cls.until(until)
-
-        changes = {
-            'time_checked': timestamp(until)
-        }
-
-        if not fields:
-            fields = cls.fields(request, 'book_fields')
-
-        added = []
-        updated = []
-        deleted = []
-
-        last_change = since
-        for book in Book.objects.filter(changed_at__gte=since,
-                    changed_at__lt=until).iterator():
-            book_d = cls.book_dict(book, fields)
-            updated.append(book_d)
-        if updated:
-            changes['updated'] = updated
-
-        for book in Deleted.objects.filter(content_type=Book, 
-                    deleted_at__gte=since,
-                    deleted_at__lt=until,
-                    created_at__lt=since).iterator():
-            deleted.append(book.id)
-        if deleted:
-            changes['deleted'] = deleted
-
-        return changes
-
-    @staticmethod
-    def tag_dict(tag, fields=None):
-        all_fields = ('name', 'category', 'sort_key', 'description',
-                      'gazeta_link', 'wiki_link',
-                      'url', 'books',
-                     )
-
-        if fields:
-            fields = (f for f in fields if f in all_fields)
-        else:
-            fields = all_fields
-
-        obj = {}
-        for field in fields:
-
-            if field == 'url':
-                obj[field] = tag.get_absolute_url()
-
-            elif field == 'books':
-                obj[field] = [b.id for b in Book.tagged_top_level([tag]).iterator()]
-
-            elif field == 'sort_key':
-                obj[field] = tag.sort_key
-
-            else:
-                f = getattr(tag, field)
-                if f:
-                    obj[field] = f
-
-        obj['id'] = tag.id
-        return obj
-
-    @classmethod
-    def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
-        since = datetime.fromtimestamp(int(since))
-        until = cls.until(until)
-
-        changes = {
-            'time_checked': timestamp(until)
-        }
-
-        if not fields:
-            fields = cls.fields(request, 'tag_fields')
-        if not categories:
-            categories = cls.fields(request, 'tag_categories')
-
-        all_categories = ('author', 'epoch', 'kind', 'genre')
-        if categories:
-            categories = (c for c in categories if c in all_categories)
-        else:
-            categories = all_categories
-
-        updated = []
-        deleted = []
-
-        for tag in Tag.objects.filter(category__in=categories, 
-                    changed_at__gte=since,
-                    changed_at__lt=until).iterator():
-            # only serve non-empty tags
-            if tag.book_count:
-                tag_d = cls.tag_dict(tag, fields)
-                updated.append(tag_d)
-            elif tag.created_at < since:
-                deleted.append(tag.id)
-        if updated:
-            changes['updated'] = updated
-
-        for tag in Deleted.objects.filter(category__in=categories,
-                content_type=Tag, 
-                    deleted_at__gte=since,
-                    deleted_at__lt=until,
-                    created_at__lt=since).iterator():
-            deleted.append(tag.id)
-        if deleted:
-            changes['deleted'] = deleted
-
-        return changes
-
-    @classmethod
-    def changes(cls, request=None, since=0, until=None, book_fields=None,
-                tag_fields=None, tag_categories=None):
-        until = cls.until(until)
-        since = int(since)
-
-        if not since:
-            cache = get_cache('api')
-            key = hash((book_fields, tag_fields, tag_categories,
-                    tuple(sorted(request.GET.items()))
-                  ))
-            value = cache.get(key)
-            if value is not None:
-                return value
-
-        changes = {
-            'time_checked': timestamp(until)
-        }
-
-        changes_by_type = {
-            'books': cls.book_changes(request, since, until, book_fields),
-            'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
-        }
-
-        for model in changes_by_type:
-            for field in changes_by_type[model]:
-                if field == 'time_checked':
-                    continue
-                changes.setdefault(field, {})[model] = changes_by_type[model][field]
-
-        if not since:
-            cache.set(key, changes)
-
-        return changes
-
-
-class BookChangesHandler(CatalogueHandler):
-    allowed_methods = ('GET',)
-
-    @piwik_track
-    def read(self, request, since):
-        return self.book_changes(request, since)
-
-
-class TagChangesHandler(CatalogueHandler):
-    allowed_methods = ('GET',)
-
-    @piwik_track
-    def read(self, request, since):
-        return self.tag_changes(request, since)
-
-
-class ChangesHandler(CatalogueHandler):
-    allowed_methods = ('GET',)
-
-    @piwik_track
-    def read(self, request, since):
-        return self.changes(request, since)
-
-
 class PictureHandler(BaseHandler):
     model = Picture
     fields = ('slug', 'title')
 class PictureHandler(BaseHandler):
     model = Picture
     fields = ('slug', 'title')