rearrangements: new core app, templates in apps, split settings;
[wolnelektury.git] / apps / api / handlers.py
index f99dd90..98a5742 100644 (file)
@@ -4,9 +4,11 @@
 
 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
+from django.core.cache import get_cache
 from django.core.urlresolvers import reverse
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
 from django.core.urlresolvers import reverse
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
@@ -16,6 +18,7 @@ from api.models import Deleted
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment
 from picture.models import Picture
 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.
@@ -86,53 +92,67 @@ class BookMediaHandler(BaseHandler):
         return MEDIA_BASE + media.file.url
 
 
         return MEDIA_BASE + media.file.url
 
 
-class BookDetailHandler(BaseHandler):
-    """ Main handler for Book objects.
+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])
 
 
-    Responsible for lists of Book objects
-    and fields used for representing Books.
+    @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, BookDetails):
+    """ Main handler for Book objects.
 
 
+    Responsible for single Book details.
     """
     allowed_methods = ['GET']
     """
     allowed_methods = ['GET']
-    fields = ['title', 'parent'] + Book.formats + [
-        'media', 'url'] + category_singular.keys()
+    fields = ['title', 'parent', 'children'] + Book.formats + [
+        'media', 'url', 'cover'] + book_tag_categories
 
     @piwik_track
     def read(self, request, book):
         """ Returns details of a book, identified by a slug and lang. """
 
     @piwik_track
     def read(self, request, book):
         """ Returns details of a book, identified by a slug and lang. """
-        kwargs = Book.split_urlid(book)
-        if not kwargs:
-            return rc.NOT_FOUND
-
         try:
         try:
-            return Book.objects.get(**kwargs)
+            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.urlid()])
-
-    @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
     def read(self, request, tags, top_level=False):
 
     @piwik_track
     def read(self, request, tags, top_level=False):
@@ -144,13 +164,19 @@ 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)
                 return books if books else rc.NOT_FOUND
             else:
                 books = Book.tagged.with_all(tags)
         if tags:
             if top_level:
                 books = Book.tagged_top_level(tags)
                 return books if books else rc.NOT_FOUND
             else:
                 books = Book.tagged.with_all(tags)
+        elif top_level:
+            books = Book.objects.filter(parent=None)
         else:
             books = Book.objects.all()
 
         else:
             books = Book.objects.all()
 
@@ -160,18 +186,13 @@ class AnonymousBooksHandler(AnonymousBaseHandler):
             return rc.NOT_FOUND
 
     def create(self, request, tags, top_level=False):
             return rc.NOT_FOUND
 
     def create(self, request, tags, top_level=False):
-        return 'aaa'
+        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
 
     def create(self, request, tags, top_level=False):
     anonymous = AnonymousBooksHandler
 
     def create(self, request, tags, top_level=False):
@@ -186,6 +207,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
@@ -193,7 +215,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):
@@ -207,13 +229,29 @@ def _file_getter(format):
             return ''
     return get_file
 for format in Book.formats:
             return ''
     return get_file
 for format in Book.formats:
-    setattr(BooksHandler, format, _file_getter(format))
+    setattr(BookDetails, format, _file_getter(format))
+
+
+class TagDetails(object):
+    """Custom Tag fields."""
+
+    @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):
+
+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):
@@ -230,7 +268,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
@@ -239,7 +277,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):
@@ -257,34 +295,42 @@ class TagsHandler(BaseHandler):
             return rc.NOT_FOUND
 
 
             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. """
 
 
-class FragmentDetailHandler(BaseHandler):
+        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')
+
+
+class FragmentDetailHandler(BaseHandler, FragmentDetails):
     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
     @piwik_track
     def read(self, request, book, anchor):
         """ Returns details of a fragment, identified by book slug and anchor. """
     fields = ['book', 'anchor', 'text', 'url', 'themes']
 
     @piwik_track
     def read(self, request, book, anchor):
         """ Returns details of a fragment, identified by book slug and anchor. """
-        kwargs = Book.split_urlid(book)
-        if not kwargs:
-            return rc.NOT_FOUND
-
-        fragment_kwargs = {}
-        for field, value in kwargs.items():
-            fragment_kwargs['book__' + field] = value
-
         try:
         try:
-            return Fragment.objects.get(anchor=anchor, **fragment_kwargs)
+            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
@@ -292,7 +338,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'])
@@ -305,32 +351,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.urlid(), 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
@@ -541,6 +571,16 @@ class CatalogueHandler(BaseHandler):
     def changes(cls, request=None, since=0, until=None, book_fields=None,
                 tag_fields=None, tag_categories=None):
         until = cls.until(until)
     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 = {
             'time_checked': timestamp(until)
@@ -556,6 +596,10 @@ class CatalogueHandler(BaseHandler):
                 if field == 'time_checked':
                     continue
                 changes.setdefault(field, {})[model] = changes_by_type[model][field]
                 if field == 'time_checked':
                     continue
                 changes.setdefault(field, {})[model] = changes_by_type[model][field]
+
+        if not since:
+            cache.set(key, changes)
+
         return changes
 
 
         return changes
 
 
@@ -589,11 +633,11 @@ class PictureHandler(BaseHandler):
     allowed_methods = ('POST',)
 
     def create(self, request):
     allowed_methods = ('POST',)
 
     def create(self, request):
-        if not request.user.has_perm('catalogue.add_book'):
+        if not request.user.has_perm('picture.add_picture'):
             return rc.FORBIDDEN
 
         data = json.loads(request.POST.get('data'))
             return rc.FORBIDDEN
 
         data = json.loads(request.POST.get('data'))
-        form = BookImportForm(data)
+        form = PictureImportForm(data)
         if form.is_valid():
             form.save()
             return rc.CREATED
         if form.is_valid():
             form.save()
             return rc.CREATED