X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/e5f7721a8da48c8c77900a58333e945a63c7e116..2d0cc46d3bc3357c19d13fc585c0a9492e5ee1b7:/apps/api/handlers.py?ds=sidebyside diff --git a/apps/api/handlers.py b/apps/api/handlers.py index eb15a4edd..e1792af13 100644 --- a/apps/api/handlers.py +++ b/apps/api/handlers.py @@ -1,7 +1,7 @@ # -*- 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 datetime import datetime, timedelta import json @@ -9,17 +9,25 @@ from django.conf import settings from django.contrib.sites.models import Site from django.core.cache import get_cache from django.core.urlresolvers import reverse +from django.utils.functional import lazy +from django.utils.timezone import utc from piston.handler import AnonymousBaseHandler, BaseHandler from piston.utils import rc +from sorl.thumbnail import default from api.helpers import timestamp from api.models import Deleted from catalogue.forms import BookImportForm -from catalogue.models import Book, Tag, BookMedia, Fragment +from catalogue.models import Book, Tag, BookMedia, Fragment, Collection +from catalogue.utils import related_tag_name +from picture.models import Picture +from picture.forms import PictureImportForm +from wolnelektury.utils import tz from stats.utils import piwik_track -API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain +API_BASE = WL_BASE = MEDIA_BASE = lazy( + lambda: u'http://' + Site.objects.get_current().domain, unicode)() category_singular = { @@ -30,10 +38,13 @@ category_singular = { 'themes': 'theme', 'books': 'book', } -category_plural={} +category_plural = {} for k, v in category_singular.items(): category_plural[v] = k +book_tag_categories = ['author', 'epoch', 'kind', 'genre'] + + def read_tags(tags, allowed): """ Reads a path of filtering tags. @@ -77,7 +88,7 @@ class BookMediaHandler(BaseHandler): """ Responsible for representing media in Books. """ model = BookMedia - fields = ['name', 'type', 'url'] + fields = ['name', 'type', 'url', 'artist', 'director'] @classmethod def url(cls, media): @@ -85,54 +96,87 @@ class BookMediaHandler(BaseHandler): 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 BookDetailHandler(BaseHandler): - """ Main handler for Book objects. - Responsible for lists of Book objects - and fields used for representing Books. +class BookDetails(object): + """Custom fields used for representing Books.""" + @classmethod + def href(cls, book): + """ Returns an URI for a Book in the API. """ + return API_BASE + reverse("api_book", args=[book.slug]) + + @classmethod + def url(cls, book): + """ Returns Book's URL on the site. """ + + return WL_BASE + book.get_absolute_url() + + @classmethod + def children(cls, book): + """ Returns all children for a book. """ + + return book.children.all() + + @classmethod + def media(cls, book): + """ Returns all media for a book. """ + return book.media.all() + + @classmethod + def cover(cls, book): + return MEDIA_BASE + book.cover.url if book.cover else '' + + @classmethod + def cover_thumb(cls, book): + return MEDIA_BASE + default.backend.get_thumbnail( + book.cover, "139x193").url if book.cover else '' + + + +class BookDetailHandler(BaseHandler, BookDetails): + """ Main handler for Book objects. + + Responsible for single Book details. """ allowed_methods = ['GET'] - fields = ['title', 'parent'] + Book.file_types + [ - 'media', 'url'] + category_singular.keys() + fields = ['title', 'parent', 'children'] + Book.formats + [ + 'media', 'url', 'cover', 'cover_thumb'] + [ + category_plural[c] for c in book_tag_categories] @piwik_track - def read(self, request, slug): - """ Returns details of a book, identified by a slug. """ - + def read(self, request, book): + """ Returns details of a book, identified by a slug and lang. """ try: - return Book.objects.get(slug=slug) + return Book.objects.get(slug=book) except Book.DoesNotExist: return rc.NOT_FOUND -class AnonymousBooksHandler(AnonymousBaseHandler): +class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): """ Main handler for Book objects. - Responsible for lists of Book objects - and fields used for representing Books. - + Responsible for lists of Book objects. """ allowed_methods = ('GET',) model = Book - fields = ['href', 'title'] - - categories = set(['author', 'epoch', 'kind', 'genre']) + fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb'] @classmethod - def href(cls, book): - """ Returns an URI for a Book in the API. """ - return API_BASE + reverse("api_book", args=[book.slug]) - - @classmethod - def url(cls, book): - """ Returns Book's URL on the site. """ - - return WL_BASE + book.get_absolute_url() + def genres(cls, book): + """ Returns all media for a book. """ + return book.tags.filter(category='genre') @piwik_track - def read(self, request, tags, top_level=False): + def read(self, request, tags, top_level=False, + audiobooks=False, daisy=False): """ Lists all books with given tags. :param tags: filtering tags; should be a path of categories @@ -141,7 +185,11 @@ class AnonymousBooksHandler(AnonymousBaseHandler): it's children are aren't. By default all books matching the tags are returned. """ - tags = read_tags(tags, allowed=self.categories) + try: + tags = read_tags(tags, allowed=book_tag_categories) + except ValueError: + return rc.NOT_FOUND + if tags: if top_level: books = Book.tagged_top_level(tags) @@ -151,27 +199,29 @@ class AnonymousBooksHandler(AnonymousBaseHandler): else: books = Book.objects.all() + if top_level: + books = books.filter(parent=None) + if audiobooks: + books = books.filter(media__type='mp3').distinct() + if daisy: + books = books.filter(media__type='daisy').distinct() + if books.exists(): return books else: return rc.NOT_FOUND - def create(self, request, tags, top_level=False): - return 'aaa' - - @classmethod - def media(self, book): - """ Returns all media for a book. """ - - return book.media.all() + def create(self, request, *args, **kwargs): + return rc.FORBIDDEN -class BooksHandler(BaseHandler): +class BooksHandler(BookDetailHandler): + allowed_methods = ('GET', 'POST') model = Book - fields = ('slug', 'title') + fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb'] anonymous = AnonymousBooksHandler - def create(self, request, tags, top_level=False): + def create(self, request, *args, **kwargs): if not request.user.has_perm('catalogue.add_book'): return rc.FORBIDDEN @@ -183,14 +233,25 @@ class BooksHandler(BaseHandler): else: return rc.NOT_FOUND + +class EBooksHandler(AnonymousBooksHandler): + fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + + # add categorized tags fields for Book def _tags_getter(category): @classmethod def get_tags(cls, book): return book.tags.filter(category=category) return get_tags +def _tag_getter(category): + @classmethod + def get_tag(cls, book): + return ", ".join(related_tag_name(t) for t in book.related_info()['tags'].get(category, [])) + return get_tag for plural, singular in category_singular.items(): - setattr(BooksHandler, plural, _tags_getter(singular)) + setattr(BookDetails, plural, _tags_getter(singular)) + setattr(BookDetails, singular, _tag_getter(singular)) # add fields for files in Book def _file_getter(format): @@ -203,14 +264,75 @@ def _file_getter(format): else: return '' return get_file -for format in Book.file_types: - setattr(BooksHandler, format, _file_getter(format)) +for format in Book.formats: + setattr(BookDetails, format, _file_getter(format)) + + +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 TagDetailHandler(BaseHandler): + +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', 'sort_key', 'description'] + fields = ['name', 'url', 'sort_key', 'description'] @piwik_track def read(self, request, category, slug): @@ -227,7 +349,7 @@ class TagDetailHandler(BaseHandler): return rc.NOT_FOUND -class TagsHandler(BaseHandler): +class TagsHandler(BaseHandler, TagDetails): """ Main handler for Tag objects. Responsible for lists of Tag objects @@ -236,7 +358,7 @@ class TagsHandler(BaseHandler): """ allowed_methods = ('GET',) model = Tag - fields = ['name', 'href'] + fields = ['name', 'href', 'url'] @piwik_track def read(self, request, category): @@ -247,35 +369,49 @@ class TagsHandler(BaseHandler): except KeyError, e: return rc.NOT_FOUND - tags = Tag.objects.filter(category=category_sng) - tags = [t for t in tags if t.get_count() > 0] - if tags: + tags = Tag.objects.filter(category=category_sng).exclude(book_count=0) + if tags.exists(): return tags else: return rc.NOT_FOUND +class FragmentDetails(object): + """Custom Fragment fields.""" + @classmethod - def href(cls, tag): - """ Returns URI in the API for the tag. """ + def href(cls, fragment): + """ Returns URI in the API for the fragment. """ - return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug]) + 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): + +class FragmentDetailHandler(BaseHandler, FragmentDetails): fields = ['book', 'anchor', 'text', 'url', 'themes'] @piwik_track - def read(self, request, slug, anchor): + def read(self, request, book, anchor): """ Returns details of a fragment, identified by book slug and anchor. """ - try: - return Fragment.objects.get(book__slug=slug, anchor=anchor) + return Fragment.objects.get(book__slug=book, anchor=anchor) except Fragment.DoesNotExist: return rc.NOT_FOUND -class FragmentsHandler(BaseHandler): +class FragmentsHandler(BaseHandler, FragmentDetails): """ Main handler for Fragments. Responsible for lists of Fragment objects @@ -283,7 +419,7 @@ class FragmentsHandler(BaseHandler): """ model = Fragment - fields = ['book', 'anchor', 'href'] + fields = ['book', 'url', 'anchor', 'href'] allowed_methods = ('GET',) categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme']) @@ -296,32 +432,16 @@ class FragmentsHandler(BaseHandler): books/book-slug/authors/an-author/themes/a-theme/ """ - tags = read_tags(tags, allowed=self.categories) + try: + tags = read_tags(tags, 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 - @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') - - # Changes handlers @@ -347,7 +467,7 @@ class CatalogueHandler(BaseHandler): """ # set to five minutes ago, to avoid concurrency issues if t is None: - t = datetime.now() - timedelta(seconds=settings.API_WAIT) + t = datetime.utcnow().replace(tzinfo=utc) - timedelta(seconds=settings.API_WAIT) # set to whole second in case DB supports something smaller return t.replace(microsecond=0) @@ -355,8 +475,7 @@ class CatalogueHandler(BaseHandler): def book_dict(book, fields=None): all_fields = ['url', 'title', 'description', 'gazeta_link', 'wiki_link', - ] + Book.file_types + [ - 'mp3', 'ogg', 'daisy', + ] + Book.formats + BookMedia.formats.keys() + [ 'parent', 'parent_number', 'tags', 'license', 'license_description', 'source_name', @@ -368,12 +487,12 @@ class CatalogueHandler(BaseHandler): else: fields = all_fields - extra_info = book.get_extra_info_value() + extra_info = book.extra_info obj = {} for field in fields: - if field in Book.file_types: + if field in Book.formats: f = getattr(book, field+'_file') if f: obj[field] = { @@ -381,9 +500,9 @@ class CatalogueHandler(BaseHandler): 'size': f.size, } - elif field in ('mp3', 'ogg', 'daisy'): + elif field in BookMedia.formats: media = [] - for m in book.media.filter(type=field): + for m in book.media.filter(type=field).iterator(): media.append({ 'url': m.file.url, 'size': m.file.size, @@ -395,10 +514,10 @@ class CatalogueHandler(BaseHandler): obj[field] = book.get_absolute_url() elif field == 'tags': - obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))] + obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set')).iterator()] elif field == 'author': - obj[field] = ", ".join(t.name for t in book.tags.filter(category='author')) + obj[field] = ", ".join(t.name for t in book.tags.filter(category='author').iterator()) elif field == 'parent': obj[field] = book.parent_id @@ -419,7 +538,7 @@ class CatalogueHandler(BaseHandler): @classmethod def book_changes(cls, request=None, since=0, until=None, fields=None): - since = datetime.fromtimestamp(int(since)) + since = datetime.fromtimestamp(int(since), tz) until = cls.until(until) changes = { @@ -435,16 +554,16 @@ class CatalogueHandler(BaseHandler): last_change = since for book in Book.objects.filter(changed_at__gte=since, - changed_at__lt=until): + changed_at__lt=until).iterator(): book_d = cls.book_dict(book, fields) updated.append(book_d) if updated: changes['updated'] = updated - for book in Deleted.objects.filter(content_type=Book, + for book in Deleted.objects.filter(content_type=Book, deleted_at__gte=since, deleted_at__lt=until, - created_at__lt=since): + created_at__lt=since).iterator(): deleted.append(book.id) if deleted: changes['deleted'] = deleted @@ -470,7 +589,7 @@ class CatalogueHandler(BaseHandler): obj[field] = tag.get_absolute_url() elif field == 'books': - obj[field] = [b.id for b in Book.tagged_top_level([tag])] + obj[field] = [b.id for b in Book.tagged_top_level([tag]).iterator()] elif field == 'sort_key': obj[field] = tag.sort_key @@ -485,7 +604,7 @@ class CatalogueHandler(BaseHandler): @classmethod def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None): - since = datetime.fromtimestamp(int(since)) + since = datetime.fromtimestamp(int(since), tz) until = cls.until(until) changes = { @@ -506,11 +625,11 @@ class CatalogueHandler(BaseHandler): updated = [] deleted = [] - for tag in Tag.objects.filter(category__in=categories, + for tag in Tag.objects.filter(category__in=categories, changed_at__gte=since, - changed_at__lt=until): + changed_at__lt=until).iterator(): # only serve non-empty tags - if tag.get_count(): + if tag.book_count: tag_d = cls.tag_dict(tag, fields) updated.append(tag_d) elif tag.created_at < since: @@ -519,10 +638,10 @@ class CatalogueHandler(BaseHandler): changes['updated'] = updated for tag in Deleted.objects.filter(category__in=categories, - content_type=Tag, + content_type=Tag, deleted_at__gte=since, deleted_at__lt=until, - created_at__lt=since): + created_at__lt=since).iterator(): deleted.append(tag.id) if deleted: changes['deleted'] = deleted @@ -587,3 +706,21 @@ class ChangesHandler(CatalogueHandler): @piwik_track def read(self, request, since): return self.changes(request, since) + + +class PictureHandler(BaseHandler): + model = Picture + fields = ('slug', 'title') + allowed_methods = ('POST',) + + def create(self, request): + if not request.user.has_perm('picture.add_picture'): + return rc.FORBIDDEN + + data = json.loads(request.POST.get('data')) + form = PictureImportForm(data) + if form.is_valid(): + form.save() + return rc.CREATED + else: + return rc.NOT_FOUND