1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 from datetime import datetime, timedelta
7 from django.conf import settings
8 from django.contrib.sites.models import Site
9 from django.core.urlresolvers import reverse
10 from django.http import Http404
11 from django.shortcuts import get_object_or_404
12 from piston.handler import BaseHandler
13 from piston.utils import rc
15 from api.helpers import timestamp
16 from api.models import Deleted
17 from catalogue.models import Book, Tag, BookMedia, Fragment
20 API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain
32 for k, v in category_singular.items():
33 category_plural[v] = k
36 def read_tags(tags, allowed):
37 """ Reads a path of filtering tags.
39 :param str tags: a path of category and slug pairs, like: authors/an-author/...
40 :returns: list of Tag objects
41 :raises: django.http.Http404
46 tags = tags.strip('/').split('/')
49 category = tags.pop(0)
53 category = category_singular[category]
57 if not category in allowed:
61 if category == 'book':
64 real_tags.append(get_object_or_404(Tag, category=category, slug=slug))
71 class BookMediaHandler(BaseHandler):
72 """ Responsible for representing media in Books. """
75 fields = ['name', 'type', 'url']
79 """ Link to media on site. """
81 return MEDIA_BASE + media.file.url
84 class BookDetailHandler(BaseHandler):
85 """ Main handler for Book objects.
87 Responsible for lists of Book objects
88 and fields used for representing Books.
91 allowed_methods = ['GET']
92 fields = ['title', 'parent',
93 'xml', 'html', 'pdf', 'epub', 'txt',
94 'media', 'url'] + category_singular.keys()
96 def read(self, request, slug):
97 """ Returns details of a book, identified by a slug. """
99 return get_object_or_404(Book, slug=slug)
102 class BooksHandler(BaseHandler):
103 """ Main handler for Book objects.
105 Responsible for lists of Book objects
106 and fields used for representing Books.
109 allowed_methods = ('GET',)
111 fields = ['href', 'title']
113 categories = set(['author', 'epoch', 'kind', 'genre'])
117 """ Returns an URI for a Book in the API. """
118 return API_BASE + reverse("api_book", args=[book.slug])
122 """ Returns Book's URL on the site. """
124 return WL_BASE + book.get_absolute_url()
126 def read(self, request, tags, top_level=False):
127 """ Lists all books with given tags.
129 :param tags: filtering tags; should be a path of categories
130 and slugs, i.e.: authors/an-author/epoch/an-epoch/
131 :param top_level: if True and a book is included in the results,
132 it's children are aren't. By default all books matching the tags
135 tags = read_tags(tags, allowed=self.categories)
138 books = Book.tagged_top_level(tags)
139 return books if books else rc.NOT_FOUND
141 books = Book.tagged.with_all(tags)
143 books = Book.objects.all()
151 # add categorized tags fields for Book
152 def _tags_getter(category):
154 def get_tags(cls, book):
155 return book.tags.filter(category=category)
157 for plural, singular in category_singular.items():
158 setattr(BooksHandler, plural, _tags_getter(singular))
160 # add fields for files in Book
161 def _file_getter(format):
162 field = "%s_file" % format
164 def get_file(cls, book):
165 f = getattr(book, field)
167 return MEDIA_BASE + f.url
171 for format in ('xml', 'txt', 'html', 'epub', 'pdf'):
172 setattr(BooksHandler, format, _file_getter(format))
175 class TagDetailHandler(BaseHandler):
176 """ Responsible for details of a single Tag object. """
178 fields = ['name', 'sort_key', 'description']
180 def read(self, request, category, slug):
181 """ Returns details of a tag, identified by category and slug. """
184 category_sng = category_singular[category]
188 return get_object_or_404(Tag, category=category_sng, slug=slug)
191 class TagsHandler(BaseHandler):
192 """ Main handler for Tag objects.
194 Responsible for lists of Tag objects
195 and fields used for representing Tags.
198 allowed_methods = ('GET',)
200 fields = ['name', 'href']
202 def read(self, request, category):
203 """ Lists all tags in the category (eg. all themes). """
206 category_sng = category_singular[category]
210 tags = Tag.objects.filter(category=category_sng)
211 tags = [t for t in tags if t.get_count() > 0]
220 """ Returns URI in the API for the tag. """
222 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
225 class FragmentDetailHandler(BaseHandler):
226 fields = ['book', 'anchor', 'text', 'url', 'themes']
228 def read(self, request, slug, anchor):
229 """ Returns details of a fragment, identified by book slug and anchor. """
231 return get_object_or_404(Fragment, book__slug=slug, anchor=anchor)
234 class FragmentsHandler(BaseHandler):
235 """ Main handler for Fragments.
237 Responsible for lists of Fragment objects
238 and fields used for representing Fragments.
242 fields = ['book', 'anchor', 'href']
243 allowed_methods = ('GET',)
245 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
247 def read(self, request, tags):
248 """ Lists all fragments with given book, tags, themes.
250 :param tags: should be a path of categories and slugs, i.e.:
251 books/book-slug/authors/an-author/themes/a-theme/
254 tags = read_tags(tags, allowed=self.categories)
255 fragmets = Fragment.tagged.with_all(tags).select_related('book')
256 if fragments.exists():
262 def href(cls, fragment):
263 """ Returns URI in the API for the fragment. """
265 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
268 def url(cls, fragment):
269 """ Returns URL on the site for the fragment. """
271 return WL_BASE + fragment.get_absolute_url()
274 def themes(cls, fragment):
275 """ Returns a list of theme tags for the fragment. """
277 return fragment.tags.filter(category='theme')
284 class CatalogueHandler(BaseHandler):
287 def fields(request, name):
288 fields_str = request.GET.get(name) if request is not None else None
289 return fields_str.split(',') if fields_str is not None else None
293 """ Returns time suitable for use as upper time boundary for check.
295 Used to avoid issues with time between setting the change stamp
296 and actually saving the model in database.
297 Cuts the microsecond part to avoid issues with DBs where time has
300 :param datetime t: manually sets the upper boundary
303 # set to five minutes ago, to avoid concurrency issues
305 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
306 # set to whole second in case DB supports something smaller
307 return t.replace(microsecond=0)
310 def book_dict(book, fields=None):
311 all_fields = ('url', 'title', 'description',
312 'gazeta_link', 'wiki_link',
313 'xml', 'epub', 'txt', 'pdf', 'html',
314 'mp3', 'ogg', 'daisy',
315 'parent', 'parent_number',
317 'license', 'license_description', 'source_name',
318 'technical_editors', 'editors',
319 'author', 'sort_key',
322 fields = (f for f in fields if f in all_fields)
326 extra_info = book.get_extra_info_value()
331 if field in ('xml', 'epub', 'txt', 'pdf', 'html'):
332 f = getattr(book, field+'_file')
339 elif field in ('mp3', 'ogg', 'daisy'):
341 for m in book.media.filter(type=field):
350 obj[field] = book.get_absolute_url()
352 elif field == 'tags':
353 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
355 elif field == 'author':
356 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
358 elif field == 'parent':
359 obj[field] = book.parent_id
361 elif field in ('license', 'license_description', 'source_name',
362 'technical_editors', 'editors'):
363 f = extra_info.get(field)
368 f = getattr(book, field)
376 def book_changes(cls, request=None, since=0, until=None, fields=None):
377 since = datetime.fromtimestamp(int(since))
378 until = cls.until(until)
381 'time_checked': timestamp(until)
385 fields = cls.fields(request, 'book_fields')
392 for book in Book.objects.filter(changed_at__gte=since,
393 changed_at__lt=until):
394 book_d = cls.book_dict(book, fields)
395 updated.append(book_d)
397 changes['updated'] = updated
399 for book in Deleted.objects.filter(content_type=Book,
400 deleted_at__gte=since,
401 deleted_at__lt=until,
402 created_at__lt=since):
403 deleted.append(book.id)
405 changes['deleted'] = deleted
410 def tag_dict(tag, fields=None):
411 all_fields = ('name', 'category', 'sort_key', 'description',
412 'gazeta_link', 'wiki_link',
417 fields = (f for f in fields if f in all_fields)
425 obj[field] = tag.get_absolute_url()
427 elif field == 'books':
428 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
430 elif field == 'sort_key':
431 obj[field] = tag.sort_key
434 f = getattr(tag, field)
442 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
443 since = datetime.fromtimestamp(int(since))
444 until = cls.until(until)
447 'time_checked': timestamp(until)
451 fields = cls.fields(request, 'tag_fields')
453 categories = cls.fields(request, 'tag_categories')
455 all_categories = ('author', 'epoch', 'kind', 'genre')
457 categories = (c for c in categories if c in all_categories)
459 categories = all_categories
464 for tag in Tag.objects.filter(category__in=categories,
465 changed_at__gte=since,
466 changed_at__lt=until):
467 # only serve non-empty tags
469 tag_d = cls.tag_dict(tag, fields)
470 updated.append(tag_d)
471 elif tag.created_at < since:
472 deleted.append(tag.id)
474 changes['updated'] = updated
476 for tag in Deleted.objects.filter(category__in=categories,
478 deleted_at__gte=since,
479 deleted_at__lt=until,
480 created_at__lt=since):
481 deleted.append(tag.id)
483 changes['deleted'] = deleted
488 def changes(cls, request=None, since=0, until=None, book_fields=None,
489 tag_fields=None, tag_categories=None):
490 until = cls.until(until)
493 'time_checked': timestamp(until)
497 'books': cls.book_changes(request, since, until, book_fields),
498 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
501 for model in changes_by_type:
502 for field in changes_by_type[model]:
503 if field == 'time_checked':
505 changes.setdefault(field, {})[model] = changes_by_type[model][field]
509 class BookChangesHandler(CatalogueHandler):
510 allowed_methods = ('GET',)
512 def read(self, request, since):
513 return self.book_changes(request, since)
516 class TagChangesHandler(CatalogueHandler):
517 allowed_methods = ('GET',)
519 def read(self, request, since):
520 return self.tag_changes(request, since)
523 class ChangesHandler(CatalogueHandler):
524 allowed_methods = ('GET',)
526 def read(self, request, since):
527 return self.changes(request, since)