Re-use: Allowing users to log in to your system¶
In the previous section, a web application was built into which users can log in using a password. As part of this, some model code needed to be written – to model such Users and their login credentials.
If you are sick of having to re-write this functionality for each new web application, you can opt to use such functionality that is implemented by Reahl itself. You have options too: you can choose to only use the model behind the scenes and write your own user interface, or use both the model and user interface shipped by Reahl.
Re-using a domain model¶
To re-use a domain model is really easy. It basically just means re-using a library of code shipped by someone. Reahl distributes such code in components that are packaged as Python Eggs. The code to allow users to log into a system is shipped in the reahl-domain component, in the module reahl.systemaccountmodel.
In there is a class, AccountManagementInterface which is
session-scoped. In other words to get an AccountManagementInterface,
you just call AccountManagementInterface.for_current_session(). The
LoginSession
class is also in this module. It is session-scoped
and serves the same purpose as the LoginSession of the
previous example.
An AccountManagementInterface has many Field
s and Event
s available for
use by user interface code. It allows for logging in and out, but also
for registration, verification of the email addresses of new
registrations, resetting forgotten passwords, etc.
The LoginSession
object itself is mostly only used for finding out who
is currently logged in.
Here is our simple login application, changed to use this code, instead of its own little model:
from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.web.fw import UserInterface
from reahl.web.ui import HTML5Page, Form, TextInput, LabelledBlockInput, Button, Panel, P, H, InputGroup, Menu, HorizontalLayout,\
PasswordInput, ErrorFeedbackMessage
from reahl.web.pure import PageColumnLayout, UnitSize
from reahl.domain.systemaccountmodel import AccountManagementInterface, LoginSession
class MenuPage(HTML5Page):
def __init__(self, view, main_bookmarks):
super(MenuPage, self).__init__(view, style='basic')
self.use_layout(PageColumnLayout(('main', UnitSize('1/3'))))
self.layout.header.add_child(Menu.from_bookmarks(view, main_bookmarks).use_layout(HorizontalLayout()))
class LoginForm(Form):
def __init__(self, view):
super(LoginForm, self).__init__(view, 'login')
accounts = AccountManagementInterface.for_current_session()
if self.exception:
self.add_child(ErrorFeedbackMessage(view, self.exception.as_user_message()))
self.add_child(LabelledBlockInput(TextInput(self, accounts.fields.email)))
self.add_child(LabelledBlockInput(PasswordInput(self, accounts.fields.password)))
self.define_event_handler(accounts.events.login_event)
self.add_child(Button(self, accounts.events.login_event))
class LoginUI(UserInterface):
def assemble(self):
login_session = LoginSession.for_current_session()
if login_session.account:
logged_in_as = login_session.account.email
else:
logged_in_as = 'Guest'
home = self.define_view('/', title='Home')
home.set_slot('main', P.factory(text='Welcome %s' % logged_in_as))
login_page = self.define_view('/login', title='Log in')
login_page.set_slot('main', LoginForm.factory())
bookmarks = [i.as_bookmark(self) for i in [home, login_page]]
self.define_page(MenuPage, bookmarks)
To be able to use the reahl-domain component in your code as shown above, you have to show that your component needs it. This is done by listing the reahl-domain component as a dependency in the .reahlproject file of your own component. Here reahl-domain is listed in the .reahlproject of this example:
<project type="egg">
<deps purpose="run">
<egg name="reahl-web"/>
<egg name="reahl-component"/>
<egg name="reahl-sqlalchemysupport"/>
<egg name="reahl-web-declarative"/>
<egg name="reahl-domain"/>
</deps>
<deps purpose="test">
<egg name="reahl-tofu"/>
<egg name="reahl-stubble"/>
<egg name="reahl-dev"/>
<egg name="reahl-webdev"/>
</deps>
<alias name="unit" command="setup -- -q nosetests -s --with-id --nologcapture --tests=login1_dev -t -i '.*'"/>
<alias name="demosetup" command="setup -- -q nosetests -F reahl.webdev.fixtures:BrowserSetup --with-setup-fixture=reahl.doc.examples.tutorial.login1.login1_dev.logintests1:DemoFixture"/>
</project>
Remember, each time you change a .reahlproject file, you need to
run reahl setup -- develop -N
, as explained in A basic model.
Code is one thing, but re-using model code like this has other implications. What about the database? Certainly some tables need to be created in the database and maintained?
The underlying component framework will take care of creating the correct database schema for all components that are listed as dependencies in your .reahlproject. Once again, refer back to A basic model if you forgot how this works.
Excercising the application¶
Our example application would be difficult to fire up and play with, because it has no user interface for registering new user accounts!
The tests can come to the rescue here. You can of course play with the application indirectly by running (or even using pdb to step through) the tests themselves, but it is always nice to just play with an application to see what it does.
In order to do the latter, a special fixture can be created. The Reahl
extensions to nosetests let you run (and commit) the setup of a
particular fixture. This creates a database for you, populated with
whatever was created during the set up of that Fixture
. After doing
this, you can serve the application using this database and play around.
DemoFixture in the example test is provided for this purpose. To use it, execute:
nosetests --with-run-fixture=reahl.webdev.fixtures:BrowserSetup --with-setup-fixture=reahl.doc.examples.tutorial.login1.login1_dev.logintests1:DemoFixture
Here is all the code of the test itself:
from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.tofu import test, set_up
from reahl.sqlalchemysupport import Session
from reahl.web_dev.fixtures import WebFixture
from reahl.webdev.tools import Browser, XPath
from reahl.doc.examples.tutorial.login1.login1 import LoginUI
from reahl.domain.systemaccountmodel import EmailAndPasswordSystemAccount
class LoginFixture(WebFixture):
def new_browser(self):
return Browser(self.new_wsgi_app(site_root=LoginUI))
password = 'bobbejaan'
def new_account(self):
account = EmailAndPasswordSystemAccount(email='[email protected]')
Session.add(account)
account.set_new_password(account.email, self.password)
account.activate()
return account
class DemoFixture(LoginFixture):
commit=True
@set_up
def do_demo_setup(self):
self.account
Session.commit()
@test(LoginFixture)
def logging_in(fixture):
"""A user can log in by going to the Log in page.
The name of the currently logged in user is displayed on the home page."""
browser = fixture.browser
account = fixture.account
browser.open('/')
browser.click(XPath.link_with_text('Log in'))
browser.type(XPath.input_labelled('Email'), '[email protected]')
browser.type(XPath.input_labelled('Password'), 'bobbejaan')
browser.click(XPath.button_labelled('Log in'))
browser.click(XPath.link_with_text('Home'))
assert browser.is_element_present(XPath.paragraph_containing('Welcome [email protected]'))
@test(LoginFixture)
def domain_exception(fixture):
"""Typing the wrong password results in an error message being shown to the user."""
browser = fixture.browser
account = fixture.account
browser.open('/')
browser.click(XPath.link_with_text('Log in'))
browser.type(XPath.input_labelled('Email'), '[email protected]')
browser.type(XPath.input_labelled('Password'), 'wrong password')
browser.click(XPath.button_labelled('Log in'))
assert browser.is_element_present(XPath.paragraph_containing('Invalid login credentials'))
Re-using the user interface as well¶
If you’re really lazy, you can write even less code. You can re-use
the UserInterface
shipped with Reahl that contains View
s for logging in and
other related account management.
Re-using bits of web-based user interface traditionally has been quite difficult. In part this is because each web application has a distinct look and layout. How does one re-use a complete bit of “page flow” in different web applications, each application with its own look and layout?
The concepts of a UserInterface
, of a page and Slot
s exist to make
such re-use possible. The basics of these concepts is explained in
Make light work of similar-looking pages, but there’s more to it than is explained
there. In examples thus far, each web application consisted of a few
related View
s packaged as a single UserInterface
. It is possible, however, to
compose your web application from multiple UserInterface
s. Hence, you can use
a UserInterface
that someone else has built beforehand.
In the reahl-domainui component, in the reahl.domainui.accounts
module, lives a UserInterface
called AccountUI
. It contains View
s for
logging in, registering a new account, and more. In order to use it,
you will need to give it its own URL in your web application – just
like you’d define a View
on a particular URL. The URLs of the View
s of
the AccountUI
will then be appended to the URL you use for the
UserInterface
itself.
The page of your web application defines a number of Slot
s. The
writer of AccountUI
(or any other UserInterface
) does not know what Slot
s
your application will have for plugging in its bits of user
interface. Hence, each UserInterface
writer chooses a couple of names for
Slot
s needed for its View
s . When you use such a UserInterface
, you need to
specify which of the UserInterface
‘s Slot
s plug into which of your own
application’s Slot
s.
A UserInterface
and its Slot
names are analogous to a method and its
arguments: the method signature contains variable names chosen by the
programmer who wrote it. You can call that method from many different
places, passing in different values for those arguments. Specifying
the Slot
s when re-using a UserInterface
is similar.
In this example, we used the PageColumnLayout
to add a Slot
named main to our MenuPage. The AccountUI
in turn has a Slot
named main_slot. Hence, it is necessary to
state that main_slot of AccountUI
plugs into main of our MenuPage.
AccountUI
also has its own requirements: it needs a number of
bookmarks. Specifically, it expects to be passed an object from which
it can get a bookmark for: a page listing the terms of service of the
site, the privacy policy of the site, and a disclaimer.
The code below is our entire little application, rewritten to use the
AccountUI
. (Note that since our application does not have View
s
for all the legal bookmarks linked to by the AccountUI
, the home
page of the application has been used for each legal bookmark in this
example.)
Just to show a few more bits of the AccountUI
, the example has
been modified to include some more items in its Menu
:
from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.web.fw import UserInterface
from reahl.web.ui import HTML5Page, P, Menu, HorizontalLayout
from reahl.web.pure import PageColumnLayout, UnitSize
from reahl.domain.systemaccountmodel import LoginSession
from reahl.domainui.accounts import AccountUI
class MenuPage(HTML5Page):
def __init__(self, view, main_bookmarks):
super(MenuPage, self).__init__(view, style='basic')
self.use_layout(PageColumnLayout(('main', UnitSize('1/3'))))
self.layout.header.add_child(Menu.from_bookmarks(view, main_bookmarks).use_layout(HorizontalLayout()))
class LoginUI(UserInterface):
def assemble(self):
login_session = LoginSession.for_current_session()
if login_session.account:
logged_in_as = login_session.account.email
else:
logged_in_as = 'Guest'
home = self.define_view('/', title='Home')
home.set_slot('main', P.factory(text='Welcome %s' % logged_in_as))
class LegalBookmarks(object):
terms_bookmark = home.as_bookmark(self, description='Terms of service')
privacy_bookmark = home.as_bookmark(self, description='Privacy policy')
disclaimer_bookmark = home.as_bookmark(self, description='Disclaimer')
accounts = self.define_user_interface('/accounts', AccountUI,
{'main_slot': 'main'},
name='accounts', bookmarks=LegalBookmarks)
account_bookmarks = [accounts.get_bookmark(relative_path=relative_path)
for relative_path in ['/login', '/register']]
bookmarks = [home.as_bookmark(self)]+account_bookmarks
self.define_page(MenuPage, bookmarks)
Excercising the lazier application¶
Warning
You have to start our test email server before running this app.
This application may try to send out email – when you register, for example. And since you do not have an email server running on your development machine that will probably break. To solve this, run the following (in a different terminal):
reahl servesmtp
This command runs a simple email server which just shows every email it receives in the terminal where it is running.
Of course, some tests are included for our lazy application. The tests once
again include a Fixture
for setting up the application database as before:
nosetests --with-run-fixture=reahl.webdev.fixtures:BrowserSetup --with-setup-fixture=reahl.doc.example.tutorial.login2.login2_dev.logintests2:DemoFixture
Finally, and for completeness, here is the test code:
from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.tofu import test, set_up
from reahl.web_dev.fixtures import WebFixture
from reahl.webdev.tools import Browser, XPath
from reahl.sqlalchemysupport import Session
from reahl.domain.systemaccountmodel import EmailAndPasswordSystemAccount
from reahl.doc.examples.tutorial.login2.login2 import LoginUI
class LoginFixture(WebFixture):
def new_browser(self):
return Browser(self.new_wsgi_app(site_root=LoginUI))
password = 'bobbejaan'
def new_account(self):
account = EmailAndPasswordSystemAccount(email='[email protected]')
Session.add(account)
account.set_new_password(account.email, self.password)
account.activate()
return account
class DemoFixture(LoginFixture):
commit=True
@set_up
def do_demo_setup(self):
self.account
Session.commit()
@test(LoginFixture)
def logging_in(fixture):
"""A user can log in by going to the Log in page.
The name of the currently logged in user is displayed on the home page."""
browser = fixture.browser
account = fixture.account
browser.open('/')
browser.click(XPath.link_with_text('Log in with email and password'))
browser.type(XPath.input_labelled('Email'), '[email protected]')
browser.type(XPath.input_labelled('Password'), 'bobbejaan')
browser.click(XPath.button_labelled('Log in'))
browser.click(XPath.link_with_text('Home'))
assert browser.is_element_present(XPath.paragraph_containing('Welcome [email protected]'))