Added OpenID registration and login.
authorEevee <git@veekun.com>
Thu, 30 Jul 2009 05:49:32 +0000 (22:49 -0700)
committerEevee <git@veekun.com>
Thu, 30 Jul 2009 05:49:32 +0000 (22:49 -0700)
12 files changed:
development.ini
floof/config/routing.py
floof/controllers/account.py [new file with mode: 0644]
floof/lib/base.py
floof/model/__init__.py
floof/model/meta.py
floof/model/users.py [new file with mode: 0644]
floof/templates/base.mako [new file with mode: 0644]
floof/templates/index.mako [new file with mode: 0644]
floof/templates/login.mako [new file with mode: 0644]
floof/tests/functional/test_account.py [new file with mode: 0644]
setup.py

index ca29659..bd6fc70 100644 (file)
@@ -12,7 +12,7 @@ error_email_from = paste@localhost
 
 [server:main]
 use = egg:Paste#http
-host = 127.0.0.1
+host = 0.0.0.0
 port = 5000
 
 [app:main]
@@ -36,7 +36,7 @@ sqlalchemy.url = sqlite:///%(here)s/development.db
 # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
 # Debug mode will enable the interactive debugging tool, allowing ANYONE to
 # execute malicious code after an exception is raised.
-#set debug = false
+set debug = false
 
 
 # Logging configuration
index 3ff8e54..1fd081a 100644 (file)
@@ -13,12 +13,16 @@ def make_map():
                  always_scan=config['debug'])
     map.minimization = False
 
+    require_POST = dict(conditions={'method': ['POST']})
+
     # The ErrorController route (handles 404/500 error pages); it should
     # likely stay at the top, ensuring it can always be resolved
     map.connect('/error/{action}', controller='error')
     map.connect('/error/{action}/{id}', controller='error')
 
-    # CUSTOM ROUTES HERE
+    map.connect('/account/login', controller='account', action='login')
+    map.connect('/account/login_begin', controller='account', action='login_begin', **require_POST)
+    map.connect('/account/login_finish', controller='account', action='login_finish')
 
     map.connect('/{controller}/{action}')
     map.connect('/{controller}/{action}/{id}')
diff --git a/floof/controllers/account.py b/floof/controllers/account.py
new file mode 100644 (file)
index 0000000..a32583c
--- /dev/null
@@ -0,0 +1,75 @@
+import logging
+from openid.consumer.consumer import Consumer
+from openid.extensions.sreg import SRegRequest, SRegResponse
+from openid.store.filestore import FileOpenIDStore
+from sqlalchemy.orm.exc import NoResultFound
+
+from pylons import request, response, session, tmpl_context as c
+from pylons.controllers.util import abort, redirect_to
+from routes import url_for, request_config
+
+from floof import model
+from floof.model.meta import Session
+from floof.lib.base import BaseController, render
+
+log = logging.getLogger(__name__)
+
+class AccountController(BaseController):
+
+    openid_store = FileOpenIDStore('/var/tmp')
+
+    def login(self):
+        return render('/login.mako')
+
+    def login_begin(self):
+        """Step one of logging in with OpenID; we redirect to the provider"""
+
+        cons = Consumer(session=session, store=self.openid_store)
+        auth_request = cons.begin(request.params['identity_url'])
+        sreg_req = SRegRequest(optional=['nickname', 'email', 'dob', 'gender',
+                                         'country', 'language', 'timezone'])
+        auth_request.addExtension(sreg_req)
+
+        host = request.headers['host']
+        protocol = request_config().protocol
+        return_url = url_for(host=host, controller='account', action='login_finish')
+        new_url = auth_request.redirectURL(return_to=return_url,
+                                           realm=protocol + '://' + host)
+        redirect_to(new_url)
+
+    def login_finish(self):
+        """Step two of logging in; the OpenID provider redirects back here."""
+
+        cons = Consumer(session=session, store=self.openid_store)
+        host = request.headers['host']
+        return_url = url_for(host=host, controller='account', action='login_finish')
+        res = cons.complete(request.params, return_url)
+
+        if res.status != 'success':
+            return 'Error!  %s' % res.message
+
+        try:
+            # Grab an existing user record, if one exists
+            q = Session.query(model.User) \
+                       .filter(model.User.identity_urls.any(url=res.identity_url))
+            user = q.one()
+        except NoResultFound:
+            # Try to pull a name out of the SReg response
+            sreg_res = SRegResponse.fromSuccessResponse(res)
+            try:
+                username = sreg_res['nickname']
+            except:
+                username = 'Anonymous'
+
+            # Create db records
+            user = model.User(name=username)
+            Session.add(user)
+            identity_url = model.IdentityURL(url=res.identity_url)
+            user.identity_urls.append(identity_url)
+            Session.commit()
+
+        # Remember who's logged in, and we're good to go
+        session['user_id'] = user.id
+        session.save()
+
+        return "Hello, %s from %s" % (user.name, res.identity_url)
index fd6ee47..d2756ba 100644 (file)
@@ -2,13 +2,22 @@
 
 Provides the BaseController class for subclassing.
 """
+from pylons import session, tmpl_context as c
 from pylons.controllers import WSGIController
 from pylons.templating import render_mako as render
 
+from floof import model
 from floof.model import meta
 
 class BaseController(WSGIController):
 
+    def __before__(self, action, **params):
+        # Fetch current user object
+        try:
+            c.user = meta.Session.query(model.User).get(session['user_id'])
+        except:
+            pass
+
     def __call__(self, environ, start_response):
         """Invoke the Controller"""
         # WSGIController.__call__ dispatches to the Controller method
index af26ba2..e3f810d 100644 (file)
@@ -4,6 +4,9 @@ from sqlalchemy import orm
 
 from floof.model import meta
 
+# Tables are defined in separate files and imported to here
+from floof.model.users import *
+
 def init_model(engine):
     """Call me before using any of the tables or classes in the model"""
     ## Reflected tables must be defined and mapped here
@@ -15,22 +18,3 @@ def init_model(engine):
     meta.Session.configure(bind=engine)
     meta.engine = engine
 
-
-## Non-reflected tables may be defined and mapped at module level
-#foo_table = sa.Table("Foo", meta.metadata,
-#    sa.Column("id", sa.types.Integer, primary_key=True),
-#    sa.Column("bar", sa.types.String(255), nullable=False),
-#    )
-#
-#class Foo(object):
-#    pass
-#
-#orm.mapper(Foo, foo_table)
-
-
-## Classes for reflected tables may be defined here, but the table and
-## mapping itself must be done in the init_model function
-#reflected_table = None
-#
-#class Reflected(object):
-#    pass
index 1a20aa7..03d1f92 100644 (file)
@@ -1,8 +1,9 @@
 """SQLAlchemy Metadata and Session object"""
 from sqlalchemy import MetaData
+from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import scoped_session, sessionmaker
 
-__all__ = ['Session', 'engine', 'metadata']
+__all__ = ['Session', 'engine', 'TableBase']
 
 # SQLAlchemy database engine. Updated by model.init_model()
 engine = None
@@ -10,6 +11,6 @@ engine = None
 # SQLAlchemy session manager. Updated by model.init_model()
 Session = scoped_session(sessionmaker())
 
-# Global metadata. If you have multiple databases with overlapping table
-# names, you'll need a metadata for each database
-metadata = MetaData()
+# Base class for declarative; creates its own metadata object
+TableBase = declarative_base()
+metadata = TableBase.metadata
diff --git a/floof/model/users.py b/floof/model/users.py
new file mode 100644 (file)
index 0000000..f51282c
--- /dev/null
@@ -0,0 +1,19 @@
+from sqlalchemy import Column, ForeignKey
+from sqlalchemy.orm import relation
+from sqlalchemy.types import Integer, Unicode
+
+from floof.model import meta
+
+__all__ = ['User', 'IdentityURL']
+
+class User(meta.TableBase):
+    __tablename__ = 'users'
+    id = Column(Integer, primary_key=True)
+    name = Column(Unicode(length=20), nullable=False)
+
+class IdentityURL(meta.TableBase):
+    __tablename__ = 'identity_urls'
+    url = Column(Unicode(length=255), primary_key=True)
+    user_id = Column(Integer, ForeignKey('users.id'))
+
+IdentityURL.user = relation(User, lazy=False, backref="identity_urls")
diff --git a/floof/templates/base.mako b/floof/templates/base.mako
new file mode 100644 (file)
index 0000000..340cdf9
--- /dev/null
@@ -0,0 +1,15 @@
+<%def name="title()">Untitled</%def>\
+<!DOCTYPE html>
+<html>
+<head>
+<title>${title()} — Floof</title>
+</head>
+<body>
+% if c.user:
+<p>sup ${c.user.name}</p>
+% else:
+<p>not logged in</p>
+% endif
+${next.body()}
+</body>
+</html>
diff --git a/floof/templates/index.mako b/floof/templates/index.mako
new file mode 100644 (file)
index 0000000..d3c64b9
--- /dev/null
@@ -0,0 +1 @@
+<%inherit file="base.mako" />
diff --git a/floof/templates/login.mako b/floof/templates/login.mako
new file mode 100644 (file)
index 0000000..4497e98
--- /dev/null
@@ -0,0 +1,8 @@
+<%inherit file="base.mako" />
+
+<form action="${url(controller='account', action='login_begin')}" method="POST">
+<p>
+    Identity URL: <input type="text" name="identity_url">
+    <input type="submit" value="Log in">
+</p>
+</form>
diff --git a/floof/tests/functional/test_account.py b/floof/tests/functional/test_account.py
new file mode 100644 (file)
index 0000000..27eb4c3
--- /dev/null
@@ -0,0 +1,7 @@
+from floof.tests import *
+
+class TestAccountController(TestController):
+
+    def test_index(self):
+        response = self.app.get(url(controller='account', action='index'))
+        # Test response...
index 57f6605..2738b65 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -15,6 +15,7 @@ setup(
     install_requires=[
         "Pylons>=0.9.7",
         "SQLAlchemy>=0.5",
+        'openid',
     ],
     setup_requires=["PasteScript>=1.6.3"],
     packages=find_packages(exclude=['ez_setup']),