From 3f6b9410cb2ac97043bb16b8593065b8602e507e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 20 Sep 2010 17:10:15 +0200 Subject: [PATCH 1/1] login support in OPDS Catalog --- apps/catalogue/feeds.py | 253 ++++++++++++++++++++++++++++++++++++---- apps/catalogue/urls.py | 8 +- 2 files changed, 237 insertions(+), 24 deletions(-) diff --git a/apps/catalogue/feeds.py b/apps/catalogue/feeds.py index 0d0bf7707..719cc910c 100644 --- a/apps/catalogue/feeds.py +++ b/apps/catalogue/feeds.py @@ -2,6 +2,96 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # + +############################################################################# +# from: http://djangosnippets.org/snippets/243/ + +import base64 + +from django.http import HttpResponse +from django.contrib.auth import authenticate, login + +# +def view_or_basicauth(view, request, test_func, realm = "", *args, **kwargs): + """ + This is a helper function used by 'logged_in_or_basicauth' and + 'has_perm_or_basicauth' (deleted) that does the nitty of determining if they + are already logged in or if they have provided proper http-authorization + and returning the view if all goes well, otherwise responding with a 401. + """ + if test_func(request.user): + # Already logged in, just return the view. + # + return view(request, *args, **kwargs) + + # They are not logged in. See if they provided login credentials + # + if 'HTTP_AUTHORIZATION' in request.META: + auth = request.META['HTTP_AUTHORIZATION'].split() + if len(auth) == 2: + # NOTE: We are only support basic authentication for now. + # + if auth[0].lower() == "basic": + uname, passwd = base64.b64decode(auth[1]).split(':') + user = authenticate(username=uname, password=passwd) + if user is not None: + if user.is_active: + login(request, user) + request.user = user + return view(request, *args, **kwargs) + + # Either they did not provide an authorization header or + # something in the authorization attempt failed. Send a 401 + # back to them to ask them to authenticate. + # + response = HttpResponse() + response.status_code = 401 + response['WWW-Authenticate'] = 'Basic realm="%s"' % realm + return response + + +# +def logged_in_or_basicauth(realm = ""): + """ + A simple decorator that requires a user to be logged in. If they are not + logged in the request is examined for a 'authorization' header. + + If the header is present it is tested for basic authentication and + the user is logged in with the provided credentials. + + If the header is not present a http 401 is sent back to the + requestor to provide credentials. + + The purpose of this is that in several django projects I have needed + several specific views that need to support basic authentication, yet the + web site as a whole used django's provided authentication. + + The uses for this are for urls that are access programmatically such as + by rss feed readers, yet the view requires a user to be logged in. Many rss + readers support supplying the authentication credentials via http basic + auth (and they do NOT support a redirect to a form where they post a + username/password.) + + Use is simple: + + @logged_in_or_basicauth + def your_view: + ... + + You can provide the name of the realm to ask for authentication within. + """ + def view_decorator(func): + def wrapper(request, *args, **kwargs): + return view_or_basicauth(func, request, + lambda u: u.is_authenticated(), + realm, *args, **kwargs) + return wrapper + return view_decorator + + +############################################################################# + + from base64 import b64encode import os.path @@ -17,32 +107,70 @@ from catalogue.models import Book, Tag _root_feeds = ( - {u"category": u"author", u"title": u"Autorzy", u"description": u"Utwory wg autorów"}, - {u"category": u"kind", u"title": u"Rodzaje", u"description": u"Utwory wg rodzajów"}, - {u"category": u"genre", u"title": u"Gatunki", u"description": u"Utwory wg gatunków"}, - {u"category": u"epoch", u"title": u"Epoki", u"description": u"Utwory wg epok"}, + { + u"category": u"", + u"link": u"opds_user", + u"link_args": [], + u"title": u"Moje półki", + u"description": u"Półki użytkownika dostępne po zalogowaniu" + }, + { + u"category": u"author", + u"link": u"opds_by_category", + u"link_args": [u"author"], + u"title": u"Autorzy", + u"description": u"Utwory wg autorów" + }, + { + u"category": u"kind", + u"link": u"opds_by_category", + u"link_args": [u"kind"], + u"title": u"Rodzaje", + u"description": u"Utwory wg rodzajów" + }, + { + u"category": u"genre", + u"link": u"opds_by_category", + u"link_args": [u"kind"], + u"title": u"Gatunki", + u"description": u"Utwory wg gatunków" + }, + { + u"category": u"epoch", + u"link": u"opds_by_category", + u"link_args": [u"epoch"], + u"title": u"Epoki", + u"description": u"Utwory wg epok" + }, ) +def factory_decorator(decorator): + """ generates a decorator for a function factory class + if A(*) == f, factory_decorator(D)(A)(*) == D(f) + """ + def fac_dec(func): + def wrapper(*args, **kwargs): + return decorator(func(*args, **kwargs)) + return wrapper + return fac_dec + + class OPDSFeed(Atom1Feed): link_rel = u"subsection" link_type = u"application/atom+xml" + _book_parent_img = "http://%s%s" % (Site.objects.get_current().domain, os.path.join(settings.STATIC_URL, "img/book-parent.png")) try: - with open(os.path.join(settings.STATIC_ROOT, "img/book-par ent.png")) as f: - t = f.read() - _book_parent_img_size = len(t) - _book_parent_img = b64encode(t) + _book_parent_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png"))) except: - _book_parent_img = _book_parent_img_size = '' + _book_parent_img_size = '' + _book_img = "http://%s%s" % (Site.objects.get_current().domain, os.path.join(settings.STATIC_URL, "img/book.png")) try: - with open(os.path.join(settings.STATIC_ROOT, "img/bo ok.png")) as f: - t = f.read() - _book_img_size = len(t) - _book_img = b64encode(t) + _book_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png"))) except: - _book_img = _book_img_size = '' + _book_img_size = '' def add_root_elements(self, handler): super(OPDSFeed, self).add_root_elements(handler) @@ -59,8 +187,8 @@ class OPDSFeed(Atom1Feed): # add a "green book" icon handler.addQuickElement(u"link", '', {u"rel": u"http://opds-spec.org/thumbnail", - u"href": u"data:image/png;base64,%s" % self._book_parent_img, - u"length": unicode(self._book_parent_img_size), + u"href": self._book_parent_img, + u"length": self._book_parent_img_size, u"type": u"image/png"}) if item['pubdate'] is not None: handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8')) @@ -97,8 +225,8 @@ class OPDSFeed(Atom1Feed): # add a "red book" icon handler.addQuickElement(u"link", '', {u"rel": u"http://opds-spec.org/thumbnail", - u"href": u"data:image/png;base64,%s" % self._book_img, - u"length": unicode(self._book_img_size), + u"href": self._book_img, + u"length": self._book_img_size, u"type": u"image/png"}) # Categories. @@ -125,7 +253,7 @@ class RootFeed(Feed): return item['title'] def item_link(self, item): - return reverse(u"opds_by_category", args=[item['category']]) + return reverse(item['link'], args=item['link_args']) def item_description(self, item): return item['description'] @@ -213,7 +341,90 @@ class ByTagFeed(Feed): return u'' def item_enclosure_url(self, book): - return "http://%s%s" % (Site.objects.get_current().domain, book.epub_file.url) + return "http://%s%s" % (Site.objects.get_current().domain, book.root_ancestor.epub_file.url) def item_enclosure_length(self, book): - return book.epub_file.size + return book.root_ancestor.epub_file.size + + +@factory_decorator(logged_in_or_basicauth()) +class UserFeed(Feed): + feed_type = OPDSFeed + link = u'http://www.wolnelektury.pl/' + description = u"Półki użytkownika na stronie http://WolneLektury.pl" + author_name = u"Wolne Lektury" + author_link = u"http://www.wolnelektury.pl/" + + def get_object(self, request): + return request.user + + def title(self, user): + return u"Półki użytkownika %s" % user.username + + def items(self, user): + return (tag for tag in Tag.objects.filter(category='set', user=user) if tag.get_count() > 0) + + def item_title(self, item): + return item.name + + def item_link(self, item): + return reverse("opds_user_set", args=[item.slug]) + + def item_description(self): + return u'' + + +@factory_decorator(logged_in_or_basicauth()) +class UserSetFeed(Feed): + feed_type = OPDSFeed + link = u'http://www.wolnelektury.pl/' + item_enclosure_mime_type = "application/epub+zip" + author_name = u"Wolne Lektury" + author_link = u"http://www.wolnelektury.pl/" + + def link(self, tag): + return tag.get_absolute_url() + + def title(self, tag): + return tag.name + + def description(self, tag): + return u"Spis utworów na stronie http://WolneLektury.pl" + + def get_object(self, request, slug): + return get_object_or_404(Tag, category='set', slug=slug, user=request.user) + + def items(self, tag): + return Book.tagged.with_any([tag]) + + def item_title(self, book): + return book.title + + def item_description(self): + return u'' + + def item_link(self, book): + return book.get_absolute_url() + + def item_author_name(self, book): + try: + return book.tags.filter(category='author')[0].name + except KeyError: + return u'' + + def item_author_link(self, book): + try: + return book.tags.filter(category='author')[0].get_absolute_url() + except KeyError: + return u'' + + def item_enclosure_url(self, book): + return "http://%s%s" % (Site.objects.get_current().domain, book.root_ancestor.epub_file.url) + + def item_enclosure_length(self, book): + return book.root_ancestor.epub_file.size + +@logged_in_or_basicauth() +def user_set_feed(request): + return UserSetFeed()(request) + diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index ba9818d7f..53a70c3de 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -3,7 +3,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.conf.urls.defaults import * -from catalogue.feeds import RootFeed, ByCategoryFeed, ByTagFeed +from catalogue.feeds import RootFeed, ByCategoryFeed, ByTagFeed, UserFeed, UserSetFeed urlpatterns = patterns('catalogue.views', @@ -25,8 +25,10 @@ urlpatterns = patterns('catalogue.views', # OPDS interface url(r'^opds/$', RootFeed(), name="opds_authors"), - url(r'^opds/(?Pauthor|kind|genre|epoch|theme)/$', ByCategoryFeed(), name="opds_by_category"), - url(r'^opds/(?Pauthor|kind|genre|epoch|theme)/(?P[a-zA-Z0-9-]+)/$', ByTagFeed(), name="opds_by_tag"), + url(r'^opds/user/$', UserFeed(), name="opds_user"), + url(r'^opds/set/(?P[a-zA-Z0-9-]+)/$', UserSetFeed(), name="opds_user_set"), + url(r'^opds/(?P[a-zA-Z0-9-]+)/$', ByCategoryFeed(), name="opds_by_category"), + url(r'^opds/(?P[a-zA-Z0-9-]+)/(?P[a-zA-Z0-9-]+)/$', ByTagFeed(), name="opds_by_tag"), # Public interface. Do not change this URLs. url(r'^lektura/(?P[a-zA-Z0-9-]+)\.html$', 'book_text', name='book_text'), -- 2.20.1