The web session problem¶
User sessions¶
Objects created during a request to the web browser only exist for the duration of that request. If you want them to live longer, they need to be persisted in a database.
Sometimes persisted objects are only needed for use in the current sequence of interactions between a browser and server, after which they can be safely deleted.
You can persist objects on the server for the duration of a
UserSession
by making them “session scoped”. Session scoped
persisted objects are retained for as long as the UserSession
exists.
In the example here we build a simplified version of the login functionality already provided by Reahl to show how session scoped objects can be used.
The example consists of User object and a LoginSession object:
The application itself has two pages—a home page on which the name of the User who is currently logged in is displayed, and a page on which you can log in using your email address and a password.
Declaring the LoginSession¶
Create a LoginSession persisted class and decorate it using the
session_scoped()
decorator.
@session_scoped
class LoginSession(Base):
__tablename__ = 'sessscope_loginsession'
id = Column(Integer, primary_key=True)
current_user_id = Column(Integer, ForeignKey(User.id))
current_user = relationship(User)
@exposed
def fields(self, fields):
fields.email_address = EmailField(label='Email', required=True)
fields.password = PasswordField(label='Password', required=True)
@exposed
def events(self, events):
events.log_in = Event(label='Log in', action=Action(self.log_in))
def log_in(self):
matching_users = Session.query(User).filter_by(email_address=self.email_address)
if matching_users.count() != 1:
raise InvalidPassword()
user = matching_users.one()
if user.matches_password(self.password):
self.current_user = user
else:
raise InvalidPassword()
The LoginSession keeps track of who is currently logged in through its .current_user relationship to a User.
The email_address and password are
Field
s on the LoginSession
– they are to be linked to Inputs on the user interface. (The same
goes for the log_in Event
.)
To complete the model itself, here is the implementation of User:
class User(Base):
__tablename__ = 'sesscope_user'
id = Column(Integer, primary_key=True)
email_address = Column(UnicodeText, nullable=False)
name = Column(UnicodeText, nullable=False)
password_hash = Column(Unicode(1024), nullable=False)
def set_password(self, password):
self.password_hash = pbkdf2_sha256.hash(password)
def matches_password(self, password):
return pbkdf2_sha256.verify(password, self.password_hash)
Using a session scoped object¶
In this example, we obtain the current LoginSession at the beginning
of .assemble() of the UserInterface
, and send it around as argument to the
login page.
LoginSession.for_current_session() will check whether there is a LoginSession for the current UserSession and create one if necessary:
class SessionScopeUI(UserInterface):
def assemble(self):
login_session = LoginSession.for_current_session()
if login_session.current_user:
user_name = login_session.current_user.name
else:
user_name = 'Guest'
home = self.define_view('/', title='Home')
home.set_slot('main', P.factory(text='Welcome %s' % user_name))
login_page = self.define_view('/login', title='Log in')
login_page.set_slot('main', LoginForm.factory(login_session))
bookmarks = [i.as_bookmark(self) for i in [home, login_page]]
self.define_page(MenuPage, bookmarks)
Dealing with DomainExceptions¶
A DomainException
is an exception that can be raised
to communicate a domain-specific error condition to the user.
In this example it may happen that a user supplies an incorrect password. That’s why an InvalidPassword exception is raised inside the .log_in() method of LoginSession.
When the InvalidPassword (a DomainException
) is raised, the Reahl
framework ignores all Transition
s defined,
and re-displays the current UrlBoundView
. The
exception
will then contain the
DomainException
that is keeping the user from progressing. Display
this exception using an Alert
to
inform the user of the issue:
class LoginForm(Form):
def __init__(self, view, login_session):
super(LoginForm, self).__init__(view, 'login')
self.use_layout(FormLayout())
if self.exception:
self.add_child(Alert(view, self.exception.as_user_message(), 'warning'))
self.layout.add_input(TextInput(self, login_session.fields.email_address))
self.layout.add_input(PasswordInput(self, login_session.fields.password))
self.define_event_handler(login_session.events.log_in)
btn = self.add_child(Button(self, login_session.events.log_in))
btn.use_layout(ButtonLayout(style='primary'))
The complete example¶
from __future__ import print_function, unicode_literals, absolute_import, division
from passlib.hash import pbkdf2_sha256
from sqlalchemy import Column, ForeignKey, Integer, Unicode, UnicodeText, String
from sqlalchemy.orm import relationship
from reahl.sqlalchemysupport import Session, Base, session_scoped
from reahl.component.exceptions import DomainException
from reahl.web.fw import UserInterface
from reahl.web.layout import PageLayout
from reahl.web.bootstrap.ui import HTML5Page, P, Alert
from reahl.web.bootstrap.forms import Form, TextInput, Button, PasswordInput, ButtonLayout, FormLayout
from reahl.web.bootstrap.navs import Nav, TabLayout
from reahl.web.bootstrap.grid import ColumnLayout, ColumnOptions, ResponsiveSize, Container
from reahl.component.modelinterface import Action, EmailField, Event, PasswordField, exposed
class User(Base):
__tablename__ = 'sesscope_user'
id = Column(Integer, primary_key=True)
email_address = Column(UnicodeText, nullable=False)
name = Column(UnicodeText, nullable=False)
password_hash = Column(Unicode(1024), nullable=False)
def set_password(self, password):
self.password_hash = pbkdf2_sha256.hash(password)
def matches_password(self, password):
return pbkdf2_sha256.verify(password, self.password_hash)
@session_scoped
class LoginSession(Base):
__tablename__ = 'sessscope_loginsession'
id = Column(Integer, primary_key=True)
current_user_id = Column(Integer, ForeignKey(User.id))
current_user = relationship(User)
@exposed
def fields(self, fields):
fields.email_address = EmailField(label='Email', required=True)
fields.password = PasswordField(label='Password', required=True)
@exposed
def events(self, events):
events.log_in = Event(label='Log in', action=Action(self.log_in))
def log_in(self):
matching_users = Session.query(User).filter_by(email_address=self.email_address)
if matching_users.count() != 1:
raise InvalidPassword()
user = matching_users.one()
if user.matches_password(self.password):
self.current_user = user
else:
raise InvalidPassword()
class MenuPage(HTML5Page):
def __init__(self, view, main_bookmarks):
super(MenuPage, self).__init__(view)
self.use_layout(PageLayout(document_layout=Container()))
contents_layout = ColumnLayout(ColumnOptions('main', size=ResponsiveSize())).with_slots()
self.layout.contents.use_layout(contents_layout)
self.layout.header.add_child(Nav(view).use_layout(TabLayout()).with_bookmarks(main_bookmarks))
class InvalidPassword(DomainException):
def as_user_message(self):
return 'The email/password given do not match.'
class LoginForm(Form):
def __init__(self, view, login_session):
super(LoginForm, self).__init__(view, 'login')
self.use_layout(FormLayout())
if self.exception:
self.add_child(Alert(view, self.exception.as_user_message(), 'warning'))
self.layout.add_input(TextInput(self, login_session.fields.email_address))
self.layout.add_input(PasswordInput(self, login_session.fields.password))
self.define_event_handler(login_session.events.log_in)
btn = self.add_child(Button(self, login_session.events.log_in))
btn.use_layout(ButtonLayout(style='primary'))
class SessionScopeUI(UserInterface):
def assemble(self):
login_session = LoginSession.for_current_session()
if login_session.current_user:
user_name = login_session.current_user.name
else:
user_name = 'Guest'
home = self.define_view('/', title='Home')
home.set_slot('main', P.factory(text='Welcome %s' % user_name))
login_page = self.define_view('/login', title='Log in')
login_page.set_slot('main', LoginForm.factory(login_session))
bookmarks = [i.as_bookmark(self) for i in [home, login_page]]
self.define_page(MenuPage, bookmarks)