Skip to content
Browse files

Login and registration. Includes migration.

  • Loading branch information...
1 parent 714e21b commit 1dfbf9fc2e87989bf096c8d0c9e9c577d6ecd70d @eevee committed May 18, 2010
View
9 floof/config/routing.py
@@ -13,11 +13,20 @@ def make_map():
always_scan=config['debug'])
map.minimization = False
+ require_POST = dict(conditions=dict(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')
+ 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('/account/register', controller='account', action='register',
+ **require_POST)
+
map.connect('/', controller='main', action='index')
return map
View
175 floof/controllers/account.py
@@ -0,0 +1,175 @@
+import logging
+import re
+
+from openid.consumer.consumer import Consumer
+from openid.extensions.sreg import SRegRequest, SRegResponse
+from openid.store.filestore import FileOpenIDStore
+from openid.yadis.discover import DiscoveryFailure
+from pylons import request, response, session, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect_to
+from routes import request_config
+from sqlalchemy.orm.exc import NoResultFound
+
+from floof.lib import helpers as h
+from floof.lib.base import BaseController, render
+from floof.model import IdentityURL, User, meta
+
+log = logging.getLogger(__name__)
+
+class AccountController(BaseController):
+
+ openid_store = FileOpenIDStore('/var/tmp')
+
+ def _username_error(self, username):
+ """Check whether the username is valid and taken.
+
+ Returns a short error string, or None if it's fine.
+ """
+
+ if not username:
+ return 'missing'
+ elif not re.match('^[_a-z0-9]{1,24}$', username):
+ return 'invalid'
+ elif meta.Session.query(User).filter_by(name=username).count():
+ return 'taken'
+ else:
+ return None
+
+ def _bail(self, reason):
+ # Used for bailing on a login attempt; reshows the login page
+ c.error = reason
+ c.attempted_openid = request.params.get('openid_identifier', '')
+ return render('/account/login.mako')
+
+
+ def login(self):
+ c.error = None
+ c.attempted_openid = None
+ return render('/account/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)
+
+ try:
+ openid_url = request.params['openid_identifier']
+ except KeyError:
+ return self._bail("Gotta enter an OpenID to log in.")
+
+ try:
+ auth_request = cons.begin(openid_url)
+ except DiscoveryFailure:
+ return self._bail(
+ "Can't connect to '{0}'. You sure it's an OpenID?"
+ .format(openid_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(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(host=host, controller='account', action='login_finish')
+ res = cons.complete(request.params, return_url)
+
+ if res.status != 'success':
+ return 'Error! %s' % res.message
+
+ identity_url = unicode(res.identity_url)
+
+ try:
+ # Grab an existing user record, if one exists
+ q = meta.Session.query(User) \
+ .filter(User.identity_urls.any(url=identity_url))
+ user = q.one()
+
+ # Remember who's logged in, and we're good to go
+ session['user_id'] = user.id
+ session.save()
+
+ #h.flash(u"""Hello, {0}!""".format(user.display_name),
+ # icon='user')
+
+ redirect_to('/', _code=303)
+
+ except NoResultFound:
+ # Nope. Give a (brief!) registration form instead
+ session['pending_identity_url'] = identity_url
+ session.save()
+ c.identity_url = identity_url
+
+ # Try to pull a name out of the SReg response
+ sreg_res = SRegResponse.fromSuccessResponse(res)
+ try:
+ c.username = sreg_res['nickname'].lower()
+ except (KeyError, TypeError):
+ # KeyError if sreg has no nickname; TypeError if sreg is None
+ c.username = u''
+
+ c.username_error = self._username_error(c.username)
+
+ return render('/account/register.mako')
+
+ def register(self):
+ # Check identity URL
+ identity_url = session.get('pending_identity_url', None)
+ if not identity_url or \
+ meta.Session.query(IdentityURL) \
+ .filter_by(url=identity_url).count():
+
+ # Not in the session or is already registered. Neither makes
+ # sense. Bail.
+ #h.flash('Your session expired. Please try logging in again.')
+ redirect_to(controller='account', action='login', _code=303)
+
+ # Check username
+ username = request.params.get('username', None)
+ c.username_error = self._username_error(username)
+ print c.username_error
+
+ if c.username_error:
+ # Somethin wrong! Make 'em try again
+ c.username = username
+ c.identity_url = identity_url
+ return render('/account/register.mako')
+
+ # Create db records
+ user = User(name=username)
+ meta.Session.add(user)
+
+ openid = IdentityURL(url=identity_url)
+ user.identity_urls.append(openid)
+
+ meta.Session.commit()
+
+ # Log 'em in
+ del session['pending_identity_url']
+ session['user_id'] = user.id
+ session.save()
+
+ # And off we go
+ redirect_to('/', _code=303)
+
+ def logout(self):
+ """Logs the user out."""
+
+ if 'user_id' in session:
+ del session['user_id']
+ session.save()
+
+ #h.flash(u"""Logged out.""",
+ # icon='user-silhouette')
+
+ redirect_to('/', _code=303)
View
5 floof/lib/helpers.py
@@ -3,5 +3,6 @@
Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to templates as 'h'.
"""
-# Import helpers as desired, or define your own, ie:
-#from webhelpers.html.tags import checkbox, password
+from pylons import url
+from webhelpers.html import escape, HTML, literal, url_escape
+from webhelpers.html.tags import *
View
58 floof/model/__init__.py
@@ -1,36 +1,42 @@
"""The application's model objects"""
-import sqlalchemy as sa
-from sqlalchemy import orm
+from sqlalchemy import Column, ForeignKey, MetaData, Table
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import relation
+from sqlalchemy.types import *
from floof.model import meta
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
- #global reflected_table
- #reflected_table = sa.Table("Reflected", meta.metadata, autoload=True,
- # autoload_with=engine)
- #orm.mapper(Reflected, reflected_table)
- #
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
+TableBase = declarative_base()
+
+### USERS
+
+class User(TableBase):
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True, nullable=False)
+ name = Column(Unicode(24), nullable=False, index=True, unique=True)
+
+ @property
+ def display_name(self):
+ """Returns a flavory string that should be used to present this user.
+ """
+
+ return self.name
+
+class IdentityURL(TableBase):
+ __tablename__ = 'identity_urls'
+ id = Column(Integer, primary_key=True, nullable=False)
+ user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
+ url = Column(Unicode(250), nullable=False, index=True, unique=True)
+
+
+
+### RELATIONS
+
+# Users
+IdentityURL.user = relation(User, backref='identity_urls')
View
2 floof/templates/account/base.mako
@@ -0,0 +1,2 @@
+<%inherit file="/base.mako" />
+${next.body()}
View
10 floof/templates/account/login.mako
@@ -0,0 +1,10 @@
+<%inherit file="base.mako" />
+
+<h1>Log in or register with OpenID</h1>
+
+${h.form(url(controller='account', action='login_begin'))}
+<p>
+ <input type="text" name="openid_identifier" value="${c.attempted_openid if c.attempted_openid else u''}">
+ <input type="submit" value="Log in">
+</p>
+${h.end_form()}
View
22 floof/templates/account/register.mako
@@ -0,0 +1,22 @@
+<%inherit file="base.mako" />
+
+<h1>Register with OpenID</h1>
+
+% if c.username_error == 'taken':
+<p class="error">Your username is already taken. Please enter another one below.</p>
+% elif c.username_error == 'missing':
+<p class="error">Please select a username below.</p>
+% elif c.username_error == 'invalid':
+<p class="error">Your username must be 1–24 characters and contain only lowercase letters, numbers, and underscores. Please select another below.</p>
+% endif
+
+${h.form(url(controller='account', action='register'))}
+<dl>
+ <dt>Registering from</dt>
+ <dd><code>${c.identity_url}</code></dd>
+ <dt>Username</dt>
+ <dd><input type="text" name="username" value="${c.username}"></dd>
+
+ <dd><button type="submit">OK, register!</button></dd>
+</dl>
+${h.end_form()}
View
7 floof/tests/functional/test_account.py
@@ -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...
View
4 migration/README
@@ -0,0 +1,4 @@
+This is a database migration repository.
+
+More information at
+http://code.google.com/p/sqlalchemy-migrate/
View
0 migration/__init__.py
No changes.
View
20 migration/migrate.cfg
@@ -0,0 +1,20 @@
+[db_settings]
+# Used to identify which repository this database is versioned under.
+# You can use the name of your project.
+repository_id={{ locals().pop('repository_id') }}
+
+# The name of the database table used to track the schema version.
+# This name shouldn't already be used by your project.
+# If this is changed once a database is under version control, you'll need to
+# change the table name in each database too.
+version_table={{ locals().pop('version_table') }}
+
+# When committing a change script, Migrate will attempt to generate the
+# sql for all supported databases; normally, if one of them fails - probably
+# because you don't have that database installed - it is ignored and the
+# commit continues, perhaps ending successfully.
+# Databases in this list MUST compile successfully during a commit, or the
+# entire commit will fail. List the databases your application will actually
+# be using to ensure your updates to that database work properly.
+# This must be a list; example: ['postgres','sqlite']
+required_dbs={{ locals().pop('required_dbs') }}
View
30 migration/versions/001_Add_user_tables.py
@@ -0,0 +1,30 @@
+from sqlalchemy import *
+from migrate import *
+
+from sqlalchemy.ext.declarative import declarative_base
+TableBase = declarative_base()
+
+
+class User(TableBase):
+ __tablename__ = 'users'
+ id = Column(Integer, primary_key=True, nullable=False)
+ name = Column(Unicode(24), nullable=False, index=True, unique=True)
+
+class IdentityURL(TableBase):
+ __tablename__ = 'identity_urls'
+ id = Column(Integer, primary_key=True, nullable=False)
+ user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
+ url = Column(Unicode(250), nullable=False, index=True, unique=True)
+
+
+def upgrade(migrate_engine):
+ TableBase.metadata.bind = migrate_engine
+
+ User.__table__.create()
+ IdentityURL.__table__.create()
+
+def downgrade(migrate_engine):
+ TableBase.metadata.bind = migrate_engine
+
+ IdentityURL.__table__.drop()
+ User.__table__.drop()
View
0 migration/versions/__init__.py
No changes.
View
1 setup.py
@@ -15,6 +15,7 @@
install_requires=[
"Pylons>=0.9.7",
"SQLAlchemy>=0.5",
+ 'python-openid',
],
setup_requires=["PasteScript>=1.6.3"],
packages=find_packages(exclude=['ez_setup']),

0 comments on commit 1dfbf9f

Please sign in to comment.
Something went wrong with that request. Please try again.