diff --git a/tests/unit_tests/test_tethys_cli/test_cli_helper.py b/tests/unit_tests/test_tethys_cli/test_cli_helper.py index 526779999..2bc7058bd 100644 --- a/tests/unit_tests/test_tethys_cli/test_cli_helper.py +++ b/tests/unit_tests/test_tethys_cli/test_cli_helper.py @@ -301,9 +301,9 @@ def test_gen_salt_string_for_setting_with_secrets_deleted_or_changed( @mock.patch("tethys_cli.cli_helpers.input") @pytest.mark.django_db -def test_prompt_yes_or_no__accept_default_yes(mock_input, test_app): +def test_prompt_yes_or_no__yes(mock_input): question = "How are you?" - mock_input.return_value = None + mock_input.return_value = "y" test_val = cli_helper.prompt_yes_or_no(question) assert test_val mock_input.assert_called_once() @@ -311,30 +311,30 @@ def test_prompt_yes_or_no__accept_default_yes(mock_input, test_app): @mock.patch("tethys_cli.cli_helpers.input") @pytest.mark.django_db -def test_prompt_yes_or_no__accept_default_no(mock_input, test_app): +def test_prompt_yes_or_no__no(mock_input): question = "How are you?" - mock_input.return_value = None - test_val = cli_helper.prompt_yes_or_no(question, default="n") + mock_input.return_value = "no" + test_val = cli_helper.prompt_yes_or_no(question) assert not test_val mock_input.assert_called_once() @mock.patch("tethys_cli.cli_helpers.input") @pytest.mark.django_db -def test_prompt_yes_or_no__invalid_first(mock_input, test_app): +def test_prompt_yes_or_no__invalid_first(mock_input): question = "How are you?" mock_input.side_effect = ["invalid", "y"] - test_val = cli_helper.prompt_yes_or_no(question, default="n") + test_val = cli_helper.prompt_yes_or_no(question) assert test_val assert mock_input.call_count == 2 @mock.patch("tethys_cli.cli_helpers.input") @pytest.mark.django_db -def test_prompt_yes_or_no__system_exit(mock_input, test_app): +def test_prompt_yes_or_no__system_exit(mock_input): question = "How are you?" mock_input.side_effect = SystemExit - test_val = cli_helper.prompt_yes_or_no(question, default="n") + test_val = cli_helper.prompt_yes_or_no(question) assert test_val is None mock_input.assert_called_once() diff --git a/tests/unit_tests/test_tethys_cli/test_install_commands.py b/tests/unit_tests/test_tethys_cli/test_install_commands.py index 8e9407aba..362be97fb 100644 --- a/tests/unit_tests/test_tethys_cli/test_install_commands.py +++ b/tests/unit_tests/test_tethys_cli/test_install_commands.py @@ -5,6 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import transaction from django.test import TestCase +from django.test.utils import override_settings from tethys_cli import install_commands from tethys_cli.cli_helpers import load_conda_commands @@ -775,10 +776,11 @@ def tearDown(self): self.app_model.delete() chdir(self.cwd) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.cli_colors.pretty_output") @mock.patch("builtins.input", side_effect=["x", "n"]) @mock.patch("tethys_cli.install_commands.call", return_value=0) - def test_install_file_not_generate(self, mock_call, _, mock_pretty_output): + def test_install_file_not_generate(self, mock_call, _, mock_pretty_output, __): chdir("..") # move to a different directory that doesn't have an install.yml args = mock.MagicMock( file=None, @@ -797,7 +799,10 @@ def test_install_file_not_generate(self, mock_call, _, mock_pretty_output): "Continuing install without configuration.", po_call_args[2][0][0] ) self.assertEqual("Running application install....", po_call_args[3][0][0]) - self.assertEqual("Successfully installed None.", po_call_args[4][0][0]) + self.assertEqual( + "Successfully installed None into the active Tethys Portal.", + po_call_args[4][0][0], + ) @mock.patch("tethys_cli.cli_colors.pretty_output") @mock.patch("builtins.input", side_effect=["y"]) @@ -820,10 +825,11 @@ def test_install_file_generate(self, mock_exit, mock_call, _, __): mock_call.assert_called_with(check_call) mock_exit.assert_called_with(0) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.run_services") @mock.patch("tethys_cli.install_commands.call", return_value=0) @mock.patch("tethys_cli.cli_colors.pretty_output") - def test_no_conda_input_file(self, mock_pretty_output, _, __): + def test_no_conda_input_file(self, mock_pretty_output, _, __, ___): file_path = self.root_app_path / "install-no-dep.yml" args = mock.MagicMock( file=file_path, @@ -845,7 +851,10 @@ def test_no_conda_input_file(self, mock_pretty_output, _, __): ) self.assertIn("Services Configuration Completed.", po_call_args[3][0][0]) self.assertIn("Skipping syncstores.", po_call_args[4][0][0]) - self.assertIn("Successfully installed test_app.", po_call_args[5][0][0]) + self.assertIn( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[5][0][0], + ) @mock.patch("tethys_cli.install_commands.run_services") @mock.patch("tethys_cli.install_commands.call", return_value=0) @@ -878,7 +887,10 @@ def test_input_file_with_post(self, mock_pretty_output, _, __): self.assertIn("Skipping syncstores.", po_call_args[5][0][0]) self.assertIn("Running post installation tasks...", po_call_args[6][0][0]) self.assertIn("Post Script Result: b'test", po_call_args[7][0][0]) - self.assertIn("Successfully installed test_app.", po_call_args[8][0][0]) + self.assertIn( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[8][0][0], + ) @mock.patch("tethys_cli.install_commands.run_services") @mock.patch("tethys_cli.install_commands.run_sync_stores") @@ -927,12 +939,13 @@ def test_skip_input_file(self, mock_pretty_output, _, __, ___, ____, _____, ____ po_call_args[1][0][0], ) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.run_services") @mock.patch("tethys_cli.install_commands.call", return_value=0) @mock.patch("tethys_cli.install_commands.conda_run", return_value=["", "", 1]) @mock.patch("tethys_cli.cli_colors.pretty_output") def test_conda_and_pip_package_install( - self, mock_pretty_output, mock_conda_run, mock_call, _ + self, mock_pretty_output, mock_conda_run, mock_call, _, __ ): file_path = self.root_app_path / "install-dep.yml" args = mock.MagicMock( @@ -974,7 +987,10 @@ def test_conda_and_pip_package_install( ) self.assertEqual("Services Configuration Completed.", po_call_args[6][0][0]) self.assertEqual("Skipping syncstores.", po_call_args[7][0][0]) - self.assertEqual("Successfully installed test_app.", po_call_args[8][0][0]) + self.assertEqual( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[8][0][0], + ) self.assertEqual( [sys.executable, "-m", "pip", "install", "see"], @@ -985,6 +1001,7 @@ def test_conda_and_pip_package_install( ) self.assertEqual(["tethys", "db", "sync"], mock_call.mock_calls[2][1][0]) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.input", side_effect=["cat", "y"]) @mock.patch("tethys_cli.install_commands.write_warning") @mock.patch("tethys_cli.install_commands.conda_available") # CHANGED @@ -992,7 +1009,7 @@ def test_conda_and_pip_package_install( @mock.patch("tethys_cli.install_commands.call", return_value=0) @mock.patch("tethys_cli.cli_colors.pretty_output") def test_conda_install_no_conda_proceed( - self, mock_pretty_output, mock_call, _, mock_conda_available, mock_warn, __ + self, mock_pretty_output, mock_call, _, mock_conda_available, mock_warn, __, ___ ): file_path = self.root_app_path / "install-dep.yml" args = mock.MagicMock( @@ -1022,7 +1039,10 @@ def test_conda_install_no_conda_proceed( ) self.assertEqual("Services Configuration Completed.", po_call_args[5][0][0]) self.assertEqual("Skipping syncstores.", po_call_args[6][0][0]) - self.assertEqual("Successfully installed test_app.", po_call_args[7][0][0]) + self.assertEqual( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[7][0][0], + ) self.assertEqual( [sys.executable, "-m", "pip", "install", "geojson"], @@ -1037,6 +1057,7 @@ def test_conda_install_no_conda_proceed( ) self.assertEqual(["tethys", "db", "sync"], mock_call.mock_calls[3][1][0]) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.input", side_effect=["cat", "y"]) @mock.patch("tethys_cli.install_commands.write_warning") @mock.patch("tethys_cli.install_commands.has_module") @@ -1044,7 +1065,7 @@ def test_conda_install_no_conda_proceed( @mock.patch("tethys_cli.install_commands.call", return_value=0) @mock.patch("tethys_cli.cli_colors.pretty_output") def test_conda_install_no_conda_proceed_quietly( - self, mock_pretty_output, mock_call, _, mock_has_module, mock_warn, __ + self, mock_pretty_output, mock_call, _, mock_has_module, mock_warn, __, ___ ): file_path = self.root_app_path / "install-dep.yml" args = mock.MagicMock( @@ -1080,7 +1101,10 @@ def test_conda_install_no_conda_proceed_quietly( ) self.assertEqual("Services Configuration Completed.", po_call_args[4][0][0]) self.assertEqual("Skipping syncstores.", po_call_args[5][0][0]) - self.assertEqual("Successfully installed test_app.", po_call_args[6][0][0]) + self.assertEqual( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[6][0][0], + ) self.assertEqual( [sys.executable, "-m", "pip", "install", "geojson"], @@ -1129,6 +1153,7 @@ def test_conda_install_no_conda_cancel( mock_exit.assert_called_with(0) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.input", side_effect=["cat", "y"]) @mock.patch("tethys_cli.install_commands.write_warning") @mock.patch("tethys_cli.install_commands.conda_available") # CHANGED @@ -1136,7 +1161,7 @@ def test_conda_install_no_conda_cancel( @mock.patch("tethys_cli.install_commands.call") @mock.patch("tethys_cli.cli_colors.pretty_output") def test_conda_install_no_conda_error( - self, mock_pretty_output, mock_call, _, mock_conda_available, mock_warn, __ + self, mock_pretty_output, mock_call, _, mock_conda_available, mock_warn, __, ___ ): file_path = self.root_app_path / "install-dep.yml" args = mock.MagicMock( @@ -1176,7 +1201,10 @@ def test_conda_install_no_conda_error( ) self.assertEqual("Services Configuration Completed.", po_call_args[5][0][0]) self.assertEqual("Skipping syncstores.", po_call_args[6][0][0]) - self.assertEqual("Successfully installed test_app.", po_call_args[7][0][0]) + self.assertEqual( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[7][0][0], + ) self.assertEqual( [sys.executable, "-m", "pip", "install", "geojson"], @@ -1191,12 +1219,13 @@ def test_conda_install_no_conda_error( ) self.assertEqual(["tethys", "db", "sync"], mock_call.mock_calls[3][1][0]) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.run_services") @mock.patch("tethys_cli.install_commands.call", return_value=0) @mock.patch("tethys_cli.install_commands.conda_run", return_value=["", "", 1]) @mock.patch("tethys_cli.cli_colors.pretty_output") def test_without_dependencies( - self, mock_pretty_output, mock_conda_run, mock_call, _ + self, mock_pretty_output, mock_conda_run, mock_call, _, __ ): file_path = self.root_app_path / "install-dep.yml" args = mock.MagicMock( @@ -1232,7 +1261,10 @@ def test_without_dependencies( ) self.assertEqual("Services Configuration Completed.", po_call_args[4][0][0]) self.assertEqual("Skipping syncstores.", po_call_args[5][0][0]) - self.assertEqual("Successfully installed test_app.", po_call_args[6][0][0]) + self.assertEqual( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[6][0][0], + ) # Verify that the application install still happens self.assertEqual( @@ -1240,12 +1272,13 @@ def test_without_dependencies( ) self.assertEqual(["tethys", "db", "sync"], mock_call.mock_calls[1][1][0]) + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.run_services") @mock.patch("tethys_cli.install_commands.call", return_value=0) @mock.patch("tethys_cli.install_commands.conda_run", return_value=["", "", 1]) @mock.patch("tethys_cli.cli_colors.pretty_output") def test_conda_and_pip_package_install_only_dependencies( - self, mock_pretty_output, mock_conda_run, mock_call, _ + self, mock_pretty_output, mock_conda_run, mock_call, _, mock_mamc ): chdir("..") file_path = self.root_app_path / "install-dep.yml" @@ -1295,13 +1328,15 @@ def test_conda_and_pip_package_install_only_dependencies( [sys.executable, "-m", "pip", "install", "see"], mock_call.mock_calls[0][1][0], ) + mock_mamc.assert_not_called() + @mock.patch("tethys_cli.install_commands.multiple_app_mode_check") @mock.patch("tethys_cli.install_commands.run_services") @mock.patch("tethys_cli.install_commands.call", return_value=0) @mock.patch("tethys_cli.install_commands.conda_run", return_value=["", "", 1]) @mock.patch("tethys_cli.cli_colors.pretty_output") def test_conda_and_pip_package_install_update_installed( - self, mock_pretty_output, mock_conda_run, mock_call, _ + self, mock_pretty_output, mock_conda_run, mock_call, _, mock_mamc ): file_path = self.root_app_path / "install-dep.yml" args = mock.MagicMock( @@ -1340,7 +1375,10 @@ def test_conda_and_pip_package_install_update_installed( ) self.assertEqual("Running pip installation tasks...", po_call_args[4][0][0]) self.assertEqual("Running application install....", po_call_args[5][0][0]) - self.assertEqual("Successfully installed test_app.", po_call_args[6][0][0]) + self.assertEqual( + "Successfully installed test_app into the active Tethys Portal.", + po_call_args[6][0][0], + ) self.assertEqual( [sys.executable, "-m", "pip", "install", "see"], @@ -1349,6 +1387,7 @@ def test_conda_and_pip_package_install_update_installed( self.assertEqual( [sys.executable, "-m", "pip", "install", "."], mock_call.mock_calls[1][1][0] ) + mock_mamc.assert_called_once() @mock.patch("builtins.input", side_effect=["x", 5]) @mock.patch("tethys_cli.install_commands.get_app_settings") @@ -2134,3 +2173,125 @@ def test_pip_error( "ERROR: Application installation failed with exit code 1.", warning_messages, ) + + @mock.patch("tethys_cli.install_commands.setup_django") + @override_settings(MULTIPLE_APP_MODE=True) + def test_multiple_app_mode_check__is_true(self, mock_setup_django): + install_commands.multiple_app_mode_check("test_app") + mock_setup_django.assert_not_called() + + @mock.patch("tethys_cli.install_commands.write_msg") + @mock.patch("tethys_cli.install_commands.settings_command") + @mock.patch("tethys_cli.install_commands.setup_django") + @override_settings(MULTIPLE_APP_MODE=False) + def test_multiple_app_mode_check__is_false_quiet_mode( + self, mock_setup_django, mock_sc, mock_wm + ): + install_commands.multiple_app_mode_check("test_app", quiet_mode=True) + mock_setup_django.assert_called_once() + self.assertTrue(hasattr(mock_sc.call_args_list[0][0][0], "set_kwargs")) + self.assertEqual( + mock_sc.call_args_list[0][0][0].set_kwargs[0][0], "TETHYS_PORTAL_CONFIG" + ) + self.assertTrue( + all( + m in mock_sc.call_args_list[0][0][0].set_kwargs[0][1] + for m in ["MULTIPLE_APP_MODE: False", "STANDALONE_APP: test_app"] + ) + ) + mock_wm.assert_called_once_with("STANDALONE_APP set to test_app.") + + @mock.patch("tethys_cli.install_commands.prompt_yes_or_no") + @mock.patch("tethys_cli.install_commands.write_msg") + @mock.patch("tethys_cli.install_commands.settings_command") + @mock.patch("tethys_cli.install_commands.get_installed_tethys_items") + @mock.patch("tethys_cli.install_commands.setup_django") + @override_settings(MULTIPLE_APP_MODE=False) + def test_multiple_app_mode_check__is_false_first_app( + self, mock_setup_django, mock_giti, mock_sc, mock_wm, mock_pyon + ): + mock_pyon.return_value = True + mock_giti.return_value = ["test_app"] + install_commands.multiple_app_mode_check("test_app", quiet_mode=False) + mock_setup_django.assert_called_once() + mock_giti.assert_called_once() + mock_pyon.assert_not_called() + mock_wm.assert_not_called() + mock_sc.assert_not_called() + + @mock.patch("tethys_cli.install_commands.prompt_yes_or_no") + @mock.patch("tethys_cli.install_commands.write_msg") + @mock.patch("tethys_cli.install_commands.settings_command") + @mock.patch("tethys_cli.install_commands.get_installed_tethys_items") + @mock.patch("tethys_cli.install_commands.setup_django") + @override_settings(MULTIPLE_APP_MODE=False) + def test_multiple_app_mode_check__is_false_prompt_yes( + self, mock_setup_django, mock_giti, mock_sc, mock_wm, mock_pyon + ): + mock_pyon.return_value = True + mock_giti.return_value = ["test_app", "another_app"] + install_commands.multiple_app_mode_check("test_app", quiet_mode=False) + mock_setup_django.assert_called_once() + mock_giti.assert_called_once() + mock_pyon.assert_called_once() + self.assertTrue(hasattr(mock_sc.call_args_list[0][0][0], "set_kwargs")) + self.assertEqual( + mock_sc.call_args_list[0][0][0].set_kwargs[0][0], "TETHYS_PORTAL_CONFIG" + ) + self.assertIn( + "MULTIPLE_APP_MODE: True", mock_sc.call_args_list[0][0][0].set_kwargs[0][1] + ) + mock_wm.assert_called_once_with("MULTIPLE_APP_MODE set to True.") + + @mock.patch("tethys_cli.install_commands.prompt_yes_or_no") + @mock.patch("tethys_cli.install_commands.write_msg") + @mock.patch("tethys_cli.install_commands.settings_command") + @mock.patch("tethys_cli.install_commands.get_installed_tethys_items") + @mock.patch("tethys_cli.install_commands.setup_django") + @override_settings(MULTIPLE_APP_MODE=False) + def test_multiple_app_mode_check__is_false_prompt_no_then_yes( + self, mock_setup_django, mock_giti, mock_sc, mock_wm, mock_pyon + ): + mock_pyon.side_effect = [False, True] + mock_giti.return_value = ["test_app", "another_app"] + install_commands.multiple_app_mode_check("test_app", quiet_mode=False) + mock_setup_django.assert_called_once() + mock_giti.assert_called_once() + self.assertTrue(hasattr(mock_sc.call_args_list[0][0][0], "set_kwargs")) + self.assertEqual( + mock_sc.call_args_list[0][0][0].set_kwargs[0][0], "TETHYS_PORTAL_CONFIG" + ) + self.assertTrue( + all( + m in mock_sc.call_args_list[0][0][0].set_kwargs[0][1] + for m in ["MULTIPLE_APP_MODE: False", "STANDALONE_APP: test_app"] + ) + ) + mock_wm.assert_has_calls( + [ + mock.call("MULTIPLE_APP_MODE left unchanged as False."), + mock.call("STANDALONE_APP set to test_app."), + ] + ) + + @mock.patch("tethys_cli.install_commands.prompt_yes_or_no") + @mock.patch("tethys_cli.install_commands.write_msg") + @mock.patch("tethys_cli.install_commands.settings_command") + @mock.patch("tethys_cli.install_commands.get_installed_tethys_items") + @mock.patch("tethys_cli.install_commands.setup_django") + @override_settings(MULTIPLE_APP_MODE=False) + def test_multiple_app_mode_check__is_false_prompt_no_then_no( + self, mock_setup_django, mock_giti, mock_sc, mock_wm, mock_pyon + ): + mock_pyon.side_effect = [False, False] + mock_giti.return_value = ["test_app", "another_app"] + install_commands.multiple_app_mode_check("test_app", quiet_mode=False) + mock_setup_django.assert_called_once() + mock_giti.assert_called_once() + mock_sc.assert_not_called() + mock_wm.assert_has_calls( + [ + mock.call("MULTIPLE_APP_MODE left unchanged as False."), + mock.call("STANDALONE_APP left unchanged."), + ] + ) diff --git a/tests/unit_tests/test_tethys_portal/test_context_processors.py b/tests/unit_tests/test_tethys_portal/test_context_processors.py index e299066ae..7c266a1db 100644 --- a/tests/unit_tests/test_tethys_portal/test_context_processors.py +++ b/tests/unit_tests/test_tethys_portal/test_context_processors.py @@ -36,8 +36,8 @@ def has_terms_side_effect(module_name): } return modules.get(module_name, False) - @override_settings(MULTIPLE_APP_MODE=True) @mock.patch("tethys_portal.context_processors.has_module") + @override_settings(MULTIPLE_APP_MODE=True) def test_context_processors_multiple_app_mode(self, mock_has_module): mock_user = mock.MagicMock(is_authenticated=True, is_active=True) mock_request = mock.MagicMock(user=mock_user) @@ -92,10 +92,10 @@ def test_context_processors_multiple_app_mode_no_request_user( } self.assertDictEqual(context, expected_context) - @override_settings(MULTIPLE_APP_MODE=False) @mock.patch("tethys_portal.context_processors.has_module") @mock.patch("tethys_portal.context_processors.messages") @mock.patch("tethys_portal.context_processors.get_configured_standalone_app") + @override_settings(MULTIPLE_APP_MODE=False) def test_context_processors_single_app_mode( self, mock_get_configured_standalone_app, mock_messages, mock_has_module ): @@ -150,3 +150,31 @@ def test_context_processors_debug_mode_true(self, mock_has_module): "debug_mode": True, } self.assertDictEqual(context, expected_context) + + @mock.patch( + "tethys_portal.context_processors.get_configured_standalone_app", + return_value=None, + ) + @mock.patch("tethys_portal.context_processors.has_module", return_value=False) + @override_settings(DEBUG=True) + @override_settings(MULTIPLE_APP_MODE=False) + def test_context_processors_no_optional_modules(self, _, __): + mock_user = mock.MagicMock(is_authenticated=True, is_active=True) + mock_request = mock.MagicMock(user=mock_user) + context = context_processors.tethys_portal_context(mock_request) + + expected_context = { + "has_analytical": False, + "has_cookieconsent": False, + "has_terms": False, + "has_mfa": False, + "has_gravatar": False, + "has_session_security": False, + "has_oauth2_provider": False, + "show_app_library_button": False, + "single_app_mode": True, + "configured_single_app": None, + "idp_backends": {}.keys(), + "debug_mode": True, + } + self.assertDictEqual(context, expected_context) diff --git a/tests/unit_tests/test_tethys_portal/test_views/test_app_lifecycle.py b/tests/unit_tests/test_tethys_portal/test_views/test_app_lifecycle.py index 6c9a860aa..9a59b4977 100644 --- a/tests/unit_tests/test_tethys_portal/test_views/test_app_lifecycle.py +++ b/tests/unit_tests/test_tethys_portal/test_views/test_app_lifecycle.py @@ -48,6 +48,33 @@ def test__execute_lifecycle_commands_called_process_error( self.assertEqual(mock_unpatched_run.call_count, 2) mock_sleep.assert_not_called() + @patch("tethys_portal.views.app_lifecycle.re") + @patch("tethys_portal.views.app_lifecycle.get_channel_layer") + @patch("tethys_portal.views.app_lifecycle.unpatched_run") + @patch("tethys_portal.views.app_lifecycle.run") + @patch("tethys_portal.views.app_lifecycle.sleep") + def test__execute_lifecycle_commands_from_import( + self, mock_sleep, mock_run, mock_unpatched_run, mock_get_channel_layer, mock_re + ): + mock_channel_layer = MagicMock() + mock_get_channel_layer.return_value = mock_channel_layer + mock_run.return_value.stdout = b"Successfully installed test_app" + commands = [("echo test", "Restarting server...")] + output = MagicMock( + stdout="Successfully installed test_app into your active Tethys Portal." + ) + mock_unpatched_run.return_value = output + mock_match = MagicMock() + mock_re.search.return_value = mock_match + app_lifecycle._execute_lifecycle_commands( + "test_app", commands, from_import=True + ) + self.assertTrue(mock_unpatched_run.called) + mock_run.assert_called_once() + mock_sleep.assert_called_once() + mock_re.search.assert_called_once() + mock_match.group.assert_called_once_with(mock_match.lastindex) + def test__execute_lifecycle_commands_cleanup_fails(self): cleanup = MagicMock() cleanup.side_effect = Exception() diff --git a/tethys_apps/base/controller.py b/tethys_apps/base/controller.py index 8d2363402..ec9cf7a7f 100644 --- a/tethys_apps/base/controller.py +++ b/tethys_apps/base/controller.py @@ -482,7 +482,7 @@ def page( name: The internal name of the page, using only letters and underscores. Must be unique within the app. Defaults to the name of the function being decorated. url: The page portion (i.e. slug) of the URL associated with this page (e.g. http://localhost:8000/app/my-app/). Defaults to the name of the function being decorated, with underscores replaced by dashes (e.g. ``my_page`` becomes "my-page"). regex: Custom regex pattern(s) for url variables. If a string is provided, it will be applied to all variables. If a list or tuple is provided, they will be applied in variable order. - handler: Dot-notation path to a handler function that will process the actual request. This is for an advanced escape-hatch pattern to get back to Django templating. + handler: Function that will process the actual request. This is for an advanced escape-hatch pattern to get back to Django templating. login_required: If user is required to be logged in to access the controller. Default is `True`. redirect_field_name: URL query string parameter for the redirect path. Default is "next". login_url: URL to send users to in order to authenticate. This defaults to the built-in login page of your Tethys Portal. diff --git a/tethys_cli/cli_helpers.py b/tethys_cli/cli_helpers.py index 107e8d509..73c143ddb 100644 --- a/tethys_cli/cli_helpers.py +++ b/tethys_cli/cli_helpers.py @@ -168,7 +168,7 @@ def wrapped(*args, **kwargs): return wrapped -def prompt_yes_or_no(question, default="y"): +def prompt_yes_or_no(question): """Handles a yes/no question cli prompt Returns: @@ -183,7 +183,7 @@ def prompt_yes_or_no(question, default="y"): valid = False while not valid: try: - response = input(f"{question} [y/n]: ") or default + response = input(f"{question} [y/n]: ") except (KeyboardInterrupt, SystemExit): return None diff --git a/tethys_cli/gen_commands.py b/tethys_cli/gen_commands.py index e80c66147..c8593aece 100644 --- a/tethys_cli/gen_commands.py +++ b/tethys_cli/gen_commands.py @@ -730,7 +730,8 @@ def render_template(file_type, context, destination_path): def write_path_to_console(file_path, args): - write_info(f'File generated at "{file_path}".') + action_performed = getattr(args, "action_performed", "generated") + write_info(f'File {action_performed} at "{file_path}".') def get_target_tethys_app_dir(args): diff --git a/tethys_cli/install_commands.py b/tethys_cli/install_commands.py index db8c6b5ed..5b852a1ed 100644 --- a/tethys_cli/install_commands.py +++ b/tethys_cli/install_commands.py @@ -8,6 +8,7 @@ from collections.abc import Mapping import sys +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, ValidationError from tethys_cli.cli_colors import ( @@ -16,6 +17,7 @@ write_warning, write_success, ) +from tethys_cli.settings_commands import settings_command from tethys_cli.services_commands import services_list_command from tethys_cli.cli_helpers import ( setup_django, @@ -23,12 +25,14 @@ load_conda_commands, conda_run_command, conda_available, + prompt_yes_or_no, ) from tethys_apps.utilities import ( link_service_to_app_setting, get_app_settings, get_service_model_from_type, get_tethys_home_dir, + get_installed_tethys_items, ) from .gen_commands import download_vendor_static_files @@ -880,8 +884,12 @@ def install_command(args): return + multiple_app_mode_check(app_name, quiet_mode=args.quiet) + if args.no_db_sync: - write_success(f"Successfully installed {app_name}.") + write_success( + f"Successfully installed {app_name} into the active Tethys Portal." + ) return call(["tethys", "db", "sync"]) @@ -927,8 +935,7 @@ def install_command(args): process = Popen(str(path_to_post), shell=True, stdout=PIPE) stdout = process.communicate()[0] write_msg("Post Script Result: {}".format(stdout)) - - write_success(f"Successfully installed {app_name}.") + write_success(f"Successfully installed {app_name} into the active Tethys Portal.") def validate_schema(check_str, check_list): @@ -962,3 +969,64 @@ def assign_json_value(value): else: write_error(f"The current value: {value} is not a dict or a valid file path") return None + + +def multiple_app_mode_check(new_app_name, quiet_mode=False): + """ + Check if MULTIPLE_APP_MODE needs to be updated based on the number of installed apps. + """ + if settings.MULTIPLE_APP_MODE: + return + setup_django() + if quiet_mode: + update_settings_args = Namespace( + set_kwargs=[ + ( + "TETHYS_PORTAL_CONFIG", + f""" + MULTIPLE_APP_MODE: False + STANDALONE_APP: {new_app_name} + """, + ) + ] + ) + settings_command(update_settings_args) + write_msg(f"STANDALONE_APP set to {new_app_name}.") + elif len(get_installed_tethys_items(apps=True)) > 1: + response = prompt_yes_or_no( + "Your portal has multiple apps installed, but MULTIPLE_APP_MODE is set to False. Would you like to change that to True now?" + ) + if response is True: + update_settings_args = Namespace( + set_kwargs=[ + ( + "TETHYS_PORTAL_CONFIG", + """ + MULTIPLE_APP_MODE: True + """, + ) + ] + ) + settings_command(update_settings_args) + write_msg("MULTIPLE_APP_MODE set to True.") + elif response is False: + write_msg("MULTIPLE_APP_MODE left unchanged as False.") + response = prompt_yes_or_no( + f"Would you like to set the STANDALONE_APP to the newly installed app: {new_app_name}?" + ) + if response is True: + update_settings_args = Namespace( + set_kwargs=[ + ( + "TETHYS_PORTAL_CONFIG", + f""" + MULTIPLE_APP_MODE: False + STANDALONE_APP: {new_app_name} + """, + ) + ] + ) + settings_command(update_settings_args) + write_msg(f"STANDALONE_APP set to {new_app_name}.") + elif response is False: + write_msg("STANDALONE_APP left unchanged.") diff --git a/tethys_cli/settings_commands.py b/tethys_cli/settings_commands.py index 4be4c3181..55dc0ec88 100644 --- a/tethys_cli/settings_commands.py +++ b/tethys_cli/settings_commands.py @@ -86,6 +86,7 @@ def write_settings(tethys_settings): tethys_portal_settings=portal_settings, directory=None, overwrite=True, + action_performed="updated", ) generate_command(args) diff --git a/tethys_portal/views/app_lifecycle.py b/tethys_portal/views/app_lifecycle.py index fd4d8ce8e..909f9b35f 100644 --- a/tethys_portal/views/app_lifecycle.py +++ b/tethys_portal/views/app_lifecycle.py @@ -64,7 +64,9 @@ def unpatched_run(main): asyncio.run(main) -def _execute_lifecycle_commands(app_package, command_message_tuples, cleanup=None): +def _execute_lifecycle_commands( + app_package, command_message_tuples, from_import=False, cleanup=None +): if cleanup and not callable(cleanup): raise ValueError('The "cleanup" argument must be a function or callable.') @@ -95,9 +97,13 @@ def _execute_lifecycle_commands(app_package, command_message_tuples, cleanup=Non ) # So the websocket has time to send the message prior to killing the server result = run(command, shell=True, check=True, capture_output=True) output = str(result.stdout) - if "Successfully installed " in output: - match = re.search(r"Successfully installed ([\w_]+)", output) - revised_app_package = match.group(match.lastindex) + if from_import: + match = re.search( + r"Successfully installed ([\w_]+) into your active Tethys Portal.", + output, + ) + if match: + revised_app_package = match.group(match.lastindex) except CalledProcessError as e: unpatched_run( @@ -194,7 +200,12 @@ def import_app(request): Timer( 1, _execute_lifecycle_commands, - args=[app_package, command_message_tuples, lambda: rmtree(tmpdir)], + args=[ + app_package, + command_message_tuples, + True, + lambda: rmtree(tmpdir), + ], ).start() context["app_name"] = import_name @@ -252,6 +263,8 @@ def create_app(request): context["app_package"] = project_name del context["form"] + context["project_location"] = Path.cwd() / f"{APP_PREFIX}-" + return render(request, "tethys_portal/create_app.html", context)