Re-use: Allowing users to log in to your system

There’s no need to build login functionality yourself, Reahl provides this already.

You can opt to use only the provided model and write your own user interface, or use both the model and the user interface shipped by Reahl.

Re-using only a domain model

The user and system account functionality is in the reahl-domain component, in the module reahl.domain.systemaccountmodel.

To be able to use it in your code, list the reahl-domain component as a dependency in the pyproject.toml file of your own project:

[build-system]
requires = [
  "setuptools >= 68",
  "wheel",
  "setuptools-git >= 1.1",
  "pytest-runner",
  "toml",
  "reahl-component-metadata >= 7.0.0"
  ]
build-backend = "setuptools.build_meta"

[project]
name = "login1bootstrap"
version = "0.1"
requires-python = ">=3.8"
dependencies = [
  "reahl-web>=7.0,<7.1",
  "reahl-component>=7.0,<7.1",
  "reahl-sqlitesupport>=7.0,<7.1",
  "reahl-sqlalchemysupport>=7.0,<7.1",
  "reahl-web-declarative>=7.0,<7.1",
  "reahl-domain>=7.0,<7.1"
  ]
authors = [ 
  {name = "Iwan Vosloo", email = "[email protected]"},
  {name = "Craig Sparks", email = "[email protected]"}
]

[tool.setuptools]
py-modules = ["login1bootstrap"]

[tool.setuptools.packages.find]
exclude = ["etc", "build", "dist"]

[tool.reahl-component]

Note

Remember, each time you change a pyproject.toml file, as explained in Fetch Addresses from the database, you need to run: python -m pip install --no-deps -e .

Use for_current_session() in your code to find the current LoginSession.

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.





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, FormLayout, PasswordInput
from reahl.web.bootstrap.navs import Nav, TabLayout
from reahl.web.bootstrap.grid import ColumnLayout, ColumnOptions, ResponsiveSize, Container
from reahl.domain.systemaccountmodel import AccountManagementInterface, LoginSession



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(md=4))).with_slots()
        self.layout.contents.use_layout(contents_layout)
        self.layout.header.add_child(Nav(view).use_layout(TabLayout()).with_bookmarks(main_bookmarks))


class LoginForm(Form):
    def __init__(self, view):
        super().__init__(view, 'login')
        self.use_layout(FormLayout())
        accounts = AccountManagementInterface.for_current_session()

        if self.exception:
            self.layout.add_alert_for_domain_exception(self.exception)

        self.layout.add_input(TextInput(self, accounts.fields.email))
        self.layout.add_input(PasswordInput(self, accounts.fields.password))

        self.define_event_handler(accounts.events.login_event)
        self.add_child(Button(self, accounts.events.login_event, style='primary'))


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)






Exercising tutorial.login1bootstrap

When you fire up the example application, there will be no users registered to play with. To create some, run:

pytest -o python_functions=demo_setup

This creates a user and password using the password and email address in:

@uses(web_fixture=WebFixture)
class LoginFixture(Fixture):

    def new_browser(self):
        return Browser(self.web_fixture.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

Re-using the user interface as well

If you want to use the provided user interface as well, you can graft an AccountUI onto its own new URL underneath LoginUI. The URLs of the Views of AccountUI are then appended to that URL.

Call define_user_interface() to graft AccountUI onto LoginUI.

In this call you need to specify which Slot name as declared by AccountUI plugs into which Slot names we have available on MenuPage.

AccountUI expects a number of Bookmarks to UrlBoundViews that contain legal text specific to your site. These Bookmarks are sent as an additional keyword argument to define_user_interface(), from where it will be delivered to AccountUI.




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.navs import Nav, TabLayout
from reahl.web.bootstrap.grid import ColumnLayout, ColumnOptions, ResponsiveSize, Container
from reahl.domain.systemaccountmodel import LoginSession
from reahl.domainui.bootstrap.accounts import AccountUI



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(md=4))).with_slots()
        self.layout.contents.use_layout(contents_layout)
        self.layout.header.add_child(Nav(view).use_layout(TabLayout()).with_bookmarks(main_bookmarks))


class LegalNotice(P):
    def __init__(self, view, text, name):
        super().__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:
            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', '/registerHelp', '/verify']]
        bookmarks = [home.as_bookmark(self)]+account_bookmarks
        self.define_page(MenuPage, bookmarks)