From: Radek Czajka Date: Thu, 31 Jan 2019 19:56:07 +0000 (+0100) Subject: Introduce DRF and start replacing the views. X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/5aa13410812d3fc4b35c039bdc3f0a2edc5185a1 Introduce DRF and start replacing the views. --- diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3037f5cd2..3b8c95e42 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -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 index 000000000..4bb44dcb9 --- /dev/null +++ b/src/api/fields.py @@ -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 diff --git a/src/api/handlers.py b/src/api/handlers.py index cd24561f4..f3cc4a7ce 100644 --- a/src/api/handlers.py +++ b/src/api/handlers.py @@ -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 index 000000000..b5b63c6fd --- /dev/null +++ b/src/api/renderers.py @@ -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' + diff --git a/src/api/templates/api/main.html b/src/api/templates/api/main.html index 67ff426fe..a718f2124 100755 --- a/src/api/templates/api/main.html +++ b/src/api/templates/api/main.html @@ -53,12 +53,12 @@
  • {% url "api_tag_list" "themes" %} – {% trans "List of all themes" %}
  • -
  • - {% url "api_collections" %} – {% trans "Collections" %}
  • +
  • + {% url "catalogue_api_collections" %} – {% trans "Collections" %}
  • - {% 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 diff --git a/src/api/tests/res/responses/books-child.json b/src/api/tests/res/responses/books-child.json index 4c5c13450..27aaee713 100644 --- a/src/api/tests/res/responses/books-child.json +++ b/src/api/tests/res/responses/books-child.json @@ -10,25 +10,25 @@ "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" } @@ -43,18 +43,18 @@ "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": "" diff --git a/src/api/tests/res/responses/books-grandchild.json b/src/api/tests/res/responses/books-grandchild.json index 5d407b823..b7dd9daaf 100644 --- a/src/api/tests/res/responses/books-grandchild.json +++ b/src/api/tests/res/responses/books-grandchild.json @@ -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, @@ -42,11 +42,11 @@ "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": "" } diff --git a/src/api/tests/res/responses/books-parent.json b/src/api/tests/res/responses/books-parent.json index ade38cc8f..6dc75a5bd 100644 --- a/src/api/tests/res/responses/books-parent.json +++ b/src/api/tests/res/responses/books-parent.json @@ -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" } @@ -14,25 +14,25 @@ "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" } @@ -40,14 +40,14 @@ "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", @@ -59,29 +59,29 @@ "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" } diff --git a/src/api/tests/res/responses/collection.json b/src/api/tests/res/responses/collection.json index 866abedbd..299246112 100644 --- a/src/api/tests/res/responses/collection.json +++ b/src/api/tests/res/responses/collection.json @@ -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", diff --git a/src/api/tests/res/responses/collections.json b/src/api/tests/res/responses/collections.json index db583243e..d76ff2e90 100644 --- a/src/api/tests/res/responses/collections.json +++ b/src/api/tests/res/responses/collections.json @@ -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" } ] diff --git a/src/api/tests/tests.py b/src/api/tests/tests.py index adaaa6002..91e5bbf84 100644 --- a/src/api/tests/tests.py +++ b/src/api/tests/tests.py @@ -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/')], diff --git a/src/api/urls.py b/src/api/urls.py index bd832fe25..3c82e1e5f 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -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\d*?)/info\.html$', catalogue.views.book_info), url(r'tag/(?P\d*?)/info\.html$', catalogue.views.tag_info), - # books by collections - url(r'^collections/$', collection_list_resource, name="api_collections"), - url(r'^collections/(?P[^/]+)/$', collection_resource, name="api_collection"), - # epub preview url(r'^epub/(?P[a-z0-9-]+)/$', epub_resource, name='api_epub'), @@ -98,7 +93,6 @@ urlpatterns = [ url(r'^like/(?P[a-z0-9-]+)/$', like_resource, name='api_like'), # objects details - url(r'^books/(?P[a-z0-9-]+)/$', book_resource, name="api_book"), url(r'^(?P[a-z0-9-]+)/(?P[a-z0-9-]+)/$', tag_resource, name="api_tag"), url(r'^books/(?P[a-z0-9-]+)/fragments/(?P[a-z0-9-]+)/$', diff --git a/src/catalogue/api/__init__.py b/src/catalogue/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/catalogue/api/fields.py b/src/catalogue/api/fields.py new file mode 100644 index 000000000..145780e2b --- /dev/null +++ b/src/catalogue/api/fields.py @@ -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 index 000000000..1705a5165 --- /dev/null +++ b/src/catalogue/api/serializers.py @@ -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 index 000000000..e476c8fbf --- /dev/null +++ b/src/catalogue/api/urls.py @@ -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[^/]+)/$', + views.CollectionDetail.as_view(), name="collection-detail"), + + url(r'^books/(?P[^/]+)/$', 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 index 000000000..143990723 --- /dev/null +++ b/src/catalogue/api/views.py @@ -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 diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 6e6c50f8f..b2289f667 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -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: diff --git a/src/catalogue/models/bookmedia.py b/src/catalogue/models/bookmedia.py index 377dbc43c..407c41969 100644 --- a/src/catalogue/models/bookmedia.py +++ b/src/catalogue/models/bookmedia.py @@ -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) diff --git a/src/catalogue/models/tag.py b/src/catalogue/models/tag.py index 7e15636da..c6b6f2663 100644 --- a/src/catalogue/models/tag.py +++ b/src/catalogue/models/tag.py @@ -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] diff --git a/src/wolnelektury/settings/apps.py b/src/wolnelektury/settings/apps.py index 87286d56a..c3be29bbe 100644 --- a/src/wolnelektury/settings/apps.py +++ b/src/wolnelektury/settings/apps.py @@ -47,6 +47,7 @@ INSTALLED_APPS_CONTRIB = [ 'django.contrib.admin', 'django.contrib.admindocs', 'django.contrib.staticfiles', + 'rest_framework', 'fnp_django_pagination', 'pipeline', 'piston', diff --git a/src/wolnelektury/settings/contrib.py b/src/wolnelektury/settings/contrib.py index de64990bc..daa119d76 100644 --- a/src/wolnelektury/settings/contrib.py +++ b/src/wolnelektury/settings/contrib.py @@ -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': ( + ) +}