Dash → Django → Dash : Unlock a New way of Building Fullstack Dash Applications
Hey Everyone, excited to announce this breakthrough. Honestly feel like this has a lot of potential as it could possibly allow an evolution within python full-stack development. Many trailblazers lead the way, Django-Plotly-Dash for instance was the first one to create a direct connection between the two frameworks.
My goal was to decouple the direct relationship setup within that package as with the introduction of pages and a need to maintain an up-to-date dash application independent from Django , my hypothesis is I would receive many benefits if I was able to connect them with an API instead of running dash in Django as has been the norm sense 2018.
While maintaining all the features and benefits of both frameworks.
The idea outlined in my project schema is to create two independent houses running separate applications instead of keeping them under the same project.
Ideally with everything in dash being designed to take advantage of .json what better way of setting up authentication, data fetching, and uploading than continuing from that basis?
This is why I’ve designed what is an advanced project, Django houses a few apps but for the purpose of this article and dash the only import one is the API. Building off FASTapi, I went with Ninja which is a powerful alternative that maintains the Django syntax along with a list of useful features.
Before we jump into the nit and gritty, let’s look at the Django application was setup and how the Dash app.py was setup.
Although this doesn’t cover everything… its a damn good start for getting Django hosted in a docker container with a Postgres database along with a bunch of useful optional features: https://youtu.be/oGHQCapKsac
The next step would be to install Ninja with pip install django-ninja
& pip install django-ninja-jwt
With this, we need to make a few changes to our Django application first create an app to manage the API and connect it within the setting.py
Then we need to go the main urls.py and it should look like:
urls.py
from django.contrib import admin
from django.urls import path, include
from ninja_extra import NinjaExtraAPI, api_controller
from api.views import router as account_router
from ninja_jwt.controller import NinjaJWTDefaultController
# Starts the API with the NinjaExtraAPI
api = NinjaExtraAPI()
# Create a base rout for user management and authentication, you can add more routers here
api.add_router('/account/', account_router)
# Register the NinjaJWTDefaultController
api.register_controllers(NinjaJWTDefaultController)
urlpatterns = [
path('admin/', admin.site.urls),
path("api/", api.urls),
]
This next section has some fluff and a starting point for register but i still need to do some tweaking:
api/views.py
import pytz
from datetime import datetime
from ninja import NinjaAPI, Form
from ninja.security import HttpBasicAuth
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth import authenticate as auth_authenticate
from email_validator import validate_email, EmailNotValidError
from django.core.signing import Signer
from colorama import Fore as Color
from django.http import HttpResponse
from ninja import Router
router = Router()
class BasicAuth(HttpBasicAuth):
def authenticate(self, request, username, password):
# user = authenticate(request, username=username, password=password)
# Request all Users on Application
users = User.objects.all()
# check username vs user in database
user_in_users = users.values_list('username', flat=True)
print('user_in_users', list(user_in_users))
emails = users.values_list('email', flat=True)
print('emails', list(emails))
print('user provided', username)
# check if username or email in database return login or none
if username in list(user_in_users):
user = auth_authenticate(username=username, password=password)
if user is not None:
def cookie_save_user_id_function(request, current_time):
signer = Signer()
user_id = User.objects.get(username=username).id
signed_obj = signer.sign_object({'username': username, 'email': user.email,'id': user_id, 'logged_in': True, 'time_logged_in': current_time})
return {'username': username, 'email': user.email,'id': user_id, 'logged_in': True, 'time_logged_in': current_time, 'signed_obj': signed_obj}
user = authenticate(request, username=username, password=password)
if user is not None:
# login user
login(request, user)
central_time = pytz.timezone('US/Central')
current_time = datetime.now(central_time)
current_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
cookie = cookie_save_user_id_function(request, current_time)
# grab httpresponse
response = HttpResponse('You are logged in')
# set cookie
response.set_cookie('cookie', cookie['signed_obj'])
print(Color.GREEN+'Was able to login user, saved cookie of user_id to logged in', cookie)
print(Color.CYAN + 'User:', user, Color.RESET + f'Just logged into the application at: {current_time}')
# the credentials are valid
return cookie
else:
print(Color.RED +'User was found but not able to authenticate on django')
# the credentials are invalid
return None
else:
# the credentials are invalid
return None
elif username in list(emails):
print(Color.YELLOW + 'Wasn\'t able to find Username, trying email')
print(Color.GREEN + 'Email in database')
# gets username from email
u = User.objects.get(email=username)
def cookie_save_user_id_function(request, current_time):
signer = Signer()
user_id = User.objects.get(email=username).id
signed_obj = signer.sign_object(
{'username': username, 'email': user.email, 'id': user_id, 'logged_in': True,
'time_logged_in': current_time})
return {'username': u.username ,'email': username, 'id': user_id, 'logged_in': True, 'time_logged_in': current_time, 'signed_obj': signed_obj}
# checks authentication but doesn't login
# user = auth_authenticate(username=u, password=password)
# authenticate
user = authenticate(request, username=u, password=password)
if user is not None:
print('testing username')
print(u.username)
# login user
login(request, user)
central_time = pytz.timezone('US/Central')
current_time = datetime.now(central_time)
current_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
cookie = cookie_save_user_id_function(request, current_time)
# grab httpresponse
response = HttpResponse('You are logged in')
# set cookie
response.set_cookie('cookie', cookie['signed_obj'])
print(Color.GREEN + 'Was able to login user, saved cookie of user_id to logged in', cookie['signed_obj'])
print(Color.CYAN + 'User:', user, Color.RESET + f'Just logged into the application at: {current_time}')
# the credentials are valid
return cookie
else:
print(Color.RED + 'Username / Email not in database')
# the credentials are invalid
return None
@router.get("/login", auth=BasicAuth())
def account_login(request):
return {"httpuser": request.auth}
@router.post("/register")
def register(request, username: str = Form(...), email: str = Form(...), password: str = Form(...), password_checker: str = Form(...)):
# Request all Users on Application
users = User.objects.all()
# check username vs user in database
user_in_users = users.values_list('username', flat=True)
# print('user_in_users', list(user_in_users))
emails = users.values_list('email', flat=True)
email_checker = validate_email(email)
print('Testing email_checker')
print(email_checker)
if username in list(user_in_users):
print('Username already taken')
return request.content_params == {'Username': 'already taken'}
elif email in list(emails):
print('Email already taken')
return request.content_params == {'Email': 'already taken'}
elif validate_email(email):
if password == password_checker:
print('Passwords match')
user = User.objects.create_user(username, email, password)
user.save()
return {'message': 'User Created'}
else:
print('Passwords don\'t match')
return request.content_params == {'Passwords': 'Do Not Match'}
else:
print('Email not valid')
return request.content_params == {'Email': 'not valid'}
Once all this is setup, run the app and make sure you have http://localhost:8000/api/docs and everything is working.
Now with the project running, we can finally work in dash and the real fun starts. The idea is basically, we need to set up a data folder to house our requests to the Django database for organizational reasons. Within the app.py file, we will create a login forum, I decided to use a modal to display the forum & run the login logic on an @callback but you are welcome to change stuff and see what you can figure out. Then when the @callback is run it will send everything to a token that was created from the API to a cookie to save the users state within the application where we create another @callback to refer to this cookie on default @callback render and change aspects of the application showing the user has successfully logged in. For example, displaying navbar for the user vs the default anonymous user navbar.
First lets setup our data folder in dash:
data/ninja_test.py
import requests
import colorama
def login(username, password):
url_login = f'http://{username}:{password}@127.0.0.1:8000/api/account/login'
response = requests.get(url_login, auth=(username, password))
if response.status_code == 200:
print(colorama.Fore.GREEN + f"Login Successful: {response.json()['httpuser']}")
print(colorama.Fore.YELLOW + f'{response.headers}')
print(colorama.Fore.RESET)
print(response.json())
return response.json()
else:
print(colorama.Fore.RED + f"Login Failed: {username}")
print(colorama.Fore.RESET)
return False
def register(username, email, password, password_checker):
print('Testing Register')
url_register = 'http://127.0.0.1:8000/api/account/register'
response = requests.post(url_register, data={'username': username, 'email': email, 'password': password, 'password_checker': password_checker})
if response.status_code == 200:
print(colorama.Fore.GREEN + f"Registration Successful: {response.json()['httpuser']}")
print(colorama.Fore.RESET)
return response.json()
else:
print(colorama.Fore.RED + f"Registration Failed: {username}")
print(colorama.Fore.RESET)
return False
def create_user_token(username, password):
url_login = f'http://127.0.0.1:8000/api/token/pair'
curl_body = {
'password': password,
'username': username
}
response = requests.post(url_login, json=curl_body)
if response.status_code == 200:
print(colorama.Fore.GREEN + f"Login Successful: {response.status_code}", colorama.Fore.RESET)
else:
print(colorama.Fore.RED + f"Login Failed: {username}", colorama.Fore.RESET)
print('headers')
print(response.headers)
print('response')
print(response.json())
return response.json()
def refresh_user_token(refresh_token):
refresh_token_url = f'http://127.0.0.1:8000/api/token/refresh'
curl_body = {
'refresh': refresh_token
}
response = requests.post(refresh_token_url, json=curl_body)
if response.status_code == 200:
print(colorama.Fore.GREEN + f"Login Successful: {response.status_code}", colorama.Fore.RESET)
else:
print(colorama.Fore.RED + f"Login Failed: {response.status_code}", colorama.Fore.RESET)
print('headers')
print(response.headers)
print('access_token')
print(response.json())
return response.json()
def verify_user_token(access_token):
verify_token_url = f'http://127.0.0.1:8000/api/token/verify'
curl_body = {
'token': access_token
}
response = requests.post(verify_token_url, json=curl_body)
if response.status_code == 200:
print(colorama.Fore.GREEN + f"Login Successful: {response.status_code}", colorama.Fore.RESET)
else:
print(colorama.Fore.RED + f"Login Failed: {response.status_code}", colorama.Fore.RESET)
print('headers')
print(response.headers)
print('access_token')
if response.json() == {}:
return True
else:
return False
if __name__ == '__main__':
# login('pip', 'NeverTellMeTheOdds')
create_user_token(username='pip', password='NeverTellMeTheOdds')
# refresh_user_token('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY3Mjc1Mzc1OSwiaWF0IjoxNjcyNjY3MzU5LCJqdGkiOiJlZGQwMzkxMWFkNDY0MWI4OTBmOWJkMDEwYjE1ZTliNCIsInVzZXJfaWQiOjF9.t4QvVzxUja2TfiBw_qEkwypFeOoIaOA1GVngKpwK258')
# print(verify_user_token('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjcyNjY3ODY0LCJpYXQiOjE2NzI2NjczNTksImp0aSI6ImQyYzBkMzU5ZTJkNDRiZTNiN2I4MjEyZmM2YjJkNDhkIiwidXNlcl9pZCI6MX0.y43WCcRgs5KnXJ5yAl-HFwKALyHIA2ZGMaxBrSeRzzM'))
Then we can look into setting up the main app.py file:
app.py
import dash
from dash import html, Dash, dcc
from dash.dependencies import Output, Input, State
import dash_mantine_components as dmc
import dash_bootstrap_components as dbc
from dash_iconify import DashIconify
from flask import Flask, render_template
from data.ninja_test import login, register, create_user_token, refresh_user_token, verify_user_token
from http.cookiejar import CookieJar
from http.cookiejar import Cookie
server = Flask(__name__)
#setup cookieJar
cookieJar = CookieJar()
app = Dash(
__name__,
assets_url_path="assets",
external_stylesheets=[
"https://use.fontawesome.com/releases/v6.2.1/css/all.css",
dbc.themes.SKETCHY,
],
external_scripts=[],
use_pages=True,
server=server,
)
login_button = dmc.Buton("Login",
id="login-modal-button",
)
login_form = dbc.Form(
[
dbc.Col(
[
dmc.Stack(
children=[
dmc.TextInput(
label="Your Username or Email:",
style={"width": "100%"},
id="login-username",
),
],
)
]
),
dbc.Col(
[
dmc.PasswordInput(
label="Your password:",
style={"width": "100%"},
placeholder="Your password",
icon=DashIconify(icon="bi:shield-lock"),
id="login-password",
)
]
),
],
className="mb-5",
)
# Create the layout for the login screen
login_modal = dmc.Card(
children=[
dmc.CardSection(
dmc.Image(
src=dash.get_asset_url("gif/login_banner.gif"),
height=160,
)
),
dmc.Group(
[
dmc.Text("Access Account", weight=500),
],
position="apart",
mt="md",
mb="xs",
),
login_form,
dmc.Button(
"Login",
variant="light",
color="blue",
fullWidth=True,
mt="md",
radius="md",
id="login-button",
),
],
withBorder=True,
shadow="sm",
radius="md",
style={"width": 350},
id="login-modal-form",
)
# modal login
login_to_account = dmc.Modal(
children=[
html.Div(id="welcome-back-alert"),
html.Div(id="User-Avatar"),
login_modal,
],
id="login-account-modal",
overflow="outside",
opened=False,
size="sm",
)
test_checking_user_active = html.Div(id='display_test')
test_checking_user_active_button = html.Button('Test', id='test-button', n_clicks=1)
app.layout = html.Div(
[
test_checking_user_active_button,
login_to_account,
test_checking_user_active,
dash.page_container,
],
style={"height": "100vh"},
)
@app.callback(Output('display_test', 'children'), [Input('test-button', 'n_clicks')])
def checking_user_active(n_clicks):
print(n_clicks)
if n_clicks % 2 != 0:
keys = []
for c in cookieJar:
print(c)
if c.name == 'account_cookie':
print('found cookie')
print(f'Refresh KEY: {c.value}')
print(f'Secret KEY: {c.comment}')
keys.append(c.comment)
return dbc.Card(id='test-checking-user-active', children=[
dbc.CardHeader(f"Testing Active Users"),
dbc.CardBody([
# html.H4(f"{}", className="card-title"),
html.H4(id='active_users', className="card-title"),
html.H4(f"Cookie Jar: {cookieJar}"),
html.P(f"{keys}", className="card-text"),
# html.H4(f"cookie_jar: {req_cookie_jar}", className="card-title"),
])])
else:
return None
# Store Data through Callbacks
@app.callback(
# Output("store", "data"), hiding the modal on success
[
Output("welcome-back-alert", "children"),
Output("login-modal-form", "hidden"),
Output("User-Avatar", "children"),
],
[
Input("login-button", "n_clicks"),
Input("login-username", "value"),
Input("login-password", "value"),
],
prevent_initial_call=True,
)
def get_data(login_button, username, password):
if login_button is None:
pass
elif login_button:
if username and password is not None:
login_test = login(username, password)
if login_test:
user_token = create_user_token(username='pip', password='12a10l1k')
c = Cookie(None, 'account_cookie', f'{user_token["refresh"]}', '8000', True, '127.0.0.1',
True, False, '/', True, False, '1370002304', False, f'{user_token["access"]}', None, None,
False)
cookieJar.set_cookie(c)
print('Cookie Set')
print(f'Cookie: {c}')
print(f'CookieJar: {cookieJar}')
welcome_back_alert = dmc.Alert(
f'Hey, {login_test["httpuser"]["username"]} you just logged in. Best way to take full advantage of this powerful Ai is to build up your account and we will help get you new upgrades, tips, tools & cheatcodes.',
title="Welcome Back to Maply.io",
color="green",
id="welcome-back-alert",
withCloseButton=True,
hide=False,
)
return (
welcome_back_alert,
True,
html.Center(
html.Img(
src=dash.get_asset_url(
"https://yt3.ggpht.com/_7416qbIfG4-6jW_WIYnIzOzYxz-xiI13Bs6wAEMIHLxCqLL__n0chPeXPkS3r7O0N7SqEVGww=s600-c-k-c0x00ffffff-no-rj-rp-mo"
),
style={"width": "100px", "height": "250px"},
)
),
)
else:
pass
else:
pass
if __name__ == "__main__":
app.run_server(
host=f"{host}",
debug=True,
port=f"{port}",
threaded=True,
)
Tried to keep it simple as possible as I took a lot of the code out of an existing project, which might need some tweaking. Regardless the end result is you are able to login with the @callback get_data then you can refer to the cookie in the @callback checking_user_active.