diff --git a/dp_creator_ii/app/analysis_panel.py b/dp_creator_ii/app/analysis_panel.py index fce6df6..ab17b60 100644 --- a/dp_creator_ii/app/analysis_panel.py +++ b/dp_creator_ii/app/analysis_panel.py @@ -28,7 +28,7 @@ def analysis_ui(): log_slider("log_epsilon_slider", 0.1, 10.0), ui.output_text("epsilon"), output_code_sample("Privacy Loss", "privacy_loss_python"), - ui.input_action_button("go_to_results", "Download results"), + ui.output_ui("download_results_button_ui"), value="analysis_panel", ) @@ -41,6 +41,10 @@ def analysis_server( contributions=None, is_demo=None, ): # pragma: no cover + @reactive.calc + def button_enabled(): + column_ids_selected = input.columns_checkbox_group() + return len(column_ids_selected) > 0 weights = reactive.value({}) @@ -67,9 +71,9 @@ def _update_checkbox_group(): @reactive.effect @reactive.event(input.columns_checkbox_group) - def _update_weights(): - column_ids_to_keep = input.columns_checkbox_group() - clear_column_weights(column_ids_to_keep) + def _on_column_set_change(): + column_ids_selected = input.columns_checkbox_group() + clear_column_weights(column_ids_selected) @render.ui def columns_ui(): @@ -115,3 +119,16 @@ def privacy_loss_python(): @reactive.event(input.go_to_results) def go_to_results(): ui.update_navs("top_level_nav", selected="results_panel") + + @render.ui + def download_results_button_ui(): + button = ui.input_action_button( + "go_to_results", "Download results", disabled=not button_enabled() + ) + + if button_enabled(): + return button + return [ + button, + "Select one or more columns before proceeding.", + ] diff --git a/dp_creator_ii/app/dataset_panel.py b/dp_creator_ii/app/dataset_panel.py index 30922bb..2be9b67 100644 --- a/dp_creator_ii/app/dataset_panel.py +++ b/dp_creator_ii/app/dataset_panel.py @@ -31,10 +31,11 @@ def dataset_ui(): "contributions", ["Contributions", ui.output_ui("contributions_demo_tooltip_ui")], cli_info.contributions, + min=1, ), ui.output_ui("python_tooltip_ui"), output_code_sample("Unit of Privacy", "unit_of_privacy_python"), - ui.input_action_button("go_to_analysis", "Define analysis"), + ui.output_ui("define_analysis_button_ui"), value="dataset_panel", ) @@ -52,6 +53,14 @@ def _on_csv_path_change(): def _on_contributions_change(): contributions.set(input.contributions()) + @reactive.calc + def button_enabled(): + contributions_is_set = input.contributions() is not None + csv_path_is_set = ( + input.csv_path() is not None and len(input.csv_path()) > 0 + ) or is_demo + return contributions_is_set and csv_path_is_set + @render.ui def choose_csv_demo_tooltip_ui(): return demo_tooltip( @@ -78,6 +87,18 @@ def python_tooltip_ui(): "for the entire calculation.", ) + @render.ui + def define_analysis_button_ui(): + button = ui.input_action_button( + "go_to_analysis", "Define analysis", disabled=not button_enabled() + ) + if button_enabled(): + return button + return [ + button, + "Choose CSV and Contributions before proceeding.", + ] + @render.code def unit_of_privacy_python(): return make_privacy_unit_block(contributions()) diff --git a/tests/test_app.py b/tests/test_app.py index 9a75dd1..624b276 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -47,12 +47,17 @@ def expect_no_error(): expect_visible("dp.unit_of(contributions=42)") expect_no_error() + # Button disabled until upload: + define_analysis_button = page.get_by_role("button", name="Define analysis") + assert define_analysis_button.is_disabled() + + # Now upload: csv_path = Path(__file__).parent / "fixtures" / "fake.csv" page.get_by_label("Choose CSV file").set_input_files(csv_path.resolve()) expect_no_error() # -- Define analysis -- - page.get_by_role("button", name="Define analysis").click() + define_analysis_button.click() expect_not_visible(pick_dataset_text) expect_visible(perform_analysis_text) expect_not_visible(download_results_text) @@ -72,12 +77,18 @@ def expect_no_error(): expect_visible("Epsilon: 0.316") page.locator(".irs-bar").click() expect_visible("Epsilon: 0.158") + + # Button disabled until column selected: + download_results_button = page.get_by_role("button", name="Download results") + assert download_results_button.is_disabled() + # Set column details: page.get_by_label("grade").check() page.get_by_label("Min").click() page.get_by_label("Min").fill("0") # TODO: All these recalculations cause timeouts: # It is still rerendering the graph after hitting "Download results". + # https://github.com/opendp/dp-creator-ii/issues/116 # page.get_by_label("Max").click() # page.get_by_label("Max").fill("100") # page.get_by_label("Bins").click() @@ -86,7 +97,7 @@ def expect_no_error(): expect_no_error() # -- Download results -- - page.get_by_role("button", name="Download results").click() + download_results_button.click() expect_not_visible(pick_dataset_text) expect_not_visible(perform_analysis_text) expect_visible(download_results_text)