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().__init__(view, 'login')
self.use_layout(FormLayout())
if self.exception:
self.layout.add_alert_for_domain_exception(self.exception)
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)
self.add_child(Button(self, login_session.events.log_in, style='primary'))
The complete example¶
from passlib.hash import pbkdf2_sha256
from sqlalchemy import Column, ForeignKey, Integer, Unicode, UnicodeText
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.page import HTML5Page
from reahl.web.bootstrap.ui import P
from reahl.web.bootstrap.forms import Form, TextInput, Button, PasswordInput, 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().__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().__init__(view, 'login')
self.use_layout(FormLayout())
if self.exception:
self.layout.add_alert_for_domain_exception(self.exception)
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)
self.add_child(Button(self, login_session.events.log_in, 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)