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'] + Book.file_types + [
97 'media', 'url'] + category_singular.keys()
100 def read(self, request, book):
101 """ Returns details of a book, identified by a slug and lang. """
102 kwargs = Book.split_urlid(book)
107 return Book.objects.get(**kwargs)
108 except Book.DoesNotExist:
112 class AnonymousBooksHandler(AnonymousBaseHandler):
113 """ Main handler for Book objects.
115 Responsible for lists of Book objects
116 and fields used for representing Books.
119 allowed_methods = ('GET',)
121 fields = ['href', 'title']
123 categories = set(['author', 'epoch', 'kind', 'genre'])
127 """ Returns an URI for a Book in the API. """
128 return API_BASE + reverse("api_book", args=[book.urlid()])
132 """ Returns Book's URL on the site. """
134 return WL_BASE + book.get_absolute_url()
137 def read(self, request, tags, top_level=False):
138 """ Lists all books with given tags.
140 :param tags: filtering tags; should be a path of categories
141 and slugs, i.e.: authors/an-author/epoch/an-epoch/
142 :param top_level: if True and a book is included in the results,
143 it's children are aren't. By default all books matching the tags
146 tags = read_tags(tags, allowed=self.categories)
149 books = Book.tagged_top_level(tags)
150 return books if books else rc.NOT_FOUND
152 books = Book.tagged.with_all(tags)
154 books = Book.objects.all()
161 def create(self, request, tags, top_level=False):
165 def media(self, book):
166 """ Returns all media for a book. """
168 return book.media.all()
171 class BooksHandler(BaseHandler):
173 fields = ('slug', 'title')
174 anonymous = AnonymousBooksHandler
176 def create(self, request, tags, top_level=False):
177 if not request.user.has_perm('catalogue.add_book'):
180 data = json.loads(request.POST.get('data'))
181 form = BookImportForm(data)
188 # add categorized tags fields for Book
189 def _tags_getter(category):
191 def get_tags(cls, book):
192 return book.tags.filter(category=category)
194 for plural, singular in category_singular.items():
195 setattr(BooksHandler, plural, _tags_getter(singular))
197 # add fields for files in Book
198 def _file_getter(format):
199 field = "%s_file" % format
201 def get_file(cls, book):
202 f = getattr(book, field)
204 return MEDIA_BASE + f.url
208 for format in Book.file_types:
209 setattr(BooksHandler, format, _file_getter(format))
212 class TagDetailHandler(BaseHandler):
213 """ Responsible for details of a single Tag object. """
215 fields = ['name', 'sort_key', 'description']
218 def read(self, request, category, slug):
219 """ Returns details of a tag, identified by category and slug. """
222 category_sng = category_singular[category]
227 return Tag.objects.get(category=category_sng, slug=slug)
228 except Tag.DoesNotExist:
232 class TagsHandler(BaseHandler):
233 """ Main handler for Tag objects.
235 Responsible for lists of Tag objects
236 and fields used for representing Tags.
239 allowed_methods = ('GET',)
241 fields = ['name', 'href']
244 def read(self, request, category):
245 """ Lists all tags in the category (eg. all themes). """
248 category_sng = category_singular[category]
252 tags = Tag.objects.filter(category=category_sng)
253 tags = [t for t in tags if t.get_count() > 0]
262 """ Returns URI in the API for the tag. """
264 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
267 class FragmentDetailHandler(BaseHandler):
268 fields = ['book', 'anchor', 'text', 'url', 'themes']
271 def read(self, request, book, anchor):
272 """ Returns details of a fragment, identified by book slug and anchor. """
273 kwargs = Book.split_urlid(book)
278 for field, value in kwargs.items():
279 fragment_kwargs['book__' + field] = value
282 return Fragment.objects.get(anchor=anchor, **fragment_kwargs)
283 except Fragment.DoesNotExist:
287 class FragmentsHandler(BaseHandler):
288 """ Main handler for Fragments.
290 Responsible for lists of Fragment objects
291 and fields used for representing Fragments.
295 fields = ['book', 'anchor', 'href']
296 allowed_methods = ('GET',)
298 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
301 def read(self, request, tags):
302 """ Lists all fragments with given book, tags, themes.
304 :param tags: should be a path of categories and slugs, i.e.:
305 books/book-slug/authors/an-author/themes/a-theme/
308 tags = read_tags(tags, allowed=self.categories)
309 fragments = Fragment.tagged.with_all(tags).select_related('book')
310 if fragments.exists():
316 def href(cls, fragment):
317 """ Returns URI in the API for the fragment. """
319 return API_BASE + reverse("api_fragment", args=[fragment.book.urlid(), fragment.anchor])
322 def url(cls, fragment):
323 """ Returns URL on the site for the fragment. """
325 return WL_BASE + fragment.get_absolute_url()
328 def themes(cls, fragment):
329 """ Returns a list of theme tags for the fragment. """
331 return fragment.tags.filter(category='theme')
338 class CatalogueHandler(BaseHandler):
341 def fields(request, name):
342 fields_str = request.GET.get(name) if request is not None else None
343 return fields_str.split(',') if fields_str is not None else None
347 """ Returns time suitable for use as upper time boundary for check.
349 Used to avoid issues with time between setting the change stamp
350 and actually saving the model in database.
351 Cuts the microsecond part to avoid issues with DBs where time has
354 :param datetime t: manually sets the upper boundary
357 # set to five minutes ago, to avoid concurrency issues
359 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
360 # set to whole second in case DB supports something smaller
361 return t.replace(microsecond=0)
364 def book_dict(book, fields=None):
365 all_fields = ['url', 'title', 'description',
366 'gazeta_link', 'wiki_link',
367 ] + Book.file_types + [
368 'mp3', 'ogg', 'daisy',
369 'parent', 'parent_number',
371 'license', 'license_description', 'source_name',
372 'technical_editors', 'editors',
373 'author', 'sort_key',
376 fields = (f for f in fields if f in all_fields)
380 extra_info = book.get_extra_info_value()
385 if field in Book.file_types:
386 f = getattr(book, field+'_file')
393 elif field in ('mp3', 'ogg', 'daisy'):
395 for m in book.media.filter(type=field):
404 obj[field] = book.get_absolute_url()
406 elif field == 'tags':
407 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
409 elif field == 'author':
410 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
412 elif field == 'parent':
413 obj[field] = book.parent_id
415 elif field in ('license', 'license_description', 'source_name',
416 'technical_editors', 'editors'):
417 f = extra_info.get(field)
422 f = getattr(book, field)
430 def book_changes(cls, request=None, since=0, until=None, fields=None):
431 since = datetime.fromtimestamp(int(since))
432 until = cls.until(until)
435 'time_checked': timestamp(until)
439 fields = cls.fields(request, 'book_fields')
446 for book in Book.objects.filter(changed_at__gte=since,
447 changed_at__lt=until):
448 book_d = cls.book_dict(book, fields)
449 updated.append(book_d)
451 changes['updated'] = updated
453 for book in Deleted.objects.filter(content_type=Book,
454 deleted_at__gte=since,
455 deleted_at__lt=until,
456 created_at__lt=since):
457 deleted.append(book.id)
459 changes['deleted'] = deleted
464 def tag_dict(tag, fields=None):
465 all_fields = ('name', 'category', 'sort_key', 'description',
466 'gazeta_link', 'wiki_link',
471 fields = (f for f in fields if f in all_fields)
479 obj[field] = tag.get_absolute_url()
481 elif field == 'books':
482 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
484 elif field == 'sort_key':
485 obj[field] = tag.sort_key
488 f = getattr(tag, field)
496 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
497 since = datetime.fromtimestamp(int(since))
498 until = cls.until(until)
501 'time_checked': timestamp(until)
505 fields = cls.fields(request, 'tag_fields')
507 categories = cls.fields(request, 'tag_categories')
509 all_categories = ('author', 'epoch', 'kind', 'genre')
511 categories = (c for c in categories if c in all_categories)
513 categories = all_categories
518 for tag in Tag.objects.filter(category__in=categories,
519 changed_at__gte=since,
520 changed_at__lt=until):
521 # only serve non-empty tags
523 tag_d = cls.tag_dict(tag, fields)
524 updated.append(tag_d)
525 elif tag.created_at < since:
526 deleted.append(tag.id)
528 changes['updated'] = updated
530 for tag in Deleted.objects.filter(category__in=categories,
532 deleted_at__gte=since,
533 deleted_at__lt=until,
534 created_at__lt=since):
535 deleted.append(tag.id)
537 changes['deleted'] = deleted
542 def changes(cls, request=None, since=0, until=None, book_fields=None,
543 tag_fields=None, tag_categories=None):
544 until = cls.until(until)
547 'time_checked': timestamp(until)
551 'books': cls.book_changes(request, since, until, book_fields),
552 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
555 for model in changes_by_type:
556 for field in changes_by_type[model]:
557 if field == 'time_checked':
559 changes.setdefault(field, {})[model] = changes_by_type[model][field]
563 class BookChangesHandler(CatalogueHandler):
564 allowed_methods = ('GET',)
567 def read(self, request, since):
568 return self.book_changes(request, since)
571 class TagChangesHandler(CatalogueHandler):
572 allowed_methods = ('GET',)
575 def read(self, request, since):
576 return self.tag_changes(request, since)
579 class ChangesHandler(CatalogueHandler):
580 allowed_methods = ('GET',)
583 def read(self, request, since):
584 return self.changes(request, since)