minor fix
[wolnelektury.git] / apps / api / handlers.py
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.
4
5 from datetime import datetime, timedelta
6 from piston.handler import BaseHandler
7 from django.conf import settings
8
9 from api.helpers import timestamp
10 from api.models import Deleted
11 from catalogue.models import Book, Tag
12
13
14 class CatalogueHandler(BaseHandler):
15
16     @staticmethod
17     def fields(request, name):
18         fields_str = request.GET.get(name) if request is not None else None
19         return fields_str.split(',') if fields_str is not None else None
20
21     @staticmethod
22     def until(t=None):
23         """ Returns time suitable for use as upper time boundary for check.
24         
25             Defaults to 'five minutes ago' to avoid issues with time between
26             change stamp set and model save.
27             Cuts the microsecond part to avoid issues with DBs where time has
28             more precision.
29
30         """
31         # set to five minutes ago, to avoid concurrency issues
32         if t is None:
33             t = datetime.now() - timedelta(seconds=settings.API_WAIT)
34         # set to whole second in case DB supports something smaller
35         return t.replace(microsecond=0)
36
37     @staticmethod
38     def book_dict(book, fields=None):
39         all_fields = ('url', 'title', 'description',
40                       'gazeta_link', 'wiki_link',
41                       'xml', 'epub', 'txt', 'pdf', 'html',
42                       'mp3', 'ogg', 'daisy',
43                       'parent', 'parent_number',
44                       'tags',
45                       'license', 'license_description', 'source_name',
46                       'technical_editors', 'editors',
47                       'author', 'sort_key',
48                      )
49         if fields:
50             fields = (f for f in fields if f in all_fields)
51         else:
52             fields = all_fields
53
54         extra_info = book.get_extra_info_value()
55
56         obj = {}
57         for field in fields:
58
59             if field in ('xml', 'epub', 'txt', 'pdf', 'html'):
60                 f = getattr(book, field+'_file')
61                 if f:
62                     obj[field] = {
63                         'url': f.url,
64                         'size': f.size,
65                     }
66
67             elif field in ('mp3', 'ogg', 'daisy'):
68                 media = []
69                 for m in book.media.filter(type=field):
70                     media.append({
71                         'url': m.file.url,
72                         'size': m.file.size,
73                     })
74                 if media:
75                     obj[field] = media
76
77             elif field == 'url':
78                 obj[field] = book.get_absolute_url()
79
80             elif field == 'tags':
81                 obj[field] = [t.id for t in book.tags.exclude(category__in=('book', 'set'))]
82
83             elif field == 'author':
84                 obj[field] = ", ".join(t.name for t in book.tags.filter(category='author'))
85
86             elif field == 'parent':
87                 obj[field] = book.parent_id
88
89             elif field in ('license', 'license_description', 'source_name',
90                       'technical_editors', 'editors'):
91                 f = extra_info.get(field)
92                 if f:
93                     obj[field] = f
94
95             else:
96                 f = getattr(book, field)
97                 if f:
98                     obj[field] = f
99
100         obj['id'] = book.id
101         return obj
102
103     @classmethod
104     def book_changes(cls, request=None, since=0, until=None, fields=None):
105         since = datetime.fromtimestamp(int(since))
106         until = cls.until(until)
107
108         changes = {
109             'time_checked': timestamp(until)
110         }
111
112         if not fields:
113             fields = cls.fields(request, 'book_fields')
114
115         added = []
116         updated = []
117         deleted = []
118
119         last_change = since
120         for book in Book.objects.filter(changed_at__gte=since,
121                     changed_at__lt=until):
122             book_d = cls.book_dict(book, fields)
123             updated.append(book_d)
124         if updated:
125             changes['updated'] = updated
126
127         for book in Deleted.objects.filter(content_type=Book, 
128                     deleted_at__gte=since,
129                     deleted_at__lt=until,
130                     created_at__lt=since):
131             deleted.append(book.id)
132         if deleted:
133             changes['deleted'] = deleted
134
135         return changes
136
137     @staticmethod
138     def tag_dict(tag, fields=None):
139         all_fields = ('name', 'category', 'sort_key', 'description',
140                       'gazeta_link', 'wiki_link',
141                       'url', 'books',
142                      )
143
144         if fields:
145             fields = (f for f in fields if f in all_fields)
146         else:
147             fields = all_fields
148
149         obj = {}
150         for field in fields:
151
152             if field == 'url':
153                 obj[field] = tag.get_absolute_url()
154
155             elif field == 'books':
156                 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
157
158             elif field == 'sort_key':
159                 obj[field] = tag.sort_key
160
161             else:
162                 f = getattr(tag, field)
163                 if f:
164                     obj[field] = f
165
166         obj['id'] = tag.id
167         return obj
168
169     @classmethod
170     def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
171         since = datetime.fromtimestamp(int(since))
172         until = cls.until(until)
173
174         changes = {
175             'time_checked': timestamp(until)
176         }
177
178         if not fields:
179             fields = cls.fields(request, 'tag_fields')
180         if not categories:
181             categories = cls.fields(request, 'tag_categories')
182
183         all_categories = ('author', 'epoch', 'kind', 'genre')
184         if categories:
185             categories = (c for c in categories if c in all_categories)
186         else:
187             categories = all_categories
188
189         updated = []
190         deleted = []
191
192         for tag in Tag.objects.filter(category__in=categories, 
193                     changed_at__gte=since,
194                     changed_at__lt=until):
195             # only serve non-empty tags
196             if tag.get_count():
197                 tag_d = cls.tag_dict(tag, fields)
198                 updated.append(tag_d)
199             elif tag.created_at < since:
200                 deleted.append(tag.id)
201         if updated:
202             changes['updated'] = updated
203
204         for tag in Deleted.objects.filter(category__in=categories,
205                 content_type=Tag, 
206                     deleted_at__gte=since,
207                     deleted_at__lt=until,
208                     created_at__lt=since):
209             deleted.append(tag.id)
210         if deleted:
211             changes['deleted'] = deleted
212
213         return changes
214
215     @classmethod
216     def changes(cls, request=None, since=0, until=None, book_fields=None,
217                 tag_fields=None, tag_categories=None):
218         until = cls.until(until)
219
220         changes = {
221             'time_checked': timestamp(until)
222         }
223
224         changes_by_type = {
225             'books': cls.book_changes(request, since, until, book_fields),
226             'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
227         }
228
229         for model in changes_by_type:
230             for field in changes_by_type[model]:
231                 if field == 'time_checked':
232                     continue
233                 changes.setdefault(field, {})[model] = changes_by_type[model][field]
234         return changes
235
236
237 class BookChangesHandler(CatalogueHandler):
238     allowed_methods = ('GET',)
239
240     def read(self, request, since):
241         return self.book_changes(request, since)
242
243
244 class TagChangesHandler(CatalogueHandler):
245     allowed_methods = ('GET',)
246
247     def read(self, request, since):
248         return self.tag_changes(request, since)
249
250
251 class ChangesHandler(CatalogueHandler):
252     allowed_methods = ('GET',)
253
254     def read(self, request, since):
255         return self.changes(request, since)