Trying to use a template object as the UI for a stage results in this exception: AttributeError: 'BootstrapTemplate' object has no attribute '_get_model'
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).
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()
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()