# PyCharm
.idea
+# Emacs
+\#*\#
+.\#*
+
# Tags file
TAGS
django-modeltranslation>=0.10,<0.11
django-allauth>=0.32,<0.33
django-extensions
+djangorestframework<3.7
+djangorestframework-xml
# contact
pyyaml
--- /dev/null
+"""
+Transitional code: bridge between Piston's OAuth implementation
+and DRF views.
+"""
+from piston.authentication import OAuthAuthentication
+from rest_framework.authentication import BaseAuthentication
+
+
+class PistonOAuthAuthentication(BaseAuthentication):
+ def __init__(self):
+ self.piston_auth = OAuthAuthentication()
+
+ def authenticate_header(self, request):
+ return 'OAuth realm="API"'
+
+ def authenticate(self, request):
+ if self.piston_auth.is_valid_request(request):
+ consumer, token, parameters = self.piston_auth.validate_token(request)
+ if consumer and token:
+ return token.user, token
+++ /dev/null
-# -*- 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.
-#
-"""
-Wrappers for piston Emitter classes.
-"""
-from piston.emitters import Emitter
-
-
-# hack
-class EpubEmitter(Emitter):
- def render(self, request):
- return self.data
-
-Emitter.register('epub', EpubEmitter, 'application/epub+zip')
--- /dev/null
+from rest_framework import serializers
+from django.core.urlresolvers import reverse
+from paypal.rest import user_is_subscribed
+
+
+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
+
+
+class UserPremiumField(serializers.ReadOnlyField):
+ def __init__(self, *args, **kwargs):
+ super(UserPremiumField, self).__init__(*args, source='*', **kwargs)
+
+ def to_representation(self, value):
+ return user_is_subscribed(value)
from sorl.thumbnail import default
from api.models import BookUserData
+from catalogue.api.helpers import books_after, order_books
from catalogue.forms import BookImportForm
-from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
+from catalogue.models import Book, Tag, BookMedia
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
from stats.utils import piwik_track
from wolnelektury.utils import re_escape
-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',
def process(category, slug):
if category == 'book':
+ # FIXME: Unused?
try:
books.append(Book.objects.get(slug=slug))
except Book.DoesNotExist:
# RESTful handlers
-class BookMediaHandler(BaseHandler):
- """ Responsible for representing media in Books. """
-
- model = BookMedia
- fields = ['name', 'type', 'url', 'artist', 'director']
-
- @classmethod
- def url(cls, media):
- """ Link to media on site. """
-
- return MEDIA_BASE + media.file.url
-
- @classmethod
- def artist(cls, media):
- return media.extra_info.get('artist_name', '')
- @classmethod
- def director(cls, media):
- return media.extra_info.get('director_name', '')
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):
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)
- except ValueError:
- return Book.objects.none()
- return books.filter(Q(sort_key_author__gt=author)
- | (Q(sort_key_author=author) & Q(sort_key__gt=title))
- | (Q(sort_key_author=author) & Q(sort_key=title) & Q(id__gt=int(book_id))))
-
- @staticmethod
- def order_books(books, new_api):
- if new_api:
- return books.order_by('sort_key_author', 'sort_key', 'id')
- else:
- return books.order_by('slug')
-
class BookDetailHandler(BaseHandler, BookDetails):
""" Main handler for Book objects.
model = Book
fields = book_list_fields
+ # FIXME: Unused?
@classmethod
def genres(cls, book):
""" Returns all media for a book. """
are returned.
"""
if pk is not None:
+ # FIXME: Unused?
try:
return Book.objects.get(pk=pk)
except Book.DoesNotExist:
books = Book.tagged.with_all(tags)
else:
books = books if books is not None else Book.objects.all()
- books = self.order_books(books, new_api)
+ books = order_books(books, new_api)
if top_level:
books = books.filter(parent=None)
books = books.order_by('-created_at')
if after:
- books = self.books_after(books, after, new_api)
+ books = books_after(books, after, new_api)
if new_api:
books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author')
return rc.NOT_FOUND
-class EpubHandler(BookDetailHandler):
- def read(self, request, slug):
- if not user_is_subscribed(request.user):
- return rc.FORBIDDEN
- try:
- book = Book.objects.get(slug=slug)
- except Book.DoesNotExist:
- return rc.NOT_FOUND
- response = HttpResponse(book.get_media('epub'))
- return response
-
-
class EBooksHandler(AnonymousBooksHandler):
fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
new_api = request.GET.get('new_api')
after = request.GET.get('after')
count = int(request.GET.get('count', 50))
- books = self.order_books(Book.objects.distinct(), new_api)
+ books = order_books(Book.objects.distinct(), new_api)
if is_lektura is not None:
books = books.filter(has_audience=is_lektura)
if is_audiobook is not None:
if after and (key_sep in after):
which, key = after.split(key_sep, 1)
if which == 'title':
- book_lists = [(self.books_after(books_title, key, new_api), 'title')]
+ book_lists = [(books_after(books_title, key, new_api), 'title')]
else: # which == 'author'
- book_lists = [(self.books_after(books_author, key, new_api), 'author'), (books_title, 'title')]
+ book_lists = [(books_after(books_author, key, new_api), 'author'), (books_title, 'title')]
else:
book_lists = [(books_author, 'author'), (books_title, 'title')]
else:
if after and key_sep in after:
which, key = after.split(key_sep, 1)
- books = self.books_after(books, key, new_api)
+ books = books_after(books, key, new_api)
book_lists = [(books, 'book')]
filtered_books = []
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:
return qsp
-class BookPreviewHandler(BookDetailHandler):
- fields = BookDetailHandler.fields + ['slug']
-
- def read(self, request):
- return Book.objects.filter(preview=True)
-
-
# add categorized tags fields for Book
def _tags_getter(category):
@classmethod
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."""
-
- @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. """
-
- fields = ['name', 'url', 'sort_key', 'description']
-
- @piwik_track
- def read(self, request, category, slug):
- """ Returns details of a tag, identified by category and slug. """
-
- try:
- category_sng = category_singular[category]
- except KeyError:
- return rc.NOT_FOUND
-
- try:
- return Tag.objects.get(category=category_sng, slug=slug)
- except Tag.DoesNotExist:
- return rc.NOT_FOUND
-
-
-class TagsHandler(BaseHandler, TagDetails):
- """ Main handler for Tag objects.
-
- Responsible for lists of Tag objects
- and fields used for representing Tags.
-
- """
- allowed_methods = ('GET',)
- model = Tag
- fields = ['name', 'href', 'url', 'slug']
-
- @piwik_track
- def read(self, request, category=None, pk=None):
- """ Lists all tags in the category (eg. all themes). """
- if pk is not None:
- try:
- return Tag.objects.exclude(category='set').get(pk=pk)
- except Book.DoesNotExist:
- return rc.NOT_FOUND
-
- try:
- category_sng = category_singular[category]
- except KeyError:
- return rc.NOT_FOUND
-
- after = request.GET.get('after')
- count = request.GET.get('count')
-
- tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
-
- book_only = request.GET.get('book_only') == 'true'
- picture_only = request.GET.get('picture_only') == 'true'
- if book_only:
- tags = tags.filter(for_books=True)
- if picture_only:
- tags = tags.filter(for_pictures=True)
-
- if after:
- tags = tags.filter(slug__gt=after)
-
- if count:
- tags = tags[:count]
-
- return tags
-
-
-class FragmentDetails(object):
- """Custom Fragment fields."""
-
- @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')
-
-
-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. """
- try:
- return Fragment.objects.get(book__slug=book, anchor=anchor)
- except Fragment.DoesNotExist:
- return rc.NOT_FOUND
-
-
-class FragmentsHandler(BaseHandler, FragmentDetails):
- """ Main handler for Fragments.
-
- Responsible for lists of Fragment objects
- and fields used for representing Fragments.
-
- """
- model = Fragment
- fields = ['book', 'url', 'anchor', 'href']
- allowed_methods = ('GET',)
-
- categories = {'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
-
- @piwik_track
- def read(self, request, tags):
- """ Lists all fragments with given book, tags, themes.
-
- :param tags: should be a path of categories and slugs, i.e.:
- books/book-slug/authors/an-author/themes/a-theme/
-
- """
- try:
- tags, ancestors = read_tags(tags, request, 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
-
-
class PictureHandler(BaseHandler):
model = Picture
fields = ('slug', 'title')
return rc.NOT_FOUND
-class UserDataHandler(BaseHandler):
- model = BookUserData
- fields = ('state', 'username', 'premium')
- allowed_methods = ('GET', 'POST')
-
- def read(self, request, slug=None):
- if not request.user.is_authenticated():
- return rc.FORBIDDEN
- if slug is None:
- return {'username': request.user.username, 'premium': user_is_subscribed(request.user)}
- try:
- book = Book.objects.get(slug=slug)
- except Book.DoesNotExist:
- return rc.NOT_FOUND
- try:
- data = BookUserData.objects.get(book=book, user=request.user)
- except BookUserData.DoesNotExist:
- return {'state': 'not_started'}
- return data
-
- def create(self, request, slug, state):
- try:
- book = Book.objects.get(slug=slug)
- except Book.DoesNotExist:
- return rc.NOT_FOUND
- if not request.user.is_authenticated():
- return rc.FORBIDDEN
- if state not in ('reading', 'complete'):
- return rc.NOT_FOUND
- data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
- data.state = state
- data.save()
- return data
-
-
-class UserShelfHandler(BookDetailHandler):
- fields = book_list_fields + ['liked']
-
- def parse_bool(self, s):
- if s in ('true', 'false'):
- return s == 'true'
- else:
- return None
-
- # hack, because piston is stupid
- @classmethod
- def liked(cls, book):
- return getattr(book, 'liked', None)
-
- def read(self, request, state):
- if not request.user.is_authenticated():
- return rc.FORBIDDEN
- likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
- if state not in ('reading', 'complete', 'likes'):
- return rc.NOT_FOUND
- new_api = request.GET.get('new_api')
- after = request.GET.get('after')
- count = int(request.GET.get('count', 50))
- if state == 'likes':
- books = Book.tagged.with_any(request.user.tag_set.all())
- else:
- ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
- .values_list('book_id', flat=True)
- books = Book.objects.filter(id__in=list(ids)).distinct()
- books = self.order_books(books, new_api)
- if after:
- books = self.books_after(books, after, new_api)
- if count:
- books = books[:count]
- new_books = []
- for book in books:
- new_books.append(BookProxy(book).set('liked', book.id in likes))
- return QuerySetProxy(new_books)
-
-
-class UserLikeHandler(BaseHandler):
- fields = []
- allowed_methods = ('GET', 'POST')
-
- def read(self, request, slug):
- if not request.user.is_authenticated():
- return rc.FORBIDDEN
- try:
- book = Book.objects.get(slug=slug)
- except Book.DoesNotExist:
- return rc.NOT_FOUND
- return {'likes': likes(request.user, book)}
-
- def create(self, request, slug):
- if not request.user.is_authenticated():
- return rc.FORBIDDEN
- try:
- book = Book.objects.get(slug=slug)
- except Book.DoesNotExist:
- return rc.NOT_FOUND
- action = request.GET.get('action', 'like')
- if action == 'like':
- book.like(request.user)
- elif action == 'unlike':
- book.unlike(request.user)
- return {}
-
-
class BlogEntryHandler(BaseHandler):
model = Entry
fields = (
complete = models.BooleanField(default=False)
last_changed = models.DateTimeField(auto_now=True)
- def get_state(self):
+ @property
+ def state(self):
return 'complete' if self.complete else 'reading'
- def set_state(self, state):
- self.complete = state == 'complete'
-
- state = property(get_state, set_state)
+ @classmethod
+ def update(cls, book, user, state):
+ instance, created = cls.objects.get_or_create(book=book, user=user)
+ instance.complete = state == 'complete'
+ instance.save()
+ return instance
--- /dev/null
+from rest_framework_xml.renderers import XMLRenderer
+
+
+class LegacyXMLRenderer(XMLRenderer):
+ """
+ Renderer which serializes to XML.
+ """
+
+ item_tag_name = 'resource'
+ root_tag_name = 'response'
+
--- /dev/null
+from django.contrib.auth.models import User
+from rest_framework import serializers
+from .fields import UserPremiumField
+from .models import BookUserData
+
+
+class UserSerializer(serializers.ModelSerializer):
+ premium = UserPremiumField()
+
+ class Meta:
+ model = User
+ fields = ['username', 'premium']
+
+
+class BookUserDataSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BookUserData
+ fields = ['state']
<ul>
<li><a href='{% url "api_book_list" "" %}'>
- {% url "api_book_list" "" %}</a> – {% trans "All books" %}</li>
+ {% url "catalogue_api_book_list" "" %}</a> – {% trans "All books" %}</li>
<li><a href='{% url "api_audiobook_list" "" %}'>
{% url "api_audiobook_list" "" %}</a> – {% trans "Audiobooks" %}</li>
<li><a href='{% url "api_daisy_list" "" %}'>
{% url "api_daisy_list" "" %}</a> – {% trans "DAISY" %}</li>
- <li><a href='{% url "api_tag_list" "authors" %}'>
- {% url "api_tag_list" "authors" %}</a> – {% trans "List of all authors" %}</li>
- <li><a href='{% url "api_tag_list" "epochs" %}'>
- {% url "api_tag_list" "epochs" %}</a> – {% trans "List of all epochs" %}</li>
- <li><a href='{% url "api_tag_list" "genres" %}'>
- {% url "api_tag_list" "genres" %}</a> – {% trans "List of all genres" %}</li>
- <li><a href='{% url "api_tag_list" "kinds" %}'>
- {% url "api_tag_list" "kinds" %}</a> – {% trans "List of all kinds" %}</li>
+ <li><a href='{% url "catalogue_api_tag_list" "author" %}'>
+ {% url "catalogue_api_tag_list" "author" %}</a> – {% trans "List of all authors" %}</li>
+ <li><a href='{% url "catalogue_api_tag_list" "epoch" %}'>
+ {% url "catalogue_api_tag_list" "epoch" %}</a> – {% trans "List of all epochs" %}</li>
+ <li><a href='{% url "catalogue_api_tag_list" "genre" %}'>
+ {% url "catalogue_api_tag_list" "genre" %}</a> – {% trans "List of all genres" %}</li>
+ <li><a href='{% url "catalogue_api_tag_list" "kind" %}'>
+ {% url "catalogue_api_tag_list" "kind" %}</a> – {% trans "List of all kinds" %}</li>
- <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_tag_list" "theme" %}'>
+ {% url "catalogue_api_tag_list" "theme" %}</a> – {% trans "List of all themes" %}</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 "api_tag" "authors" "edgar-allan-poe" as e2 %}
+ {% url "catalogue_api_book" "studnia-i-wahadlo" as e1 %}
+ {% url "catalogue_api_tag" "author" "edgar-allan-poe" as e2 %}
{% blocktrans %}
Each element of those lists contains a link (in a "href") attibute
which points to individual resource's details, i.e.:
"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": "",
"epub": "",
"cover_thumb": "",
"mobi": "",
- "url": "https://example.com/katalog/lektura/child/",
+ "url": "http://testserver/katalog/lektura/child/",
"cover": "",
"pdf": "",
"simple_thumb": ""
"html": "Fragment",
"title": "Parent, Child"
},
- "txt": "https://example.com/media/txt/grandchild.txt",
+ "txt": "http://testserver/katalog/pobierz/grandchild.txt",
"children": [],
- "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/media/html/grandchild.html",
- "preview": false,
- "fb2": "https://example.com/media/fb2/grandchild.fb2",
+ "html": "http://testserver/katalog/pobierz/grandchild.html",
+ "preview": true,
+ "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": "",
+ "epub": "http://testserver/katalog/pobierz/grandchild.epub",
"cover_thumb": "",
- "mobi": "",
- "url": "https://example.com/katalog/lektura/grandchild/",
+ "mobi": "http://testserver/katalog/pobierz/grandchild.mobi",
+ "url": "http://testserver/katalog/lektura/grandchild/",
"cover": "",
- "pdf": "",
+ "pdf": "http://testserver/katalog/pobierz/grandchild.pdf",
"simple_thumb": ""
}
{
"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"
}
"full_sort_key": "$child$2",
"cover_color": "#000000",
"author": "",
- "url": "https://example.com/katalog/lektura/child/",
+ "url": "http://testserver/katalog/lektura/child/",
"has_audio": false,
"title": "Child",
"cover": "",
"epoch": "",
- "href": "https://example.com/api/books/child/",
+ "href": "http://testserver/api/books/child/",
"genre": "Wiersz",
"simple_thumb": "",
"slug": "child",
"full_sort_key": "$grandchild$3",
"cover_color": "#000000",
"author": "",
- "url": "https://example.com/katalog/lektura/grandchild/",
+ "url": "http://testserver/katalog/lektura/grandchild/",
"has_audio": false,
"title": "Grandchild",
"cover": "",
"epoch": "",
- "href": "https://example.com/api/books/grandchild/",
+ "href": "http://testserver/api/books/grandchild/",
"genre": "Sonet",
"simple_thumb": "",
"slug": "grandchild",
"full_sort_key": "john doe$parent$1",
"cover_color": "#a6820a",
"author": "John Doe",
- "url": "https://example.com/katalog/lektura/parent/",
+ "url": "http://testserver/katalog/lektura/parent/",
"has_audio": true,
"title": "Parent",
"cover": "cover/parent.jpg",
"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",
"slug": "parent",
"cover_thumb": "cover_thumb/parent.jpg"
}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<response><resource><kind></kind><full_sort_key>$child$2</full_sort_key><title>Child</title><url>https://example.com/katalog/lektura/child/</url><has_audio>False</has_audio><author></author><cover></cover><slug>child</slug><epoch></epoch><href>https://example.com/api/books/child/</href><genre>Wiersz</genre><simple_thumb></simple_thumb><cover_color>#000000</cover_color><cover_thumb></cover_thumb></resource><resource><kind></kind><full_sort_key>$grandchild$3</full_sort_key><title>Grandchild</title><url>https://example.com/katalog/lektura/grandchild/</url><has_audio>False</has_audio><author></author><cover></cover><slug>grandchild</slug><epoch></epoch><href>https://example.com/api/books/grandchild/</href><genre>Sonet</genre><simple_thumb></simple_thumb><cover_color>#000000</cover_color><cover_thumb></cover_thumb></resource><resource><kind>Liryka</kind><full_sort_key>john doe$parent$1</full_sort_key><title>Parent</title><url>https://example.com/katalog/lektura/parent/</url><has_audio>True</has_audio><author>John Doe</author><cover>cover/parent.jpg</cover><slug>parent</slug><epoch>Barok</epoch><href>https://example.com/api/books/parent/</href><genre>Sonet</genre><simple_thumb>https://example.com/media/cover_api_thumb/parent.jpg</simple_thumb><cover_color>#a6820a</cover_color><cover_thumb>https://example.com/media/cover/parent.jpg-139x193</cover_thumb></resource></response>
+
{
- "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",
[
{
- "url": "https://example.com/katalog/lektury/a-collection/",
- "href": "https://example.com/dapi/collections/a-collection/",
+ "url": "http://testserver/katalog/lektury/a-collection/",
+ "href": "http://testserver/api/collections/a-collection/",
"title": "A Collection"
}
]
"epub": ""
},
{
- "fb2": "https://example.com/media/fb2/grandchild.fb2",
- "mobi": "",
+ "fb2": "https://example.com/katalog/pobierz/grandchild.fb2",
+ "mobi": "https://example.com/katalog/pobierz/grandchild.mobi",
"title": "Grandchild",
"author": "",
"cover": "",
"href": "https://example.com/api/books/grandchild/",
- "pdf": "",
- "txt": "https://example.com/media/txt/grandchild.txt",
+ "pdf": "https://example.com/katalog/pobierz/grandchild.pdf",
+ "txt": "https://example.com/katalog/pobierz/grandchild.txt",
"slug": "grandchild",
- "epub": ""
+ "epub": "https://example.com/katalog/pobierz/grandchild.epub"
},
{
"fb2": "",
{
- "url": "https://example.com/katalog/lektura/child.html#man-anchor",
+ "url": "http://testserver/katalog/lektura/child.html#man-anchor",
"text": "A fragment",
"book": {
"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,
"anchor": "an-anchor",
"themes": [
{
- "url": "https://example.com/katalog/motyw/koniec/",
- "href": "https://example.com/api/themes/koniec/",
+ "url": "http://testserver/katalog/motyw/koniec/",
+ "href": "http://testserver/api/themes/koniec/",
"name": "Koniec",
"slug": "koniec"
}
[
{
- "url": "https://example.com/katalog/lektura/child.html#man-anchor",
+ "url": "http://testserver/katalog/lektura/child.html#man-anchor",
"book": {
"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": ""
},
"anchor": "an-anchor",
- "href": "https://example.com/api/books/child/fragments/an-anchor/"
+ "href": "http://testserver/api/books/child/fragments/an-anchor/"
}
]
[
{
- "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"
},
{
- "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"
}
# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from base64 import b64encode
from os import path
+import hashlib
+import hmac
import json
+from StringIO import StringIO
+from time import time
+from urllib import quote, urlencode
+from urlparse import parse_qs
+from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.test.utils import override_settings
+from mock import patch
+from piston.models import Consumer, Token
from catalogue.models import Book, Tag
from picture.forms import PictureImportForm
self.fail('No JSON could be decoded: %s' % content)
return data
+ def assert_response(self, url, name):
+ content = self.client.get(url).content.rstrip()
+ filename = path.join(path.dirname(__file__), 'res', 'responses', name)
+ with open(filename) as f:
+ good_content = f.read().rstrip()
+ self.assertEqual(content, good_content, content)
+
def assert_json_response(self, url, name):
data = self.load_json(url)
- with open(path.join(path.dirname(__file__), 'res', 'responses', name)) as f:
+ filename = path.join(path.dirname(__file__), 'res', 'responses', name)
+ with open(filename) as f:
good_data = json.load(f)
self.assertEqual(data, good_data, json.dumps(data, indent=4))
def setUp(self):
self.tag = Tag.objects.create(category='author', slug='joe')
self.book = Book.objects.create(title='A Book', slug='a-book')
- self.book_tagged = Book.objects.create(title='Tagged Book', slug='tagged-book')
+ self.book_tagged = Book.objects.create(
+ title='Tagged Book', slug='tagged-book')
self.book_tagged.tags = [self.tag]
self.book_tagged.save()
class TagTests(ApiTest):
def setUp(self):
- self.tag = Tag.objects.create(category='author', slug='joe', name='Joe')
+ self.tag = Tag.objects.create(
+ category='author', slug='joe', name='Joe')
self.book = Book.objects.create(title='A Book', slug='a-book')
self.book.tags = [self.tag]
self.book.save()
def test_publish(self):
slug = "kandinsky-composition-viii"
xml = SimpleUploadedFile(
- 'composition8.xml', open(path.join(picture.tests.__path__[0], "files", slug + ".xml")).read())
+ 'composition8.xml',
+ open(path.join(
+ picture.tests.__path__[0], "files", slug + ".xml"
+ )).read())
img = SimpleUploadedFile(
- 'kompozycja-8.png', open(path.join(picture.tests.__path__[0], "files", slug + ".png")).read())
+ 'kompozycja-8.png',
+ open(path.join(
+ picture.tests.__path__[0], "files", slug + ".png"
+ )).read())
import_form = PictureImportForm({}, {
'picture_xml_file': xml,
fixtures = ['test-books.yaml']
def test_books(self):
- self.assert_json_response('/api/books/', 'books.json')
- self.assert_json_response('/api/books/?new_api=true', 'books.json')
+ self.assert_json_response('/api/books/', 'books.json')
+ self.assert_json_response('/api/books/?new_api=true', 'books.json')
+ self.assert_response('/api/books/?format=xml', 'books.xml')
self.assert_slugs('/api/audiobooks/', ['parent'])
self.assert_slugs('/api/daisy/', ['parent'])
# Book paging.
self.assert_slugs('/api/books/after/grandchild/count/1/', ['parent'])
- self.assert_slugs('/api/books/?new_api=true&after=$grandchild$3&count=1', ['parent'])
+ self.assert_slugs(
+ '/api/books/?new_api=true&after=$grandchild$3&count=1', ['parent'])
# By tag.
- self.assert_slugs('/api/authors/john-doe/books/', ['parent'])
- self.assert_slugs('/api/genres/sonet/books/?authors=john-doe', ['parent'])
+ self.assert_slugs('/api/authors/john-doe/books/', ['parent'])
+ self.assert_slugs(
+ '/api/genres/sonet/books/?authors=john-doe',
+ ['parent'])
# It is probably a mistake that this doesn't filter:
- self.assert_slugs('/api/books/?authors=john-doe', ['child', 'grandchild', 'parent'])
+ self.assert_slugs(
+ '/api/books/?authors=john-doe',
+ ['child', 'grandchild', 'parent'])
- # Parent books by tag.
+ # Parent books by tag.
# Notice this contains a grandchild, if a child doesn't have the tag.
# This probably isn't really intended behavior and should be redefined.
- self.assert_slugs('/api/genres/sonet/parent_books/', ['grandchild', 'parent'])
+ self.assert_slugs(
+ '/api/genres/sonet/parent_books/',
+ ['grandchild', 'parent'])
def test_ebooks(self):
self.assert_json_response('/api/ebooks/', 'ebooks.json')
def test_filter_books(self):
self.assert_json_response('/api/filter-books/', 'filter-books.json')
self.assert_slugs(
- '/api/filter-books/?lektura=false&preview=false',
+ '/api/filter-books/?lektura=false',
['child', 'grandchild', 'parent'])
self.assert_slugs(
'/api/filter-books/?lektura=true',
[])
- Book.objects.filter(slug='child').update(preview=True)
- self.assert_slugs('/api/filter-books/?preview=true', ['child'])
- self.assert_slugs('/api/filter-books/?preview=false', ['grandchild', 'parent'])
+ self.assert_slugs(
+ '/api/filter-books/?preview=true',
+ ['grandchild'])
+ self.assert_slugs(
+ '/api/filter-books/?preview=false',
+ ['child', 'parent'])
- self.assert_slugs('/api/filter-books/?audiobook=true', ['parent'])
- self.assert_slugs('/api/filter-books/?audiobook=false', ['child', 'grandchild'])
+ self.assert_slugs(
+ '/api/filter-books/?audiobook=true',
+ ['parent'])
+ self.assert_slugs(
+ '/api/filter-books/?audiobook=false',
+ ['child', 'grandchild'])
self.assert_slugs('/api/filter-books/?genres=wiersz', ['child'])
def test_collections(self):
self.assert_json_response('/api/collections/', 'collections.json')
- self.assert_json_response('/api/collections/a-collection/', 'collection.json')
+ self.assert_json_response(
+ '/api/collections/a-collection/',
+ 'collection.json')
def test_book(self):
- self.assert_json_response('/api/books/parent/', 'books-parent.json')
- self.assert_json_response('/api/books/child/', 'books-child.json')
- self.assert_json_response('/api/books/grandchild/', 'books-grandchild.json')
+ self.assert_json_response('/api/books/parent/', 'books-parent.json')
+ self.assert_json_response('/api/books/child/', 'books-child.json')
+ self.assert_json_response(
+ '/api/books/grandchild/',
+ 'books-grandchild.json')
def test_tags(self):
- # List of tags by category.
- self.assert_json_response('/api/genres/', 'tags.json')
+ # List of tags by category.
+ self.assert_json_response('/api/genres/', 'tags.json')
def test_fragments(self):
# This is not supported, though it probably should be.
- #self.assert_json_response('/api/books/child/fragments/', 'fragments.json')
+ # self.assert_json_response(
+ # '/api/books/child/fragments/',
+ # 'fragments.json')
- self.assert_json_response('/api/genres/wiersz/fragments/', 'fragments.json')
- self.assert_json_response('/api/genres/wiersz/fragments/', 'fragments.json')
-
- self.assert_json_response('/api/books/child/fragments/an-anchor/', 'fragment.json')
+ self.assert_json_response(
+ '/api/genres/wiersz/fragments/',
+ 'fragments.json')
+ self.assert_json_response(
+ '/api/books/child/fragments/an-anchor/',
+ 'fragment.json')
class BlogTests(ApiTest):
self.assert_json_response('/api/preview/', 'preview.json')
+class OAuth1Tests(ApiTest):
+ @classmethod
+ def setUpClass(cls):
+ cls.user = User.objects.create(username='test')
+ cls.consumer_secret = 'len(quote(consumer secret))>=32'
+ Consumer.objects.create(
+ key='client',
+ secret=cls.consumer_secret
+ )
+
+ @classmethod
+ def tearDownClass(cls):
+ User.objects.all().delete()
+
+ def test_create_token(self):
+ base_query = ("oauth_consumer_key=client&oauth_nonce=123&"
+ "oauth_signature_method=HMAC-SHA1&oauth_timestamp={}&"
+ "oauth_version=1.0".format(int(time())))
+ raw = '&'.join([
+ 'GET',
+ quote('http://testserver/api/oauth/request_token/', safe=''),
+ quote(base_query, safe='')
+ ])
+ h = hmac.new(
+ quote(self.consumer_secret) + '&', raw, hashlib.sha1
+ ).digest()
+ h = b64encode(h).rstrip('\n')
+ sign = quote(h)
+ query = "{}&oauth_signature={}".format(base_query, sign)
+ response = self.client.get('/api/oauth/request_token/?' + query)
+ request_token = parse_qs(response.content)
+
+ Token.objects.filter(
+ key=request_token['oauth_token'][0], token_type=Token.REQUEST
+ ).update(user=self.user, is_approved=True)
+
+ base_query = ("oauth_consumer_key=client&oauth_nonce=123&"
+ "oauth_signature_method=HMAC-SHA1&oauth_timestamp={}&"
+ "oauth_token={}&oauth_version=1.0".format(
+ int(time()), request_token['oauth_token'][0]))
+ raw = '&'.join([
+ 'GET',
+ quote('http://testserver/api/oauth/access_token/', safe=''),
+ quote(base_query, safe='')
+ ])
+ h = hmac.new(
+ quote(self.consumer_secret) + '&' +
+ quote(request_token['oauth_token_secret'][0], safe=''),
+ raw,
+ hashlib.sha1
+ ).digest()
+ h = b64encode(h).rstrip('\n')
+ sign = quote(h)
+ query = u"{}&oauth_signature={}".format(base_query, sign)
+ response = self.client.get(u'/api/oauth/access_token/?' + query)
+ access_token = parse_qs(response.content)
+
+ self.assertTrue(
+ Token.objects.filter(
+ key=access_token['oauth_token'][0],
+ token_type=Token.ACCESS,
+ user=self.user
+ ).exists())
+
+
+class AuthorizedTests(ApiTest):
+ fixtures = ['test-books.yaml']
+
+ @classmethod
+ def setUpClass(cls):
+ super(AuthorizedTests, cls).setUpClass()
+ cls.user = User.objects.create(username='test')
+ cls.consumer = Consumer.objects.create(
+ key='client', secret='12345678901234567890123456789012')
+ cls.token = Token.objects.create(
+ key='123456789012345678',
+ secret='12345678901234567890123456789012',
+ user=cls.user,
+ consumer=cls.consumer,
+ token_type=Token.ACCESS,
+ timestamp=time())
+ cls.key = cls.consumer.secret + '&' + cls.token.secret
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.user.delete()
+ cls.consumer.delete()
+ super(AuthorizedTests, cls).tearDownClass()
+
+ def signed(self, url, method='GET', params=None):
+ auth_params = {
+ "oauth_consumer_key": self.consumer.key,
+ "oauth_nonce": "%f" % time(),
+ "oauth_signature_method": "HMAC-SHA1",
+ "oauth_timestamp": int(time()),
+ "oauth_token": self.token.key,
+ "oauth_version": "1.0",
+ }
+
+ sign_params = {}
+ if params:
+ sign_params.update(params)
+ sign_params.update(auth_params)
+ raw = "&".join([
+ method.upper(),
+ quote('http://testserver' + url, safe=''),
+ quote("&".join(
+ quote(str(k)) + "=" + quote(str(v))
+ for (k, v) in sorted(sign_params.items())))
+ ])
+ auth_params["oauth_signature"] = quote(b64encode(hmac.new(
+ self.key, raw, hashlib.sha1).digest()).rstrip('\n'))
+ auth = 'OAuth realm="API", ' + ', '.join(
+ '{}="{}"'.format(k, v) for (k, v) in auth_params.items())
+
+ if params:
+ url = url + '?' + urlencode(params)
+ return getattr(self.client, method.lower())(
+ url,
+ HTTP_AUTHORIZATION=auth
+ )
+
+ def signed_json(self, url, method='GET', params=None):
+ 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}
+ )
+ self.signed('/api/like/parent/', 'POST')
+ self.assertEqual(
+ self.signed_json('/api/like/parent/'),
+ {"likes": True}
+ )
+ # There are several endpoints where 'liked' appears.
+ 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/')],
+ ['parent'])
+
+ self.signed('/api/like/parent/', 'POST', {"action": "unlike"})
+ self.assertEqual(
+ self.signed_json('/api/like/parent/'),
+ {"likes": False}
+ )
+ self.assertFalse(self.signed_json('/api/parent_books/')[0]['liked'])
+
+ def test_reading(self):
+ self.assertEqual(
+ self.signed_json('/api/reading/parent/'),
+ {"state": "not_started"}
+ )
+ self.signed('/api/reading/parent/reading/', 'post')
+ self.assertEqual(
+ self.signed_json('/api/reading/parent/'),
+ {"state": "reading"}
+ )
+ self.assertEqual(
+ [x['slug'] for x in self.signed_json('/api/shelf/reading/')],
+ ['parent'])
+
+ def test_subscription(self):
+ self.assert_slugs('/api/preview/', ['grandchild'])
+ self.assertEqual(
+ self.signed_json('/api/username/'),
+ {"username": "test", "premium": False})
+ self.assertEqual(
+ self.signed('/api/epub/grandchild/').status_code,
+ 403)
+
+ with patch('api.fields.user_is_subscribed', return_value=True):
+ self.assertEqual(
+ self.signed_json('/api/username/'),
+ {"username": "test", "premium": True})
+ with patch('paypal.permissions.user_is_subscribed', return_value=True):
+ with patch('django.core.files.storage.Storage.open',
+ return_value=StringIO("<epub>")):
+ self.assertEqual(
+ self.signed('/api/epub/grandchild/').content,
+ "<epub>")
# 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
from api import handlers
from api.helpers import CsrfExemptResource
from api.piston_patch import oauth_user_auth
+from . import views
auth = OAuthAuthentication(realm="Wolne Lektury")
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)
-
-preview_resource = Resource(handler=handlers.BookPreviewHandler)
-
-reading_resource = auth_resource(handler=handlers.UserDataHandler)
-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)
-
-fragment_resource = Resource(handler=handlers.FragmentDetailHandler)
-fragment_list_resource = Resource(handler=handlers.FragmentsHandler)
picture_resource = auth_resource(handler=handlers.PictureHandler)
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'),
-
# reading data
- url(r'^reading/(?P<slug>[a-z0-9-]+)/$', reading_resource, name='api_reading'),
- url(r'^reading/(?P<slug>[a-z0-9-]+)/(?P<state>[a-z]+)/$', reading_resource, name='api_reading'),
- url(r'^shelf/(?P<state>[a-z]+)/$', shelf_resource, name='api_shelf'),
- url(r'^username/$', reading_resource, name='api_username'),
-
- 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-]+)/$',
- fragment_resource, name="api_fragment"),
+ url(r'^reading/(?P<slug>[a-z0-9-]+)/$', views.BookUserDataView.as_view(), name='api_reading'),
+ url(r'^reading/(?P<slug>[a-z0-9-]+)/(?P<state>[a-z]+)/$', views.BookUserDataView.as_view(), name='api_reading'),
+ url(r'^username/$', views.UserView.as_view(), name='api_username'),
# books by tags
- url(tags_re + r'books/' + paginate_re,
- book_list_resource, name='api_book_list'),
url(tags_re + r'ebooks/' + paginate_re,
ebook_list_resource, name='api_ebook_list'),
- url(tags_re + r'parent_books/' + paginate_re,
- book_list_resource, {"top_level": True}, name='api_parent_book_list'),
url(tags_re + r'parent_ebooks/' + paginate_re,
ebook_list_resource, {"top_level": True}, name='api_parent_ebook_list'),
- url(tags_re + r'audiobooks/' + paginate_re,
- book_list_resource, {"audiobooks": True}, name='api_audiobook_list'),
- url(tags_re + r'daisy/' + paginate_re,
- book_list_resource, {"daisy": True}, name='api_daisy_list'),
- url(r'^recommended/' + paginate_re, book_list_resource, {"recommended": True}, name='api_recommended_list'),
- url(r'^newest/$', book_list_resource, {"newest": True, "top_level": True, "count": 20}, name='api_newest_list'),
url(r'^filter-books/$', filter_book_resource, name='api_filter_books'),
- url(r'^preview/$', preview_resource, name='api_preview'),
-
url(r'^pictures/$', picture_resource),
url(r'^blog/$', blog_resource),
- # fragments by book, tags, themes
- # this should be paged
- url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){1,6})fragments/$', fragment_list_resource),
-
- # tags by category
- url(r'^(?P<category>[a-z0-9-]+)/$', tag_list_resource, name='api_tag_list'),
+ url(r'^', include('social.api.urls')),
+ url(r'^', include('catalogue.api.urls')),
]
--- /dev/null
+from django.http import Http404
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework.generics import RetrieveAPIView, get_object_or_404
+from catalogue.models import Book
+from .models import BookUserData
+from . import serializers
+
+
+class UserView(RetrieveAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = serializers.UserSerializer
+
+ def get_object(self):
+ return self.request.user
+
+
+class BookUserDataView(RetrieveAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = serializers.BookUserDataSerializer
+ lookup_field = 'book__slug'
+ lookup_url_kwarg = 'slug'
+
+ def get_queryset(self):
+ return BookUserData.objects.filter(user=self.request.user)
+
+ def get(self, *args, **kwargs):
+ try:
+ return super(BookUserDataView, self).get(*args, **kwargs)
+ except Http404:
+ return Response({"state": "not_started"})
+
+ def post(self, request, slug, state):
+ if state not in ('reading', 'complete'):
+ raise Http404
+
+ book = get_object_or_404(Book, slug=slug)
+ instance = BookUserData.update(book, request.user, state)
+ serializer = self.get_serializer(instance)
+ return Response(serializer.data)
--- /dev/null
+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)
+ )
--- /dev/null
+from django.db.models import Q
+from catalogue.models import Book
+
+
+def books_after(books, after, new_api):
+ if not new_api:
+ return books.filter(slug__gt=after)
+ try:
+ 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)
+ | (Q(sort_key_author=author) & Q(sort_key__gt=title))
+ | (Q(sort_key_author=author) & Q(sort_key=title) & Q(id__gt=int(book_id))))
+
+
+def order_books(books, new_api):
+ if new_api:
+ return books.order_by('sort_key_author', 'sort_key', 'id')
+ else:
+ return books.order_by('slug')
+
--- /dev/null
+from rest_framework import serializers
+from api.fields import AbsoluteURLField, LegacyMixin
+from catalogue.models import Book, Collection, Tag, BookMedia, Fragment
+from .fields import BookLiked, ThumbnailField
+
+
+class TagSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(
+ view_name='catalogue_api_tag',
+ view_args=('category', 'slug')
+ )
+
+ class Meta:
+ model = Tag
+ fields = ['url', 'href', 'name', 'slug']
+
+
+class TagDetailSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+
+ class Meta:
+ model = Tag
+ fields = ['name', 'url', 'sort_key', 'description']
+
+
+class BaseBookSerializer(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()
+ cover = serializers.CharField()
+ cover_thumb = ThumbnailField('139x193', source='cover')
+
+ class Meta:
+ model = Book
+ fields = [
+ 'kind', 'full_sort_key', 'title', 'url', 'cover_color', 'author',
+ 'cover', 'epoch', 'href', 'has_audio', 'genre',
+ 'simple_thumb', 'slug', 'cover_thumb']
+ legacy_non_null_fields = [
+ 'kind', 'author', 'epoch', 'genre',
+ 'cover', 'simple_thumb', 'cover_thumb']
+
+
+class BookSerializer(BaseBookSerializer):
+ liked = BookLiked()
+ cover = serializers.FileField()
+
+ class Meta:
+ model = Book
+ fields = BaseBookSerializer.Meta.fields + ['liked']
+ legacy_non_null_fields = BaseBookSerializer.Meta.legacy_non_null_fields
+
+
+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 BookPreviewSerializer(BookDetailSerializer):
+ class Meta:
+ model = Book
+ fields = BookDetailSerializer.Meta.fields + ['slug']
+ legacy_non_null_fields = BookDetailSerializer.Meta.legacy_non_null_fields
+
+
+class EbookSerializer(BookSerializer):
+ class Meta:
+ model = Book
+ fields = ['author', 'href', 'title', 'cover', 'slug'] + Book.ebook_formats
+ legacy_non_null_fields = ['author', 'cover'] + Book.ebook_formats
+
+
+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']
+
+
+class FragmentSerializer(serializers.ModelSerializer):
+ book = BookSerializer()
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(source='get_api_url')
+
+ class Meta:
+ model = Fragment
+ fields = ['book', 'url', 'anchor', 'href']
+
+
+class FragmentDetailSerializer(serializers.ModelSerializer):
+ book = BookSerializer()
+ url = AbsoluteURLField()
+ themes = TagSerializer(many=True)
+
+ class Meta:
+ model = Fragment
+ fields = ['book', 'anchor', 'text', 'url', 'themes']
--- /dev/null
+# -*- 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
+
+
+tags_re = r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})'
+paginate_re = r'(?:after/(?P<after>[a-z0-9-]+)/)?(?:count/(?P<count>[0-9]+)/)?$'
+
+
+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(tags_re + r'books/' + paginate_re,
+ views.BookList.as_view(), name='catalogue_api_book_list'),
+ url(tags_re + r'parent_books/' + paginate_re,
+ views.BookList.as_view(), {"top_level": True}, name='catalogue_api_parent_book_list'),
+ url(tags_re + r'audiobooks/' + paginate_re,
+ views.BookList.as_view(), {"audiobooks": True}, name='catalogue_api_audiobook_list'),
+ url(tags_re + r'daisy/' + paginate_re,
+ views.BookList.as_view(), {"daisy": True}, name='catalogue_api_daisy_list'),
+ url(r'^recommended/' + paginate_re,
+ views.BookList.as_view(),
+ {"recommended": True}, name='catalogue_api_recommended_list'),
+ url(r'^newest/$',
+ views.BookList.as_view(),
+ {"newest": True, "top_level": True, "count": 20}, name='catalogue_api_newest_list'),
+
+ url(r'^books/(?P<slug>[^/]+)/$', views.BookDetail.as_view(), name='catalogue_api_book'),
+
+ url(r'^epub/(?P<slug>[a-z0-9-]+)/$', views.EpubView.as_view(), name='catalogue_api_epub'),
+
+ url(r'^preview/$', views.Preview.as_view(), name='catalogue_api_preview'),
+
+ url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){1,6})fragments/$', views.FragmentList.as_view()),
+ url(r'^books/(?P<book>[a-z0-9-]+)/fragments/(?P<anchor>[a-z0-9-]+)/$',
+ views.FragmentView.as_view(), name="catalogue_api_fragment"),
+
+ url(r'^(?P<category>[a-z]+)s/$', views.TagCategoryView.as_view(), name='catalogue_api_tag_list'),
+ url(r'^(?P<category>[a-z]+)s/(?P<slug>[a-z0-9-]+)/$', views.TagView.as_view(), name="catalogue_api_tag"),
+]
--- /dev/null
+from django.http import Http404, HttpResponse
+from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404
+from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
+from rest_framework import status
+from paypal.permissions import IsSubscribed
+from api.handlers import read_tags
+from .helpers import books_after, order_books
+from . import serializers
+from catalogue.models import Book, Collection, Tag, Fragment
+from catalogue.models.tag import prefetch_relations
+
+
+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 BookList(ListAPIView):
+ permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
+ queryset = Book.objects.none() # Required for DjangoModelPermissions
+ serializer_class = serializers.BaseBookSerializer
+
+ def get_queryset(self):
+ try:
+ tags, ancestors = read_tags(
+ self.kwargs['tags'], self.request,
+ allowed=('author', 'epoch', 'kind', 'genre')
+ )
+ except ValueError:
+ raise Http404
+
+ new_api = self.request.query_params.get('new_api')
+ after = self.request.query_params.get('after', self.kwargs.get('after'))
+ count = self.request.query_params.get('count', self.kwargs.get('count'))
+
+ if tags:
+ if self.kwargs.get('top_level'):
+ books = Book.tagged_top_level(tags)
+ if not books:
+ raise Http404
+ return books
+ else:
+ books = Book.tagged.with_all(tags)
+ else:
+ books = Book.objects.all()
+ books = order_books(books, new_api)
+
+ if self.kwargs.get('top_level'):
+ books = books.filter(parent=None)
+ if self.kwargs.get('audiobooks'):
+ books = books.filter(media__type='mp3').distinct()
+ if self.kwargs.get('daisy'):
+ books = books.filter(media__type='daisy').distinct()
+ if self.kwargs.get('recommended'):
+ books = books.filter(recommended=True)
+ if self.kwargs.get('newest'):
+ books = books.order_by('-created_at')
+
+ if after:
+ books = books_after(books, after, new_api)
+
+ prefetch_relations(books, 'author')
+ prefetch_relations(books, 'genre')
+ prefetch_relations(books, 'kind')
+ prefetch_relations(books, 'epoch')
+
+ if count:
+ books = books[:count]
+
+ return books
+
+ def post(self, request):
+ # Permission needed.
+ data = json.loads(request.POST.get('data'))
+ form = BookImportForm(data)
+ if form.is_valid():
+ form.save()
+ return Response({}, status=status.HTTP_201_CREATED)
+ else:
+ raise Http404
+
+
+class BookDetail(RetrieveAPIView):
+ queryset = Book.objects.all()
+ lookup_field = 'slug'
+ serializer_class = serializers.BookDetailSerializer
+
+
+class Preview(ListAPIView):
+ queryset = Book.objects.filter(preview=True)
+ serializer_class = serializers.BookPreviewSerializer
+
+
+class EpubView(RetrieveAPIView):
+ queryset = Book.objects.all()
+ lookup_field = 'slug'
+ permission_classes = [IsSubscribed]
+
+ def get(self, *args, **kwargs):
+ return HttpResponse(self.get_object().get_media('epub'))
+
+
+class TagCategoryView(ListAPIView):
+ serializer_class = serializers.TagSerializer
+
+ def get_queryset(self):
+ category = self.kwargs['category']
+ tags = Tag.objects.filter(category=category).exclude(items=None).order_by('slug')
+ if self.request.query_params.get('book_only') == 'true':
+ tags = tags.filter(for_books=True)
+ if self.request.GET.get('picture_only') == 'true':
+ tags = filter(for_pictures=True)
+
+ after = self.request.query_params.get('after')
+ count = self.request.query_params.get('count')
+ if after:
+ tags = tags.filter(slug__gt=after)
+ if count:
+ tags = tags[:count]
+
+ return tags
+
+
+class TagView(RetrieveAPIView):
+ serializer_class = serializers.TagDetailSerializer
+
+ def get_object(self):
+ return get_object_or_404(
+ Tag,
+ category=self.kwargs['category'],
+ slug=self.kwargs['slug']
+ )
+
+
+class FragmentList(ListAPIView):
+ serializer_class = serializers.FragmentSerializer
+
+ def get_queryset(self):
+ try:
+ tags, ancestors = read_tags(
+ self.kwargs['tags'],
+ self.request,
+ allowed={'author', 'epoch', 'kind', 'genre', 'book', 'theme'}
+ )
+ except ValueError:
+ raise Http404
+ return Fragment.tagged.with_all(tags).select_related('book')
+
+
+class FragmentView(RetrieveAPIView):
+ serializer_class = serializers.FragmentDetailSerializer
+
+ def get_object(self):
+ return get_object_or_404(
+ Fragment,
+ book__slug=self.kwargs['book'],
+ anchor=self.kwargs['anchor']
+ )
+
- model: catalogue.book
pk: 1
fields:
fields:
slug: grandchild
title: Grandchild
+ preview: true
sort_key: grandchild
parent: 2
- txt_file: txt/grandchild.txt
- html_file: html/grandchild.html
- fb2_file: fb2/grandchild.fb2
+ xml_file: secret/grandchild.xml
+ txt_file: secret/grandchild.txt
+ html_file: secret/grandchild.html
+ epub_file: secret/grandchild.epub
+ mobi_file: secret/grandchild.mobi
+ pdf_file: secret/grandchild.pdf
+ fb2_file: secret/grandchild.fb2
created_at: "1970-01-01 0:0Z"
changed_at: "1970-01-01 0:0Z"
uploaded_at: "1970-01-03 0:0Z"
- model: catalogue.fragment
- id: 1
+ pk: 1
fields:
short_text: "Fragment"
text: "A fragment"
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
html_built = django.dispatch.Signal()
published = django.dispatch.Signal()
+ SORT_KEY_SEP = '$'
+
class AlreadyExists(Exception):
pass
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:
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:
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:
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)
def get_absolute_url(self):
return '%s#m%s' % (reverse('book_text', args=[self.book.slug]), self.anchor)
+ def get_api_url(self):
+ return reverse('catalogue_api_fragment', args=[self.book.slug, self.anchor])
+
def get_short_text(self):
"""Returns short version of the fragment."""
return self.short_text if self.short_text else self.text
+ @property
+ def themes(self):
+ return self.tags.filter(category='theme')
+
def flush_includes(self, languages=True):
if not languages:
return
else:
return ''
+ @property
+ def category_plural(self):
+ return self.category + 's'
+
@permalink
def get_absolute_url(self):
return 'tagged_object_list', [self.url_chunk]
--- /dev/null
+from rest_framework.permissions import BasePermission
+from .rest import user_is_subscribed
+
+
+class IsSubscribed(BasePermission):
+ def has_permission(self, request, view):
+ return request.user.is_authenticated and user_is_subscribed(request.user)
--- /dev/null
+# -*- 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 = [
+ url(r'^like/(?P<slug>[a-z0-9-]+)/$', views.LikeView.as_view(), name='social_api_like'),
+ url(r'^shelf/(?P<state>[a-z]+)/$', views.ShelfView.as_view(), name='social_api_shelf'),
+]
--- /dev/null
+# -*- 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.http import Http404
+from rest_framework.generics import ListAPIView, get_object_or_404
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from api.models import BookUserData
+from catalogue.api.helpers import order_books, books_after
+from catalogue.api.serializers import BookSerializer
+from catalogue.models import Book
+from social.utils import likes
+
+
+class LikeView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ return Response({"likes": likes(request.user, book)})
+
+ def post(self, request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ action = request.query_params.get('action', 'like')
+ if action == 'like':
+ book.like(request.user)
+ elif action == 'unlike':
+ book.unlike(request.user)
+ return Response({})
+
+
+class ShelfView(ListAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = BookSerializer
+
+ def get_queryset(self):
+ state = self.kwargs['state']
+ if state not in ('reading', 'complete', 'likes'):
+ raise Http404
+ new_api = self.request.query_params.get('new_api')
+ after = self.request.query_params.get('after')
+ count = int(self.request.query_params.get('count', 50))
+ if state == 'likes':
+ books = Book.tagged.with_any(self.request.user.tag_set.all())
+ else:
+ ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
+ .values_list('book_id', flat=True)
+ books = Book.objects.filter(id__in=list(ids)).distinct()
+ books = order_books(books, new_api)
+ if after:
+ books = books_after(books, after, new_api)
+ if count:
+ books = books[:count]
+
+ return books
+
'django.contrib.admin',
'django.contrib.admindocs',
'django.contrib.staticfiles',
+ 'rest_framework',
'fnp_django_pagination',
'pipeline',
'piston',
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': (
+ 'api.drf_auth.PistonOAuthAuthentication',
+ )
+}
THUMBNAIL_BACKEND = 'wolnelektury.test_utils.DummyThumbnailBackend'
CATALOGUE_GET_MP3_LENGTH = 'catalogue.test_utils.get_mp3_length'
+MEDIA_URL = '/media/'