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
8 from django.conf import settings
9 from django.contrib.sites.models import Site
10 from django.core.urlresolvers import reverse
11 from piston.handler import AnonymousBaseHandler, BaseHandler
12 from piston.utils import rc
14 from api.helpers import timestamp
15 from api.models import Deleted
16 from catalogue.forms import BookImportForm
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: ValueError when tags can't be found
46 tags = tags.strip('/').split('/')
49 category = tags.pop(0)
53 category = category_singular[category]
55 raise ValueError('Unknown category.')
57 if not category in allowed:
58 raise ValueError('Category not allowed.')
61 if category == 'book':
65 real_tags.append(Tag.objects.get(category=category, slug=slug))
66 except Tag.DoesNotExist:
67 raise ValueError('Tag not found')
74 class BookMediaHandler(BaseHandler):
75 """ Responsible for representing media in Books. """
78 fields = ['name', 'type', 'url']
82 """ Link to media on site. """
84 return MEDIA_BASE + media.file.url
87 class BookDetailHandler(BaseHandler):
88 """ Main handler for Book objects.
90 Responsible for lists of Book objects
91 and fields used for representing Books.
94 allowed_methods = ['GET']
95 fields = ['title', 'parent',
96 'xml', 'html', 'pdf', 'epub', 'txt',
97 'media', 'url'] + category_singular.keys()
99 def read(self, request, slug):
100 """ Returns details of a book, identified by a slug. """
103 return Book.objects.get(slug=slug)
104 except Book.DoesNotExist:
108 class AnonymousBooksHandler(AnonymousBaseHandler):
109 """ Main handler for Book objects.
111 Responsible for lists of Book objects
112 and fields used for representing Books.
115 allowed_methods = ('GET',)
117 fields = ['href', 'title']
119 categories = set(['author', 'epoch', 'kind', 'genre'])
123 """ Returns an URI for a Book in the API. """
124 return API_BASE + reverse("api_book", args=[book.slug])
128 """ Returns Book's URL on the site. """
130 return WL_BASE + book.get_absolute_url()
132 def read(self, request, tags, top_level=False):
133 """ Lists all books with given tags.
135 :param tags: filtering tags; should be a path of categories
136 and slugs, i.e.: authors/an-author/epoch/an-epoch/
137 :param top_level: if True and a book is included in the results,
138 it's children are aren't. By default all books matching the tags
141 tags = read_tags(tags, allowed=self.categories)
144 books = Book.tagged_top_level(tags)
145 return books if books else rc.NOT_FOUND
147 books = Book.tagged.with_all(tags)
149 books = Book.objects.all()
156 def create(self, request, tags, top_level=False):
160 def media(self, book):
161 """ Returns all media for a book. """
163 return book.media.all()
166 class BooksHandler(BaseHandler):
168 fields = ('slug', 'title')
169 anonymous = AnonymousBooksHandler
171 def create(self, request, tags, top_level=False):
172 if not request.user.has_perm('catalogue.add_book'):
175 data = json.loads(request.POST.get('data'))
176 form = BookImportForm(data)
183 # add categorized tags fields for Book
184 def _tags_getter(category):
186 def get_tags(cls, book):
187 return book.tags.filter(category=category)
189 for plural, singular in category_singular.items():
190 setattr(BooksHandler, plural, _tags_getter(singular))
192 # add fields for files in Book
193 def _file_getter(format):
194 field = "%s_file" % format
196 def get_file(cls, book):
197 f = getattr(book, field)
199 return MEDIA_BASE + f.url
203 for format in ('xml', 'txt', 'html', 'epub', 'pdf'):
204 setattr(BooksHandler, format, _file_getter(format))
207 class TagDetailHandler(BaseHandler):
208 """ Responsible for details of a single Tag object. """
210 fields = ['name', 'sort_key', 'description']
212 def read(self, request, category, slug):
213 """ Returns details of a tag, identified by category and slug. """
216 category_sng = category_singular[category]
221 return Tag.objects.get(category=category_sng, slug=slug)
222 except Tag.DoesNotExist:
226 class TagsHandler(BaseHandler):
227 """ Main handler for Tag objects.
229 Responsible for lists of Tag objects
230 and fields used for representing Tags.
233 allowed_methods = ('GET',)
235 fields = ['name', 'href']
237 def read(self, request, category):
238 """ Lists all tags in the category (eg. all themes). """
241 category_sng = category_singular[category]
245 tags = Tag.objects.filter(category=category_sng)
246 tags = [t for t in tags if t.get_count() > 0]
255 """ Returns URI in the API for the tag. """
257 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
260 class FragmentDetailHandler(BaseHandler):
261 fields = ['book', 'anchor', 'text', 'url', 'themes']
263 def read(self, request, slug, anchor):
264 """ Returns details of a fragment, identified by book slug and anchor. """
267 return Fragment.objects.get(book__slug=slug, anchor=anchor)
268 except Fragment.DoesNotExist:
272 class FragmentsHandler(BaseHandler):
273 """ Main handler for Fragments.
275 Responsible for lists of Fragment objects
276 and fields used for representing Fragments.
280 fields = ['book', 'anchor', 'href']
281 allowed_methods = ('GET',)
283 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
285 def read(self, request, tags):
286 """ Lists all fragments with given book, tags, themes.
288 :param tags: should be a path of categories and slugs, i.e.:
289 books/book-slug/authors/an-author/themes/a-theme/
292 tags = read_tags(tags, allowed=self.categories)
293 fragments = Fragment.tagged.with_all(tags).select_related('book')
294 if fragments.exists():
300 def href(cls, fragment):
301 """ Returns URI in the API for the fragment. """
303 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
306 def url(cls, fragment):
307 """ Returns URL on the site for the fragment. """
309 return WL_BASE + fragment.get_absolute_url()
312 def themes(cls, fragment):
313 """ Returns a list of theme tags for the fragment. """
315 return fragment.tags.filter(category='theme')
322 class CatalogueHandler(BaseHandler):
325 def fields(request, name):
326 fields_str = request.GET.get(name) if request is not None else None
327 return fields_str.split(',') if fields_str is not None else None
331 """ Returns time suitable for use as upper time boundary for check.
333 Used to avoid issues with time between setting the change stamp
334 and actually saving the model in database.
335 Cuts the microsecond part to avoid issues with DBs where time has
338 :param datetime t: manually sets the upper boundary
341 # set to five minutes ago, to avoid concurrency issues
343 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
344 # set to whole second in case DB supports something smaller
345 return t.replace(microsecond=0)
348 def book_dict(book, fields=None):
349 all_fields = ('url', 'title', 'description',
350 'gazeta_link', 'wiki_link',
351 'xml', 'epub', 'txt', 'pdf', 'html',
352 'mp3', 'ogg', 'daisy',
353 'parent', 'parent_number',
355 'license', 'license_description', 'source_name',
356 'technical_editors', 'editors',
357 'author', 'sort_key',
360 fields = (f for f in fields if f in all_fields)
364 extra_info = book.get_extra_info_value()
369 if field in ('xml', 'epub', 'txt', 'pdf', 'html'):
370 f = getattr(book, field+'_file')
377 elif field in ('mp3', 'ogg', 'daisy'):
379 for m in book.media.filter(type=field):
388 obj[field] = book.get_absolute_url()
390 elif field == 'tags':
391 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
393 elif field == 'author':
394 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
396 elif field == 'parent':
397 obj[field] = book.parent_id
399 elif field in ('license', 'license_description', 'source_name',
400 'technical_editors', 'editors'):
401 f = extra_info.get(field)
406 f = getattr(book, field)
414 def book_changes(cls, request=None, since=0, until=None, fields=None):
415 since = datetime.fromtimestamp(int(since))
416 until = cls.until(until)
419 'time_checked': timestamp(until)
423 fields = cls.fields(request, 'book_fields')
430 for book in Book.objects.filter(changed_at__gte=since,
431 changed_at__lt=until):
432 book_d = cls.book_dict(book, fields)
433 updated.append(book_d)
435 changes['updated'] = updated
437 for book in Deleted.objects.filter(content_type=Book,
438 deleted_at__gte=since,
439 deleted_at__lt=until,
440 created_at__lt=since):
441 deleted.append(book.id)
443 changes['deleted'] = deleted
448 def tag_dict(tag, fields=None):
449 all_fields = ('name', 'category', 'sort_key', 'description',
450 'gazeta_link', 'wiki_link',
455 fields = (f for f in fields if f in all_fields)
463 obj[field] = tag.get_absolute_url()
465 elif field == 'books':
466 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
468 elif field == 'sort_key':
469 obj[field] = tag.sort_key
472 f = getattr(tag, field)
480 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
481 since = datetime.fromtimestamp(int(since))
482 until = cls.until(until)
485 'time_checked': timestamp(until)
489 fields = cls.fields(request, 'tag_fields')
491 categories = cls.fields(request, 'tag_categories')
493 all_categories = ('author', 'epoch', 'kind', 'genre')
495 categories = (c for c in categories if c in all_categories)
497 categories = all_categories
502 for tag in Tag.objects.filter(category__in=categories,
503 changed_at__gte=since,
504 changed_at__lt=until):
505 # only serve non-empty tags
507 tag_d = cls.tag_dict(tag, fields)
508 updated.append(tag_d)
509 elif tag.created_at < since:
510 deleted.append(tag.id)
512 changes['updated'] = updated
514 for tag in Deleted.objects.filter(category__in=categories,
516 deleted_at__gte=since,
517 deleted_at__lt=until,
518 created_at__lt=since):
519 deleted.append(tag.id)
521 changes['deleted'] = deleted
526 def changes(cls, request=None, since=0, until=None, book_fields=None,
527 tag_fields=None, tag_categories=None):
528 until = cls.until(until)
531 'time_checked': timestamp(until)
535 'books': cls.book_changes(request, since, until, book_fields),
536 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
539 for model in changes_by_type:
540 for field in changes_by_type[model]:
541 if field == 'time_checked':
543 changes.setdefault(field, {})[model] = changes_by_type[model][field]
547 class BookChangesHandler(CatalogueHandler):
548 allowed_methods = ('GET',)
550 def read(self, request, since):
551 return self.book_changes(request, since)
554 class TagChangesHandler(CatalogueHandler):
555 allowed_methods = ('GET',)
557 def read(self, request, since):
558 return self.tag_changes(request, since)
561 class ChangesHandler(CatalogueHandler):
562 allowed_methods = ('GET',)
564 def read(self, request, since):
565 return self.changes(request, since)