diff --git a/src/pages/tableau.py b/src/pages/tableau.py index ba938b5..85563c3 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -1,8 +1,10 @@ +import json import os +import urllib.parse from datetime import datetime import polars as pl -from dash import Input, Output, State, callback, dcc, html, register_page +from dash import Input, Output, State, callback, dcc, html, no_update, register_page from src.figures import DataTable from src.utils import ( @@ -38,18 +40,21 @@ datatable = html.Div( filter_action="custom", sort_action="custom", hidden_columns=get_default_hidden_columns(None), + columns=[{"id": col, "name": col} for col in df.columns], ), ) layout = [ + dcc.Location(id="url", refresh=False), html.Div( html.Details( children=[ html.Summary( - html.H3("Mode d'emploi", style={"text-decoration": "underline"}), + html.H3("Mode d'emploi", style={"textDecoration": "underline"}), ), dcc.Markdown( - """ + dangerously_allow_html=True, + children=""" ##### Définition des colonnes Pour voir la définition d'une colonne, passez votre souris sur son en-tête. @@ -59,10 +64,10 @@ layout = [ Vous pouvez appliquer un filtre pour chaque colonne en entrant du texte sous le nom de la colonne, puis en tapant sur `Entrée`. - Champs textuels : la recherche est insensible à la casse (majuscules/minuscules) et retourne les valeurs qui contiennent - le texte recherché. Exemple : `rennes` retourne "RENNES METROPOLE". + le texte recherché. Exemple : `rennes` retourne "RENNES METROPOLE". Lorsque vous ouvrez une URL de vue, le format équivalent `icontains rennes` est utilisé. - Champs numériques : vous pouvez soit taper un nombre pour trouver les valeurs égales, soit le précéder de **>** ou **<** pour filtrer les valeurs supérieures ou inférieures. Exemple pour les offres reçues : `> 4` retourne les marchés ayant reçu plus de 4 offres. - Champs date : vous pouvez également utiliser **>** ou **<**. Exemples : `< 2024-01-31` pour "avant le 31 janvier 2024", - `2024` pour "en 2024", `> 2022` pour "à partir de 2022" + `2024` pour "en 2024", `> 2022` pour "à partir de 2022". Lorsque vous ouvrez une URL de vue, le format équivalent `i<` ou `i>` est utilisé. Vous pouvez filtrer plusieurs colonnes à la fois. Vos filtres sont remis à zéro quand vous rafraîchissez la page. @@ -70,6 +75,10 @@ layout = [ Pour trier une colonne, utilisez les flèches grises à côté des noms de colonnes. Chaque clic change le tri dans cet ordre : tri ascendant, tri descendant, pas de tri. + ##### Partager une vue + + Une vue est un ensemble de filtres, de tris et de choix de colonnes que vous avez appliqué. Vous pouvez copier une adresse Web qui reproduit la vue courante à l'identique en cliquant sur l'icône drawing. En la collant dans la barre d'adresse d'un navigateur, vous ouvrez la vue Tableau avec les mêmes paramètres. + ##### Télécharger le résultat Vous pouvez télécharger le résultat de vos filtres et tris, pour les colonnes affichées, en cliquant sur **Télécharger au format Excel**. @@ -79,7 +88,7 @@ layout = [ Les liens dans les colonnes Identifiant unique, Acheteur et Titulaire vous permettent de consulter une vue qui leur est dédiée (informations, marchés attribués/remportés, etc.) - """ + """, ), ], id="instructions", @@ -100,6 +109,8 @@ layout = [ html.Div( [ html.P("lignes", id="nb_rows"), + html.Div(id="copy-container"), + dcc.Input(id="share-url", readOnly=True, style={"display": "none"}), html.Button( "Téléchargement désactivé au-delà de 65 000 lignes", id="btn-download-data", @@ -133,6 +144,10 @@ layout = [ State("table", "data_timestamp"), ) def update_table(page_current, page_size, filter_query, sort_by, data_timestamp): + # if ctx.triggered_id != "url": + # search_params = None + # else: + # search_params = urllib.parse.parse_qs(search_params.lstrip("?")) return prepare_table_data( None, data_timestamp, filter_query, page_current, page_size, sort_by ) @@ -164,3 +179,98 @@ def download_data(n_clicks, filter_query, sort_by, hidden_columns: list = None): date = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") return dcc.send_bytes(to_bytes, filename=f"decp_{date}.xlsx") + + +@callback( + Output("table", "filter_query"), + Output("table", "sort_by"), + Output("table", "hidden_columns"), + Output("url", "search", allow_duplicate=True), + Input("url", "search"), + prevent_initial_call=True, +) +def restore_view_from_url(search): + if not search: + return no_update, no_update, no_update, no_update + + params = urllib.parse.parse_qs(search.lstrip("?")) + print("params", params) + + filter_query = no_update + sort_by = no_update + hidden_columns = no_update + + if "filtres" in params: + filter_query = params["filtres"][0] + + if "tris" in params: + try: + sort_by = json.loads(params["tris"][0]) + except json.JSONDecodeError: + pass + + if "colonnes" in params: + try: + hidden_columns = json.loads(params["colonnes"][0]) + except json.JSONDecodeError: + pass + + return filter_query, sort_by, hidden_columns, "" + + +@callback( + Output("share-url", "value"), + Output("copy-container", "children"), + Input("table", "filter_query"), + Input("table", "sort_by"), + Input("table", "hidden_columns"), + State("url", "href"), +) +def sync_url_and_reset_button(filter_query, sort_by, hidden_columns, href): + if not href: + return no_update, no_update + + # Extract base URL (remove existing query params) + base_url = href.split("?")[0] + + params = {} + if filter_query: + params["filtres"] = filter_query + + if sort_by: + params["tris"] = json.dumps(sort_by) + + if hidden_columns: + params["colonnes"] = json.dumps(hidden_columns) + + query_string = urllib.parse.urlencode(params) + full_url = f"{base_url}?{query_string}" if query_string else base_url + + copy_button = dcc.Clipboard( + id="btn-copy-url", + target_id="share-url", + title="Copier l'URL de cette vue", + style={ + "display": "inline-block", + "fontSize": 20, + "verticalAlign": "top", + "cursor": "pointer", + }, + className="fa fa-link", + ) + + return full_url, copy_button + + +@callback( + Output("copy-container", "children", allow_duplicate=True), + Input("btn-copy-url", "n_clicks", allow_optional=True), + prevent_initial_call=True, +) +def show_confirmation(n_clicks): + if n_clicks: + return html.Span( + "Adresse de la vue copiée", + style={"color": "green", "fontWeight": "bold", "marginLeft": "10px"}, + ) + return no_update diff --git a/src/utils.py b/src/utils.py index 81a7925..e7d47ad 100644 --- a/src/utils.py +++ b/src/utils.py @@ -27,6 +27,7 @@ def split_filter_part(filter_part): ["i<", "<"], ["i>", ">"], ["icontains", "contains"], + # [" ", "contains"] ] print("filter part", filter_part) for operator_group in operators: @@ -543,6 +544,7 @@ def prepare_table_data( :param page_current: :param page_size: :param sort_by: + :param search_params: :return: """ @@ -555,6 +557,24 @@ def prepare_table_data( else: lff: pl.LazyFrame = df.lazy() # start from the original data + # if search_params: + # if "filtres" in search_params: + # filter_query = search_params["filtres"][0] + # + # if "tris" in search_params: + # try: + # sort_by = json.loads(search_params["tris"][0]) + # except json.JSONDecodeError: + # pass + # + # if "colonnes" in search_params: + # try: + # hidden_columns = json.loads(search_params["colonnes"][0]) + # print(hidden_columns) + # lff = lff.drop(hidden_columns) + # except json.JSONDecodeError: + # pass + # Application des filtres if filter_query: lff = filter_table_data(lff, filter_query)