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.cache import get_cache
11 from django.core.urlresolvers import reverse
12 from piston.handler import AnonymousBaseHandler, BaseHandler
13 from piston.utils import rc
15 from api.helpers import timestamp
16 from api.models import Deleted
17 from catalogue.forms import BookImportForm
18 from catalogue.models import Book, Tag, BookMedia, Fragment
19 from picture.models import Picture
20 from picture.forms import PictureImportForm
22 from stats.utils import piwik_track
24 API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain
36 for k, v in category_singular.items():
37 category_plural[v] = k
40 def read_tags(tags, allowed):
41 """ Reads a path of filtering tags.
43 :param str tags: a path of category and slug pairs, like: authors/an-author/...
44 :returns: list of Tag objects
45 :raises: ValueError when tags can't be found
50 tags = tags.strip('/').split('/')
53 category = tags.pop(0)
57 category = category_singular[category]
59 raise ValueError('Unknown category.')
61 if not category in allowed:
62 raise ValueError('Category not allowed.')
65 if category == 'book':
69 real_tags.append(Tag.objects.get(category=category, slug=slug))
70 except Tag.DoesNotExist:
71 raise ValueError('Tag not found')
78 class BookMediaHandler(BaseHandler):
79 """ Responsible for representing media in Books. """
82 fields = ['name', 'type', 'url']
86 """ Link to media on site. """
88 return MEDIA_BASE + media.file.url
91 class BookDetailHandler(BaseHandler):
92 """ Main handler for Book objects.
94 Responsible for lists of Book objects
95 and fields used for representing Books.
98 allowed_methods = ['GET']
99 fields = ['title', 'parent'] + Book.formats + [
100 'media', 'url'] + category_singular.keys()
103 def read(self, request, slug):
104 """ Returns details of a book, identified by a slug and lang. """
106 return Book.objects.get(slug=slug)
107 except Book.DoesNotExist:
111 class AnonymousBooksHandler(AnonymousBaseHandler):
112 """ Main handler for Book objects.
114 Responsible for lists of Book objects
115 and fields used for representing Books.
118 allowed_methods = ('GET',)
120 fields = ['href', 'title']
122 categories = set(['author', 'epoch', 'kind', 'genre'])
126 """ Returns an URI for a Book in the API. """
127 return API_BASE + reverse("api_book", args=[book.slug])
131 """ Returns Book's URL on the site. """
133 return WL_BASE + book.get_absolute_url()
136 def read(self, request, tags, top_level=False):
137 """ Lists all books with given tags.
139 :param tags: filtering tags; should be a path of categories
140 and slugs, i.e.: authors/an-author/epoch/an-epoch/
141 :param top_level: if True and a book is included in the results,
142 it's children are aren't. By default all books matching the tags
145 tags = read_tags(tags, allowed=self.categories)
148 books = Book.tagged_top_level(tags)
149 return books if books else rc.NOT_FOUND
151 books = Book.tagged.with_all(tags)
153 books = Book.objects.all()
160 def create(self, request, tags, top_level=False):
164 def media(self, book):
165 """ Returns all media for a book. """
167 return book.media.all()
170 class BooksHandler(BaseHandler):
172 fields = ('slug', 'title')
173 anonymous = AnonymousBooksHandler
175 def create(self, request, tags, top_level=False):
176 if not request.user.has_perm('catalogue.add_book'):
179 data = json.loads(request.POST.get('data'))
180 form = BookImportForm(data)
187 # add categorized tags fields for Book
188 def _tags_getter(category):
190 def get_tags(cls, book):
191 return book.tags.filter(category=category)
193 for plural, singular in category_singular.items():
194 setattr(BooksHandler, plural, _tags_getter(singular))
196 # add fields for files in Book
197 def _file_getter(format):
198 field = "%s_file" % format
200 def get_file(cls, book):
201 f = getattr(book, field)
203 return MEDIA_BASE + f.url
207 for format in Book.formats:
208 setattr(BooksHandler, format, _file_getter(format))
211 class TagDetailHandler(BaseHandler):
212 """ Responsible for details of a single Tag object. """
214 fields = ['name', 'sort_key', 'description']
217 def read(self, request, category, slug):
218 """ Returns details of a tag, identified by category and slug. """
221 category_sng = category_singular[category]
226 return Tag.objects.get(category=category_sng, slug=slug)
227 except Tag.DoesNotExist:
231 class TagsHandler(BaseHandler):
232 """ Main handler for Tag objects.
234 Responsible for lists of Tag objects
235 and fields used for representing Tags.
238 allowed_methods = ('GET',)
240 fields = ['name', 'href']
243 def read(self, request, category):
244 """ Lists all tags in the category (eg. all themes). """
247 category_sng = category_singular[category]
251 tags = Tag.objects.filter(category=category_sng).exclude(book_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. """
272 return Fragment.objects.get(book__slug=slug, anchor=anchor)
273 except Fragment.DoesNotExist:
277 class FragmentsHandler(BaseHandler):
278 """ Main handler for Fragments.
280 Responsible for lists of Fragment objects
281 and fields used for representing Fragments.
285 fields = ['book', 'anchor', 'href']
286 allowed_methods = ('GET',)
288 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
291 def read(self, request, tags):
292 """ Lists all fragments with given book, tags, themes.
294 :param tags: should be a path of categories and slugs, i.e.:
295 books/book-slug/authors/an-author/themes/a-theme/
298 tags = read_tags(tags, allowed=self.categories)
299 fragments = Fragment.tagged.with_all(tags).select_related('book')
300 if fragments.exists():
306 def href(cls, fragment):
307 """ Returns URI in the API for the fragment. """
309 return API_BASE + reverse("api_fragment",
310 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 ] + Book.formats + BookMedia.formats + [
359 'parent', 'parent_number',
361 'license', 'license_description', 'source_name',
362 'technical_editors', 'editors',
363 'author', 'sort_key',
366 fields = (f for f in fields if f in all_fields)
370 extra_info = book.get_extra_info_value()
375 if field in Book.formats:
376 f = getattr(book, field+'_file')
383 elif field in BookMedia.formats:
385 for m in book.media.filter(type=field):
394 obj[field] = book.get_absolute_url()
396 elif field == 'tags':
397 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
399 elif field == 'author':
400 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
402 elif field == 'parent':
403 obj[field] = book.parent_id
405 elif field in ('license', 'license_description', 'source_name',
406 'technical_editors', 'editors'):
407 f = extra_info.get(field)
412 f = getattr(book, field)
420 def book_changes(cls, request=None, since=0, until=None, fields=None):
421 since = datetime.fromtimestamp(int(since))
422 until = cls.until(until)
425 'time_checked': timestamp(until)
429 fields = cls.fields(request, 'book_fields')
436 for book in Book.objects.filter(changed_at__gte=since,
437 changed_at__lt=until):
438 book_d = cls.book_dict(book, fields)
439 updated.append(book_d)
441 changes['updated'] = updated
443 for book in Deleted.objects.filter(content_type=Book,
444 deleted_at__gte=since,
445 deleted_at__lt=until,
446 created_at__lt=since):
447 deleted.append(book.id)
449 changes['deleted'] = deleted
454 def tag_dict(tag, fields=None):
455 all_fields = ('name', 'category', 'sort_key', 'description',
456 'gazeta_link', 'wiki_link',
461 fields = (f for f in fields if f in all_fields)
469 obj[field] = tag.get_absolute_url()
471 elif field == 'books':
472 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
474 elif field == 'sort_key':
475 obj[field] = tag.sort_key
478 f = getattr(tag, field)
486 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
487 since = datetime.fromtimestamp(int(since))
488 until = cls.until(until)
491 'time_checked': timestamp(until)
495 fields = cls.fields(request, 'tag_fields')
497 categories = cls.fields(request, 'tag_categories')
499 all_categories = ('author', 'epoch', 'kind', 'genre')
501 categories = (c for c in categories if c in all_categories)
503 categories = all_categories
508 for tag in Tag.objects.filter(category__in=categories,
509 changed_at__gte=since,
510 changed_at__lt=until):
511 # only serve non-empty tags
513 tag_d = cls.tag_dict(tag, fields)
514 updated.append(tag_d)
515 elif tag.created_at < since:
516 deleted.append(tag.id)
518 changes['updated'] = updated
520 for tag in Deleted.objects.filter(category__in=categories,
522 deleted_at__gte=since,
523 deleted_at__lt=until,
524 created_at__lt=since):
525 deleted.append(tag.id)
527 changes['deleted'] = deleted
532 def changes(cls, request=None, since=0, until=None, book_fields=None,
533 tag_fields=None, tag_categories=None):
534 until = cls.until(until)
538 cache = get_cache('api')
539 key = hash((book_fields, tag_fields, tag_categories,
540 tuple(sorted(request.GET.items()))
542 value = cache.get(key)
543 if value is not None:
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]
562 cache.set(key, changes)
567 class BookChangesHandler(CatalogueHandler):
568 allowed_methods = ('GET',)
571 def read(self, request, since):
572 return self.book_changes(request, since)
575 class TagChangesHandler(CatalogueHandler):
576 allowed_methods = ('GET',)
579 def read(self, request, since):
580 return self.tag_changes(request, since)
583 class ChangesHandler(CatalogueHandler):
584 allowed_methods = ('GET',)
587 def read(self, request, since):
588 return self.changes(request, since)
591 class PictureHandler(BaseHandler):
593 fields = ('slug', 'title')
594 allowed_methods = ('POST',)
596 def create(self, request):
597 if not request.user.has_perm('picture.add_picture'):
600 data = json.loads(request.POST.get('data'))
601 form = PictureImportForm(data)