Skip to content

Commit

Permalink
Login and registration. Includes migration.
Browse files Browse the repository at this point in the history
  • Loading branch information
eevee committed May 19, 2010
1 parent 714e21b commit 1dfbf9f
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 28 deletions.
9 changes: 9 additions & 0 deletions floof/config/routing.py
Expand Up @@ -13,11 +13,20 @@ def make_map():
always_scan=config['debug']) always_scan=config['debug'])
map.minimization = False map.minimization = False


require_POST = dict(conditions=dict(method=['POST']))

# The ErrorController route (handles 404/500 error pages); it should # The ErrorController route (handles 404/500 error pages); it should
# likely stay at the top, ensuring it can always be resolved # likely stay at the top, ensuring it can always be resolved
map.connect('/error/{action}', controller='error') map.connect('/error/{action}', controller='error')
map.connect('/error/{action}/{id}', 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') map.connect('/', controller='main', action='index')


return map return map
175 changes: 175 additions & 0 deletions 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)
5 changes: 3 additions & 2 deletions floof/lib/helpers.py
Expand Up @@ -3,5 +3,6 @@
Consists of functions to typically be used within templates, but also Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to templates as 'h'. available to Controllers. This module is available to templates as 'h'.
""" """
# Import helpers as desired, or define your own, ie: from pylons import url
#from webhelpers.html.tags import checkbox, password from webhelpers.html import escape, HTML, literal, url_escape
from webhelpers.html.tags import *
58 changes: 32 additions & 26 deletions floof/model/__init__.py
@@ -1,36 +1,42 @@
"""The application's model objects""" """The application's model objects"""
import sqlalchemy as sa from sqlalchemy import Column, ForeignKey, MetaData, Table
from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relation
from sqlalchemy.types import *


from floof.model import meta from floof.model import meta


def init_model(engine): def init_model(engine):
"""Call me before using any of the tables or classes in the model""" """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.Session.configure(bind=engine)
meta.engine = engine meta.engine = engine




## Non-reflected tables may be defined and mapped at module level TableBase = declarative_base()
#foo_table = sa.Table("Foo", meta.metadata,
# sa.Column("id", sa.types.Integer, primary_key=True), ### USERS
# sa.Column("bar", sa.types.String(255), nullable=False),
# ) class User(TableBase):
# __tablename__ = 'users'
#class Foo(object): id = Column(Integer, primary_key=True, nullable=False)
# pass name = Column(Unicode(24), nullable=False, index=True, unique=True)
#
#orm.mapper(Foo, foo_table) @property

def display_name(self):

"""Returns a flavory string that should be used to present this user.
## 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 return self.name
#
#class Reflected(object): class IdentityURL(TableBase):
# pass __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')
2 changes: 2 additions & 0 deletions floof/templates/account/base.mako
@@ -0,0 +1,2 @@
<%inherit file="/base.mako" />
${next.body()}
10 changes: 10 additions & 0 deletions 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()}
22 changes: 22 additions & 0 deletions 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()}
7 changes: 7 additions & 0 deletions 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...
4 changes: 4 additions & 0 deletions migration/README
@@ -0,0 +1,4 @@
This is a database migration repository.

More information at
http://code.google.com/p/sqlalchemy-migrate/
Empty file added migration/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions 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') }}
30 changes: 30 additions & 0 deletions 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()
Empty file added migration/versions/__init__.py
Empty file.
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -15,6 +15,7 @@
install_requires=[ install_requires=[
"Pylons>=0.9.7", "Pylons>=0.9.7",
"SQLAlchemy>=0.5", "SQLAlchemy>=0.5",
'python-openid',
], ],
setup_requires=["PasteScript>=1.6.3"], setup_requires=["PasteScript>=1.6.3"],
packages=find_packages(exclude=['ez_setup']), packages=find_packages(exclude=['ez_setup']),
Expand Down

0 comments on commit 1dfbf9f

Please sign in to comment.