from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
+from django.db.models import Q
from django.http.response import HttpResponse
from django.utils.functional import lazy
from django.db import models
+from migdal.models import Entry
from piston.handler import AnonymousBaseHandler, BaseHandler
from piston.utils import rc
from sorl.thumbnail import default
from catalogue.forms import BookImportForm
from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
from catalogue.models.tag import prefetch_relations
-from catalogue.utils import is_subscribed
+from paypal.rest import user_is_subscribed
from picture.models import Picture
from picture.forms import PictureImportForm
+from social.utils import likes
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'http://' + Site.objects.get_current().domain, unicode)()
-
+ lambda: u'https://' + Site.objects.get_current().domain, unicode)()
category_singular = {
'authors': 'author',
book_tag_categories = ['author', 'epoch', 'kind', 'genre']
+book_list_fields = book_tag_categories + [
+ 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color', 'full_sort_key']
+
def read_tags(tags, request, allowed):
""" Reads a path of filtering tags.
def process(category, slug):
if category == 'book':
+ # FIXME: Unused?
try:
books.append(Book.objects.get(slug=slug))
except Book.DoesNotExist:
@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 ''
+ @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(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))))
+
+ @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.
"""
allowed_methods = ['GET']
fields = ['title', 'parent', 'children'] + Book.formats + [
- 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'preview'] + [
+ 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
+ 'preview', 'cover_color'] + [
category_plural[c] for c in book_tag_categories]
@piwik_track
"""
allowed_methods = ('GET',)
model = Book
- fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb']
+ fields = book_list_fields
+ # FIXME: Unused?
@classmethod
def genres(cls, book):
""" Returns all media for a book. """
@piwik_track
def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
recommended=False, newest=False, books=None,
- after=None, before=None, count=None):
+ after=None, count=None):
""" Lists all books with given tags.
:param tags: filtering tags; should be a path of categories
are returned.
"""
if pk is not None:
+ # FIXME: Unused?
try:
return Book.objects.get(pk=pk)
except Book.DoesNotExist:
except ValueError:
return rc.NOT_FOUND
+ new_api = request.GET.get('new_api')
if 'after' in request.GET:
after = request.GET['after']
- if 'before' in request.GET:
- before = request.GET['before']
if 'count' in request.GET:
count = request.GET['count']
books = Book.tagged.with_all(tags)
else:
books = books if books is not None else Book.objects.all()
- books = books.order_by('slug')
+ books = self.order_books(books, new_api)
if top_level:
books = books.filter(parent=None)
books = books.order_by('-created_at')
if after:
- books = books.filter(slug__gt=after)
- if before:
- books = books.filter(slug__lt=before)
+ books = self.books_after(books, after, new_api)
- books = books.only('slug', 'title', 'cover', 'cover_thumb')
+ if new_api:
+ books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author')
+ else:
+ books = books.only('slug', 'title', 'cover', 'cover_thumb')
for category in book_tag_categories:
books = prefetch_relations(books, category)
if count:
- if before:
- books = list(reversed(books.order_by('-slug')[:count]))
- else:
- books = books[:count]
+ books = books[:count]
return books
class BooksHandler(BookDetailHandler):
allowed_methods = ('GET', 'POST')
model = Book
- fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
+ fields = book_list_fields + ['liked']
anonymous = AnonymousBooksHandler
+ # hack, because piston is stupid
+ @classmethod
+ def liked(cls, book):
+ return getattr(book, 'liked', None)
+
+ def read(self, request, **kwargs):
+ books = AnonymousBooksHandler().read(request, **kwargs)
+ likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
+
+ new_books = [
+ BookProxy(book).set('liked', book.id in likes)
+ for book in books]
+ return QuerySetProxy(new_books)
+
def create(self, request, *args, **kwargs):
if not request.user.has_perm('catalogue.add_book'):
return rc.FORBIDDEN
class EpubHandler(BookDetailHandler):
def read(self, request, slug):
- if not is_subscribed(request.user):
+ if not user_is_subscribed(request.user):
return rc.FORBIDDEN
try:
book = Book.objects.get(slug=slug)
class Meta:
managed = False
- def __init__(self, book, key):
+ def __init__(self, book, key=None):
self.book = book
self.key = key
+ def set(self, attr, value):
+ self.__setattr__(attr, value)
+ return self
+
def __getattr__(self, item):
- if item not in ('book', 'key'):
- return self.book.__getattribute__(item)
- else:
- return self.__getattribute__(item)
+ return self.book.__getattribute__(item)
class QuerySetProxy(models.QuerySet):
return iter(self.list)
-class FilterBooksHandler(AnonymousBooksHandler):
- fields = book_tag_categories + [
- 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
+class AnonFilterBooksHandler(AnonymousBooksHandler):
+ fields = book_list_fields + ['key']
def parse_bool(self, s):
if s in ('true', 'false'):
is_audiobook = self.parse_bool(request.GET.get('audiobook'))
preview = self.parse_bool(request.GET.get('preview'))
+ new_api = request.GET.get('new_api')
after = request.GET.get('after')
count = int(request.GET.get('count', 50))
- books = Book.objects.distinct().order_by('slug')
+ books = self.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:
books_title = books.filter(title__iregex='\m' + search_string)
books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
if after and (key_sep in after):
- which, slug = after.split(key_sep, 1)
+ which, key = after.split(key_sep, 1)
if which == 'title':
- book_lists = [(books_title.filter(slug__gt=slug), 'title')]
+ book_lists = [(self.books_after(books_title, key, new_api), 'title')]
else: # which == 'author'
- book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
+ book_lists = [(self.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, slug = after.split(key_sep, 1)
- books = books.filter(slug__gt=slug)
+ which, key = after.split(key_sep, 1)
+ books = self.books_after(books, key, new_api)
book_lists = [(books, 'book')]
filtered_books = []
for book_list, label in book_lists:
- book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
+ book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key_author', 'sort_key')
for category in book_tag_categories:
book_list = prefetch_relations(book_list, category)
remaining_count = count - len(filtered_books)
- new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
- for book in book_list[:remaining_count]]
+ new_books = [
+ BookProxy(book, '%s%s%s' % (
+ 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:
break
return QuerySetProxy(filtered_books)
+class FilterBooksHandler(BooksHandler):
+ anonymous = AnonFilterBooksHandler
+ fields = book_list_fields + ['key', 'liked']
+
+ # hack, because piston is stupid
+ @classmethod
+ def liked(cls, book):
+ return getattr(book, 'liked', None)
+
+ def read(self, request):
+ qsp = AnonFilterBooksHandler().read(request)
+ likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True))
+ for book in qsp.list:
+ book.set('liked', book.id in likes)
+ 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
for book_format in Book.formats:
setattr(BookDetails, book_format, _file_getter(book_format))
-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()
+add_file_getters()
class TagDetails(object):
def read(self, request, category=None, pk=None):
""" Lists all tags in the category (eg. all themes). """
if pk is not None:
+ # FIXME: Unused?
try:
return Tag.objects.exclude(category='set').get(pk=pk)
except Book.DoesNotExist:
return rc.NOT_FOUND
after = request.GET.get('after')
- before = request.GET.get('before')
count = request.GET.get('count')
tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
if after:
tags = tags.filter(slug__gt=after)
- if before:
- tags = tags.filter(slug__lt=before)
if count:
- if before:
- tags = list(reversed(tags.order_by('-slug')[:count]))
- else:
- tags = tags[:count]
+ tags = tags[:count]
return tags
"""
try:
- tags, ancestors = read_tags(tags, allowed=self.categories)
+ tags, ancestors = read_tags(tags, request, allowed=self.categories)
except ValueError:
return rc.NOT_FOUND
fragments = Fragment.tagged.with_all(tags).select_related('book')
class UserDataHandler(BaseHandler):
model = BookUserData
- fields = ('state',)
+ fields = ('state', 'username', 'premium')
allowed_methods = ('GET', 'POST')
- def read(self, request, slug):
+ 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
- if not request.user.is_authenticated():
- return rc.FORBIDDEN
try:
data = BookUserData.objects.get(book=book, user=request.user)
except BookUserData.DoesNotExist:
class UserShelfHandler(BookDetailHandler):
- fields = book_tag_categories + [
- 'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
+ fields = book_list_fields + ['liked']
+ # FIXME: Unused?
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
- if state not in ('reading', 'complete'):
+ 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))
- 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().order_by('slug')
+ 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 = books.filter(slug__gt=after)
+ books = self.books_after(books, after, new_api)
if count:
books = books[:count]
- return books
+ 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 = (
+ 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url')
+
+ def read(self, request):
+ after = request.GET.get('after')
+ count = int(request.GET.get('count', 20))
+ entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at')
+ if after:
+ entries = entries.filter(first_published_at__lt=after)
+ if count:
+ entries = entries[:count]
+ return entries
+
+ @classmethod
+ def image_url(cls, entry):
+ return (WL_BASE + entry.image.url) if entry.image else None
+
+ @classmethod
+ def image_thumb(cls, entry):
+ return MEDIA_BASE + default.backend.get_thumbnail(
+ entry.image, "193x193").url if entry.image else ''
+
+ @classmethod
+ def gallery_urls(cls, entry):
+ return [WL_BASE + photo.url() for photo in entry.photo_set.all()]
+
+ @classmethod
+ def key(cls, entry):
+ return entry.first_published_at
+
+ @classmethod
+ def url(cls, entry):
+ return WL_BASE + entry.get_absolute_url()