Callback not working at all in my app

Hello everyone,

I am building an app for my job, it is my first app so I tried to make it scalable and easy to maintain. I separated many things in different files and folders to keep the code clean. When I am navigating on the app, I first see a homepage, then I must select a client from a dropdown, once done two buttons appears in the navbar, I must select a cycle (revenue analysis, logistic analysis…) and then a dashboard for this client and this cycle is shown. Everything is dynamic, as soon as I add a new folder in my clients folder, I can select it from the dropdown. Same for the cycle, as soon as I add a new folder in the client folder, it is available as a new cycle. Everything is almost perfect, yet, when selecting a client and a cycle, I have a layout that is just 4 tabs with content for the 4 tabs. I can see the 4 tabs and interact with them but the content inside each tab is not showing. I have a callback that populate the tabs’ content when I select one but it is not working. I put a print statement in the callback but it does not show in my terminal so the callback is definitly not firing. I don’t have issue error on my terminal nor on the debug. I spent 5 hours and I still cannot see what is happening. If someone has suggestions, I am all ears!!

tl;dr: the callback in my index.py is not working, I can only see the tabs but not the content inside.

Here is the structure of my app:

app_directory/
│
├── app.py                 # Main file where the Dash app is initialized
│
├── assets/                # Directory for CSS, JS, and other static files
│   └── ...                # Static files like custom stylesheets, scripts, etc.
│
├── clients/               # Directory for client-specific modules
│   └── GIE/               # Sub-directory for the GIE client
│       └── revenues/      # Sub-directory for the revenues cycle of the GIE client
│           └── index.py   # File for the GIE client's revenues layout and callbacks
│
├── default/               # Directory for default layouts
│   ├── home/              
│   │   └── home_layout.py # Layout for the home page
│   ├── summary/           
│   │   └── ...            # Layout for the summary page
│   ├── reporting/         
│   │   └── ...            # Layout for the reporting page
│   └── client/            
│       └── ...            # Layout for client-specific pages
│
├── functions.py           # Utility functions, including dynamic layout imports
│
└── data/                  # Directory for datasets
    ├── GIE/               # Data specific to the GIE client
    │   └── CA_2022.csv    # Dataset file
    └── ...                # Other datasets

Here is the code for app.py:

# app.py
import os
import dash
import dash_bootstrap_components as dbc
from dash import html, dcc, callback_context
from dash.dependencies import Input, Output

# Import functions
from functions import get_cycles, format_cycle_name, import_cycle_layout

# Import layouts from the 'default' folder
from default.home.home_layout import home_layout
from default.summary.summary_layout import summary_layout
from default.reporting.reporting_layout import reporting_layout
from default.client.client_layout import client_layout


# Initialize the app with the external LUX theme from Bootswatch
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX])
app.config.suppress_callback_exceptions = True  # Allows us to navigate between pages
server = app.server

# List of clients (dynamically generated from the 'clients' folder)
client_options = [{'label': client, 'value': client} for client in os.listdir('clients') if client not in ["__pycache__", "__init__.py"]]


# Initial Navbar setup with the logo on the far left
navbar = dbc.Navbar(
    dbc.Container([
        # Logo on the far left
        html.A(
            html.Img(src=dash.get_asset_url("logo-hdm.png"), height="50px"),
            href="/"
        ),
        # Navigation items
        dbc.NavbarBrand("HDM FINANCIAL EXPLORER", href="/", className="ml-2"),
        dbc.Nav(
            [
                # dbc.NavItem(dbc.NavLink("Home", href="/")),
                dbc.NavItem(dbc.NavLink("Général", href="/summary")),
                dbc.DropdownMenu(
                    children=[dbc.DropdownMenuItem(client['label'], href=f"/client/{client['value']}") for client in client_options],
                    nav=True,
                    in_navbar=True,
                    label="Sélectionner un Client",
                ),
                # Placeholder pour le dropdown cycle qui est caché si on ne choisit pas un client
                html.Div(id='cycle-dropdown-container', children=[], style={'display': 'none'}),
                # Placeholder pout le boutton reporting qui est caché si on ne choisit pas un client
                html.Div(id="reporting-button-container", children=[
                    dbc.NavItem(dbc.NavLink("Reporting", href="", id="reporting-button"))
                ], style={"display": "none"})
            ],
            className="ml-auto",  # Pushes the nav items to the right
        ),
    ]),
    color="primary",
    dark=True,
)



# Define the initial layout
app.layout = html.Div([
    dcc.Location(id='url', refresh=False),  # Tracks the URL
    navbar,
    html.Div(id='page-content')  # Content will be rendered in this element
])

# Callback for multi-page app
@app.callback(Output('page-content', 'children'),
              [Input('url', 'pathname')])
def display_page(pathname):
    path_parts = pathname.strip('/').split('/')
    if len(path_parts) == 3 and path_parts[0] == 'client':
        client_id = path_parts[1]
        cycle_name = path_parts[2]
        # Use the import_cycle_layout function to dynamically load the layout
        return import_cycle_layout(client_id, cycle_name) 
    elif len(path_parts) == 2 and path_parts[0] == "client":
        return client_layout(path_parts[1])
    elif pathname == '/summary':
        return summary_layout
    elif pathname == '/':
        return home_layout
    else:
        return '404 - Not found'


# Updated callback for the navbar to include dynamic Cycle dropdown and Reporting button
@app.callback(
    [Output('cycle-dropdown-container', 'children'),
     Output('cycle-dropdown-container', 'style'),
     Output('reporting-button-container', 'style'),
     Output('reporting-button', 'href')],  # Update the button's URL dynamically
    [Input('url', 'pathname')])
def update_navbar(pathname):
    if '/client/' in pathname:
        split_path = pathname.split('/')
        client_id = split_path[2]
        cycle = split_path[3] if len(split_path) > 3 else None

        cycles = get_cycles(client_id)
        cycle_dropdown = dbc.DropdownMenu(
            children=[dbc.DropdownMenuItem(format_cycle_name(cycle_name), href=f"/client/{client_id}/{cycle_name}") for cycle_name in cycles],
            nav=True,
            in_navbar=True,
            label="Cycle",
            id='cycle-dropdown'
        )

        # Ensure the reporting URL doesn't duplicate '/reporting'
        reporting_url = f"/client/{client_id}/{cycle}/reporting" if cycle else f"/client/{client_id}/reporting"
        return cycle_dropdown, {}, {}, reporting_url

    return [], {'display': 'none'}, {'display': 'none'}, ""


# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)

Here is the code for functions.py:

"""
"""
import os
from dash import html
import importlib


# Trouver le nom du cycle en parcourant les fichier
def get_cycles(client_name):
    client_path = os.path.join('clients', client_name)
    if os.path.exists(client_path):
        return [cycle for cycle in os.listdir(client_path) if os.path.isdir(os.path.join(client_path, cycle)) and cycle not in ["__pycache__"]]
    return []

# Avoir dans le dropdown Cycle le nom en du cycle en capital et avec un espace au lieu d'un dash
def format_cycle_name(name):
    return name.replace('_', ' ').upper()

# Importer les layouts client de façon dynamiques
def import_cycle_layout(client_id, cycle_name):
    try:
        # Construct the module path and import the layout
        module_path = f'clients.{client_id}.{cycle_name}.index'
        cycle_module = importlib.import_module(module_path)
        return cycle_module.layout
    except (ModuleNotFoundError, AttributeError):
        # Return an error message or default layout if the specific layout doesn't exist
        return html.Div(f"Layout not found for client {client_id} and cycle {cycle_name}")

and finally the code for the index.py file:

from dash import html, dcc, Input, Output
from dash import callback
from dash.dash_table import DataTable
import dash_bootstrap_components as dbc
from dash.exceptions import PreventUpdate
# from app import app
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np

from assets.couleurs import couleurs_theme

# from functions import sequencialite

# Import du Dataset
ca_2022_df = pd.read_csv("data/GIE/CA_2022.csv", index_col=False)

# Ordonner les mois pour les graphiques:
mois_ordonés = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
ca_2022_df['Mois'] = pd.Categorical(ca_2022_df['Mois'], categories=mois_ordonés, ordered=True)
    
# App layout
layout = html.Div([
    dcc.Tabs(id="tabs_gie", value="tab_vision_globale", children=[
        dcc.Tab(label="Vision Globale", value="tab_vision_globale"),
        dcc.Tab(label="Interne Rhum", value="tab_interne_rhum"),
        dcc.Tab(label="Externe Négoce", value="tab_externe_negoce"),
        dcc.Tab(label="Séquentialité", value="tab_sequentialite")
    ]),
    html.Div(id="contenu_tabs")
])

@callback(Output("contenu_tabs", "children"),
              Input("tabs_gie", "value"))

def afficher_contenu(tab):
    return html.Div("Test content")

Here is a screenshot of the tab but “Test Content” must be shown.

Thanks in advance,
Louis

Hi @ldeco, is there a reason for not using the pages feature? It usually simplifies the creation of multipage apps a lot.

I did not know about this, I will take a look!

1 Like