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
19 from stats.utils import piwik_track
21 API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain
33 for k, v in category_singular.items():
34 category_plural[v] = k
37 def read_tags(tags, allowed):
38 """ Reads a path of filtering tags.
40 :param str tags: a path of category and slug pairs, like: authors/an-author/...
41 :returns: list of Tag objects
42 :raises: ValueError when tags can't be found
47 tags = tags.strip('/').split('/')
50 category = tags.pop(0)
54 category = category_singular[category]
56 raise ValueError('Unknown category.')
58 if not category in allowed:
59 raise ValueError('Category not allowed.')
62 if category == 'book':
66 real_tags.append(Tag.objects.get(category=category, slug=slug))
67 except Tag.DoesNotExist:
68 raise ValueError('Tag not found')
75 class BookMediaHandler(BaseHandler):
76 """ Responsible for representing media in Books. """
79 fields = ['name', 'type', 'url']
83 """ Link to media on site. """
85 return MEDIA_BASE + media.file.url
88 class BookDetailHandler(BaseHandler):
89 """ Main handler for Book objects.
91 Responsible for lists of Book objects
92 and fields used for representing Books.
95 allowed_methods = ['GET']
96 fields = ['title', 'parent',
97 'xml', 'html', 'pdf', 'epub', 'txt',
98 'media', 'url'] + category_singular.keys()
101 def read(self, request, slug):
102 """ Returns details of a book, identified by a slug. """
105 return Book.objects.get(slug=slug)
106 except Book.DoesNotExist:
110 class AnonymousBooksHandler(AnonymousBaseHandler):
111 """ Main handler for Book objects.
113 Responsible for lists of Book objects
114 and fields used for representing Books.
117 allowed_methods = ('GET',)
119 fields = ['href', 'title']
121 categories = set(['author', 'epoch', 'kind', 'genre'])
125 """ Returns an URI for a Book in the API. """
126 return API_BASE + reverse("api_book", args=[book.slug])
130 """ Returns Book's URL on the site. """
132 return WL_BASE + book.get_absolute_url()
135 def read(self, request, tags, top_level=False):
136 """ Lists all books with given tags.
138 :param tags: filtering tags; should be a path of categories
139 and slugs, i.e.: authors/an-author/epoch/an-epoch/
140 :param top_level: if True and a book is included in the results,
141 it's children are aren't. By default all books matching the tags
144 tags = read_tags(tags, allowed=self.categories)
147 books = Book.tagged_top_level(tags)
148 return books if books else rc.NOT_FOUND
150 books = Book.tagged.with_all(tags)
152 books = Book.objects.all()
159 def create(self, request, tags, top_level=False):
163 def media(self, book):
164 """ Returns all media for a book. """
166 return book.media.all()
169 class BooksHandler(BaseHandler):
171 fields = ('slug', 'title')
172 anonymous = AnonymousBooksHandler
174 def create(self, request, tags, top_level=False):
175 if not request.user.has_perm('catalogue.add_book'):
178 data = json.loads(request.POST.get('data'))
179 form = BookImportForm(data)
186 # add categorized tags fields for Book
187 def _tags_getter(category):
189 def get_tags(cls, book):
190 return book.tags.filter(category=category)
192 for plural, singular in category_singular.items():
193 setattr(BooksHandler, plural, _tags_getter(singular))
195 # add fields for files in Book
196 def _file_getter(format):
197 field = "%s_file" % format
199 def get_file(cls, book):
200 f = getattr(book, field)
202 return MEDIA_BASE + f.url
206 for format in ('xml', 'txt', 'html', 'epub', 'pdf'):
207 setattr(BooksHandler, format, _file_getter(format))
210 class TagDetailHandler(BaseHandler):
211 """ Responsible for details of a single Tag object. """
213 fields = ['name', 'sort_key', 'description']
216 def read(self, request, category, slug):
217 """ Returns details of a tag, identified by category and slug. """
220 category_sng = category_singular[category]
225 return Tag.objects.get(category=category_sng, slug=slug)
226 except Tag.DoesNotExist:
230 class TagsHandler(BaseHandler):
231 """ Main handler for Tag objects.
233 Responsible for lists of Tag objects
234 and fields used for representing Tags.
237 allowed_methods = ('GET',)
239 fields = ['name', 'href']
242 def read(self, request, category):
243 """ Lists all tags in the category (eg. all themes). """
246 category_sng = category_singular[category]
250 tags = Tag.objects.filter(category=category_sng)
251 tags = [t for t in tags if t.get_count() > 0]
260 """ Returns URI in the API for the tag. """
262 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
265 class FragmentDetailHandler(BaseHandler):
266 fields = ['book', 'anchor', 'text', 'url', 'themes']
269 def read(self, request, slug, anchor):
270 """ Returns details of a fragment, identified by book slug and anchor. """
273 return Fragment.objects.get(book__slug=slug, anchor=anchor)
274 except Fragment.DoesNotExist:
278 class FragmentsHandler(BaseHandler):
279 """ Main handler for Fragments.
281 Responsible for lists of Fragment objects
282 and fields used for representing Fragments.
286 fields = ['book', 'anchor', 'href']
287 allowed_methods = ('GET',)
289 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
292 def read(self, request, tags):
293 """ Lists all fragments with given book, tags, themes.
295 :param tags: should be a path of categories and slugs, i.e.:
296 books/book-slug/authors/an-author/themes/a-theme/
299 tags = read_tags(tags, allowed=self.categories)
300 fragments = Fragment.tagged.with_all(tags).select_related('book')
301 if fragments.exists():
307 def href(cls, fragment):
308 """ Returns URI in the API for the fragment. """
310 return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor])
313 def url(cls, fragment):
314 """ Returns URL on the site for the fragment. """
316 return WL_BASE + fragment.get_absolute_url()
319 def themes(cls, fragment):
320 """ Returns a list of theme tags for the fragment. """
322 return fragment.tags.filter(category='theme')
329 class CatalogueHandler(BaseHandler):
332 def fields(request, name):
333 fields_str = request.GET.get(name) if request is not None else None
334 return fields_str.split(',') if fields_str is not None else None
338 """ Returns time suitable for use as upper time boundary for check.
340 Used to avoid issues with time between setting the change stamp
341 and actually saving the model in database.
342 Cuts the microsecond part to avoid issues with DBs where time has
345 :param datetime t: manually sets the upper boundary
348 # set to five minutes ago, to avoid concurrency issues
350 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
351 # set to whole second in case DB supports something smaller
352 return t.replace(microsecond=0)
355 def book_dict(book, fields=None):
356 all_fields = ('url', 'title', 'description',
357 'gazeta_link', 'wiki_link',
358 'xml', 'epub', 'txt', 'pdf', 'html',
359 'mp3', 'ogg', 'daisy',
360 'parent', 'parent_number',
362 'license', 'license_description', 'source_name',
363 'technical_editors', 'editors',
364 'author', 'sort_key',
367 fields = (f for f in fields if f in all_fields)
371 extra_info = book.get_extra_info_value()
376 if field in ('xml', 'epub', 'txt', 'pdf', 'html'):
377 f = getattr(book, field+'_file')
384 elif field in ('mp3', 'ogg', 'daisy'):
386 for m in book.media.filter(type=field):
395 obj[field] = book.get_absolute_url()
397 elif field == 'tags':
398 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
400 elif field == 'author':
401 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
403 elif field == 'parent':
404 obj[field] = book.parent_id
406 elif field in ('license', 'license_description', 'source_name',
407 'technical_editors', 'editors'):
408 f = extra_info.get(field)
413 f = getattr(book, field)
421 def book_changes(cls, request=None, since=0, until=None, fields=None):
422 since = datetime.fromtimestamp(int(since))
423 until = cls.until(until)
426 'time_checked': timestamp(until)
430 fields = cls.fields(request, 'book_fields')
437 for book in Book.objects.filter(changed_at__gte=since,
438 changed_at__lt=until):
439 book_d = cls.book_dict(book, fields)
440 updated.append(book_d)
442 changes['updated'] = updated
444 for book in Deleted.objects.filter(content_type=Book,
445 deleted_at__gte=since,
446 deleted_at__lt=until,
447 created_at__lt=since):
448 deleted.append(book.id)
450 changes['deleted'] = deleted
455 def tag_dict(tag, fields=None):
456 all_fields = ('name', 'category', 'sort_key', 'description',
457 'gazeta_link', 'wiki_link',
462 fields = (f for f in fields if f in all_fields)
470 obj[field] = tag.get_absolute_url()
472 elif field == 'books':
473 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
475 elif field == 'sort_key':
476 obj[field] = tag.sort_key
479 f = getattr(tag, field)
487 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
488 since = datetime.fromtimestamp(int(since))
489 until = cls.until(until)
492 'time_checked': timestamp(until)
496 fields = cls.fields(request, 'tag_fields')
498 categories = cls.fields(request, 'tag_categories')
500 all_categories = ('author', 'epoch', 'kind', 'genre')
502 categories = (c for c in categories if c in all_categories)
504 categories = all_categories
509 for tag in Tag.objects.filter(category__in=categories,
510 changed_at__gte=since,
511 changed_at__lt=until):
512 # only serve non-empty tags
514 tag_d = cls.tag_dict(tag, fields)
515 updated.append(tag_d)
516 elif tag.created_at < since:
517 deleted.append(tag.id)
519 changes['updated'] = updated
521 for tag in Deleted.objects.filter(category__in=categories,
523 deleted_at__gte=since,
524 deleted_at__lt=until,
525 created_at__lt=since):
526 deleted.append(tag.id)
528 changes['deleted'] = deleted
533 def changes(cls, request=None, since=0, until=None, book_fields=None,
534 tag_fields=None, tag_categories=None):
535 until = cls.until(until)
538 'time_checked': timestamp(until)
542 'books': cls.book_changes(request, since, until, book_fields),
543 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
546 for model in changes_by_type:
547 for field in changes_by_type[model]:
548 if field == 'time_checked':
550 changes.setdefault(field, {})[model] = changes_by_type[model][field]
554 class BookChangesHandler(CatalogueHandler):
555 allowed_methods = ('GET',)
558 def read(self, request, since):
559 return self.book_changes(request, since)
562 class TagChangesHandler(CatalogueHandler):
563 allowed_methods = ('GET',)
566 def read(self, request, since):
567 return self.tag_changes(request, since)
570 class ChangesHandler(CatalogueHandler):
571 allowed_methods = ('GET',)
574 def read(self, request, since):
575 return self.changes(request, since)