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, slug):
103 """ Returns details of a book, identified by a slug and lang. """
105 return Book.objects.get(slug=slug)
106 except Book.DoesNotExist:
110 class AnonymousBooksHandler(AnonymousBaseHandler):
111 """ Main handler for Book objects.
113 Responsible for lists of Book objects
114 and fields used for representing Books.
117 allowed_methods = ('GET',)
119 fields = ['href', 'title']
121 categories = set(['author', 'epoch', 'kind', 'genre'])
125 """ Returns an URI for a Book in the API. """
126 return API_BASE + reverse("api_book", args=[book.slug])
130 """ Returns Book's URL on the site. """
132 return WL_BASE + book.get_absolute_url()
135 def read(self, request, tags, top_level=False):
136 """ Lists all books with given tags.
138 :param tags: filtering tags; should be a path of categories
139 and slugs, i.e.: authors/an-author/epoch/an-epoch/
140 :param top_level: if True and a book is included in the results,
141 it's children are aren't. By default all books matching the tags
144 tags = read_tags(tags, allowed=self.categories)
147 books = Book.tagged_top_level(tags)
148 return books if books else rc.NOT_FOUND
150 books = Book.tagged.with_all(tags)
152 books = Book.objects.all()
159 def create(self, request, tags, top_level=False):
163 def media(self, book):
164 """ Returns all media for a book. """
166 return book.media.all()
169 class BooksHandler(BaseHandler):
171 fields = ('slug', 'title')
172 anonymous = AnonymousBooksHandler
174 def create(self, request, tags, top_level=False):
175 if not request.user.has_perm('catalogue.add_book'):
178 data = json.loads(request.POST.get('data'))
179 form = BookImportForm(data)
186 # add categorized tags fields for Book
187 def _tags_getter(category):
189 def get_tags(cls, book):
190 return book.tags.filter(category=category)
192 for plural, singular in category_singular.items():
193 setattr(BooksHandler, plural, _tags_getter(singular))
195 # add fields for files in Book
196 def _file_getter(format):
197 field = "%s_file" % format
199 def get_file(cls, book):
200 f = getattr(book, field)
202 return MEDIA_BASE + f.url
206 for format in Book.formats:
207 setattr(BooksHandler, format, _file_getter(format))
210 class TagDetailHandler(BaseHandler):
211 """ Responsible for details of a single Tag object. """
213 fields = ['name', 'sort_key', 'description']
216 def read(self, request, category, slug):
217 """ Returns details of a tag, identified by category and slug. """
220 category_sng = category_singular[category]
225 return Tag.objects.get(category=category_sng, slug=slug)
226 except Tag.DoesNotExist:
230 class TagsHandler(BaseHandler):
231 """ Main handler for Tag objects.
233 Responsible for lists of Tag objects
234 and fields used for representing Tags.
237 allowed_methods = ('GET',)
239 fields = ['name', 'href']
242 def read(self, request, category):
243 """ Lists all tags in the category (eg. all themes). """
246 category_sng = category_singular[category]
250 tags = Tag.objects.filter(category=category_sng).exclude(book_count=0)
259 """ Returns URI in the API for the tag. """
261 return API_BASE + reverse("api_tag", args=[category_plural[tag.category], tag.slug])
264 class FragmentDetailHandler(BaseHandler):
265 fields = ['book', 'anchor', 'text', 'url', 'themes']
268 def read(self, request, slug, anchor):
269 """ Returns details of a fragment, identified by book slug and anchor. """
271 return Fragment.objects.get(book__slug=slug, anchor=anchor)
272 except Fragment.DoesNotExist:
276 class FragmentsHandler(BaseHandler):
277 """ Main handler for Fragments.
279 Responsible for lists of Fragment objects
280 and fields used for representing Fragments.
284 fields = ['book', 'anchor', 'href']
285 allowed_methods = ('GET',)
287 categories = set(['author', 'epoch', 'kind', 'genre', 'book', 'theme'])
290 def read(self, request, tags):
291 """ Lists all fragments with given book, tags, themes.
293 :param tags: should be a path of categories and slugs, i.e.:
294 books/book-slug/authors/an-author/themes/a-theme/
297 tags = read_tags(tags, allowed=self.categories)
298 fragments = Fragment.tagged.with_all(tags).select_related('book')
299 if fragments.exists():
305 def href(cls, fragment):
306 """ Returns URI in the API for the fragment. """
308 return API_BASE + reverse("api_fragment",
309 args=[fragment.book.slug, fragment.anchor])
312 def url(cls, fragment):
313 """ Returns URL on the site for the fragment. """
315 return WL_BASE + fragment.get_absolute_url()
318 def themes(cls, fragment):
319 """ Returns a list of theme tags for the fragment. """
321 return fragment.tags.filter(category='theme')
328 class CatalogueHandler(BaseHandler):
331 def fields(request, name):
332 fields_str = request.GET.get(name) if request is not None else None
333 return fields_str.split(',') if fields_str is not None else None
337 """ Returns time suitable for use as upper time boundary for check.
339 Used to avoid issues with time between setting the change stamp
340 and actually saving the model in database.
341 Cuts the microsecond part to avoid issues with DBs where time has
344 :param datetime t: manually sets the upper boundary
347 # set to five minutes ago, to avoid concurrency issues
349 t = datetime.now() - timedelta(seconds=settings.API_WAIT)
350 # set to whole second in case DB supports something smaller
351 return t.replace(microsecond=0)
354 def book_dict(book, fields=None):
355 all_fields = ['url', 'title', 'description',
356 'gazeta_link', 'wiki_link',
357 ] + Book.formats + BookMedia.formats + [
358 'parent', 'parent_number',
360 'license', 'license_description', 'source_name',
361 'technical_editors', 'editors',
362 'author', 'sort_key',
365 fields = (f for f in fields if f in all_fields)
369 extra_info = book.get_extra_info_value()
374 if field in Book.formats:
375 f = getattr(book, field+'_file')
382 elif field in BookMedia.formats:
384 for m in book.media.filter(type=field):
393 obj[field] = book.get_absolute_url()
395 elif field == 'tags':
396 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
398 elif field == 'author':
399 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
401 elif field == 'parent':
402 obj[field] = book.parent_id
404 elif field in ('license', 'license_description', 'source_name',
405 'technical_editors', 'editors'):
406 f = extra_info.get(field)
411 f = getattr(book, field)
419 def book_changes(cls, request=None, since=0, until=None, fields=None):
420 since = datetime.fromtimestamp(int(since))
421 until = cls.until(until)
424 'time_checked': timestamp(until)
428 fields = cls.fields(request, 'book_fields')
435 for book in Book.objects.filter(changed_at__gte=since,
436 changed_at__lt=until):
437 book_d = cls.book_dict(book, fields)
438 updated.append(book_d)
440 changes['updated'] = updated
442 for book in Deleted.objects.filter(content_type=Book,
443 deleted_at__gte=since,
444 deleted_at__lt=until,
445 created_at__lt=since):
446 deleted.append(book.id)
448 changes['deleted'] = deleted
453 def tag_dict(tag, fields=None):
454 all_fields = ('name', 'category', 'sort_key', 'description',
455 'gazeta_link', 'wiki_link',
460 fields = (f for f in fields if f in all_fields)
468 obj[field] = tag.get_absolute_url()
470 elif field == 'books':
471 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
473 elif field == 'sort_key':
474 obj[field] = tag.sort_key
477 f = getattr(tag, field)
485 def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
486 since = datetime.fromtimestamp(int(since))
487 until = cls.until(until)
490 'time_checked': timestamp(until)
494 fields = cls.fields(request, 'tag_fields')
496 categories = cls.fields(request, 'tag_categories')
498 all_categories = ('author', 'epoch', 'kind', 'genre')
500 categories = (c for c in categories if c in all_categories)
502 categories = all_categories
507 for tag in Tag.objects.filter(category__in=categories,
508 changed_at__gte=since,
509 changed_at__lt=until):
510 # only serve non-empty tags
512 tag_d = cls.tag_dict(tag, fields)
513 updated.append(tag_d)
514 elif tag.created_at < since:
515 deleted.append(tag.id)
517 changes['updated'] = updated
519 for tag in Deleted.objects.filter(category__in=categories,
521 deleted_at__gte=since,
522 deleted_at__lt=until,
523 created_at__lt=since):
524 deleted.append(tag.id)
526 changes['deleted'] = deleted
531 def changes(cls, request=None, since=0, until=None, book_fields=None,
532 tag_fields=None, tag_categories=None):
533 until = cls.until(until)
536 'time_checked': timestamp(until)
540 'books': cls.book_changes(request, since, until, book_fields),
541 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
544 for model in changes_by_type:
545 for field in changes_by_type[model]:
546 if field == 'time_checked':
548 changes.setdefault(field, {})[model] = changes_by_type[model][field]
552 class BookChangesHandler(CatalogueHandler):
553 allowed_methods = ('GET',)
556 def read(self, request, since):
557 return self.book_changes(request, since)
560 class TagChangesHandler(CatalogueHandler):
561 allowed_methods = ('GET',)
564 def read(self, request, since):
565 return self.tag_changes(request, since)
568 class ChangesHandler(CatalogueHandler):
569 allowed_methods = ('GET',)
572 def read(self, request, since):
573 return self.changes(request, since)
576 class PictureHandler(BaseHandler):
578 fields = ('slug', 'title')
579 allowed_methods = ('POST',)
581 def create(self, request):
582 if not request.user.has_perm('picture.add_picture'):
585 data = json.loads(request.POST.get('data'))
586 form = PictureImportForm(data)