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
18 from picture.models import Picture
19 from picture.forms import PictureImportForm
21 from stats.utils import piwik_track
23 API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain
35 for k, v in category_singular.items():
36 category_plural[v] = k
39 def read_tags(tags, allowed):
40 """ Reads a path of filtering tags.
42 :param str tags: a path of category and slug pairs, like: authors/an-author/...
43 :returns: list of Tag objects
44 :raises: ValueError when tags can't be found
49 tags = tags.strip('/').split('/')
52 category = tags.pop(0)
56 category = category_singular[category]
58 raise ValueError('Unknown category.')
60 if not category in allowed:
61 raise ValueError('Category not allowed.')
64 if category == 'book':
68 real_tags.append(Tag.objects.get(category=category, slug=slug))
69 except Tag.DoesNotExist:
70 raise ValueError('Tag not found')
77 class BookMediaHandler(BaseHandler):
78 """ Responsible for representing media in Books. """
81 fields = ['name', 'type', 'url']
85 """ Link to media on site. """
87 return MEDIA_BASE + media.file.url
90 class BookDetailHandler(BaseHandler):
91 """ Main handler for Book objects.
93 Responsible for lists of Book objects
94 and fields used for representing Books.
97 allowed_methods = ['GET']
98 fields = ['title', 'parent'] + Book.formats + [
99 'media', 'url'] + category_singular.keys()
102 def read(self, request, book):
103 """ Returns details of a book, identified by a slug and lang. """
104 kwargs = Book.split_urlid(book)
109 return Book.objects.get(**kwargs)
110 except Book.DoesNotExist:
114 class AnonymousBooksHandler(AnonymousBaseHandler):
115 """ Main handler for Book objects.
117 Responsible for lists of Book objects
118 and fields used for representing Books.
121 allowed_methods = ('GET',)
123 fields = ['href', 'title']
125 categories = set(['author', 'epoch', 'kind', 'genre'])
129 """ Returns an URI for a Book in the API. """
130 return API_BASE + reverse("api_book", args=[book.urlid()])
134 """ Returns Book's URL on the site. """
136 return WL_BASE + book.get_absolute_url()
139 def read(self, request, tags, top_level=False):
140 """ Lists all books with given tags.
142 :param tags: filtering tags; should be a path of categories
143 and slugs, i.e.: authors/an-author/epoch/an-epoch/
144 :param top_level: if True and a book is included in the results,
145 it's children are aren't. By default all books matching the tags
148 tags = read_tags(tags, allowed=self.categories)
151 books = Book.tagged_top_level(tags)
152 return books if books else rc.NOT_FOUND
154 books = Book.tagged.with_all(tags)
156 books = Book.objects.all()
163 def create(self, request, tags, top_level=False):
167 def media(self, book):
168 """ Returns all media for a book. """
170 return book.media.all()
173 class BooksHandler(BaseHandler):
175 fields = ('slug', 'title')
176 anonymous = AnonymousBooksHandler
178 def create(self, request, tags, top_level=False):
179 if not request.user.has_perm('catalogue.add_book'):
182 data = json.loads(request.POST.get('data'))
183 form = BookImportForm(data)
190 # add categorized tags fields for Book
191 def _tags_getter(category):
193 def get_tags(cls, book):
194 return book.tags.filter(category=category)
196 for plural, singular in category_singular.items():
197 setattr(BooksHandler, plural, _tags_getter(singular))
199 # add fields for files in Book
200 def _file_getter(format):
201 field = "%s_file" % format
203 def get_file(cls, book):
204 f = getattr(book, field)
206 return MEDIA_BASE + f.url
210 for format in Book.formats:
211 setattr(BooksHandler, format, _file_getter(format))
214 class TagDetailHandler(BaseHandler):
215 """ Responsible for details of a single Tag object. """
217 fields = ['name', 'sort_key', 'description']
220 def read(self, request, category, slug):
221 """ Returns details of a tag, identified by category and slug. """
224 category_sng = category_singular[category]
229 return Tag.objects.get(category=category_sng, slug=slug)
230 except Tag.DoesNotExist:
234 class TagsHandler(BaseHandler):
235 """ Main handler for Tag objects.
237 Responsible for lists of Tag objects
238 and fields used for representing Tags.
241 allowed_methods = ('GET',)
243 fields = ['name', 'href']
246 def read(self, request, category):
247 """ Lists all tags in the category (eg. all themes). """
250 category_sng = category_singular[category]
254 tags = Tag.objects.filter(category=category_sng).exclude(book_count=0)
263 """ Returns URI in the API for the tag. """
265 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
268 class FragmentDetailHandler(BaseHandler):
269 fields = ['book', 'anchor', 'text', 'url', 'themes']
272 def read(self, request, book, anchor):
273 """ Returns details of a fragment, identified by book slug and anchor. """
274 kwargs = Book.split_urlid(book)
279 for field, value in kwargs.items():
280 fragment_kwargs['book__' + field] = value
283 return Fragment.objects.get(anchor=anchor, **fragment_kwargs)
284 except Fragment.DoesNotExist:
288 class FragmentsHandler(BaseHandler):
289 """ Main handler for Fragments.
291 Responsible for lists of Fragment objects
292 and fields used for representing Fragments.
296 fields = ['book', 'anchor', 'href']
297 allowed_methods = ('GET',)
299 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
302 def read(self, request, tags):
303 """ Lists all fragments with given book, tags, themes.
305 :param tags: should be a path of categories and slugs, i.e.:
306 books/book-slug/authors/an-author/themes/a-theme/
309 tags = read_tags(tags, allowed=self.categories)
310 fragments = Fragment.tagged.with_all(tags).select_related('book')
311 if fragments.exists():
317 def href(cls, fragment):
318 """ Returns URI in the API for the fragment. """
320 return API_BASE + reverse("api_fragment", args=[fragment.book.urlid(), fragment.anchor])
323 def url(cls, fragment):
324 """ Returns URL on the site for the fragment. """
326 return WL_BASE + fragment.get_absolute_url()
329 def themes(cls, fragment):
330 """ Returns a list of theme tags for the fragment. """
332 return fragment.tags.filter(category='theme')
339 class CatalogueHandler(BaseHandler):
342 def fields(request, name):
343 fields_str = request.GET.get(name) if request is not None else None
344 return fields_str.split(',') if fields_str is not None else None
348 """ Returns time suitable for use as upper time boundary for check.
350 Used to avoid issues with time between setting the change stamp
351 and actually saving the model in database.
352 Cuts the microsecond part to avoid issues with DBs where time has
355 :param datetime t: manually sets the upper boundary
358 # set to five minutes ago, to avoid concurrency issues
360 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
361 # set to whole second in case DB supports something smaller
362 return t.replace(microsecond=0)
365 def book_dict(book, fields=None):
366 all_fields = ['url', 'title', 'description',
367 'gazeta_link', 'wiki_link',
368 ] + Book.formats + BookMedia.formats + [
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.formats:
386 f = getattr(book, field+'_file')
393 elif field in BookMedia.formats:
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)
587 class PictureHandler(BaseHandler):
589 fields = ('slug', 'title')
590 allowed_methods = ('POST',)
592 def create(self, request):
593 if not request.user.has_perm('picture.add_picture'):
596 data = json.loads(request.POST.get('data'))
597 form = PictureImportForm(data)