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.file_types + [
97 'media', 'url'] + category_singular.keys()
100 def read(self, request, slug):
101 """ Returns details of a book, identified by a slug. """
104 return Book.objects.get(slug=slug)
105 except Book.DoesNotExist:
109 class AnonymousBooksHandler(AnonymousBaseHandler):
110 """ Main handler for Book objects.
112 Responsible for lists of Book objects
113 and fields used for representing Books.
116 allowed_methods = ('GET',)
118 fields = ['href', 'title']
120 categories = set(['author', 'epoch', 'kind', 'genre'])
124 """ Returns an URI for a Book in the API. """
125 return API_BASE + reverse("api_book", args=[book.slug])
129 """ Returns Book's URL on the site. """
131 return WL_BASE + book.get_absolute_url()
134 def read(self, request, tags, top_level=False):
135 """ Lists all books with given tags.
137 :param tags: filtering tags; should be a path of categories
138 and slugs, i.e.: authors/an-author/epoch/an-epoch/
139 :param top_level: if True and a book is included in the results,
140 it's children are aren't. By default all books matching the tags
143 tags = read_tags(tags, allowed=self.categories)
146 books = Book.tagged_top_level(tags)
147 return books if books else rc.NOT_FOUND
149 books = Book.tagged.with_all(tags)
151 books = Book.objects.all()
158 def create(self, request, tags, top_level=False):
162 def media(self, book):
163 """ Returns all media for a book. """
165 return book.media.all()
168 class BooksHandler(BaseHandler):
170 fields = ('slug', 'title')
171 anonymous = AnonymousBooksHandler
173 def create(self, request, tags, top_level=False):
174 if not request.user.has_perm('catalogue.add_book'):
177 data = json.loads(request.POST.get('data'))
178 form = BookImportForm(data)
185 # add categorized tags fields for Book
186 def _tags_getter(category):
188 def get_tags(cls, book):
189 return book.tags.filter(category=category)
191 for plural, singular in category_singular.items():
192 setattr(BooksHandler, plural, _tags_getter(singular))
194 # add fields for files in Book
195 def _file_getter(format):
196 field = "%s_file" % format
198 def get_file(cls, book):
199 f = getattr(book, field)
201 return MEDIA_BASE + f.url
205 for format in Book.file_types:
206 setattr(BooksHandler, format, _file_getter(format))
209 class TagDetailHandler(BaseHandler):
210 """ Responsible for details of a single Tag object. """
212 fields = ['name', 'sort_key', 'description']
215 def read(self, request, category, slug):
216 """ Returns details of a tag, identified by category and slug. """
219 category_sng = category_singular[category]
224 return Tag.objects.get(category=category_sng, slug=slug)
225 except Tag.DoesNotExist:
229 class TagsHandler(BaseHandler):
230 """ Main handler for Tag objects.
232 Responsible for lists of Tag objects
233 and fields used for representing Tags.
236 allowed_methods = ('GET',)
238 fields = ['name', 'href']
241 def read(self, request, category):
242 """ Lists all tags in the category (eg. all themes). """
245 category_sng = category_singular[category]
249 tags = Tag.objects.filter(category=category_sng)
250 tags = [t for t in tags if t.get_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. """
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", 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.file_types + [
358 'mp3', 'ogg', 'daisy',
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.file_types:
376 f = getattr(book, field+'_file')
383 elif field in ('mp3', 'ogg', 'daisy'):
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)
537 'time_checked': timestamp(until)
541 'books': cls.book_changes(request, since, until, book_fields),
542 'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
545 for model in changes_by_type:
546 for field in changes_by_type[model]:
547 if field == 'time_checked':
549 changes.setdefault(field, {})[model] = changes_by_type[model][field]
553 class BookChangesHandler(CatalogueHandler):
554 allowed_methods = ('GET',)
557 def read(self, request, since):
558 return self.book_changes(request, since)
561 class TagChangesHandler(CatalogueHandler):
562 allowed_methods = ('GET',)
565 def read(self, request, since):
566 return self.tag_changes(request, since)
569 class ChangesHandler(CatalogueHandler):
570 allowed_methods = ('GET',)
573 def read(self, request, since):
574 return self.changes(request, since)