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 def media(self, book):
152 """ Returns all media for a book. """
154 return book.media.all()
157 # add categorized tags fields for Book
158 def _tags_getter(category):
160 def get_tags(cls, book):
161 return book.tags.filter(category=category)
163 for plural, singular in category_singular.items():
164 setattr(BooksHandler, plural, _tags_getter(singular))
166 # add fields for files in Book
167 def _file_getter(format):
168 field = "%s_file" % format
170 def get_file(cls, book):
171 f = getattr(book, field)
173 return MEDIA_BASE + f.url
177 for format in ('xml', 'txt', 'html', 'epub', 'pdf'):
178 setattr(BooksHandler, format, _file_getter(format))
181 class TagDetailHandler(BaseHandler):
182 """ Responsible for details of a single Tag object. """
184 fields = ['name', 'sort_key', 'description']
186 def read(self, request, category, slug):
187 """ Returns details of a tag, identified by category and slug. """
190 category_sng = category_singular[category]
194 return get_object_or_404(Tag, category=category_sng, slug=slug)
197 class TagsHandler(BaseHandler):
198 """ Main handler for Tag objects.
200 Responsible for lists of Tag objects
201 and fields used for representing Tags.
204 allowed_methods = ('GET',)
206 fields = ['name', 'href']
208 def read(self, request, category):
209 """ Lists all tags in the category (eg. all themes). """
212 category_sng = category_singular[category]
216 tags = Tag.objects.filter(category=category_sng)
217 tags = [t for t in tags if t.get_count() > 0]
226 """ Returns URI in the API for the tag. """
228 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
231 class FragmentDetailHandler(BaseHandler):
232 fields = ['book', 'anchor', 'text', 'url', 'themes']
234 def read(self, request, slug, anchor):
235 """ Returns details of a fragment, identified by book slug and anchor. """
237 return get_object_or_404(Fragment, book__slug=slug, anchor=anchor)
240 class FragmentsHandler(BaseHandler):
241 """ Main handler for Fragments.
243 Responsible for lists of Fragment objects
244 and fields used for representing Fragments.
248 fields = ['book', 'anchor', 'href']
249 allowed_methods = ('GET',)
251 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
253 def read(self, request, tags):
254 """ Lists all fragments with given book, tags, themes.
256 :param tags: should be a path of categories and slugs, i.e.:
257 books/book-slug/authors/an-author/themes/a-theme/
260 tags = read_tags(tags, allowed=self.categories)
261 fragments = Fragment.tagged.with_all(tags).select_related('book')
262 if fragments.exists():
268 def href(cls, fragment):
269 """ Returns URI in the API for the fragment. """
271 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
274 def url(cls, fragment):
275 """ Returns URL on the site for the fragment. """
277 return WL_BASE + fragment.get_absolute_url()
280 def themes(cls, fragment):
281 """ Returns a list of theme tags for the fragment. """
283 return fragment.tags.filter(category='theme')
290 class CatalogueHandler(BaseHandler):
293 def fields(request, name):
294 fields_str = request.GET.get(name) if request is not None else None
295 return fields_str.split(',') if fields_str is not None else None
299 """ Returns time suitable for use as upper time boundary for check.
301 Used to avoid issues with time between setting the change stamp
302 and actually saving the model in database.
303 Cuts the microsecond part to avoid issues with DBs where time has
306 :param datetime t: manually sets the upper boundary
309 # set to five minutes ago, to avoid concurrency issues
311 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
312 # set to whole second in case DB supports something smaller
313 return t.replace(microsecond=0)
316 def book_dict(book, fields=None):
317 all_fields = ('url', 'title', 'description',
318 'gazeta_link', 'wiki_link',
319 'xml', 'epub', 'txt', 'pdf', 'html',
320 'mp3', 'ogg', 'daisy',
321 'parent', 'parent_number',
323 'license', 'license_description', 'source_name',
324 'technical_editors', 'editors',
325 'author', 'sort_key',
328 fields = (f for f in fields if f in all_fields)
332 extra_info = book.get_extra_info_value()
337 if field in ('xml', 'epub', 'txt', 'pdf', 'html'):
338 f = getattr(book, field+'_file')
345 elif field in ('mp3', 'ogg', 'daisy'):
347 for m in book.media.filter(type=field):
356 obj[field] = book.get_absolute_url()
358 elif field == 'tags':
359 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
361 elif field == 'author':
362 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
364 elif field == 'parent':
365 obj[field] = book.parent_id
367 elif field in ('license', 'license_description', 'source_name',
368 'technical_editors', 'editors'):
369 f = extra_info.get(field)
374 f = getattr(book, field)
382 def book_changes(cls, request=None, since=0, until=None, fields=None):
383 since = datetime.fromtimestamp(int(since))
384 until = cls.until(until)
387 'time_checked': timestamp(until)
391 fields = cls.fields(request, 'book_fields')
398 for book in Book.objects.filter(changed_at__gte=since,
399 changed_at__lt=until):
400 book_d = cls.book_dict(book, fields)
401 updated.append(book_d)
403 changes['updated'] = updated
405 for book in Deleted.objects.filter(content_type=Book,
406 deleted_at__gte=since,
407 deleted_at__lt=until,
408 created_at__lt=since):
409 deleted.append(book.id)
411 changes['deleted'] = deleted
416 def tag_dict(tag, fields=None):
417 all_fields = ('name', 'category', 'sort_key', 'description',
418 'gazeta_link', 'wiki_link',
423 fields = (f for f in fields if f in all_fields)
431 obj[field] = tag.get_absolute_url()
433 elif field == 'books':
434 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
436 elif field == 'sort_key':
437 obj[field] = tag.sort_key
440 f = getattr(tag, field)
448 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
449 since = datetime.fromtimestamp(int(since))
450 until = cls.until(until)
453 'time_checked': timestamp(until)
457 fields = cls.fields(request, 'tag_fields')
459 categories = cls.fields(request, 'tag_categories')
461 all_categories = ('author', 'epoch', 'kind', 'genre')
463 categories = (c for c in categories if c in all_categories)
465 categories = all_categories
470 for tag in Tag.objects.filter(category__in=categories,
471 changed_at__gte=since,
472 changed_at__lt=until):
473 # only serve non-empty tags
475 tag_d = cls.tag_dict(tag, fields)
476 updated.append(tag_d)
477 elif tag.created_at < since:
478 deleted.append(tag.id)
480 changes['updated'] = updated
482 for tag in Deleted.objects.filter(category__in=categories,
484 deleted_at__gte=since,
485 deleted_at__lt=until,
486 created_at__lt=since):
487 deleted.append(tag.id)
489 changes['deleted'] = deleted
494 def changes(cls, request=None, since=0, until=None, book_fields=None,
495 tag_fields=None, tag_categories=None):
496 until = cls.until(until)
499 'time_checked': timestamp(until)
503 'books': cls.book_changes(request, since, until, book_fields),
504 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
507 for model in changes_by_type:
508 for field in changes_by_type[model]:
509 if field == 'time_checked':
511 changes.setdefault(field, {})[model] = changes_by_type[model][field]
515 class BookChangesHandler(CatalogueHandler):
516 allowed_methods = ('GET',)
518 def read(self, request, since):
519 return self.book_changes(request, since)
522 class TagChangesHandler(CatalogueHandler):
523 allowed_methods = ('GET',)
525 def read(self, request, since):
526 return self.tag_changes(request, since)
529 class ChangesHandler(CatalogueHandler):
530 allowed_methods = ('GET',)
532 def read(self, request, since):
533 return self.changes(request, since)