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 piston.handler import BaseHandler
11 from piston.utils import rc
13 from api.helpers import timestamp
14 from api.models import Deleted
15 from catalogue.models import Book, Tag, BookMedia, Fragment
18 API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain
30 for k, v in category_singular.items():
31 category_plural[v] = k
34 def read_tags(tags, allowed):
35 """ Reads a path of filtering tags.
37 :param str tags: a path of category and slug pairs, like: authors/an-author/...
38 :returns: list of Tag objects
39 :raises: ValueError when tags can't be found
44 tags = tags.strip('/').split('/')
47 category = tags.pop(0)
51 category = category_singular[category]
53 raise ValueError('Unknown category.')
55 if not category in allowed:
56 raise ValueError('Category not allowed.')
59 if category == 'book':
63 real_tags.append(Tag.objects.get(category=category, slug=slug))
64 except Tag.DoesNotExist:
65 raise ValueError('Tag not found')
72 class BookMediaHandler(BaseHandler):
73 """ Responsible for representing media in Books. """
76 fields = ['name', 'type', 'url']
80 """ Link to media on site. """
82 return MEDIA_BASE + media.file.url
85 class BookDetailHandler(BaseHandler):
86 """ Main handler for Book objects.
88 Responsible for lists of Book objects
89 and fields used for representing Books.
92 allowed_methods = ['GET']
93 fields = ['title', 'parent',
94 'xml', 'html', 'pdf', 'epub', 'txt',
95 'media', 'url'] + category_singular.keys()
97 def read(self, request, slug):
98 """ Returns details of a book, identified by a slug. """
101 return Book.objects.get(slug=slug)
102 except Book.DoesNotExist:
106 class BooksHandler(BaseHandler):
107 """ Main handler for Book objects.
109 Responsible for lists of Book objects
110 and fields used for representing Books.
113 allowed_methods = ('GET',)
115 fields = ['href', 'title']
117 categories = set(['author', 'epoch', 'kind', 'genre'])
121 """ Returns an URI for a Book in the API. """
122 return API_BASE + reverse("api_book", args=[book.slug])
126 """ Returns Book's URL on the site. """
128 return WL_BASE + book.get_absolute_url()
130 def read(self, request, tags, top_level=False):
131 """ Lists all books with given tags.
133 :param tags: filtering tags; should be a path of categories
134 and slugs, i.e.: authors/an-author/epoch/an-epoch/
135 :param top_level: if True and a book is included in the results,
136 it's children are aren't. By default all books matching the tags
139 tags = read_tags(tags, allowed=self.categories)
142 books = Book.tagged_top_level(tags)
143 return books if books else rc.NOT_FOUND
145 books = Book.tagged.with_all(tags)
147 books = Book.objects.all()
155 def media(self, book):
156 """ Returns all media for a book. """
158 return book.media.all()
161 # add categorized tags fields for Book
162 def _tags_getter(category):
164 def get_tags(cls, book):
165 return book.tags.filter(category=category)
167 for plural, singular in category_singular.items():
168 setattr(BooksHandler, plural, _tags_getter(singular))
170 # add fields for files in Book
171 def _file_getter(format):
172 field = "%s_file" % format
174 def get_file(cls, book):
175 f = getattr(book, field)
177 return MEDIA_BASE + f.url
181 for format in ('xml', 'txt', 'html', 'epub', 'pdf'):
182 setattr(BooksHandler, format, _file_getter(format))
185 class TagDetailHandler(BaseHandler):
186 """ Responsible for details of a single Tag object. """
188 fields = ['name', 'sort_key', 'description']
190 def read(self, request, category, slug):
191 """ Returns details of a tag, identified by category and slug. """
194 category_sng = category_singular[category]
199 return Tag.objects.get(category=category_sng, slug=slug)
200 except Tag.DoesNotExist:
204 class TagsHandler(BaseHandler):
205 """ Main handler for Tag objects.
207 Responsible for lists of Tag objects
208 and fields used for representing Tags.
211 allowed_methods = ('GET',)
213 fields = ['name', 'href']
215 def read(self, request, category):
216 """ Lists all tags in the category (eg. all themes). """
219 category_sng = category_singular[category]
223 tags = Tag.objects.filter(category=category_sng)
224 tags = [t for t in tags if t.get_count() > 0]
233 """ Returns URI in the API for the tag. """
235 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
238 class FragmentDetailHandler(BaseHandler):
239 fields = ['book', 'anchor', 'text', 'url', 'themes']
241 def read(self, request, slug, anchor):
242 """ Returns details of a fragment, identified by book slug and anchor. """
245 return Fragment.objects.get(book__slug=slug, anchor=anchor)
246 except Fragment.DoesNotExist:
250 class FragmentsHandler(BaseHandler):
251 """ Main handler for Fragments.
253 Responsible for lists of Fragment objects
254 and fields used for representing Fragments.
258 fields = ['book', 'anchor', 'href']
259 allowed_methods = ('GET',)
261 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
263 def read(self, request, tags):
264 """ Lists all fragments with given book, tags, themes.
266 :param tags: should be a path of categories and slugs, i.e.:
267 books/book-slug/authors/an-author/themes/a-theme/
270 tags = read_tags(tags, allowed=self.categories)
271 fragments = Fragment.tagged.with_all(tags).select_related('book')
272 if fragments.exists():
278 def href(cls, fragment):
279 """ Returns URI in the API for the fragment. """
281 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
284 def url(cls, fragment):
285 """ Returns URL on the site for the fragment. """
287 return WL_BASE + fragment.get_absolute_url()
290 def themes(cls, fragment):
291 """ Returns a list of theme tags for the fragment. """
293 return fragment.tags.filter(category='theme')
300 class CatalogueHandler(BaseHandler):
303 def fields(request, name):
304 fields_str = request.GET.get(name) if request is not None else None
305 return fields_str.split(',') if fields_str is not None else None
309 """ Returns time suitable for use as upper time boundary for check.
311 Used to avoid issues with time between setting the change stamp
312 and actually saving the model in database.
313 Cuts the microsecond part to avoid issues with DBs where time has
316 :param datetime t: manually sets the upper boundary
319 # set to five minutes ago, to avoid concurrency issues
321 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
322 # set to whole second in case DB supports something smaller
323 return t.replace(microsecond=0)
326 def book_dict(book, fields=None):
327 all_fields = ('url', 'title', 'description',
328 'gazeta_link', 'wiki_link',
329 'xml', 'epub', 'txt', 'pdf', 'html',
330 'mp3', 'ogg', 'daisy',
331 'parent', 'parent_number',
333 'license', 'license_description', 'source_name',
334 'technical_editors', 'editors',
335 'author', 'sort_key',
338 fields = (f for f in fields if f in all_fields)
342 extra_info = book.get_extra_info_value()
347 if field in ('xml', 'epub', 'txt', 'pdf', 'html'):
348 f = getattr(book, field+'_file')
355 elif field in ('mp3', 'ogg', 'daisy'):
357 for m in book.media.filter(type=field):
366 obj[field] = book.get_absolute_url()
368 elif field == 'tags':
369 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
371 elif field == 'author':
372 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
374 elif field == 'parent':
375 obj[field] = book.parent_id
377 elif field in ('license', 'license_description', 'source_name',
378 'technical_editors', 'editors'):
379 f = extra_info.get(field)
384 f = getattr(book, field)
392 def book_changes(cls, request=None, since=0, until=None, fields=None):
393 since = datetime.fromtimestamp(int(since))
394 until = cls.until(until)
397 'time_checked': timestamp(until)
401 fields = cls.fields(request, 'book_fields')
408 for book in Book.objects.filter(changed_at__gte=since,
409 changed_at__lt=until):
410 book_d = cls.book_dict(book, fields)
411 updated.append(book_d)
413 changes['updated'] = updated
415 for book in Deleted.objects.filter(content_type=Book,
416 deleted_at__gte=since,
417 deleted_at__lt=until,
418 created_at__lt=since):
419 deleted.append(book.id)
421 changes['deleted'] = deleted
426 def tag_dict(tag, fields=None):
427 all_fields = ('name', 'category', 'sort_key', 'description',
428 'gazeta_link', 'wiki_link',
433 fields = (f for f in fields if f in all_fields)
441 obj[field] = tag.get_absolute_url()
443 elif field == 'books':
444 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
446 elif field == 'sort_key':
447 obj[field] = tag.sort_key
450 f = getattr(tag, field)
458 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
459 since = datetime.fromtimestamp(int(since))
460 until = cls.until(until)
463 'time_checked': timestamp(until)
467 fields = cls.fields(request, 'tag_fields')
469 categories = cls.fields(request, 'tag_categories')
471 all_categories = ('author', 'epoch', 'kind', 'genre')
473 categories = (c for c in categories if c in all_categories)
475 categories = all_categories
480 for tag in Tag.objects.filter(category__in=categories,
481 changed_at__gte=since,
482 changed_at__lt=until):
483 # only serve non-empty tags
485 tag_d = cls.tag_dict(tag, fields)
486 updated.append(tag_d)
487 elif tag.created_at < since:
488 deleted.append(tag.id)
490 changes['updated'] = updated
492 for tag in Deleted.objects.filter(category__in=categories,
494 deleted_at__gte=since,
495 deleted_at__lt=until,
496 created_at__lt=since):
497 deleted.append(tag.id)
499 changes['deleted'] = deleted
504 def changes(cls, request=None, since=0, until=None, book_fields=None,
505 tag_fields=None, tag_categories=None):
506 until = cls.until(until)
509 'time_checked': timestamp(until)
513 'books': cls.book_changes(request, since, until, book_fields),
514 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
517 for model in changes_by_type:
518 for field in changes_by_type[model]:
519 if field == 'time_checked':
521 changes.setdefault(field, {})[model] = changes_by_type[model][field]
525 class BookChangesHandler(CatalogueHandler):
526 allowed_methods = ('GET',)
528 def read(self, request, since):
529 return self.book_changes(request, since)
532 class TagChangesHandler(CatalogueHandler):
533 allowed_methods = ('GET',)
535 def read(self, request, since):
536 return self.tag_changes(request, since)
539 class ChangesHandler(CatalogueHandler):
540 allowed_methods = ('GET',)
542 def read(self, request, since):
543 return self.changes(request, since)