Pipeline stage fails when returning a Template

Trying to use a template object as the UI for a stage results in this exception: AttributeError: 'BootstrapTemplate' object has no attribute '_get_model'

Code to replicate:

import param
import panel as pn

class Test(param.Parameterized):
    def panel(self):
        return pn.template.BootstrapTemplate()
        # return pn.Column('## Hello')
    
pipeline = pn.pipeline.Pipeline()
pipeline.add_stage('Stage1', Test)
2 Likes

Hi @sahvtsl

Welcome to the community.

A template is different than Columns, Rows and other panel components. It should only be used to wrap the final product.

You can use it as below.

import param
import panel as pn

class Stage1(param.Parameterized):
    def panel(self):
        return pn.Column('## Hello')

class Stage2(param.Parameterized):
    def panel(self):
        return pn.Column('## Hello')
    
pipeline = pn.pipeline.Pipeline()
pipeline.add_stage('Stage1', Stage1)
pipeline.add_stage('Stage2', Stage2)

pn.template.FastListTemplate(
    site="Panel", title="Pipeline",
    main=[pipeline]
).servable()

1 Like

Thanks a lot @Marc. Appreciate your help. The issue I am trying to really solve is that Stage 1 is a login page. The second stage is really the app and can really benefit from having the templatized layout. I am sorry for asking an open ended question but is there a better pattern I should be following than using pipelines? (I tried using pn.state.location.reload after the login data entry callback but that does not work).

1 Like

Hi @sahvtsl

How do you plan to log in users? Via username and password you store? Or via some external service like Github, Google, Azure etc?

There is documentation for “real” authentication here Authentication

There is a discussion about authentication here Tips/guide on creating a simple username/password auth for panel app? - Panel - HoloViz Discourse

1 Like

DON’T USE THIS VERSION. IMPROVED VERSION BELOW.

Here is an example of really basic authentication. I use the panel-modal extension.

Please note if you plan to use this for anything serious you should encrypt your users password. But this is out of scope of this example.

simple-auth

import param
import panel as pn
from panel_modal import Modal

pn.extension(sizing_mode="stretch_width")

content = pn.Column("Hello World")

class SimpleAuth(pn.viewable.Viewer):
    users = param.Dict({})
    user = param.String()
    password = param.String()
    
    authenticate = param.Event(label="Submit")
    
    authenticated = param.Boolean(default=False)
    login_attempts = param.Integer(0)

    def __init__(self, users, **params):
        super().__init__(users=users, **params)

        self._panel = Modal(pn.Column(
            self._login_message, 
            pn.widgets.TextInput.from_param(self.param.user, placeholder="Enter username here ..."), 
            pn.widgets.PasswordInput.from_param(self.param.password, placeholder="Enter password here ..."), 
            pn.widgets.Button.from_param(self.param.authenticate, sizing_mode="fixed", width=305, margin=(25,10), button_type="primary"), 
            sizing_mode="fixed", width=300, margin=(25,40,25,10)))

        pn.state.onload(self.open)

    def __panel__(self):
        return self._panel

    @pn.depends("authenticated", "login_attempts")
    def _login_message(self):
        if self.authenticated:
            return "You have been successfully authenticated"
        if self.login_attempts==0:
            return "Enter your username and password to log in"
        return "Your username and password is not valid"
    
    @pn.depends("authenticate", watch=True)
    def _handle_authenticate(self):
        # This is not strong security. You should never store your users passwords unencrypted
        # Ask Google or ChatGPT how to support encryption
        try:
            if self.users[self.user]==self.password:
                self.param.update(authenticated=True, login_attempts=self.login_attempts+1)
                self.close()
            else:
                self.login_attempts +=1
        except:
            self.login_attempts +=1

    def close(self):
        self._panel.close=True
    
    def open(self):
        self._panel.open=True

login = SimpleAuth(users={"marc": "logmein", "sahvtsl": "please"})
page = pn.Column(login, content)


pn.template.FastListTemplate(
    site="Panel", title="Page with simple auth",
    main=[page]
).servable()
2 Likes

Thanks @Marc. This is very helpful. This almost works for me except one thing. In the handle_authenticate callback, any updates to the template don’t show up. This is really what is the blocker for me. Depending on who logs in the content in the template sidebar needs to be different. Is there a better way for me to do force the template to show new content?

import param
import panel as pn
from panel_modal import Modal

pn.extension(sizing_mode="stretch_width")

content = pn.Column("Hello World")

class SimpleAuth(pn.viewable.Viewer):
    users = param.Dict({})
    user = param.String()
    password = param.String()
    
    authenticate = param.Event(label="Submit")
    
    authenticated = param.Boolean(default=False)
    login_attempts = param.Integer(0)

    def __init__(self, users, **params):
        super().__init__(users=users, **params)

        self._panel = Modal(pn.Column(
            self._login_message, 
            pn.widgets.TextInput.from_param(self.param.user, placeholder="Enter username here ..."), 
            pn.widgets.PasswordInput.from_param(self.param.password, placeholder="Enter password here ..."), 
            pn.widgets.Button.from_param(self.param.authenticate, sizing_mode="fixed", width=305, margin=(25,10), button_type="primary"), 
            sizing_mode="fixed", width=300, margin=(25,40,25,10)))

        pn.state.onload(self.open)

    def __panel__(self):
        return self._panel

    @pn.depends("authenticated", "login_attempts")
    def _login_message(self):
        if self.authenticated:
            return "You have been successfully authenticated"
        if self.login_attempts==0:
            return "Enter your username and password to log in"
        return "Your username and password is not valid"
    
    @pn.depends("authenticate", watch=True)
    def _handle_authenticate(self):
        # This is not strong security. You should never store your users passwords unencrypted
        # Ask Google or ChatGPT how to support encryption
        try:
            if self.users[self.user]==self.password:
                self.param.update(authenticated=True, login_attempts=self.login_attempts+1)
                content.extend(['content update'])
                ui.sidebar.extend(['logged in']) # THIS DOES NOT WORK
                self.close()
            else:
                self.login_attempts +=1
        except:
            self.login_attempts +=1

    def close(self):
        self._panel.close=True
    
    def open(self):
        self._panel.open=True

login = SimpleAuth(users={"marc": "logmein", "sahvtsl": "please"})
page = pn.Column(login, content)


ui = pn.template.FastListTemplate(
    site="Panel", title="Page with simple auth",
    main=[page]
)
ui.servable()

Yes. You can define a layout as a pn.Column. This works like a normal list. You can for example replace one or more items. So until the user is authenticated the layout can contain the login component. You can then depend on the authenticated event and replace the content on the layout when its trigger depending on which username logged in.

There is an example below.

import param
import panel as pn
from panel_modal import Modal

pn.extension(sizing_mode="stretch_width")

content = pn.Column("Hello World")

# Workaround so that user cannot close the dialog via escape or clicking outside of it
# See: https://a11y-dialog.netlify.app/advanced/alert-dialog for more detail
# I will probably support some parameters on the python side to handle this in a future version of panel-modal
Modal._template = (
    Modal._template
    .replace(
        """<div class="dialog-overlay" data-a11y-dialog-hide></div>""",
        """<div class="dialog-overlay" ></div>"""
    )
    .replace(
        """<div id="pnx_dialog" class="dialog-container bk-root" aria-hidden="true">""",
        """<div id="pnx_dialog" class="dialog-container bk-root" aria-hidden="true" role="alertdialog" aria-labelledby="your-dialog-title-id">"""
    )
)


class SimpleAuth(pn.viewable.Viewer):
    users = param.Dict({})
    user = param.String()
    password = param.String()
    
    authenticate = param.Event(label="Submit")
    
    authenticated = param.Boolean(default=False)
    login_attempts = param.Integer(0)

    def __init__(self, users, **params):
        super().__init__(users=users, **params)

        self._panel = Modal(pn.Column(
            self._login_message, 
            pn.widgets.TextInput.from_param(self.param.user, placeholder="Enter username here ..."), 
            pn.widgets.PasswordInput.from_param(self.param.password, placeholder="Enter password here ..."), 
            pn.widgets.Button.from_param(self.param.authenticate, sizing_mode="fixed", width=305, margin=(25,10), button_type="primary"), 
            sizing_mode="fixed", width=300, margin=(25,40,25,10)), show_close_button=False
            )

        pn.state.onload(self.open)

    def __panel__(self):
        return self._panel

    @pn.depends("authenticated", "login_attempts")
    def _login_message(self):
        if self.authenticated:
            return "You have been successfully authenticated"
        if self.login_attempts==0:
            return "Enter your username and password to log in"
        return "Your username and password is not valid"
    
    @pn.depends("authenticate", watch=True)
    def _handle_authenticate(self):
        # This is not strong security. You should never store your users passwords unencrypted
        # Ask Google or ChatGPT how to support encryption
        try:
            if self.users[self.user]==self.password:
                self.param.update(authenticated=True, login_attempts=self.login_attempts+1)
                self.close()
            else:
                self.login_attempts +=1
        except:
            self.login_attempts +=1

    def close(self):
        self._panel.close=True
    
    def open(self):
        self._panel.open=True

login = SimpleAuth(users={"marc": "logmein", "sahvtsl": "please"})
layout = pn.Column(login)

@pn.depends(authenticated=login.param.authenticated, user=login.param.user)
def page(authenticated, user):
    if not authenticated:
        layout[0]=login
    elif user=="marc":
        layout[0]="Hi Marc. You created this example"
    elif user=="sahvtsl":
        layout[0]="Hi sahvtsl. You can use this example"
    else:
        layout[0]=f"Hi {user}. Welcome to my site"
    return layout

pn.template.FastListTemplate(
    site="Panel", title="Page with simple auth",
    main=[page]
).servable()
1 Like

Beautiful! This works. Thanks a lot for your help @Marc

1 Like

Hi @Marc,

This is a very insightful solution you have here. How can you add a generic authenticator with its environment variables in a way it can take me to the organizational azure login upon licking on a SSO button found on the modal.

I am passing the oauth parameters all on CLI but would rather have them configured in my python script. Nevertheless, I want to hav ethe redirect URI take me to the template with the modal closed. I followed your steps but I don’t seem to know how to use routing and the environment variables.

Script below:

import panel as pn
import os
from panel_modal import Modal

pn.extension(sizing_mode="stretch_width")

content = pn.Column("Hello World")

class SimpleAuth(pn.viewable.Viewer):
    authenticate = param.Event(label="Login")
    
    authenticated = param.Boolean(default=False)
    login_attempts = param.Integer(0)

    def __init__(self):
        super().__init__()

        self._panel = Modal(pn.Column(
            self._login_message,
            pn.widgets.Button.from_param(self.param.authenticate, sizing_mode="fixed", width=305, height=15, margin=(25,10), button_type="primary"), 
            width=300, margin=(25,40,25,10)))

        pn.state.onload(self.open)

    def __panel__(self):
        return self._panel

    @pn.depends("authenticated", "login_attempts")
    def _login_message(self):
        if self.authenticated:
            return "You have been successfully authenticated"
        if self.login_attempts==0:
            return "Enter your username and password to log in"
        return "Your username and password is not valid"
    
    @pn.depends("authenticate", watch=True)
    def _handle_authenticate(self):
        # This is not strong security. You should never store your users passwords unencrypted
        # Ask Google or ChatGPT how to support encryption
        try:
#             if self.users[self.user]==self.password:
            os.environ['OAUTH_KEY'] = '*****************'
            os.environ['OAUTH_SECRET'] = '*************'
            os.environ['OAUTH_REDIRECT_URI'] = 'http://localhost:5006/app'
            os.environ['OAUTH_EXTRA_PARAMS'] = {'TOKEN_URL':'http://*****/connect/token', 
                                                'AUTHORIZE_URL':'http://*****/connect/authorize'}
            os.environ['COOKIE_SECRET'] = '******'
            self.param.update(authenticated=True, login_attempts=self.login_attempts+1)
            self.close()
#         else:
            self.login_attempts +=1
        except:
            self.login_attempts +=1

    def close(self):
        self._panel.close=True
    
    def open(self):
        self._panel.open=True

login = SimpleAuth()
page = pn.Column(login, content)

pn.template.FastListTemplate(
    site="Panel", 
    title="Page with simple auth",
    main=[page]
).servable().show()