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