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.formats + [
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.formats:
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).exclude(book_count=0)
261 """ Returns URI in the API for the tag. """
263 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
266 class FragmentDetailHandler(BaseHandler):
267 fields = ['book', 'anchor', 'text', 'url', 'themes']
270 def read(self, request, book, anchor):
271 """ Returns details of a fragment, identified by book slug and anchor. """
272 kwargs = Book.split_urlid(book)
277 for field, value in kwargs.items():
278 fragment_kwargs['book__' + field] = value
281 return Fragment.objects.get(anchor=anchor, **fragment_kwargs)
282 except Fragment.DoesNotExist:
286 class FragmentsHandler(BaseHandler):
287 """ Main handler for Fragments.
289 Responsible for lists of Fragment objects
290 and fields used for representing Fragments.
294 fields = ['book', 'anchor', 'href']
295 allowed_methods = ('GET',)
297 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
300 def read(self, request, tags):
301 """ Lists all fragments with given book, tags, themes.
303 :param tags: should be a path of categories and slugs, i.e.:
304 books/book-slug/authors/an-author/themes/a-theme/
307 tags = read_tags(tags, allowed=self.categories)
308 fragments = Fragment.tagged.with_all(tags).select_related('book')
309 if fragments.exists():
315 def href(cls, fragment):
316 """ Returns URI in the API for the fragment. """
318 return API_BASE + reverse("api_fragment", args=[fragment.book.urlid(), fragment.anchor])
321 def url(cls, fragment):
322 """ Returns URL on the site for the fragment. """
324 return WL_BASE + fragment.get_absolute_url()
327 def themes(cls, fragment):
328 """ Returns a list of theme tags for the fragment. """
330 return fragment.tags.filter(category='theme')
337 class CatalogueHandler(BaseHandler):
340 def fields(request, name):
341 fields_str = request.GET.get(name) if request is not None else None
342 return fields_str.split(',') if fields_str is not None else None
346 """ Returns time suitable for use as upper time boundary for check.
348 Used to avoid issues with time between setting the change stamp
349 and actually saving the model in database.
350 Cuts the microsecond part to avoid issues with DBs where time has
353 :param datetime t: manually sets the upper boundary
356 # set to five minutes ago, to avoid concurrency issues
358 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
359 # set to whole second in case DB supports something smaller
360 return t.replace(microsecond=0)
363 def book_dict(book, fields=None):
364 all_fields = ['url', 'title', 'description',
365 'gazeta_link', 'wiki_link',
366 ] + Book.formats + BookMedia.formats + [
367 'parent', 'parent_number',
369 'license', 'license_description', 'source_name',
370 'technical_editors', 'editors',
371 'author', 'sort_key',
374 fields = (f for f in fields if f in all_fields)
378 extra_info = book.get_extra_info_value()
383 if field in Book.formats:
384 f = getattr(book, field+'_file')
391 elif field in BookMedia.formats:
393 for m in book.media.filter(type=field):
402 obj[field] = book.get_absolute_url()
404 elif field == 'tags':
405 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
407 elif field == 'author':
408 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
410 elif field == 'parent':
411 obj[field] = book.parent_id
413 elif field in ('license', 'license_description', 'source_name',
414 'technical_editors', 'editors'):
415 f = extra_info.get(field)
420 f = getattr(book, field)
428 def book_changes(cls, request=None, since=0, until=None, fields=None):
429 since = datetime.fromtimestamp(int(since))
430 until = cls.until(until)
433 'time_checked': timestamp(until)
437 fields = cls.fields(request, 'book_fields')
444 for book in Book.objects.filter(changed_at__gte=since,
445 changed_at__lt=until):
446 book_d = cls.book_dict(book, fields)
447 updated.append(book_d)
449 changes['updated'] = updated
451 for book in Deleted.objects.filter(content_type=Book,
452 deleted_at__gte=since,
453 deleted_at__lt=until,
454 created_at__lt=since):
455 deleted.append(book.id)
457 changes['deleted'] = deleted
462 def tag_dict(tag, fields=None):
463 all_fields = ('name', 'category', 'sort_key', 'description',
464 'gazeta_link', 'wiki_link',
469 fields = (f for f in fields if f in all_fields)
477 obj[field] = tag.get_absolute_url()
479 elif field == 'books':
480 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
482 elif field == 'sort_key':
483 obj[field] = tag.sort_key
486 f = getattr(tag, field)
494 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
495 since = datetime.fromtimestamp(int(since))
496 until = cls.until(until)
499 'time_checked': timestamp(until)
503 fields = cls.fields(request, 'tag_fields')
505 categories = cls.fields(request, 'tag_categories')
507 all_categories = ('author', 'epoch', 'kind', 'genre')
509 categories = (c for c in categories if c in all_categories)
511 categories = all_categories
516 for tag in Tag.objects.filter(category__in=categories,
517 changed_at__gte=since,
518 changed_at__lt=until):
519 # only serve non-empty tags
521 tag_d = cls.tag_dict(tag, fields)
522 updated.append(tag_d)
523 elif tag.created_at < since:
524 deleted.append(tag.id)
526 changes['updated'] = updated
528 for tag in Deleted.objects.filter(category__in=categories,
530 deleted_at__gte=since,
531 deleted_at__lt=until,
532 created_at__lt=since):
533 deleted.append(tag.id)
535 changes['deleted'] = deleted
540 def changes(cls, request=None, since=0, until=None, book_fields=None,
541 tag_fields=None, tag_categories=None):
542 until = cls.until(until)
545 'time_checked': timestamp(until)
549 'books': cls.book_changes(request, since, until, book_fields),
550 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
553 for model in changes_by_type:
554 for field in changes_by_type[model]:
555 if field == 'time_checked':
557 changes.setdefault(field, {})[model] = changes_by_type[model][field]
561 class BookChangesHandler(CatalogueHandler):
562 allowed_methods = ('GET',)
565 def read(self, request, since):
566 return self.book_changes(request, since)
569 class TagChangesHandler(CatalogueHandler):
570 allowed_methods = ('GET',)
573 def read(self, request, since):
574 return self.tag_changes(request, since)
577 class ChangesHandler(CatalogueHandler):
578 allowed_methods = ('GET',)
581 def read(self, request, since):
582 return self.changes(request, since)
585 class PictureHandler(BaseHandler):
587 fields = ('slug', 'title')
588 allowed_methods = ('POST',)
590 def create(self, request):
591 if not request.user.has_perm('catalogue.add_book'):
594 data = json.loads(request.POST.get('data'))
595 form = BookImportForm(data)