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 return Book.tagged_top_level(tags)
140 return Book.tagged.with_all(tags)
142 return Book.objects.all()
145 # add categorized tags fields for Book
146 def _tags_getter(category):
148 def get_tags(cls, book):
149 return book.tags.filter(category=category)
151 for plural, singular in category_singular.items():
152 setattr(BooksHandler, plural, _tags_getter(singular))
154 # add fields for files in Book
155 def _file_getter(format):
156 field = "%s_file" % format
158 def get_file(cls, book):
159 f = getattr(book, field)
161 return MEDIA_BASE + f.url
165 for format in ('xml', 'txt', 'html', 'epub', 'pdf'):
166 setattr(BooksHandler, format, _file_getter(format))
169 class TagDetailHandler(BaseHandler):
170 """ Responsible for details of a single Tag object. """
172 fields = ['name', 'sort_key', 'description']
174 def read(self, request, category, slug):
175 """ Returns details of a tag, identified by category and slug. """
178 category_sng = category_singular[category]
182 return get_object_or_404(Tag, category=category_sng, slug=slug)
185 class TagsHandler(BaseHandler):
186 """ Main handler for Tag objects.
188 Responsible for lists of Tag objects
189 and fields used for representing Tags.
192 allowed_methods = ('GET',)
194 fields = ['name', 'href']
196 def read(self, request, category):
197 """ Lists all tags in the category (eg. all themes). """
200 category_sng = category_singular[category]
204 return Tag.objects.filter(category=category_sng)
208 """ Returns URI in the API for the tag. """
210 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
213 class FragmentDetailHandler(BaseHandler):
214 fields = ['book', 'anchor', 'text', 'url', 'themes']
216 def read(self, request, slug, anchor):
217 """ Returns details of a fragment, identified by book slug and anchor. """
219 return get_object_or_404(Fragment, book__slug=slug, anchor=anchor)
222 class FragmentsHandler(BaseHandler):
223 """ Main handler for Fragments.
225 Responsible for lists of Fragment objects
226 and fields used for representing Fragments.
230 fields = ['book', 'anchor', 'href']
231 allowed_methods = ('GET',)
233 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
235 def read(self, request, tags):
236 """ Lists all fragments with given book, tags, themes.
238 :param tags: should be a path of categories and slugs, i.e.:
239 books/book-slug/authors/an-author/themes/a-theme/
242 tags = read_tags(tags, allowed=self.categories)
243 return Fragment.tagged.with_all(tags).select_related('book')
246 def href(cls, fragment):
247 """ Returns URI in the API for the fragment. """
249 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
252 def url(cls, fragment):
253 """ Returns URL on the site for the fragment. """
255 return WL_BASE + fragment.get_absolute_url()
258 def themes(cls, fragment):
259 """ Returns a list of theme tags for the fragment. """
261 return fragment.tags.filter(category='theme')
268 class CatalogueHandler(BaseHandler):
271 def fields(request, name):
272 fields_str = request.GET.get(name) if request is not None else None
273 return fields_str.split(',') if fields_str is not None else None
277 """ Returns time suitable for use as upper time boundary for check.
279 Used to avoid issues with time between setting the change stamp
280 and actually saving the model in database.
281 Cuts the microsecond part to avoid issues with DBs where time has
284 :param datetime t: manually sets the upper boundary
287 # set to five minutes ago, to avoid concurrency issues
289 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
290 # set to whole second in case DB supports something smaller
291 return t.replace(microsecond=0)
294 def book_dict(book, fields=None):
295 all_fields = ('url', 'title', 'description',
296 'gazeta_link', 'wiki_link',
297 'xml', 'epub', 'txt', 'pdf', 'html',
298 'mp3', 'ogg', 'daisy',
299 'parent', 'parent_number',
301 'license', 'license_description', 'source_name',
302 'technical_editors', 'editors',
303 'author', 'sort_key',
306 fields = (f for f in fields if f in all_fields)
310 extra_info = book.get_extra_info_value()
315 if field in ('xml', 'epub', 'txt', 'pdf', 'html'):
316 f = getattr(book, field+'_file')
323 elif field in ('mp3', 'ogg', 'daisy'):
325 for m in book.media.filter(type=field):
334 obj[field] = book.get_absolute_url()
336 elif field == 'tags':
337 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
339 elif field == 'author':
340 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
342 elif field == 'parent':
343 obj[field] = book.parent_id
345 elif field in ('license', 'license_description', 'source_name',
346 'technical_editors', 'editors'):
347 f = extra_info.get(field)
352 f = getattr(book, field)
360 def book_changes(cls, request=None, since=0, until=None, fields=None):
361 since = datetime.fromtimestamp(int(since))
362 until = cls.until(until)
365 'time_checked': timestamp(until)
369 fields = cls.fields(request, 'book_fields')
376 for book in Book.objects.filter(changed_at__gte=since,
377 changed_at__lt=until):
378 book_d = cls.book_dict(book, fields)
379 updated.append(book_d)
381 changes['updated'] = updated
383 for book in Deleted.objects.filter(content_type=Book,
384 deleted_at__gte=since,
385 deleted_at__lt=until,
386 created_at__lt=since):
387 deleted.append(book.id)
389 changes['deleted'] = deleted
394 def tag_dict(tag, fields=None):
395 all_fields = ('name', 'category', 'sort_key', 'description',
396 'gazeta_link', 'wiki_link',
401 fields = (f for f in fields if f in all_fields)
409 obj[field] = tag.get_absolute_url()
411 elif field == 'books':
412 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
414 elif field == 'sort_key':
415 obj[field] = tag.sort_key
418 f = getattr(tag, field)
426 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
427 since = datetime.fromtimestamp(int(since))
428 until = cls.until(until)
431 'time_checked': timestamp(until)
435 fields = cls.fields(request, 'tag_fields')
437 categories = cls.fields(request, 'tag_categories')
439 all_categories = ('author', 'epoch', 'kind', 'genre')
441 categories = (c for c in categories if c in all_categories)
443 categories = all_categories
448 for tag in Tag.objects.filter(category__in=categories,
449 changed_at__gte=since,
450 changed_at__lt=until):
451 # only serve non-empty tags
453 tag_d = cls.tag_dict(tag, fields)
454 updated.append(tag_d)
455 elif tag.created_at < since:
456 deleted.append(tag.id)
458 changes['updated'] = updated
460 for tag in Deleted.objects.filter(category__in=categories,
462 deleted_at__gte=since,
463 deleted_at__lt=until,
464 created_at__lt=since):
465 deleted.append(tag.id)
467 changes['deleted'] = deleted
472 def changes(cls, request=None, since=0, until=None, book_fields=None,
473 tag_fields=None, tag_categories=None):
474 until = cls.until(until)
477 'time_checked': timestamp(until)
481 'books': cls.book_changes(request, since, until, book_fields),
482 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
485 for model in changes_by_type:
486 for field in changes_by_type[model]:
487 if field == 'time_checked':
489 changes.setdefault(field, {})[model] = changes_by_type[model][field]
493 class BookChangesHandler(CatalogueHandler):
494 allowed_methods = ('GET',)
496 def read(self, request, since):
497 return self.book_changes(request, since)
500 class TagChangesHandler(CatalogueHandler):
501 allowed_methods = ('GET',)
503 def read(self, request, since):
504 return self.tag_changes(request, since)
507 class ChangesHandler(CatalogueHandler):
508 allowed_methods = ('GET',)
510 def read(self, request, since):
511 return self.changes(request, since)