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
20 from stats.utils import piwik_track
22 API_BASE = WL_BASE = MEDIA_BASE = 'http://' + Site.objects.get_current().domain
34 for k, v in category_singular.items():
35 category_plural[v] = k
38 def read_tags(tags, allowed):
39 """ Reads a path of filtering tags.
41 :param str tags: a path of category and slug pairs, like: authors/an-author/...
42 :returns: list of Tag objects
43 :raises: ValueError when tags can't be found
48 tags = tags.strip('/').split('/')
51 category = tags.pop(0)
55 category = category_singular[category]
57 raise ValueError('Unknown category.')
59 if not category in allowed:
60 raise ValueError('Category not allowed.')
63 if category == 'book':
67 real_tags.append(Tag.objects.get(category=category, slug=slug))
68 except Tag.DoesNotExist:
69 raise ValueError('Tag not found')
76 class BookMediaHandler(BaseHandler):
77 """ Responsible for representing media in Books. """
80 fields = ['name', 'type', 'url']
84 """ Link to media on site. """
86 return MEDIA_BASE + media.file.url
89 class BookDetailHandler(BaseHandler):
90 """ Main handler for Book objects.
92 Responsible for lists of Book objects
93 and fields used for representing Books.
96 allowed_methods = ['GET']
97 fields = ['title', 'parent'] + Book.formats + [
98 'media', 'url'] + category_singular.keys()
101 def read(self, request, book):
102 """ Returns details of a book, identified by a slug and lang. """
103 kwargs = Book.split_urlid(book)
108 return Book.objects.get(**kwargs)
109 except Book.DoesNotExist:
113 class AnonymousBooksHandler(AnonymousBaseHandler):
114 """ Main handler for Book objects.
116 Responsible for lists of Book objects
117 and fields used for representing Books.
120 allowed_methods = ('GET',)
122 fields = ['href', 'title']
124 categories = set(['author', 'epoch', 'kind', 'genre'])
128 """ Returns an URI for a Book in the API. """
129 return API_BASE + reverse("api_book", args=[book.urlid()])
133 """ Returns Book's URL on the site. """
135 return WL_BASE + book.get_absolute_url()
138 def read(self, request, tags, top_level=False):
139 """ Lists all books with given tags.
141 :param tags: filtering tags; should be a path of categories
142 and slugs, i.e.: authors/an-author/epoch/an-epoch/
143 :param top_level: if True and a book is included in the results,
144 it's children are aren't. By default all books matching the tags
147 tags = read_tags(tags, allowed=self.categories)
150 books = Book.tagged_top_level(tags)
151 return books if books else rc.NOT_FOUND
153 books = Book.tagged.with_all(tags)
155 books = Book.objects.all()
162 def create(self, request, tags, top_level=False):
166 def media(self, book):
167 """ Returns all media for a book. """
169 return book.media.all()
172 class BooksHandler(BaseHandler):
174 fields = ('slug', 'title')
175 anonymous = AnonymousBooksHandler
177 def create(self, request, tags, top_level=False):
178 if not request.user.has_perm('catalogue.add_book'):
181 data = json.loads(request.POST.get('data'))
182 form = BookImportForm(data)
189 # add categorized tags fields for Book
190 def _tags_getter(category):
192 def get_tags(cls, book):
193 return book.tags.filter(category=category)
195 for plural, singular in category_singular.items():
196 setattr(BooksHandler, plural, _tags_getter(singular))
198 # add fields for files in Book
199 def _file_getter(format):
200 field = "%s_file" % format
202 def get_file(cls, book):
203 f = getattr(book, field)
205 return MEDIA_BASE + f.url
209 for format in Book.formats:
210 setattr(BooksHandler, format, _file_getter(format))
213 class TagDetailHandler(BaseHandler):
214 """ Responsible for details of a single Tag object. """
216 fields = ['name', 'sort_key', 'description']
219 def read(self, request, category, slug):
220 """ Returns details of a tag, identified by category and slug. """
223 category_sng = category_singular[category]
228 return Tag.objects.get(category=category_sng, slug=slug)
229 except Tag.DoesNotExist:
233 class TagsHandler(BaseHandler):
234 """ Main handler for Tag objects.
236 Responsible for lists of Tag objects
237 and fields used for representing Tags.
240 allowed_methods = ('GET',)
242 fields = ['name', 'href']
245 def read(self, request, category):
246 """ Lists all tags in the category (eg. all themes). """
249 category_sng = category_singular[category]
253 tags = Tag.objects.filter(category=category_sng).exclude(book_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)
586 class PictureHandler(BaseHandler):
588 fields = ('slug', 'title')
589 allowed_methods = ('POST',)
591 def create(self, request):
592 if not request.user.has_perm('catalogue.add_book'):
595 data = json.loads(request.POST.get('data'))
596 form = BookImportForm(data)