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 Fields and Events 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, Div, P, H, InputGroup, Menu, HorizontalLayout,\
                         PasswordInput, ErrorFeedbackMessage
from reahl.web.layout import PageLayout
from reahl.web.pure import ColumnLayout, 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(PageLayout())
        contents_layout = ColumnLayout(('main', UnitSize('1/3'))).with_slots()
        self.layout.contents.use_layout(contents_layout)
        self.layout.header.add_child(Menu(view).use_layout(HorizontalLayout()).with_bookmarks(main_bookmarks))


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.login1tests: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.

Exercising 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.login1tests: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 = 'topsecret'

    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'), 'topsecret')
    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 Views 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 Slots 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 Views packaged as a single UserInterface. It is possible, however, to compose your web application from multiple UserInterfaces. 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 Views 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 Views 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 Slots. The writer of AccountUI (or any other UserInterface) does not know what Slots your application will have for plugging in its bits of user interface. Hence, each UserInterface writer chooses a couple of names for Slots needed for its Views . When you use such a UserInterface, you need to specify which of the UserInterface‘s Slots plug into which of your own application’s Slots.

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 Slots when re-using a UserInterface is similar.

In this example, we used a PageLayout with a ColumnLayout 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 Views 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.layout import PageLayout
from reahl.web.pure import ColumnLayout, 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(PageLayout())
        contents_layout = ColumnLayout(('main', UnitSize('1/3'))).with_slots()
        self.layout.contents.use_layout(contents_layout)
        self.layout.header.add_child(Menu(view).use_layout(HorizontalLayout()).with_bookmarks(main_bookmarks))


class LegalNotice(P):
    def __init__(self, view, text, name):
        super(LegalNotice, self).__init__(view, text=text, css_id=name)
        self.set_as_security_sensitive()


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))

        terms_of_service = self.define_view('/terms_of_service', title='Terms of service')
        terms_of_service.set_slot('main', LegalNotice.factory('The terms of services defined as ...', 'terms'))

        privacy_policy = self.define_view('/privacy_policy', title='Privacy policy')
        privacy_policy.set_slot('main', LegalNotice.factory('You have the right to remain silent ...', 'privacypolicy'))

        disclaimer = self.define_view('/disclaimer', title='Disclaimer')
        disclaimer.set_slot('main', LegalNotice.factory('Disclaim ourselves from negligence ...', 'disclaimer'))

        class LegalBookmarks(object):
            terms_bookmark = terms_of_service.as_bookmark(self)
            privacy_bookmark = privacy_policy.as_bookmark(self)
            disclaimer_bookmark = disclaimer.as_bookmark(self)

        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)



Exercising 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.login2tests: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 = 'topsecret'

    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'), 'topsecret')
    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]'))