-# -*- 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.
#
import json
+import os.path
+from urllib.request import urlopen
+from django.conf import settings
+from django.core.files.base import ContentFile
from django.http import Http404, HttpResponse
-from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import never_cache
+from rest_framework.generics import (ListAPIView, RetrieveAPIView,
+ RetrieveUpdateAPIView, get_object_or_404)
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from rest_framework.response import Response
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 api.utils import vary_on_auth
from catalogue.forms import BookImportForm
-from catalogue.models import Book, Collection, Tag, Fragment
+from catalogue.models import Book, Collection, Tag, Fragment, BookMedia
from catalogue.models.tag import prefetch_relations
+from club.models import Membership
+from club.permissions import IsClubMember
+from sortify import sortify
from wolnelektury.utils import re_escape
+from .helpers import books_after, order_books
+from . import serializers
book_tag_categories = ['author', 'epoch', 'kind', 'genre']
+class CreateOnPutMixin:
+ '''
+ Creates a new model instance when PUTting a nonexistent resource.
+ '''
+ def get_object(self):
+ try:
+ return super().get_object()
+ except Http404:
+ if self.request.method == 'PUT':
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ return self.get_queryset().model(**{
+ self.lookup_field: self.kwargs[lookup_url_kwarg]
+ })
+ else:
+ raise
+
+
class CollectionList(ListAPIView):
- queryset = Collection.objects.all()
+ queryset = Collection.objects.filter(listed=True)
serializer_class = serializers.CollectionListSerializer
-class CollectionDetail(RetrieveAPIView):
+@vary_on_auth # Because of 'liked'.
+class CollectionDetail(CreateOnPutMixin, RetrieveUpdateAPIView):
+ permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
queryset = Collection.objects.all()
lookup_field = 'slug'
serializer_class = serializers.CollectionSerializer
+@vary_on_auth # Because of 'liked'.
class BookList(ListAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
queryset = Book.objects.none() # Required for DjangoModelPermissions
serializer_class = serializers.BookListSerializer
+ def get(self, request, filename=None, **kwargs):
+ if filename and not kwargs.get('tags') and 'count' not in request.query_params:
+ try:
+ with open(os.path.join(settings.MEDIA_ROOT, 'api', '%s.%s' % (filename, request.accepted_renderer.format)), 'rb') as f:
+ content = f.read()
+ return HttpResponse(content, content_type=request.accepted_media_type)
+ except:
+ pass
+ return super().get(request, filename=filename, **kwargs)
+
def get_queryset(self):
try:
tags, ancestors = read_tags(
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 count:
+ try:
+ count = int(count)
+ except TypeError:
+ raise Http404 # Fixme
if tags:
if self.kwargs.get('top_level'):
books = Book.tagged.with_all(tags)
else:
books = Book.objects.all()
+ books = books.filter(findable=True)
books = order_books(books, new_api)
+ if not Membership.is_active_for(self.request.user):
+ books = books.exclude(preview=True)
+
if self.kwargs.get('top_level'):
books = books.filter(parent=None)
if self.kwargs.get('audiobooks'):
return books
def post(self, request, **kwargs):
- # Permission needed.
+ if kwargs.get('audiobooks'):
+ return self.post_audiobook(request, **kwargs)
+ else:
+ return self.post_book(request, **kwargs)
+
+ def post_book(self, request, **kwargs):
data = json.loads(request.POST.get('data'))
form = BookImportForm(data)
if form.is_valid():
else:
raise Http404
+ def post_audiobook(self, request, **kwargs):
+ index = int(request.POST['part_index'])
+ parts_count = int(request.POST['parts_count'])
+ media_type = request.POST['type'].lower()
+ source_sha1 = request.POST.get('source_sha1')
+ name = request.POST.get('name', '')
+ part_name = request.POST.get('part_name', '')
+
+ project_description = request.POST.get('project_description', '')
+ project_icon = request.POST.get('project_icon', '')
+ _rest, slug = request.POST['book'].rstrip('/').rsplit('/', 1)
+ book = Book.objects.get(slug=slug)
+
+ try:
+ assert source_sha1
+ bm = book.media.get(type=media_type, source_sha1=source_sha1)
+ except (AssertionError, BookMedia.DoesNotExist):
+ bm = BookMedia(book=book, type=media_type)
+ bm.name = name
+ bm.part_name = part_name
+ bm.index = index
+ bm.project_description = project_description
+ bm.project_icon = project_icon
+ bm.file.save(None, request.data['file'], save=False)
+ bm.save(parts_count=parts_count)
+
+ return Response({}, status=status.HTTP_201_CREATED)
+
+
+@vary_on_auth # Because of 'liked'.
class BookDetail(RetrieveAPIView):
queryset = Book.objects.all()
lookup_field = 'slug'
serializer_class = serializers.BookDetailSerializer
+@vary_on_auth # Because of embargo links.
class EbookList(BookList):
serializer_class = serializers.EbookSerializer
+@method_decorator(never_cache, name='dispatch')
class Preview(ListAPIView):
- queryset = Book.objects.filter(preview=True)
+ #queryset = Book.objects.filter(preview=True)
serializer_class = serializers.BookPreviewSerializer
+ def get_queryset(self):
+ qs = Book.objects.filter(preview=True)
+ # FIXME: temporary workaround for a problem with iOS app; see #3954.
+ if 'Darwin' in self.request.META.get('HTTP_USER_AGENT', '') and 'debug' not in self.request.GET:
+ qs = qs.none()
+ return qs
+
+@vary_on_auth # Because of 'liked'.
class FilterBookList(ListAPIView):
serializer_class = serializers.FilterBookListSerializer
is_lektura = self.parse_bool(self.request.query_params.get('lektura'))
is_audiobook = self.parse_bool(self.request.query_params.get('audiobook'))
preview = self.parse_bool(self.request.query_params.get('preview'))
+ if not Membership.is_active_for(self.request.user):
+ preview = False
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))
books = order_books(Book.objects.distinct(), new_api)
+ books = books.filter(findable=True)
if is_lektura is not None:
books = books.filter(has_audience=is_lektura)
if is_audiobook is not None:
class EpubView(RetrieveAPIView):
queryset = Book.objects.all()
lookup_field = 'slug'
- permission_classes = [IsSubscribed]
+ permission_classes = [IsClubMember]
+ @method_decorator(never_cache)
def get(self, *args, **kwargs):
return HttpResponse(self.get_object().get_media('epub'))
class TagView(RetrieveAPIView):
+ permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
serializer_class = serializers.TagDetailSerializer
-
+ queryset = Tag.objects.all()
+
def get_object(self):
- return get_object_or_404(
- Tag,
- category=self.kwargs['category'],
- slug=self.kwargs['slug']
- )
+ try:
+ return get_object_or_404(
+ Tag,
+ category=self.kwargs['category'],
+ slug=self.kwargs['slug']
+ )
+ except Http404:
+ if self.request.method == 'POST':
+ return Tag(
+ category=self.kwargs['category'],
+ slug=self.kwargs['slug']
+ )
+ else:
+ raise
+
+ def post(self, request, **kwargs):
+ data = json.loads(request.POST.get('data'))
+ fields = {
+ "name_pl": "name_pl",
+ "description_pl": "description_pl",
+ "plural": "plural",
+ "is_epoch_specific": "genre_epoch_specific",
+ "collective_noun": "collective_noun",
+ "adjective_feminine_singular": "adjective_feminine_singular",
+ "adjective_nonmasculine_plural": "adjective_nonmasculine_plural",
+ "genitive": "genitive",
+ "collective_noun": "collective_noun",
+ "gazeta_link": "gazeta_link",
+ "culturepl_link": "culturepl_link",
+ "wiki_link_pl": "wiki_link_pl",
+ "photo_attribution": "photo_attribution",
+ }
+ obj = self.get_object()
+ updated = set()
+ for data_field, model_field in fields.items():
+ value = data.get(data_field)
+ if value:
+ if obj.category == 'author' and model_field == 'name_pl':
+ obj.sort_key = sortify(value.lower())
+ updated.add('sort_key')
+ value = ' '.join(reversed([t.strip() for t in value.split(',', 1)]))
+ setattr(obj, model_field, value)
+ updated.add(model_field)
+ if data.get('photo'):
+ response = urlopen(data['photo'])
+ ext = response.headers.get('Content-Type', '').rsplit('/', 1)[-1]
+ obj.photo.save(
+ "{}.{}".format(self.kwargs['slug'], ext),
+ ContentFile(response.read()),
+ save=False,
+ )
+ updated.add('photo')
+
+ if obj.pk:
+ obj.save(update_fields=updated, quick=True)
+ else:
+ obj.save()
+ return Response({})
+@vary_on_auth # Because of 'liked'.
class FragmentList(ListAPIView):
serializer_class = serializers.FragmentSerializer
)
except ValueError:
raise Http404
- return Fragment.tagged.with_all(tags).select_related('book')
+ return Fragment.tagged.with_all(tags).filter(book__findable=True).select_related('book')
+@vary_on_auth # Because of 'liked'.
class FragmentView(RetrieveAPIView):
serializer_class = serializers.FragmentDetailSerializer