Search engine now should be db change change-proof
[wolnelektury.git] / apps / api / handlers.py
index eb15a4e..260384d 100644 (file)
@@ -4,6 +4,7 @@
 
 from datetime import datetime, timedelta
 import json
 
 from datetime import datetime, timedelta
 import json
+from urlparse import urljoin
 
 from django.conf import settings
 from django.contrib.sites.models import Site
 
 from django.conf import settings
 from django.contrib.sites.models import Site
@@ -16,6 +17,8 @@ from api.helpers import timestamp
 from api.models import Deleted
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment
 from api.models import Deleted
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment
+from picture.models import Picture
+from picture.forms import PictureImportForm
 
 from stats.utils import piwik_track
 
 
 from stats.utils import piwik_track
 
@@ -34,6 +37,9 @@ category_plural={}
 for k, v in category_singular.items():
     category_plural[v] = k
 
 for k, v in category_singular.items():
     category_plural[v] = k
 
+book_tag_categories = ['author', 'epoch', 'kind', 'genre']
+
+
 
 def read_tags(tags, allowed):
     """ Reads a path of filtering tags.
 
 def read_tags(tags, allowed):
     """ Reads a path of filtering tags.
@@ -77,7 +83,7 @@ class BookMediaHandler(BaseHandler):
     """ Responsible for representing media in Books. """
 
     model = BookMedia
     """ Responsible for representing media in Books. """
 
     model = BookMedia
-    fields = ['name', 'type', 'url']
+    fields = ['name', 'type', 'url', 'artist', 'director']
 
     @classmethod
     def url(cls, media):
 
     @classmethod
     def url(cls, media):
@@ -85,54 +91,82 @@ class BookMediaHandler(BaseHandler):
 
         return MEDIA_BASE + media.file.url
 
 
         return MEDIA_BASE + media.file.url
 
+    @classmethod
+    def artist(cls, media):
+        return media.get_extra_info_value().get('artist_name', '')
+
+    @classmethod
+    def director(cls, media):
+        return media.get_extra_info_value().get('director_name', '')
+        
+
+
+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']['author'])
+
+    @classmethod
+    def href(cls, book):
+        """ Returns an URI for a Book in the API. """
+        return API_BASE + reverse("api_book", args=[book.slug])
+
+    @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 book.children.all()
+
+    @classmethod
+    def media(cls, book):
+        """ Returns all media for a book. """
+        return book.media.all()
+
+    @classmethod
+    def cover(cls, book):
+        return MEDIA_BASE + book.cover.url if book.cover else ''
 
 
-class BookDetailHandler(BaseHandler):
-    """ Main handler for Book objects.
 
 
-    Responsible for lists of Book objects
-    and fields used for representing Books.
 
 
+class BookDetailHandler(BaseHandler, BookDetails):
+    """ Main handler for Book objects.
+
+    Responsible for single Book details.
     """
     allowed_methods = ['GET']
     """
     allowed_methods = ['GET']
-    fields = ['title', 'parent'] + Book.file_types + [
-        'media', 'url'] + category_singular.keys()
+    fields = ['title', 'parent', 'children'] + Book.formats + [
+        'media', 'url', 'cover'] + [
+            category_plural[c] for c in book_tag_categories]
 
     @piwik_track
 
     @piwik_track
-    def read(self, request, slug):
-        """ Returns details of a book, identified by a slug. """
-
+    def read(self, request, book):
+        """ Returns details of a book, identified by a slug and lang. """
         try:
         try:
-            return Book.objects.get(slug=slug)
+            return Book.objects.get(slug=book)
         except Book.DoesNotExist:
             return rc.NOT_FOUND
 
 
         except Book.DoesNotExist:
             return rc.NOT_FOUND
 
 
-class AnonymousBooksHandler(AnonymousBaseHandler):
+class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
     """ Main handler for Book objects.
 
     """ Main handler for Book objects.
 
-    Responsible for lists of Book objects
-    and fields used for representing Books.
-
+    Responsible for lists of Book objects.
     """
     allowed_methods = ('GET',)
     model = Book
     """
     allowed_methods = ('GET',)
     model = Book
-    fields = ['href', 'title']
-
-    categories = set(['author', 'epoch', 'kind', 'genre'])
-
-    @classmethod
-    def href(cls, book):
-        """ Returns an URI for a Book in the API. """
-        return API_BASE + reverse("api_book", args=[book.slug])
-
-    @classmethod
-    def url(cls, book):
-        """ Returns Book's URL on the site. """
-
-        return WL_BASE + book.get_absolute_url()
+    fields = ['author', 'href', 'title', 'url', 'cover']
 
     @piwik_track
 
     @piwik_track
-    def read(self, request, tags, top_level=False):
+    def read(self, request, tags, top_level=False,
+                audiobooks=False, daisy=False):
         """ 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
@@ -141,7 +175,11 @@ class AnonymousBooksHandler(AnonymousBaseHandler):
              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.
         """
-        tags = read_tags(tags, allowed=self.categories)
+        try:
+            tags = read_tags(tags, allowed=book_tag_categories)
+        except ValueError:
+            return rc.NOT_FOUND
+
         if tags:
             if top_level:
                 books = Book.tagged_top_level(tags)
         if tags:
             if top_level:
                 books = Book.tagged_top_level(tags)
@@ -150,28 +188,30 @@ class AnonymousBooksHandler(AnonymousBaseHandler):
                 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:
+            books = books.filter(media__type='mp3')
+        if daisy:
+            books = books.filter(media__type='daisy')
 
         if books.exists():
             return books
         else:
             return rc.NOT_FOUND
 
 
         if books.exists():
             return books
         else:
             return rc.NOT_FOUND
 
-    def create(self, request, tags, top_level=False):
-        return 'aaa'
+    def create(self, request, *args, **kwargs):
+        return rc.FORBIDDEN
 
 
-    @classmethod
-    def media(self, book):
-        """ Returns all media for a book. """
-
-        return book.media.all()
 
 
-
-class BooksHandler(BaseHandler):
+class BooksHandler(BookDetailHandler):
+    allowed_methods = ('GET', 'POST')
     model = Book
     model = Book
-    fields = ('slug', 'title')
+    fields = ['author', 'href', 'title', 'url']
     anonymous = AnonymousBooksHandler
 
     anonymous = AnonymousBooksHandler
 
-    def create(self, request, tags, top_level=False):
+    def create(self, request, *args, **kwargs):
         if not request.user.has_perm('catalogue.add_book'):
             return rc.FORBIDDEN
 
         if not request.user.has_perm('catalogue.add_book'):
             return rc.FORBIDDEN
 
@@ -183,6 +223,7 @@ class BooksHandler(BaseHandler):
         else:
             return rc.NOT_FOUND
 
         else:
             return rc.NOT_FOUND
 
+
 # add categorized tags fields for Book
 def _tags_getter(category):
     @classmethod
 # add categorized tags fields for Book
 def _tags_getter(category):
     @classmethod
@@ -190,7 +231,7 @@ def _tags_getter(category):
         return book.tags.filter(category=category)
     return get_tags
 for plural, singular in category_singular.items():
         return book.tags.filter(category=category)
     return get_tags
 for plural, singular in category_singular.items():
-    setattr(BooksHandler, plural, _tags_getter(singular))
+    setattr(BookDetails, plural, _tags_getter(singular))
 
 # add fields for files in Book
 def _file_getter(format):
 
 # add fields for files in Book
 def _file_getter(format):
@@ -203,14 +244,30 @@ def _file_getter(format):
         else:
             return ''
     return get_file
         else:
             return ''
     return get_file
-for format in Book.file_types:
-    setattr(BooksHandler, format, _file_getter(format))
+for format in Book.formats:
+    setattr(BookDetails, format, _file_getter(format))
+
 
 
+class TagDetails(object):
+    """Custom Tag fields."""
 
 
-class TagDetailHandler(BaseHandler):
+    @classmethod
+    def href(cls, tag):
+        """ Returns URI in the API for the tag. """
+
+        return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
+
+    @classmethod
+    def url(cls, tag):
+        """ Returns URL on the site. """
+
+        return WL_BASE + tag.get_absolute_url()
+
+
+class TagDetailHandler(BaseHandler, TagDetails):
     """ Responsible for details of a single Tag object. """
 
     """ Responsible for details of a single Tag object. """
 
-    fields = ['name', 'sort_key', 'description']
+    fields = ['name', 'url', 'sort_key', 'description']
 
     @piwik_track
     def read(self, request, category, slug):
 
     @piwik_track
     def read(self, request, category, slug):
@@ -227,7 +284,7 @@ class TagDetailHandler(BaseHandler):
             return rc.NOT_FOUND
 
 
             return rc.NOT_FOUND
 
 
-class TagsHandler(BaseHandler):
+class TagsHandler(BaseHandler, TagDetails):
     """ Main handler for Tag objects.
 
     Responsible for lists of Tag objects
     """ Main handler for Tag objects.
 
     Responsible for lists of Tag objects
@@ -236,7 +293,7 @@ class TagsHandler(BaseHandler):
     """
     allowed_methods = ('GET',)
     model = Tag
     """
     allowed_methods = ('GET',)
     model = Tag
-    fields = ['name', 'href']
+    fields = ['name', 'href', 'url']
 
     @piwik_track
     def read(self, request, category):
 
     @piwik_track
     def read(self, request, category):
@@ -247,35 +304,49 @@ class TagsHandler(BaseHandler):
         except KeyError, e:
             return rc.NOT_FOUND
 
         except KeyError, e:
             return rc.NOT_FOUND
 
-        tags = Tag.objects.filter(category=category_sng)
-        tags = [t for t in tags if t.get_count() > 0]
-        if tags:
+        tags = Tag.objects.filter(category=category_sng).exclude(book_count=0)
+        if tags.exists():
             return tags
         else:
             return rc.NOT_FOUND
 
 
             return tags
         else:
             return rc.NOT_FOUND
 
 
+class FragmentDetails(object):
+    """Custom Fragment fields."""
+
     @classmethod
     @classmethod
-    def href(cls, tag):
-        """ Returns URI in the API for the tag. """
+    def href(cls, fragment):
+        """ Returns URI in the API for the fragment. """
 
 
-        return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
+        return API_BASE + reverse("api_fragment", 
+            args=[fragment.book.slug, fragment.anchor])
+
+    @classmethod
+    def url(cls, fragment):
+        """ Returns URL on the site for the fragment. """
+
+        return WL_BASE + fragment.get_absolute_url()
 
 
+    @classmethod
+    def themes(cls, fragment):
+        """ Returns a list of theme tags for the fragment. """
 
 
-class FragmentDetailHandler(BaseHandler):
+        return fragment.tags.filter(category='theme')
+
+
+class FragmentDetailHandler(BaseHandler, FragmentDetails):
     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
     @piwik_track
     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
     @piwik_track
-    def read(self, request, slug, anchor):
+    def read(self, request, book, anchor):
         """ Returns details of a fragment, identified by book slug and anchor. """
         """ Returns details of a fragment, identified by book slug and anchor. """
-
         try:
         try:
-            return Fragment.objects.get(book__slug=slug, anchor=anchor)
+            return Fragment.objects.get(book__slug=book, anchor=anchor)
         except Fragment.DoesNotExist:
             return rc.NOT_FOUND
 
 
         except Fragment.DoesNotExist:
             return rc.NOT_FOUND
 
 
-class FragmentsHandler(BaseHandler):
+class FragmentsHandler(BaseHandler, FragmentDetails):
     """ Main handler for Fragments.
 
     Responsible for lists of Fragment objects
     """ Main handler for Fragments.
 
     Responsible for lists of Fragment objects
@@ -283,7 +354,7 @@ class FragmentsHandler(BaseHandler):
 
     """
     model = Fragment
 
     """
     model = Fragment
-    fields = ['book', 'anchor', 'href']
+    fields = ['book', 'url', 'anchor', 'href']
     allowed_methods = ('GET',)
 
     categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
     allowed_methods = ('GET',)
 
     categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
@@ -296,32 +367,16 @@ class FragmentsHandler(BaseHandler):
              books/book-slug/authors/an-author/themes/a-theme/
 
         """
              books/book-slug/authors/an-author/themes/a-theme/
 
         """
-        tags = read_tags(tags, allowed=self.categories)
+        try:
+            tags = read_tags(tags, allowed=self.categories)
+        except ValueError:
+            return rc.NOT_FOUND
         fragments = Fragment.tagged.with_all(tags).select_related('book')
         if fragments.exists():
             return fragments
         else:
             return rc.NOT_FOUND
 
         fragments = Fragment.tagged.with_all(tags).select_related('book')
         if fragments.exists():
             return fragments
         else:
             return rc.NOT_FOUND
 
-    @classmethod
-    def href(cls, fragment):
-        """ Returns URI in the API for the fragment. """
-
-        return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
-
-    @classmethod
-    def url(cls, fragment):
-        """ Returns URL on the site for the fragment. """
-
-        return WL_BASE + fragment.get_absolute_url()
-
-    @classmethod
-    def themes(cls, fragment):
-        """ Returns a list of theme tags for the fragment. """
-
-        return fragment.tags.filter(category='theme')
-
-
 
 
 # Changes handlers
 
 
 # Changes handlers
@@ -355,8 +410,7 @@ class CatalogueHandler(BaseHandler):
     def book_dict(book, fields=None):
         all_fields = ['url', 'title', 'description',
                       'gazeta_link', 'wiki_link',
     def book_dict(book, fields=None):
         all_fields = ['url', 'title', 'description',
                       'gazeta_link', 'wiki_link',
-                      ] + Book.file_types + [
-                      'mp3', 'ogg', 'daisy',
+                      ] + Book.formats + BookMedia.formats + [
                       'parent', 'parent_number',
                       'tags',
                       'license', 'license_description', 'source_name',
                       'parent', 'parent_number',
                       'tags',
                       'license', 'license_description', 'source_name',
@@ -373,7 +427,7 @@ class CatalogueHandler(BaseHandler):
         obj = {}
         for field in fields:
 
         obj = {}
         for field in fields:
 
-            if field in Book.file_types:
+            if field in Book.formats:
                 f = getattr(book, field+'_file')
                 if f:
                     obj[field] = {
                 f = getattr(book, field+'_file')
                 if f:
                     obj[field] = {
@@ -381,7 +435,7 @@ class CatalogueHandler(BaseHandler):
                         'size': f.size,
                     }
 
                         'size': f.size,
                     }
 
-            elif field in ('mp3', 'ogg', 'daisy'):
+            elif field in BookMedia.formats:
                 media = []
                 for m in book.media.filter(type=field):
                     media.append({
                 media = []
                 for m in book.media.filter(type=field):
                     media.append({
@@ -510,7 +564,7 @@ class CatalogueHandler(BaseHandler):
                     changed_at__gte=since,
                     changed_at__lt=until):
             # only serve non-empty tags
                     changed_at__gte=since,
                     changed_at__lt=until):
             # only serve non-empty tags
-            if tag.get_count():
+            if tag.book_count:
                 tag_d = cls.tag_dict(tag, fields)
                 updated.append(tag_d)
             elif tag.created_at < since:
                 tag_d = cls.tag_dict(tag, fields)
                 updated.append(tag_d)
             elif tag.created_at < since:
@@ -587,3 +641,21 @@ class ChangesHandler(CatalogueHandler):
     @piwik_track
     def read(self, request, since):
         return self.changes(request, since)
     @piwik_track
     def read(self, request, since):
         return self.changes(request, since)
+
+
+class PictureHandler(BaseHandler):
+    model = Picture
+    fields = ('slug', 'title')
+    allowed_methods = ('POST',)
+
+    def create(self, request):
+        if not request.user.has_perm('picture.add_picture'):
+            return rc.FORBIDDEN
+
+        data = json.loads(request.POST.get('data'))
+        form = PictureImportForm(data)
+        if form.is_valid():
+            form.save()
+            return rc.CREATED
+        else:
+            return rc.NOT_FOUND