Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Login and registration. Includes migration.
- Loading branch information
Showing
14 changed files
with
315 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,2 @@ | |||
<%inherit file="/base.mako" /> | |||
${next.body()} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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()} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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()} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,4 @@ | |||
This is a database migration repository. | |||
|
|||
More information at | |||
http://code.google.com/p/sqlalchemy-migrate/ |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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') }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters