f2341675183b4043230a6d01745ae06a4fbfeb1f
[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 in ('license', 'license_description', 'source_name',
87                       'technical_editors', 'editors'):
88                 f = extra_info.get(field)
89                 if f:
90                     obj[field] = f
91
92             else:
93                 f = getattr(book, field)
94                 if f:
95                     obj[field] = f
96
97         obj['id'] = book.id
98         return obj
99
100     @classmethod
101     def book_changes(cls, request=None, since=0, until=None, fields=None):
102         since = datetime.fromtimestamp(int(since))
103         until = cls.until(until)
104
105         changes = {
106             'time_checked': timestamp(until)
107         }
108
109         if not fields:
110             fields = cls.fields(request, 'book_fields')
111
112         added = []
113         updated = []
114         deleted = []
115
116         last_change = since
117         for book in Book.objects.filter(changed_at__gte=since,
118                     changed_at__lt=until):
119             book_d = cls.book_dict(book, fields)
120             updated.append(book_d)
121         if updated:
122             changes['updated'] = updated
123
124         for book in Deleted.objects.filter(content_type=Book, 
125                     deleted_at__gte=since,
126                     deleted_at__lt=until,
127                     created_at__lt=since):
128             deleted.append(book.id)
129         if deleted:
130             changes['deleted'] = deleted
131
132         return changes
133
134     @staticmethod
135     def tag_dict(tag, fields=None):
136         all_fields = ('name', 'category', 'sort_key', 'description',
137                       'gazeta_link', 'wiki_link',
138                       'url', 'books',
139                      )
140
141         if fields:
142             fields = (f for f in fields if f in all_fields)
143         else:
144             fields = all_fields
145
146         obj = {}
147         for field in fields:
148
149             if field == 'url':
150                 obj[field] = tag.get_absolute_url()
151
152             elif field == 'books':
153                 obj[field] = [b.id for b in Book.tagged_top_level([tag])]
154
155             elif field == 'sort_key':
156                 obj[field] = tag.sort_key
157
158             else:
159                 f = getattr(tag, field)
160                 if f:
161                     obj[field] = f
162
163         obj['id'] = tag.id
164         return obj
165
166     @classmethod
167     def tag_changes(cls, request=None, since=0, until=None, fields=None, categories=None):
168         since = datetime.fromtimestamp(int(since))
169         until = cls.until(until)
170
171         changes = {
172             'time_checked': timestamp(until)
173         }
174
175         if not fields:
176             fields = cls.fields(request, 'tag_fields')
177         if not categories:
178             categories = cls.fields(request, 'tag_categories')
179
180         all_categories = ('author', 'epoch', 'kind', 'genre')
181         if categories:
182             categories = (c for c in categories if c in all_categories)
183         else:
184             categories = all_categories
185
186         updated = []
187         deleted = []
188
189         for tag in Tag.objects.filter(category__in=categories, 
190                     changed_at__gte=since,
191                     changed_at__lt=until):
192             # only serve non-empty tags
193             if tag.get_count():
194                 tag_d = cls.tag_dict(tag, fields)
195                 updated.append(tag_d)
196             elif tag.created_at < since:
197                 deleted.append(tag.id)
198         if updated:
199             changes['updated'] = updated
200
201         for tag in Deleted.objects.filter(category__in=categories,
202                 content_type=Tag, 
203                     deleted_at__gte=since,
204                     deleted_at__lt=until,
205                     created_at__lt=since):
206             deleted.append(tag.id)
207         if deleted:
208             changes['deleted'] = deleted
209
210         return changes
211
212     @classmethod
213     def changes(cls, request=None, since=0, until=None, book_fields=None,
214                 tag_fields=None, tag_categories=None):
215         until = cls.until(until)
216
217         changes = {
218             'time_checked': timestamp(until)
219         }
220
221         changes_by_type = {
222             'books': cls.book_changes(request, since, until, book_fields),
223             'tags': cls.tag_changes(request, since, until, tag_fields, tag_categories),
224         }
225
226         for model in changes_by_type:
227             for field in changes_by_type[model]:
228                 if field == 'time_checked':
229                     continue
230                 changes.setdefault(field, {})[model] = changes_by_type[model][field]
231         return changes
232
233
234 class BookChangesHandler(CatalogueHandler):
235     allowed_methods = ('GET',)
236
237     def read(self, request, since):
238         return self.book_changes(request, since)
239
240
241 class TagChangesHandler(CatalogueHandler):
242     allowed_methods = ('GET',)
243
244     def read(self, request, since):
245         return self.tag_changes(request, since)
246
247
248 class ChangesHandler(CatalogueHandler):
249     allowed_methods = ('GET',)
250
251     def read(self, request, since):
252         return self.changes(request, since)