Multipage App - dynamic callbacks

I’m trying to have a site with multiple pages that are all tied to a postgreSQL database, In each of the pages I move items between these tables.

I’ve read the information at Live Updates | Dash for Python Documentation | Plotly

but for multipage, if I try to return "app.layout = serve_layout not app.layout = serve_layout()" this results in an error. if I return serve_layout() - the movement occurs in the database but the datatable I generate does not update.

Has anyone found a solution for multipage dynamic callbacks?

Index.py:

import dash
import dash_bootstrap_components as dbc
from dash import Input, Output, dcc, html
from apps import deactlotassign, ovendash, packqueue, stuffboard
from app import app


# the style arguments for the sidebar. We use position:fixed and a fixed width
SIDEBAR_STYLE = {
    "position": "fixed",
    "top": 0,
    "left": 0,
    "bottom": 0,
    "width": "16rem",
    "padding": "2rem 1rem",
    "background-color": "#f8f9fa",
}

# the styles for the main content position it to the right of the sidebar and
# add some padding.
CONTENT_STYLE = {
    "margin-left": "18rem",
    "margin-right": "2rem",
    "padding": "2rem 1rem",
}

sidebar = html.Div(
    [
        html.H2("Liners", className="display-4"),
        html.Hr(),
        html.P( 
            "Manufacturing Dashboard", className="lead"
        ),
        dbc.Nav(
            [
                dbc.NavLink("Home", href="/", active="exact"),
                dbc.NavLink("Oven Queue", href="/oven", active="exact"),
                dbc.NavLink("Stuffing Dashboard", href="/stuff", active="exact"),
                dbc.NavLink("Deactivation Lot Assignment", href="/deactq", active="exact"),
                dbc.NavLink("Packaging Dashboard", href="/pack", active="exact"),
            ],
            vertical=True,
            pills=True,
        ),
    ],
    style=SIDEBAR_STYLE,
)

content = html.Div(id="page-content", style=CONTENT_STYLE)

### Page container ###
def page_container():
    return html.Div(
    children=[
        # represents the URL bar, doesn't render anything
        dcc.Location(
            id='url', refresh=True
        ), 
        sidebar,
        # content will be rendered in this element
        html.Div(id='page-content', style=CONTENT_STYLE)
    ]
)

### Set app layout to page container ###
app.layout = page_container
### Assemble all layouts ###
app.validation_layout = html.Div(
    children = [
        page_container,
        sidebar,
        ovendash.layout,
        stuffboard.layout,
        deactlotassign.layout,
        packqueue.layout
    ]
)

#app.layout = html.Div([dcc.Location(id="url"), sidebar, content])


@app.callback(
    Output(
        component_id='page-content',
        component_property='children',
        ),
    [Input(
        component_id='url',
        component_property='pathname',
        )]
    )
def render_page_content(pathname):
    if pathname == "/":
        return html.P("This is the content of the home page!")
    elif pathname == "/oven":
        return ovendash.layout
    elif pathname == "/stuff":
        return stuffboard.layout
    elif pathname == "/deactq":
        return deactlotassign.layout
    elif pathname == "/pack":
        return packqueue.layout
    # If the user tries to reach a different page, return a 404 message
    return dbc.Jumbotron(
        [
            html.H1("404: Not found", className="text-danger"),
            html.Hr(),
            html.P(f"The pathname {pathname} was not recognised..."),
        ]
    )


if __name__ == "__main__":
    app.run_server(debug = True, port=8888)

One of the Pages:

import logging
import dash
import datetime
from datetime import date
from dash.dash_table.Format import Align
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_table as dt
from dash_table.Format import Format, Group
import dash_table as dt
import dash_bootstrap_components as dbc
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
import plotly.figure_factory as ff
from openpyxl import load_workbook
import config
import linerplan
import psycopg2
from app import app

# assume you have a "long-form" data frame
# see https://plotly.com/python/px-arguments/ for more options

CNXPOSTGRE = {"host":config.host, "port":config.port, "user":config.user, "password":config.pwd, "dbname":config.dbname}
conn = psycopg2.connect(**CNXPOSTGRE)
cursor = conn.cursor()

""" PostGRES Fetch command, returns Lookup Lists above """
def PGconnect():
    try:
        conn = psycopg2.connect(dbname=config.dbname, host=config.host, port=config.port,\
         user=config.user, password=config.pwd)
        cursor = conn.cursor()
        pndata = linerplan.RAWlookup(cursor)
        kitplan = linerplan.Kitlookup(cursor)
        woolneed = linerplan.Woollookup(cursor)
        woolplan = linerplan.Woolstuff(cursor)
        packsize = linerplan.Packlookup(cursor)
        packplan = linerplan.PackDist(cursor)
        csubdata = linerplan.CSUBlookup(cursor)
        req = linerplan.Reqplan(cursor)
        stfq = linerplan.stuffqlookup(cursor)
        deactq = linerplan.deactqlookup(cursor)
                                           
    except (Exception, psycopg2.Error) as error :
            logging.fatal("Error while fetching raw data from PostGRES DB")
    finally:
        #closing database connection.
        if(conn):
            cursor.close()
            conn.close()
            logging.debug("PG read connection successfully closed")
            return req, pndata, packsize, woolneed, kitplan, woolplan, packplan, csubdata, stfq, deactq

req, pndata, packsize, woolneed, kitplan, woolplan, packplan, csubdata, stfq, deactq = PGconnect()
csubdict = dict(csubdata)
csubdict['G3901-56001'] = "5190-2292 (100 PK)"
csubdict['G3901-56002'] = "5190-2293 (100 PK)"
csubdict['G3901-56003'] = "5190-2294 (100 PK)"
csubdict['G3901-56004'] = "5190-2295 (100 PK)"

def deactiveq(cursor, csubdict):
    tempq = linerplan.deactqlookup(cursor)
    deactq = []
    deactwip = 0
    deacttable = {}
    for item in tempq:
        deactq.append(list(item))
    for item in deactq:
        deacttable[csubdict[item[0]]] = deacttable.get(csubdict[item[0]], 0)+item[1]
        if item[0][0:5] == "G3901":
            deactwip = deactwip + (item[1]/90)
    return (deactwip, deacttable)


ovenfull = 42
deactwip, deacttable = deactiveq(cursor, csubdict)
df = pd.DataFrame(deacttable.items(), columns =['material', 'count'])
df['id'] = df['material']
df.set_index('id', inplace=True, drop=False)

def serve_layout():
    deactwip, deacttable = deactiveq(cursor, csubdict)
    df = pd.DataFrame(deacttable.items(), columns =['material', 'count'])
    df['id'] = df['material']
    df.set_index('id', inplace=True, drop=False)
    return html.Div([
        dcc.Store(id='local', storage_type='local'),
        dcc.ConfirmDialog(
            id='confirm-danger',
            message='Deact ID About to be issued. Are you sure you want to continue?',
        ),
        html.H1(children="Deactivation Lot Assignment", style={'textAlign':'center', 'font-family': 'Comic Sans MS'}),
        html.Div(
            dt.DataTable(
            id='datatable-interactivity',
            columns=[
                {'name': i, 'id': i, 'deletable': True} for i in df.columns
                # omit the id column
                if i != 'id'
            ],
            data=df.to_dict('records'),
            editable=True,
            filter_action="native",
            sort_action="native",
            sort_mode='multi',
            row_deletable=True,
            selected_rows=[],
            page_action='native',
            page_current= 0,
            page_size= 10),
        style={'margin-left':'25%', 'textAlign':'center', 'width':'50%'}),
        html.Div(id='queue-container'),
        html.Div(
            dcc.ConfirmDialogProvider(
            children=html.Button('Submit',),
            id='danger-danger-provider',
            message='Issuing Deactivation Batch Number! Are you sure you want to continue?'),
        style={'margin-left':'25%', 'textAlign':'center', 'width':'50%'}),
        html.Div(id='output-provider', style={'font-size': '44px', 'font-family': 'Comic Sans MS', 'textAlign':'center'})
    ])

layout = serve_layout()

@app.callback(
    Output('queue-container', "children"),
    Input('datatable-interactivity', "derived_virtual_data"),
    Input('datatable-interactivity', "derived_virtual_selected_rows"))
def update_graphs(rows, derived_virtual_selected_rows):
    # When the table is first rendered, `derived_virtual_data` and
    # `derived_virtual_selected_rows` will be `None`. This is due to an
    # idiosyncrasy in Dash (unsupplied properties are always None and Dash
    # calls the dependent callbacks when the component is first rendered).
    # So, if `rows` is `None`, then the component was just rendered
    # and its value will be the same as the component's dataframe.
    # Instead of setting `None` in here, you could also set
    # `derived_virtual_data=df.to_rows('dict')` when you initialize
    # the component.

    conn = psycopg2.connect(dbname=config.dbname, host=config.host, port=config.port, user=config.user, password=config.pwd)
    cursor = conn.cursor()

    deactwip, deacttable2 = deactiveq(cursor, csubdict)
    ovenfull = 42
    df = pd.DataFrame(deacttable2.items(), columns =['material', 'count'])
    df['id'] = df['material']
    df.set_index('id', inplace=True, drop=False)

    if derived_virtual_selected_rows is None:
        derived_virtual_selected_rows = []

    dff = df if rows is None else pd.DataFrame(rows)
    dff['count'] = dff['count'].astype(int)
    total = dff['count'].sum()
    trace2 = go.Indicator(
        mode = "gauge+number",
        value = (total/ovenfull),
        number = {'suffix': "%"},
        title = {'text': "Oven Capacity", 'font': {'size': 36}},
        gauge = {
            'axis': {'range': [None, 100], 'tickwidth': 1, 'tickcolor': "darkblue"},
            'bar': {'color': "darkblue"},
            'bgcolor': "white",
            'borderwidth': 2,
            'bordercolor': "gray",
            'steps': [
                {'range': [0, 75], 'color': 'lightblue'},
                {'range': [75, 90], 'color': 'yellow'},
                {'range': [90, 100], 'color': 'lightgreen'}],
            'threshold': {
                'line': {'color': "red", 'width': 4},
                'thickness': 0.75,
                'value': 95}})

    fig = go.Figure(data = trace2)
    fig.update_layout(paper_bgcolor = "white", font = {'color': "darkblue", 'family': "Arial"})

    products_list = dff.values.tolist()
    # erase temptable
    postgres_erase = """ TRUNCATE linertemp """
    cursor.execute(postgres_erase)
    conn.commit()
    # populate table with edited dataframe
    for item in products_list:
        if '-' in item[1]:
            try:
                postgres_insert_query = """ INSERT INTO linertemp (material, count) VALUES (%s,%s)"""   
                record_to_insert = (item[1], item[0])
                cursor.execute(postgres_insert_query, record_to_insert)
                conn.commit()
            except Exception as e:
                print("Error: {}".format(str(e)))
    conn.close()
    return [
        dcc.Graph(
            id=column,
            figure=fig,
            style={"height": 400, "margin": {"t": 10, "l": 10, "r": 10}}
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ['count'] if column in dff
    ]

@app.callback(Output('output-provider', 'children'),
              Input('danger-danger-provider', 'submit_n_clicks'))
def update_output(submit_n_clicks):
    if not submit_n_clicks:
        return ''
    x = datetime.datetime.now()
    year = str(x.year)
    weekno = str(x.strftime("%U"))
    lotno = year[3] + weekno + str(1).zfill(3)
    try:
        conn = psycopg2.connect(dbname=config.dbname, host=config.host, port=config.port,\
         user=config.user, password=config.pwd)
        cursor = conn.cursor()
        linertemp = linerplan.linertemp(cursor)
        maxid = linerplan.maxid(cursor)
        if maxid == 0:
            print ("in loop")
        elif lotno <= maxid[0][0]:
            lotno = int(maxid[0][0])+1                       
        
        for item in linertemp:    
            livecount = deacttable[item[0]]
            key = next(key for key, value in csubdict.items() if value == item[0])

            if item[1] == livecount:
                postgres_idupdate = """ UPDATE linerdeactq SET deactid = %s WHERE pn = %s """  
                recordupdate = (lotno, key)
                cursor.execute(postgres_idupdate, recordupdate)
                conn.commit()

                postgres_insert_query = """ INSERT INTO linertestq (material, count, deactid, testcomplete) VALUES (%s,%s,%s,%s)"""   
                record_to_insert = (item[0], item[1], lotno, 'FALSE')
                cursor.execute(postgres_insert_query, record_to_insert)
                conn.commit()

                postgres_copy_query = """ INSERT INTO linerarchive (pn, tech, qty, date, deactid) SELECT * FROM linerdeactq WHERE pn = %s"""   
                record_copy = (key,)
                cursor.execute(postgres_copy_query, record_copy)
                conn.commit()

                PG_delete_query = """ DELETE FROM linerdeactq WHERE pn = %s """
                record_to_delete = (key,)
                cursor.execute(PG_delete_query, record_to_delete)
                conn.commit()
            else:
                modvalue = livecount-item[1]
                print (modvalue)
                postgres_insert_query = """ INSERT INTO linertestq (material, count, deactid, testcomplete) VALUES (%s,%s,%s,%s)"""   
                record_to_insert = (item[0], item[1], lotno, 'FALSE')
                cursor.execute(postgres_insert_query, record_to_insert)
                conn.commit()
                PG_update_query = """ UPDATE linerdeactq SET qty = %s WHERE pn = %s """
                record_to_update = (modvalue, key)
                cursor.execute(PG_update_query, record_to_update)
                conn.commit()
        # erase temptable
        postgres_erase = """ TRUNCATE linertemp """
        cursor.execute(postgres_erase)
        conn.commit()
    except (Exception, psycopg2.Error) as error :
            logging.fatal("Error while fetching raw data from PostGRES DB")
    finally:
        #closing database connection.
        if(conn):
            cursor.close()
            conn.close()
            logging.debug("PG read connection successfully closed")


    return """
        Deactivation Lot Number: {}
    """.format(str(lotno))
  

Hi,

I believe that the pages aren’t updating because you are using layout = serve_layout() and then page.layout in your “router” callback (render_page_content), so serve_layout() gets evaluated only on import and therefore is static.

If that is the case, then it would be enough to use page.serve_layout() in the callback, which will be then evaluated each time a new page is rendered/loaded.