Introduce DRF and start replacing the views.
authorRadek Czajka <rczajka@rczajka.pl>
Thu, 31 Jan 2019 19:56:07 +0000 (20:56 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Thu, 31 Jan 2019 19:56:07 +0000 (20:56 +0100)
22 files changed:
requirements/requirements.txt
src/api/fields.py [new file with mode: 0644]
src/api/handlers.py
src/api/renderers.py [new file with mode: 0644]
src/api/templates/api/main.html
src/api/tests/res/responses/books-child.json
src/api/tests/res/responses/books-grandchild.json
src/api/tests/res/responses/books-parent.json
src/api/tests/res/responses/collection.json
src/api/tests/res/responses/collections.json
src/api/tests/tests.py
src/api/urls.py
src/catalogue/api/__init__.py [new file with mode: 0644]
src/catalogue/api/fields.py [new file with mode: 0644]
src/catalogue/api/serializers.py [new file with mode: 0644]
src/catalogue/api/urls.py [new file with mode: 0644]
src/catalogue/api/views.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/models/bookmedia.py
src/catalogue/models/tag.py
src/wolnelektury/settings/apps.py
src/wolnelektury/settings/contrib.py

index 3037f5c..3b8c95e 100644 (file)
@@ -13,6 +13,8 @@ django-picklefield>=1.0,<1.1
 django-modeltranslation>=0.10,<0.11
 django-allauth>=0.32,<0.33
 django-extensions
+djangorestframework<3.7
+djangorestframework-xml
 
 # contact
 pyyaml
diff --git a/src/api/fields.py b/src/api/fields.py
new file mode 100644 (file)
index 0000000..4bb44dc
--- /dev/null
@@ -0,0 +1,34 @@
+from rest_framework import serializers
+from django.core.urlresolvers import reverse
+
+
+class AbsoluteURLField(serializers.ReadOnlyField):
+    def __init__(self, view_name=None, view_args=None, source='get_absolute_url', *args, **kwargs):
+        if view_name is not None:
+            source = '*'
+        super(AbsoluteURLField, self).__init__(*args, source=source, **kwargs)
+        self.view_name = view_name
+        self.view_args = {}
+        if view_args:
+            for v in view_args:
+                fields = v.split(':', 1)
+                self.view_args[fields[0]] = fields[1] if len(fields)>1 else fields[0]
+
+    def to_representation(self, value):
+        if self.view_name is not None:
+            kwargs = {
+                arg: getattr(value, field)
+                for (arg, field) in self.view_args.items()
+            }
+            value = reverse(self.view_name, kwargs=kwargs)
+        return self.context['request'].build_absolute_uri(value)
+
+
+class LegacyMixin(object):
+    def to_representation(self, value):
+        value = super(LegacyMixin, self).to_representation(value)
+        non_null_fields = getattr(getattr(self, 'Meta', None), 'legacy_non_null_fields', [])
+        for field in non_null_fields:
+            if field in value and value[field] is None:
+                value[field] = ''
+        return value
index cd24561..f3cc4a7 100644 (file)
@@ -19,7 +19,6 @@ from api.models import BookUserData
 from catalogue.forms import BookImportForm
 from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
 from catalogue.models.tag import prefetch_relations
-from librarian.cover import WLCover
 from paypal.rest import user_is_subscribed
 from picture.models import Picture
 from picture.forms import PictureImportForm
@@ -33,8 +32,6 @@ from . import emitters  # Register our emitters
 API_BASE = WL_BASE = MEDIA_BASE = lazy(
     lambda: u'https://' + Site.objects.get_current().domain, unicode)()
 
-SORT_KEY_SEP = '$'
-
 category_singular = {
     'authors': 'author',
     'kinds': 'kind',
@@ -131,7 +128,7 @@ class BookDetails(object):
     @classmethod
     def href(cls, book):
         """ Returns an URI for a Book in the API. """
-        return API_BASE + reverse("api_book", args=[book.slug])
+        return API_BASE + reverse("catalogue_api_book", args=[book.slug])
 
     @classmethod
     def url(cls, book):
@@ -165,20 +162,12 @@ class BookDetails(object):
     def simple_cover(cls, book):
         return MEDIA_BASE + book.simple_cover.url if book.simple_cover else ''
 
-    @classmethod
-    def cover_color(cls, book):
-        return WLCover.epoch_colors.get(book.extra_info.get('epoch'), '#000000')
-
-    @classmethod
-    def full_sort_key(cls, book):
-        return '%s%s%s%s%s' % (book.sort_key_author, SORT_KEY_SEP, book.sort_key, SORT_KEY_SEP, book.id)
-
     @staticmethod
     def books_after(books, after, new_api):
         if not new_api:
             return books.filter(slug__gt=after)
         try:
-            author, title, book_id = after.split(SORT_KEY_SEP)
+            author, title, book_id = after.split(Book.SORT_KEY_SEP)
         except ValueError:
             return Book.objects.none()
         return books.filter(Q(sort_key_author__gt=author)
@@ -436,7 +425,7 @@ class AnonFilterBooksHandler(AnonymousBooksHandler):
             remaining_count = count - len(filtered_books)
             new_books = [
                 BookProxy(book, '%s%s%s' % (
-                    label, key_sep, book.slug if not new_api else self.full_sort_key(book)))
+                    label, key_sep, book.slug if not new_api else book.full_sort_key()))
                 for book in book_list[:remaining_count]]
             filtered_books += new_books
             if len(filtered_books) == count:
@@ -514,50 +503,6 @@ def add_file_getters():
 add_file_getters()
 
 
-class CollectionDetails(object):
-    """Custom Collection fields."""
-
-    @classmethod
-    def href(cls, collection):
-        """ Returns URI in the API for the collection. """
-
-        return API_BASE + reverse("api_collection", args=[collection.slug])
-
-    @classmethod
-    def url(cls, collection):
-        """ Returns URL on the site. """
-
-        return WL_BASE + collection.get_absolute_url()
-
-    @classmethod
-    def books(cls, collection):
-        return Book.objects.filter(collection.get_query())
-
-
-class CollectionDetailHandler(BaseHandler, CollectionDetails):
-    allowed_methods = ('GET',)
-    fields = ['url', 'title', 'description', 'books']
-
-    @piwik_track
-    def read(self, request, slug):
-        """ Returns details of a collection, identified by slug. """
-        try:
-            return Collection.objects.get(slug=slug)
-        except Collection.DoesNotExist:
-            return rc.NOT_FOUND
-
-
-class CollectionsHandler(BaseHandler, CollectionDetails):
-    allowed_methods = ('GET',)
-    model = Collection
-    fields = ['url', 'href', 'title']
-
-    @piwik_track
-    def read(self, request):
-        """ Returns all collections. """
-        return Collection.objects.all()
-
-
 class TagDetails(object):
     """Custom Tag fields."""
 
diff --git a/src/api/renderers.py b/src/api/renderers.py
new file mode 100644 (file)
index 0000000..b5b63c6
--- /dev/null
@@ -0,0 +1,11 @@
+from rest_framework_xml.renderers import XMLRenderer
+
+
+class LegacyXMLRenderer(XMLRenderer):
+    """
+    Renderer which serializes to XML.
+    """
+
+    item_tag_name = 'resource'
+    root_tag_name = 'response'
+
index 67ff426..a718f21 100755 (executable)
 
         <li><a href='{% url "api_tag_list" "themes" %}'>
           {% url "api_tag_list" "themes" %}</a> – {% trans "List of all themes" %}</li>
-        <li><a href='{% url "api_collections" %}'>
-          {% url "api_collections" %}</a> – {% trans "Collections" %}</li>
+        <li><a href='{% url "catalogue_api_collections" %}'>
+          {% url "catalogue_api_collections" %}</a> – {% trans "Collections" %}</li>
     </ul>
 
     <p>
-      {% url "api_book" "studnia-i-wahadlo" as e1 %}
+      {% url "catalogue_api_book" "studnia-i-wahadlo" as e1 %}
       {% url "api_tag" "authors" "edgar-allan-poe" as e2 %}
       {% blocktrans %}
         Each element of those lists contains a link (in a "href") attibute
index 4c5c134..27aaee7 100644 (file)
             "kind": "", 
             "full_sort_key": "$grandchild$3", 
             "author": "", 
-            "url": "https://example.com/katalog/lektura/grandchild/", 
+            "url": "http://testserver/katalog/lektura/grandchild/",
             "cover_color": "#000000", 
             "title": "Grandchild", 
             "cover": "", 
             "liked": null, 
             "slug": "grandchild", 
             "epoch": "", 
-            "href": "https://example.com/api/books/grandchild/", 
+            "href": "http://testserver/api/books/grandchild/",
             "genre": "Sonet", 
             "simple_thumb": "", 
             "has_audio": false, 
             "cover_thumb": ""
         }
     ], 
-    "xml": "", 
+    "xml": null,
     "genres": [
         {
-            "url": "https://example.com/katalog/gatunek/wiersz/", 
-            "href": "https://example.com/api/genres/wiersz/", 
+            "url": "http://testserver/katalog/gatunek/wiersz/",
+            "href": "http://testserver/api/genres/wiersz/",
             "name": "Wiersz", 
             "slug": "wiersz"
         }
         "kind": "Liryka", 
         "full_sort_key": "john doe$parent$1", 
         "author": "John Doe", 
-        "url": "https://example.com/katalog/lektura/parent/", 
+        "url": "http://testserver/katalog/lektura/parent/",
         "cover_color": "#a6820a", 
         "title": "Parent", 
-        "cover": "https://example.com/media/cover/parent.jpg", 
+        "cover": "http://testserver/media/cover/parent.jpg",
         "liked": null, 
         "slug": "parent", 
         "epoch": "Barok", 
-        "href": "https://example.com/api/books/parent/", 
+        "href": "http://testserver/api/books/parent/",
         "genre": "Sonet", 
-        "simple_thumb": "https://example.com/media/cover_api_thumb/parent.jpg", 
+        "simple_thumb": "http://testserver/media/cover_api_thumb/parent.jpg",
         "has_audio": true, 
-        "cover_thumb": "https://example.com/media/cover/parent.jpg-139x193"
+        "cover_thumb": "http://testserver/media/cover/parent.jpg-139x193"
     }, 
     "cover_color": "#000000", 
     "simple_cover": "", 
@@ -63,7 +63,7 @@
     "epub": "", 
     "cover_thumb": "", 
     "mobi": "", 
-    "url": "https://example.com/katalog/lektura/child/", 
+    "url": "http://testserver/katalog/lektura/child/",
     "cover": "", 
     "pdf": "", 
     "simple_thumb": ""
index 5d407b8..b7dd9da 100644 (file)
@@ -4,35 +4,35 @@
         "html": "Fragment", 
         "title": "Parent, Child"
     }, 
-    "txt": "https://example.com/katalog/pobierz/grandchild.txt", 
+    "txt": "http://testserver/katalog/pobierz/grandchild.txt",
     "children": [], 
-    "xml": "https://example.com/katalog/pobierz/grandchild.xml", 
+    "xml": "http://testserver/katalog/pobierz/grandchild.xml",
     "genres": [
         {
-            "url": "https://example.com/katalog/gatunek/sonet/", 
-            "href": "https://example.com/api/genres/sonet/", 
+            "url": "http://testserver/katalog/gatunek/sonet/",
+            "href": "http://testserver/api/genres/sonet/",
             "name": "Sonet", 
             "slug": "sonet"
         }
     ], 
     "title": "Grandchild", 
     "media": [], 
-    "html": "https://example.com/katalog/pobierz/grandchild.html", 
+    "html": "http://testserver/katalog/pobierz/grandchild.html",
     "preview": true, 
-    "fb2": "https://example.com/katalog/pobierz/grandchild.fb2", 
+    "fb2": "http://testserver/katalog/pobierz/grandchild.fb2",
     "kinds": [], 
     "parent": {
         "kind": "", 
         "full_sort_key": "$child$2", 
         "author": "", 
-        "url": "https://example.com/katalog/lektura/child/", 
+        "url": "http://testserver/katalog/lektura/child/",
         "cover_color": "#000000", 
         "title": "Child", 
         "cover": "", 
         "liked": null, 
         "slug": "child", 
         "epoch": "", 
-        "href": "https://example.com/api/books/child/", 
+        "href": "http://testserver/api/books/child/",
         "genre": "Wiersz", 
         "simple_thumb": "", 
         "has_audio": false, 
     "simple_cover": "", 
     "authors": [], 
     "audio_length": "", 
-    "epub": "https://example.com/katalog/pobierz/grandchild.epub", 
+    "epub": "http://testserver/katalog/pobierz/grandchild.epub",
     "cover_thumb": "", 
-    "mobi": "https://example.com/katalog/pobierz/grandchild.mobi", 
-    "url": "https://example.com/katalog/lektura/grandchild/", 
+    "mobi": "http://testserver/katalog/pobierz/grandchild.mobi",
+    "url": "http://testserver/katalog/lektura/grandchild/",
     "cover": "", 
-    "pdf": "https://example.com/katalog/pobierz/grandchild.pdf", 
+    "pdf": "http://testserver/katalog/pobierz/grandchild.pdf",
     "simple_thumb": ""
 }
index ade38cc..6dc75a5 100644 (file)
@@ -1,8 +1,8 @@
 {
     "epochs": [
         {
-            "url": "https://example.com/katalog/epoka/barok/", 
-            "href": "https://example.com/api/epochs/barok/", 
+            "url": "http://testserver/katalog/epoka/barok/",
+            "href": "http://testserver/api/epochs/barok/",
             "name": "Barok", 
             "slug": "barok"
         }
             "kind": "", 
             "full_sort_key": "$child$2", 
             "author": "", 
-            "url": "https://example.com/katalog/lektura/child/", 
+            "url": "http://testserver/katalog/lektura/child/",
             "cover_color": "#000000", 
             "title": "Child", 
             "cover": "", 
             "liked": null, 
             "slug": "child", 
             "epoch": "", 
-            "href": "https://example.com/api/books/child/", 
+            "href": "http://testserver/api/books/child/",
             "genre": "Wiersz", 
             "simple_thumb": "", 
             "has_audio": false, 
             "cover_thumb": ""
         }
     ], 
-    "xml": "https://example.com/media/xml/parent.xml", 
+    "xml": "http://testserver/media/xml/parent.xml",
     "genres": [
         {
-            "url": "https://example.com/katalog/gatunek/sonet/", 
-            "href": "https://example.com/api/genres/sonet/", 
+            "url": "http://testserver/katalog/gatunek/sonet/",
+            "href": "http://testserver/api/genres/sonet/",
             "name": "Sonet", 
             "slug": "sonet"
         }
     "title": "Parent", 
     "media": [
         {
-            "url": "https://example.com/media/daisy/parent.daisy", 
+            "url": "http://testserver/media/daisy/parent.daisy",
             "director": "", 
             "type": "daisy", 
             "name": "Parent DAISY", 
             "artist": ""
         }, 
         {
-            "url": "https://example.com/media/mp3/parent.mp3", 
+            "url": "http://testserver/media/mp3/parent.mp3",
             "director": "Director", 
             "type": "mp3", 
             "name": "Parent Audiobook", 
     "fb2": "", 
     "kinds": [
         {
-            "url": "https://example.com/katalog/rodzaj/liryka/", 
-            "href": "https://example.com/api/kinds/liryka/", 
+            "url": "http://testserver/katalog/rodzaj/liryka/",
+            "href": "http://testserver/api/kinds/liryka/",
             "name": "Liryka", 
             "slug": "liryka"
         }
     ], 
     "parent": null, 
     "cover_color": "#a6820a", 
-    "simple_cover": "https://example.com/media/simple_cover/parent.jpg", 
+    "simple_cover": "http://testserver/media/simple_cover/parent.jpg",
     "authors": [
         {
-            "url": "https://example.com/katalog/autor/john-doe/", 
-            "href": "https://example.com/api/authors/john-doe/", 
+            "url": "http://testserver/katalog/autor/john-doe/",
+            "href": "http://testserver/api/authors/john-doe/",
             "name": "John Doe", 
             "slug": "john-doe"
         }
     ], 
     "audio_length": "1:00", 
-    "epub": "https://example.com/media/epub/parent.epub", 
-    "cover_thumb": "https://example.com/media/cover/parent.jpg-139x193", 
-    "mobi": "https://example.com/media/mobi/parent.mobi", 
-    "url": "https://example.com/katalog/lektura/parent/", 
-    "cover": "https://example.com/media/cover/parent.jpg", 
-    "pdf": "https://example.com/media/pdf/parent.pdf", 
-    "simple_thumb": "https://example.com/media/cover_api_thumb/parent.jpg"
+    "epub": "http://testserver/media/epub/parent.epub",
+    "cover_thumb": "http://testserver/media/cover/parent.jpg-139x193",
+    "mobi": "http://testserver/media/mobi/parent.mobi",
+    "url": "http://testserver/katalog/lektura/parent/",
+    "cover": "http://testserver/media/cover/parent.jpg",
+    "pdf": "http://testserver/media/pdf/parent.pdf",
+    "simple_thumb": "http://testserver/media/cover_api_thumb/parent.jpg"
 }
index 866abed..2992461 100644 (file)
@@ -1,22 +1,22 @@
 {
-    "url": "https://example.com/katalog/lektury/a-collection/", 
+    "url": "http://testserver/katalog/lektury/a-collection/",
     "books": [
         {
             "kind": "Liryka", 
             "full_sort_key": "john doe$parent$1", 
             "author": "John Doe", 
-            "url": "https://example.com/katalog/lektura/parent/", 
+            "url": "http://testserver/katalog/lektura/parent/",
             "cover_color": "#a6820a", 
             "title": "Parent", 
-            "cover": "https://example.com/media/cover/parent.jpg", 
+            "cover": "http://testserver/media/cover/parent.jpg",
             "liked": null, 
             "slug": "parent", 
             "epoch": "Barok", 
-            "href": "https://example.com/api/books/parent/", 
+            "href": "http://testserver/api/books/parent/",
             "genre": "Sonet", 
-            "simple_thumb": "https://example.com/media/cover_api_thumb/parent.jpg", 
+            "simple_thumb": "http://testserver/media/cover_api_thumb/parent.jpg",
             "has_audio": true, 
-            "cover_thumb": "https://example.com/media/cover/parent.jpg-139x193"
+            "cover_thumb": "http://testserver/media/cover/parent.jpg-139x193"
         }
     ], 
     "description": "Description", 
index db58324..d76ff2e 100644 (file)
@@ -1,7 +1,7 @@
 [
     {
-        "url": "https://example.com/katalog/lektury/a-collection/", 
-        "href": "https://example.com/api/collections/a-collection/", 
+        "url": "http://testserver/katalog/lektury/a-collection/",
+        "href": "http://testserver/api/collections/a-collection/",
         "title": "A Collection"
     }
 ]
index adaaa60..91e5bbf 100644 (file)
@@ -364,6 +364,15 @@ class AuthorizedTests(ApiTest):
         return json.loads(self.signed(url, method, params).content)
 
     def test_books(self):
+        self.assertEqual(
+            [b['liked'] for b in self.signed_json('/api/books/')],
+            [False, False, False]
+        )
+        # This one fails in the legacy implementation
+        # data = self.signed_json('/api/books/child/')
+        # self.assertFalse(data['parent']['liked'])
+        # self.assertFalse(data['children'][0]['liked'])
+
         self.assertEqual(
             self.signed_json('/api/like/parent/'),
             {"likes": False}
@@ -377,6 +386,10 @@ class AuthorizedTests(ApiTest):
         self.assertTrue(self.signed_json('/api/parent_books/')[0]['liked'])
         self.assertTrue(self.signed_json(
             '/api/filter-books/', params={"search": "parent"})[0]['liked'])
+
+        # This one fails in the legacy implementation.
+        #self.assertTrue(self.signed_json(
+        #    '/api/books/child/')['parent']['liked'])
         # Liked books go on shelf.
         self.assertEqual(
             [x['slug'] for x in self.signed_json('/api/shelf/likes/')],
index bd832fe..3c82e1e 100644 (file)
@@ -2,7 +2,7 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-from django.conf.urls import url
+from django.conf.urls import url, include
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import TemplateView
 from piston.authentication import OAuthAuthentication, oauth_access_token, oauth_request_token
@@ -42,7 +42,6 @@ def auth_resource(handler):
 book_list_resource = auth_resource(handler=handlers.BooksHandler)
 ebook_list_resource = Resource(handler=handlers.EBooksHandler)
 # book_list_resource = Resource(handler=handlers.BooksHandler)
-book_resource = Resource(handler=handlers.BookDetailHandler)
 filter_book_resource = auth_resource(handler=handlers.FilterBooksHandler)
 epub_resource = auth_resource(handler=handlers.EpubHandler)
 
@@ -53,9 +52,6 @@ shelf_resource = auth_resource(handler=handlers.UserShelfHandler)
 
 like_resource = auth_resource(handler=handlers.UserLikeHandler)
 
-collection_resource = Resource(handler=handlers.CollectionDetailHandler)
-collection_list_resource = Resource(handler=handlers.CollectionsHandler)
-
 tag_list_resource = Resource(handler=handlers.TagsHandler)
 tag_resource = Resource(handler=handlers.TagDetailHandler)
 
@@ -78,14 +74,13 @@ urlpatterns = [
 
     url(r'^$', TemplateView.as_view(template_name='api/main.html'), name='api'),
 
+    # These are the new ones.
+    url(r'^', include('catalogue.api.urls')),
+
     # info boxes (used by mobile app)
     url(r'book/(?P<book_id>\d*?)/info\.html$', catalogue.views.book_info),
     url(r'tag/(?P<tag_id>\d*?)/info\.html$', catalogue.views.tag_info),
 
-    # books by collections
-    url(r'^collections/$', collection_list_resource, name="api_collections"),
-    url(r'^collections/(?P<slug>[^/]+)/$', collection_resource, name="api_collection"),
-
     # epub preview
     url(r'^epub/(?P<slug>[a-z0-9-]+)/$', epub_resource, name='api_epub'),
 
@@ -98,7 +93,6 @@ urlpatterns = [
     url(r'^like/(?P<slug>[a-z0-9-]+)/$', like_resource, name='api_like'),
 
     # objects details
-    url(r'^books/(?P<book>[a-z0-9-]+)/$', book_resource, name="api_book"),
     url(r'^(?P<category>[a-z0-9-]+)/(?P<slug>[a-z0-9-]+)/$',
         tag_resource, name="api_tag"),
     url(r'^books/(?P<book>[a-z0-9-]+)/fragments/(?P<anchor>[a-z0-9-]+)/$',
diff --git a/src/catalogue/api/__init__.py b/src/catalogue/api/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/catalogue/api/fields.py b/src/catalogue/api/fields.py
new file mode 100644 (file)
index 0000000..145780e
--- /dev/null
@@ -0,0 +1,30 @@
+from rest_framework import serializers
+from sorl.thumbnail import default
+from catalogue.models import Book
+
+
+class BookLiked(serializers.ReadOnlyField):
+    def __init__(self, source='pk', **kwargs):
+        super(BookLiked, self).__init__(source=source, **kwargs)
+
+    def to_representation(self, value):
+        request = self.context['request']
+        if not hasattr(request, 'liked_books'):
+            if request.user.is_authenticated():
+                request.liked_books = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
+            else:
+                request.liked_books = None
+        if request.liked_books is not None:
+            return value in request.liked_books
+
+
+class ThumbnailField(serializers.FileField):
+    def __init__(self, geometry, *args, **kwargs):
+        self.geometry = geometry
+        super(ThumbnailField, self).__init__(*args, **kwargs)
+        
+    def to_representation(self, value):
+        if value:
+            return super(ThumbnailField, self).to_representation(
+                default.backend.get_thumbnail(value, self.geometry)
+            )
diff --git a/src/catalogue/api/serializers.py b/src/catalogue/api/serializers.py
new file mode 100644 (file)
index 0000000..1705a51
--- /dev/null
@@ -0,0 +1,102 @@
+from rest_framework import serializers
+from api.fields import AbsoluteURLField, LegacyMixin
+from catalogue.models import Book, Collection, Tag, BookMedia
+from .fields import BookLiked, ThumbnailField
+
+
+class TagSerializer(serializers.ModelSerializer):
+    url = AbsoluteURLField()
+    href = AbsoluteURLField(
+        view_name='api_tag',
+        view_args=('category:category_plural', 'slug')
+    )
+
+    class Meta:
+        model = Tag
+        fields = ['url', 'href', 'name', 'slug']
+
+
+class BookSerializer(LegacyMixin, serializers.ModelSerializer):
+    author = serializers.CharField(source='author_unicode')
+    kind = serializers.CharField(source='kind_unicode')
+    epoch = serializers.CharField(source='epoch_unicode')
+    genre = serializers.CharField(source='genre_unicode')
+
+    simple_thumb = serializers.FileField(source='cover_api_thumb')
+    href = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug'])
+    url = AbsoluteURLField()
+    liked = BookLiked()
+    cover_thumb = ThumbnailField('139x193', source='cover')
+
+    class Meta:
+        model = Book
+        fields = [
+            'kind', 'full_sort_key', 'title', 'url', 'cover_color', 'author',
+            'cover', 'liked', 'epoch', 'href', 'has_audio', 'genre',
+            'simple_thumb', 'slug', 'cover_thumb']
+        legacy_non_null_fields = [
+            'kind', 'author', 'epoch', 'genre',
+            'cover', 'simple_thumb', 'cover_thumb']
+
+
+class MediaSerializer(LegacyMixin, serializers.ModelSerializer):
+    url = serializers.FileField(source='file')
+
+    class Meta:
+        model = BookMedia
+        fields = ['url', 'director', 'type', 'name', 'artist']
+        legacy_non_null_fields = ['director', 'artist']
+
+
+class BookDetailSerializer(LegacyMixin, serializers.ModelSerializer):
+    url = AbsoluteURLField()
+
+    authors = TagSerializer(many=True)
+    epochs = TagSerializer(many=True)
+    genres = TagSerializer(many=True)
+    kinds = TagSerializer(many=True)
+
+    fragment_data = serializers.DictField()
+    parent = BookSerializer()
+    children = BookSerializer(many=True)
+
+    xml = AbsoluteURLField(source='xml_url')
+    html = AbsoluteURLField(source='html_url')
+    txt = AbsoluteURLField(source='txt_url')
+    fb2 = AbsoluteURLField(source='fb2_url')
+    epub = AbsoluteURLField(source='epub_url')
+    mobi = AbsoluteURLField(source='mobi_url')
+    pdf = AbsoluteURLField(source='pdf_url')
+    media = MediaSerializer(many=True)
+    cover_thumb = ThumbnailField('139x193', source='cover')
+    simple_thumb = serializers.FileField(source='cover_api_thumb')
+
+    class Meta:
+        model = Book
+        fields = [
+            'title', 'url',
+            'epochs', 'genres', 'kinds', 'authors',
+            'fragment_data', 'children', 'parent', 'preview',
+            'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml', 'media', 'audio_length',
+            'cover_color', 'simple_cover', 'cover_thumb', 'cover', 'simple_thumb'
+        ]
+        legacy_non_null_fields = ['html', 'txt', 'fb2', 'epub', 'mobi', 'pdf',
+                                  'cover', 'simple_cover', 'cover_thumb', 'simple_thumb']
+
+
+class CollectionListSerializer(serializers.ModelSerializer):
+    url = AbsoluteURLField()
+    href = AbsoluteURLField(view_name='collection-detail', view_args=['slug'])
+
+    class Meta:
+        model = Collection
+        fields = ['url', 'href', 'title']
+
+
+class CollectionSerializer(serializers.ModelSerializer):
+    books = BookSerializer(many=True, source='get_books')
+    url = AbsoluteURLField()
+
+    class Meta:
+        model = Collection
+        fields = ['url', 'books', 'description', 'title']
diff --git a/src/catalogue/api/urls.py b/src/catalogue/api/urls.py
new file mode 100644 (file)
index 0000000..e476c8f
--- /dev/null
@@ -0,0 +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.
+#
+from django.conf.urls import include, url
+from . import views
+
+
+urlpatterns = [
+    # books by collections
+    url(r'^collections/$', views.CollectionList.as_view(), name="api_collections"),
+    url(r'^collections/(?P<slug>[^/]+)/$',
+        views.CollectionDetail.as_view(), name="collection-detail"),
+
+    url(r'^books/(?P<slug>[^/]+)/$', views.BookDetail.as_view(), name='catalogue_api_book'),
+]
diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py
new file mode 100644 (file)
index 0000000..1439907
--- /dev/null
@@ -0,0 +1,20 @@
+from rest_framework.generics import ListAPIView, RetrieveAPIView
+from . import serializers
+from catalogue.models import Book, Collection
+
+
+class CollectionList(ListAPIView):
+    queryset = Collection.objects.all()
+    serializer_class = serializers.CollectionListSerializer
+
+
+class CollectionDetail(RetrieveAPIView):
+    queryset = Collection.objects.all()
+    lookup_field = 'slug'
+    serializer_class = serializers.CollectionSerializer
+
+
+class BookDetail(RetrieveAPIView):
+    queryset = Book.objects.all()
+    lookup_field = 'slug'
+    serializer_class = serializers.BookDetailSerializer
index 6e6c50f..b2289f6 100644 (file)
@@ -20,6 +20,7 @@ import jsonfield
 from fnpdjango.storage import BofhFileSystemStorage
 from ssify import flush_ssi_includes
 
+from librarian.cover import WLCover
 from librarian.html import transform_abstrakt
 from newtagging import managers
 from catalogue import constants
@@ -115,6 +116,8 @@ class Book(models.Model):
     html_built = django.dispatch.Signal()
     published = django.dispatch.Signal()
 
+    SORT_KEY_SEP = '$'
+
     class AlreadyExists(Exception):
         pass
 
@@ -136,6 +139,15 @@ class Book(models.Model):
     def authors(self):
         return self.tags.filter(category='author')
 
+    def epochs(self):
+        return self.tags.filter(category='epoch')
+
+    def genres(self):
+        return self.tags.filter(category='genre')
+
+    def kinds(self):
+        return self.tags.filter(category='kind')
+
     def tag_unicode(self, category):
         relations = prefetched_relations(self, category)
         if relations:
@@ -149,6 +161,15 @@ class Book(models.Model):
     def author_unicode(self):
         return self.cached_author
 
+    def kind_unicode(self):
+        return self.tag_unicode('kind')
+
+    def epoch_unicode(self):
+        return self.tag_unicode('epoch')
+
+    def genre_unicode(self):
+        return self.tag_unicode('genre')
+
     def translator(self):
         translators = self.extra_info.get('translators')
         if not translators:
@@ -780,6 +801,12 @@ class Book(models.Model):
         if likes(user, self):
             set_sets(user, self, [])
 
+    def full_sort_key(self):
+        return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
+
+    def cover_color(self):
+        return WLCover.epoch_colors.get(self.extra_info.get('epoch'), '#000000')
+
 
 def add_file_fields():
     for format_ in Book.formats:
index 377dbc4..407c419 100644 (file)
@@ -146,3 +146,11 @@ class BookMedia(models.Model):
                 return None
         else:
             return None
+
+    @property
+    def director(self):
+        return self.extra_info.get('director_name', None)
+
+    @property
+    def artist(self):
+        return self.extra_info.get('artist_name', None)
index 7e15636..c6b6f26 100644 (file)
@@ -166,6 +166,10 @@ class Tag(TagBase):
         else:
             return ''
 
+    @property
+    def category_plural(self):
+        return self.category + 's'
+
     @permalink
     def get_absolute_url(self):
         return 'tagged_object_list', [self.url_chunk]
index 87286d5..c3be29b 100644 (file)
@@ -47,6 +47,7 @@ INSTALLED_APPS_CONTRIB = [
     'django.contrib.admin',
     'django.contrib.admindocs',
     'django.contrib.staticfiles',
+    'rest_framework',
     'fnp_django_pagination',
     'pipeline',
     'piston',
index de64990..daa119d 100644 (file)
@@ -44,3 +44,13 @@ MIGDAL_TYPES = (
     EntryType('info', _('info'), commentable=False),
     EntryType('event', _('events'), commentable=False),
 )
+
+REST_FRAMEWORK = {
+    "DEFAULT_RENDERER_CLASSES": (
+        'rest_framework.renderers.JSONRenderer',
+        'rest_framework.renderers.BrowsableAPIRenderer',
+        'api.renderers.LegacyXMLRenderer',
+    ),
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+    )
+}