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)
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.formats + BookMedia.formats + [
368 'parent', 'parent_number',
370 'license', 'license_description', 'source_name',
371 'technical_editors', 'editors',
372 'author', 'sort_key',
375 fields = (f for f in fields if f in all_fields)
379 extra_info = book.get_extra_info_value()
384 if field in Book.formats:
385 f = getattr(book, field+'_file')
392 elif field in BookMedia.formats:
394 for m in book.media.filter(type=field):
403 obj[field] = book.get_absolute_url()
405 elif field == 'tags':
406 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
408 elif field == 'author':
409 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
411 elif field == 'parent':
412 obj[field] = book.parent_id
414 elif field in ('license', 'license_description', 'source_name',
415 'technical_editors', 'editors'):
416 f = extra_info.get(field)
421 f = getattr(book, field)
429 def book_changes(cls, request=None, since=0, until=None, fields=None):
430 since = datetime.fromtimestamp(int(since))
431 until = cls.until(until)
434 'time_checked': timestamp(until)
438 fields = cls.fields(request, 'book_fields')
445 for book in Book.objects.filter(changed_at__gte=since,
446 changed_at__lt=until):
447 book_d = cls.book_dict(book, fields)
448 updated.append(book_d)
450 changes['updated'] = updated
452 for book in Deleted.objects.filter(content_type=Book,
453 deleted_at__gte=since,
454 deleted_at__lt=until,
455 created_at__lt=since):
456 deleted.append(book.id)
458 changes['deleted'] = deleted
463 def tag_dict(tag, fields=None):
464 all_fields = ('name', 'category', 'sort_key', 'description',
465 'gazeta_link', 'wiki_link',
470 fields = (f for f in fields if f in all_fields)
478 obj[field] = tag.get_absolute_url()
480 elif field == 'books':
481 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
483 elif field == 'sort_key':
484 obj[field] = tag.sort_key
487 f = getattr(tag, field)
495 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
496 since = datetime.fromtimestamp(int(since))
497 until = cls.until(until)
500 'time_checked': timestamp(until)
504 fields = cls.fields(request, 'tag_fields')
506 categories = cls.fields(request, 'tag_categories')
508 all_categories = ('author', 'epoch', 'kind', 'genre')
510 categories = (c for c in categories if c in all_categories)
512 categories = all_categories
517 for tag in Tag.objects.filter(category__in=categories,
518 changed_at__gte=since,
519 changed_at__lt=until):
520 # only serve non-empty tags
522 tag_d = cls.tag_dict(tag, fields)
523 updated.append(tag_d)
524 elif tag.created_at < since:
525 deleted.append(tag.id)
527 changes['updated'] = updated
529 for tag in Deleted.objects.filter(category__in=categories,
531 deleted_at__gte=since,
532 deleted_at__lt=until,
533 created_at__lt=since):
534 deleted.append(tag.id)
536 changes['deleted'] = deleted
541 def changes(cls, request=None, since=0, until=None, book_fields=None,
542 tag_fields=None, tag_categories=None):
543 until = cls.until(until)
546 'time_checked': timestamp(until)
550 'books': cls.book_changes(request, since, until, book_fields),
551 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
554 for model in changes_by_type:
555 for field in changes_by_type[model]:
556 if field == 'time_checked':
558 changes.setdefault(field, {})[model] = changes_by_type[model][field]
562 class BookChangesHandler(CatalogueHandler):
563 allowed_methods = ('GET',)
566 def read(self, request, since):
567 return self.book_changes(request, since)
570 class TagChangesHandler(CatalogueHandler):
571 allowed_methods = ('GET',)
574 def read(self, request, since):
575 return self.tag_changes(request, since)
578 class ChangesHandler(CatalogueHandler):
579 allowed_methods = ('GET',)
582 def read(self, request, since):
583 return self.changes(request, since)