8869668366eaf9628b751d7b8d206029095968b6
[wolnelektury.git] / apps / catalogue / feeds.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
6 #############################################################################
7 # from: http://djangosnippets.org/snippets/243/
8
9 import base64
10
11 from django.http import HttpResponse
12 from django.contrib.auth import authenticate, login
13
14 #
15 def view_or_basicauth(view, request, test_func, realm = "", *args, **kwargs):
16     """
17     This is a helper function used by 'logged_in_or_basicauth' and
18     'has_perm_or_basicauth' (deleted) that does the nitty of determining if they
19     are already logged in or if they have provided proper http-authorization
20     and returning the view if all goes well, otherwise responding with a 401.
21     """
22     if test_func(request.user):
23         # Already logged in, just return the view.
24         #
25         return view(request, *args, **kwargs)
26
27     # They are not logged in. See if they provided login credentials
28     #
29     if 'HTTP_AUTHORIZATION' in request.META:
30         auth = request.META['HTTP_AUTHORIZATION'].split()
31         if len(auth) == 2:
32             # NOTE: We are only support basic authentication for now.
33             #
34             if auth[0].lower() == "basic":
35                 uname, passwd = base64.b64decode(auth[1]).split(':')
36                 user = authenticate(username=uname, password=passwd)
37                 if user is not None:
38                     if user.is_active:
39                         login(request, user)
40                         request.user = user
41                         return view(request, *args, **kwargs)
42
43     # Either they did not provide an authorization header or
44     # something in the authorization attempt failed. Send a 401
45     # back to them to ask them to authenticate.
46     #
47     response = HttpResponse()
48     response.status_code = 401
49     response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
50     return response
51     
52
53 #
54 def logged_in_or_basicauth(realm = ""):
55     """
56     A simple decorator that requires a user to be logged in. If they are not
57     logged in the request is examined for a 'authorization' header.
58
59     If the header is present it is tested for basic authentication and
60     the user is logged in with the provided credentials.
61
62     If the header is not present a http 401 is sent back to the
63     requestor to provide credentials.
64
65     The purpose of this is that in several django projects I have needed
66     several specific views that need to support basic authentication, yet the
67     web site as a whole used django's provided authentication.
68
69     The uses for this are for urls that are access programmatically such as
70     by rss feed readers, yet the view requires a user to be logged in. Many rss
71     readers support supplying the authentication credentials via http basic
72     auth (and they do NOT support a redirect to a form where they post a
73     username/password.)
74
75     Use is simple:
76
77     @logged_in_or_basicauth
78     def your_view:
79         ...
80
81     You can provide the name of the realm to ask for authentication within.
82     """
83     def view_decorator(func):
84         def wrapper(request, *args, **kwargs):
85             return view_or_basicauth(func, request,
86                                      lambda u: u.is_authenticated(),
87                                      realm, *args, **kwargs)
88         return wrapper
89     return view_decorator
90
91
92 #############################################################################
93
94
95 from base64 import b64encode
96 import os.path
97
98 from django.contrib.syndication.views import Feed
99 from django.core.urlresolvers import reverse
100 from django.shortcuts import get_object_or_404
101 from django.utils.feedgenerator import Atom1Feed
102 from django.conf import settings
103 from django.http import Http404
104 from django.contrib.sites.models import Site
105
106 from catalogue.models import Book, Tag
107
108
109 _root_feeds = (
110     {
111         u"category": u"",
112         u"link": u"opds_user",
113         u"link_args": [],
114         u"title": u"Moje półki",
115         u"description": u"Półki użytkownika dostępne po zalogowaniu"
116     },
117     {
118         u"category": u"author",
119         u"link": u"opds_by_category",
120         u"link_args": [u"author"],
121         u"title": u"Autorzy",
122         u"description": u"Utwory wg autorów"
123     },
124     {
125         u"category": u"kind",
126         u"link": u"opds_by_category",
127         u"link_args": [u"kind"],
128         u"title": u"Rodzaje",
129         u"description": u"Utwory wg rodzajów"
130     },
131     {
132         u"category": u"genre",
133         u"link": u"opds_by_category",
134         u"link_args": [u"genre"],
135         u"title": u"Gatunki",
136         u"description": u"Utwory wg gatunków"
137     },
138     {
139         u"category": u"epoch",
140         u"link": u"opds_by_category",
141         u"link_args": [u"epoch"],
142         u"title": u"Epoki",
143         u"description": u"Utwory wg epok"
144     },
145 )
146
147
148 def factory_decorator(decorator):
149     """ generates a decorator for a function factory class
150     if A(*) == f, factory_decorator(D)(A)(*) == D(f)
151     """
152     def fac_dec(func):
153         def wrapper(*args, **kwargs):
154             return decorator(func(*args, **kwargs))
155         return wrapper
156     return fac_dec
157
158
159 class OPDSFeed(Atom1Feed):
160     link_rel = u"subsection"
161     link_type = u"application/atom+xml"
162
163     _book_parent_img = "http://%s%s" % (Site.objects.get_current().domain, os.path.join(settings.STATIC_URL, "img/book-parent.png"))
164     try:
165         _book_parent_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
166     except:
167         _book_parent_img_size = ''
168
169     _book_img = "http://%s%s" % (Site.objects.get_current().domain, os.path.join(settings.STATIC_URL, "img/book.png"))
170     try:
171         _book_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
172     except:
173         _book_img_size = ''
174
175     def add_root_elements(self, handler):
176         super(OPDSFeed, self).add_root_elements(handler)
177         handler.addQuickElement(u"link", u"", {u"href": reverse("opds_authors"), u"rel": u"start", u"type": u"application/atom+xml"})
178
179
180     def add_item_elements(self, handler, item):
181         """ modified from Atom1Feed.add_item_elements """
182         handler.addQuickElement(u"title", item['title'])
183
184         # add a OPDS Navigation link if there's no enclosure
185         if item['enclosure'] is None:
186             handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"subsection", u"type": u"application/atom+xml"})
187             # add a "green book" icon
188             handler.addQuickElement(u"link", '',
189                 {u"rel": u"http://opds-spec.org/thumbnail",
190                  u"href": self._book_parent_img,
191                  u"length": self._book_parent_img_size,
192                  u"type": u"image/png"})
193         if item['pubdate'] is not None:
194             handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8'))
195
196         # Author information.
197         if item['author_name'] is not None:
198             handler.startElement(u"author", {})
199             handler.addQuickElement(u"name", item['author_name'])
200             if item['author_email'] is not None:
201                 handler.addQuickElement(u"email", item['author_email'])
202             if item['author_link'] is not None:
203                 handler.addQuickElement(u"uri", item['author_link'])
204             handler.endElement(u"author")
205
206         # Unique ID.
207         if item['unique_id'] is not None:
208             unique_id = item['unique_id']
209         else:
210             unique_id = get_tag_uri(item['link'], item['pubdate'])
211         handler.addQuickElement(u"id", unique_id)
212
213         # Summary.
214         # OPDS needs type=text
215         if item['description'] is not None:
216             handler.addQuickElement(u"summary", item['description'], {u"type": u"text"})
217
218         # Enclosure as OPDS Acquisition Link
219         if item['enclosure'] is not None:
220             handler.addQuickElement(u"link", '',
221                 {u"rel": u"http://opds-spec.org/acquisition",
222                  u"href": item['enclosure'].url,
223                  u"length": item['enclosure'].length,
224                  u"type": item['enclosure'].mime_type})
225             # add a "red book" icon
226             handler.addQuickElement(u"link", '',
227                 {u"rel": u"http://opds-spec.org/thumbnail",
228                  u"href": self._book_img,
229                  u"length": self._book_img_size,
230                  u"type": u"image/png"})
231
232         # Categories.
233         for cat in item['categories']:
234             handler.addQuickElement(u"category", u"", {u"term": cat})
235
236         # Rights.
237         if item['item_copyright'] is not None:
238             handler.addQuickElement(u"rights", item['item_copyright'])
239
240
241 class RootFeed(Feed):
242     feed_type = OPDSFeed
243     title = u'Wolne Lektury'
244     link = u'http://www.wolnelektury.pl/'
245     description = u"Spis utworów na stronie http://WolneLektury.pl"
246     author_name = u"Wolne Lektury"
247     author_link = u"http://www.wolnelektury.pl/"
248
249     def items(self):
250         return _root_feeds
251
252     def item_title(self, item):
253         return item['title']
254
255     def item_link(self, item):
256         return reverse(item['link'], args=item['link_args'])
257
258     def item_description(self, item):
259         return item['description']
260
261
262 class ByCategoryFeed(Feed):
263     feed_type = OPDSFeed
264     link = u'http://www.wolnelektury.pl/'
265     description = u"Spis utworów na stronie http://WolneLektury.pl"
266     author_name = u"Wolne Lektury"
267     author_link = u"http://www.wolnelektury.pl/"
268
269     def get_object(self, request, category):
270         feed = [feed for feed in _root_feeds if feed['category']==category]
271         if feed:
272             feed = feed[0]
273         else:
274             raise Http404
275
276         return feed
277
278     def title(self, feed):
279         return feed['title']
280
281     def items(self, feed):
282         return (tag for tag in Tag.objects.filter(category=feed['category']) if tag.get_count() > 0)
283
284     def item_title(self, item):
285         return item.name
286
287     def item_link(self, item):
288         return reverse("opds_by_tag", args=[item.category, item.slug])
289
290     def item_description(self):
291         return u''
292
293
294 class ByTagFeed(Feed):
295     feed_type = OPDSFeed
296     link = u'http://www.wolnelektury.pl/'
297     item_enclosure_mime_type = "application/epub+zip"
298     author_name = u"Wolne Lektury"
299     author_link = u"http://www.wolnelektury.pl/"
300
301     def link(self, tag):
302         return tag.get_absolute_url()
303
304     def title(self, tag):
305         return tag.name
306
307     def description(self, tag):
308         return u"Spis utworów na stronie http://WolneLektury.pl"
309
310     def get_object(self, request, category, slug):
311         return get_object_or_404(Tag, category=category, slug=slug)
312
313     def items(self, tag):
314         books = Book.tagged.with_any([tag])
315         l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books])
316         descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)]
317         if descendants_keys:
318             books = books.exclude(pk__in=descendants_keys)
319
320         return books
321
322     def item_title(self, book):
323         return book.title
324
325     def item_description(self):
326         return u''
327
328     def item_link(self, book):
329         return book.get_absolute_url()
330
331     def item_author_name(self, book):
332         try:
333             return book.tags.filter(category='author')[0].name
334         except KeyError:
335             return u''
336
337     def item_author_link(self, book):
338         try:
339             return book.tags.filter(category='author')[0].get_absolute_url()
340         except KeyError:
341             return u''
342
343     def item_enclosure_url(self, book):
344         return "http://%s%s" % (Site.objects.get_current().domain, book.root_ancestor.epub_file.url)
345
346     def item_enclosure_length(self, book):
347         return book.root_ancestor.epub_file.size
348
349
350 @factory_decorator(logged_in_or_basicauth())
351 class UserFeed(Feed):
352     feed_type = OPDSFeed
353     link = u'http://www.wolnelektury.pl/'
354     description = u"Półki użytkownika na stronie http://WolneLektury.pl"
355     author_name = u"Wolne Lektury"
356     author_link = u"http://www.wolnelektury.pl/"
357
358     def get_object(self, request):
359         return request.user
360
361     def title(self, user):
362         return u"Półki użytkownika %s" % user.username
363
364     def items(self, user):
365         return (tag for tag in Tag.objects.filter(category='set', user=user) if tag.get_count() > 0)
366
367     def item_title(self, item):
368         return item.name
369
370     def item_link(self, item):
371         return reverse("opds_user_set", args=[item.slug])
372
373     def item_description(self):
374         return u''
375
376
377 @factory_decorator(logged_in_or_basicauth())
378 class UserSetFeed(Feed):
379     feed_type = OPDSFeed
380     link = u'http://www.wolnelektury.pl/'
381     item_enclosure_mime_type = "application/epub+zip"
382     author_name = u"Wolne Lektury"
383     author_link = u"http://www.wolnelektury.pl/"
384
385     def link(self, tag):
386         return tag.get_absolute_url()
387
388     def title(self, tag):
389         return tag.name
390
391     def description(self, tag):
392         return u"Spis utworów na stronie http://WolneLektury.pl"
393
394     def get_object(self, request, slug):
395         return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
396
397     def items(self, tag):
398         return Book.tagged.with_any([tag])
399
400     def item_title(self, book):
401         return book.title
402
403     def item_description(self):
404         return u''
405
406     def item_link(self, book):
407         return book.get_absolute_url()
408
409     def item_author_name(self, book):
410         try:
411             return book.tags.filter(category='author')[0].name
412         except KeyError:
413             return u''
414
415     def item_author_link(self, book):
416         try:
417             return book.tags.filter(category='author')[0].get_absolute_url()
418         except KeyError:
419             return u''
420
421     def item_enclosure_url(self, book):
422         return "http://%s%s" % (Site.objects.get_current().domain, book.root_ancestor.epub_file.url)
423
424     def item_enclosure_length(self, book):
425         return book.root_ancestor.epub_file.size
426
427 @logged_in_or_basicauth()
428 def user_set_feed(request):
429     return UserSetFeed()(request)
430