From 5c6b2b5bec0542734e6526245550cf7a70a9343b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 3 Jun 2025 12:36:22 +0530 Subject: [PATCH] refactor: track completed app setup wizards and re-run the setup wizard upon new app installation. (#32640) --- frappe/__init__.py | 12 +++ frappe/apps.py | 33 ++++++++ frappe/boot.py | 33 +++++++- .../address_template/address_template.py | 2 +- .../installed_application.json | 29 ++++++- .../installed_application.py | 2 + .../installed_applications.py | 76 +++++++++++++++++++ .../desk/doctype/desktop_icon/desktop_icon.py | 2 +- frappe/desk/notifications.py | 2 +- frappe/desk/page/setup_wizard/setup_wizard.js | 14 +++- frappe/desk/page/setup_wizard/setup_wizard.py | 54 +++++++++++-- frappe/installer.py | 2 + frappe/patches.txt | 1 + frappe/patches/v16_0/enable_setup_complete.py | 24 ++++++ frappe/public/js/frappe/desk.js | 1 + frappe/public/js/frappe/router.js | 7 +- frappe/sessions.py | 2 +- frappe/utils/__init__.py | 2 +- frappe/utils/install.py | 3 +- frappe/www/apps.html | 22 +++++- frappe/www/billing.py | 2 +- 21 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 frappe/patches/v16_0/enable_setup_complete.py diff --git a/frappe/__init__.py b/frappe/__init__.py index 967b851e0e..39ae573211 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1931,6 +1931,18 @@ def get_active_domains(): return get_active_domains() +@request_cache +def is_setup_complete(): + is_setup_complete = False + if not frappe.db.table_exists("Installed Application"): + return is_setup_complete + + if all(frappe.get_all("Installed Application", {"has_setup_wizard": 1}, pluck="is_setup_complete")): + is_setup_complete = True + + return is_setup_complete + + @whitelist(allow_guest=True) def ping(): return "pong" diff --git a/frappe/apps.py b/frappe/apps.py index a8476816a2..08a68ba3de 100644 --- a/frappe/apps.py +++ b/frappe/apps.py @@ -5,6 +5,11 @@ import re import frappe from frappe import _ +from frappe.core.doctype.installed_applications.installed_applications import ( + get_apps_with_incomplete_dependencies, + get_setup_wizard_completed_apps, + get_setup_wizard_not_required_apps, +) # check if route is /app or /app/* and not /app1 or /app1/* DESK_APP_PATTERN = re.compile(r"^/app(/.*)?$") @@ -15,6 +20,13 @@ def get_apps(): apps = frappe.get_installed_apps() app_list = [] for app in apps: + if ( + app not in get_setup_wizard_completed_apps() + and app not in get_setup_wizard_not_required_apps() + and "System Manager" not in frappe.get_roles() + ): + continue + if app == "frappe": continue app_details = frappe.get_hooks("add_to_apps_screen", app_name=app) @@ -79,3 +91,24 @@ def set_app_as_default(app_name): frappe.db.set_value("User", frappe.session.user, "default_app", "") else: frappe.db.set_value("User", frappe.session.user, "default_app", app_name) + + +@frappe.whitelist() +def get_incomplete_setup_route(current_app, app_route): + pending_apps = get_apps_with_incomplete_dependencies(current_app) + + if not pending_apps: + return app_route + + for app in pending_apps: + if app == "frappe": + return "app" + + app_details = frappe.get_hooks("add_to_apps_screen", app_name=app) + if not app_details: + continue + + if route := app_details[0].get("route"): + return route + + return app_route diff --git a/frappe/boot.py b/frappe/boot.py index 636948359d..612e2f75be 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -9,6 +9,10 @@ import os import frappe import frappe.defaults import frappe.desk.desk_page +from frappe.core.doctype.installed_applications.installed_applications import ( + get_setup_wizard_completed_apps, + get_setup_wizard_not_required_apps, +) from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours @@ -42,6 +46,8 @@ def get_bootinfo(): # system info bootinfo.sitename = frappe.local.site bootinfo.sysdefaults = frappe.defaults.get_defaults() + bootinfo.sysdefaults["setup_complete"] = frappe.is_setup_complete() + bootinfo.server_date = frappe.utils.nowdate() if frappe.session["user"] != "Guest": @@ -114,9 +120,34 @@ def get_bootinfo(): if sentry_dsn := get_sentry_dsn(): bootinfo.sentry_dsn = sentry_dsn + bootinfo.setup_wizard_completed_apps = get_setup_wizard_completed_apps() or [] + bootinfo.setup_wizard_not_required_apps = get_setup_wizard_not_required_apps() or [] + remove_apps_with_incomplete_dependencies(bootinfo) + return bootinfo +def remove_apps_with_incomplete_dependencies(bootinfo): + remove_apps = [] + + for app in bootinfo.setup_wizard_not_required_apps: + if app in bootinfo.setup_wizard_completed_apps: + continue + + for required_apps in frappe.get_hooks("required_apps"): + required_apps = required_apps.split("/") + + for required_app in required_apps: + if app not in bootinfo.setup_wizard_not_required_apps: + continue + + if required_app not in bootinfo.setup_wizard_completed_apps: + remove_apps.append(app) + + for app in remove_apps: + bootinfo.setup_wizard_not_required_apps.remove(app) + + def get_letter_heads(): letter_heads = {} @@ -345,7 +376,7 @@ def add_home_page(bootinfo, docs): return home_page = frappe.db.get_default("desktop:home_page") - if home_page == "setup-wizard": + if not frappe.is_setup_complete(): bootinfo.setup_wizard_requires = frappe.get_hooks("setup_wizard_requires") try: diff --git a/frappe/contacts/doctype/address_template/address_template.py b/frappe/contacts/doctype/address_template/address_template.py index d993a54af1..256986cc0c 100644 --- a/frappe/contacts/doctype/address_template/address_template.py +++ b/frappe/contacts/doctype/address_template/address_template.py @@ -29,7 +29,7 @@ class AddressTemplate(Document): if not self.is_default and not self._get_previous_default(): self.is_default = 1 - if frappe.get_system_settings("setup_complete"): + if frappe.is_setup_complete(): frappe.msgprint(_("Setting this Address Template as default as there is no other default")) def on_update(self): diff --git a/frappe/core/doctype/installed_application/installed_application.json b/frappe/core/doctype/installed_application/installed_application.json index 3b185e9576..fa0cda8cc4 100644 --- a/frappe/core/doctype/installed_application/installed_application.json +++ b/frappe/core/doctype/installed_application/installed_application.json @@ -6,10 +6,13 @@ "field_order": [ "app_name", "app_version", - "git_branch" + "git_branch", + "has_setup_wizard", + "is_setup_complete" ], "fields": [ { + "columns": 2, "fieldname": "git_branch", "fieldtype": "Data", "in_list_view": 1, @@ -18,6 +21,7 @@ "reqd": 1 }, { + "columns": 2, "fieldname": "app_name", "fieldtype": "Data", "in_list_view": 1, @@ -26,25 +30,44 @@ "reqd": 1 }, { + "columns": 2, "fieldname": "app_version", "fieldtype": "Data", "in_list_view": 1, "label": "Application Version", "read_only": 1, "reqd": 1 + }, + { + "columns": 2, + "default": "0", + "fieldname": "has_setup_wizard", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Has Setup Wizard" + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_setup_complete", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Setup Complete?" } ], + "grid_page_length": 50, "istable": 1, "links": [], - "modified": "2024-03-23 16:03:27.892752", + "modified": "2025-05-22 12:26:49.523690", "modified_by": "Administrator", "module": "Core", "name": "Installed Application", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/installed_application/installed_application.py b/frappe/core/doctype/installed_application/installed_application.py index 52b6c4ce31..af5f172178 100644 --- a/frappe/core/doctype/installed_application/installed_application.py +++ b/frappe/core/doctype/installed_application/installed_application.py @@ -17,6 +17,8 @@ class InstalledApplication(Document): app_name: DF.Data app_version: DF.Data git_branch: DF.Data + has_setup_wizard: DF.Check + is_setup_complete: DF.Check parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/frappe/core/doctype/installed_applications/installed_applications.py b/frappe/core/doctype/installed_applications/installed_applications.py index 4f8e987533..bcb6c8cdec 100644 --- a/frappe/core/doctype/installed_applications/installed_applications.py +++ b/frappe/core/doctype/installed_applications/installed_applications.py @@ -26,18 +26,40 @@ class InstalledApplications(Document): # end: auto-generated types def update_versions(self): + app_wise_setup_details = self.get_app_wise_setup_details() + self.delete_key("installed_applications") for app in frappe.utils.get_installed_apps_info(): + has_setup_wizard = 0 + if app.get("app_name") == "frappe" or frappe.get_hooks(app_name=app.get("app_name")).get( + "setup_wizard_stages" + ): + has_setup_wizard = 1 + self.append( "installed_applications", { "app_name": app.get("app_name"), "app_version": app.get("version") or "UNVERSIONED", "git_branch": app.get("branch") or "UNVERSIONED", + "has_setup_wizard": has_setup_wizard, + "is_setup_complete": app_wise_setup_details.get(app.get("app_name")) or 0, }, ) + self.save() + def get_app_wise_setup_details(self): + """Get app wise setup details from the Installed Application doctype""" + return frappe._dict( + frappe.get_all( + "Installed Application", + fields=["app_name", "is_setup_complete"], + filters={"has_setup_wizard": 1}, + as_list=True, + ) + ) + @frappe.whitelist() def update_installed_apps_order(new_order: list[str] | str): @@ -85,3 +107,57 @@ def get_installed_app_order() -> list[str]: frappe.only_for("System Manager") return frappe.get_installed_apps(_ensure_on_bench=True) + + +@frappe.request_cache +def get_setup_wizard_completed_apps(): + """Get list of apps that have completed setup wizard""" + return frappe.get_all( + "Installed Application", + filters={"has_setup_wizard": 1, "is_setup_complete": 1}, + pluck="app_name", + ) + + +@frappe.request_cache +def get_setup_wizard_not_required_apps(): + """Get list of apps that do not require setup wizard""" + return frappe.get_all( + "Installed Application", + filters={"has_setup_wizard": 0}, + pluck="app_name", + ) + + +@frappe.request_cache +def get_apps_with_incomplete_dependencies(current_app): + """Get apps with incomplete dependencies.""" + dependent_apps = ["frappe"] + + if apps := frappe.get_hooks("required_apps", app_name=current_app): + dependent_apps.extend(apps) + + parsed_apps = [] + for apps in dependent_apps: + apps = apps.split("/") + parsed_apps.extend(apps) + + pending_apps = get_setup_wizard_pending_apps(parsed_apps) + + return pending_apps + + +@frappe.request_cache +def get_setup_wizard_pending_apps(apps=None): + """Get list of apps that have completed setup wizard""" + + filters = {"has_setup_wizard": 1, "is_setup_complete": 0} + if apps: + filters["app_name"] = ["in", apps] + + return frappe.get_all( + "Installed Application", + filters=filters, + order_by="idx", + pluck="app_name", + ) diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 716693b113..9236cfde6c 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -258,7 +258,7 @@ def set_desktop_icons(visible_list, ignore_duplicate=True): an icon for the doctype""" # clear all custom only if setup is not complete - if not frappe.defaults.get_defaults().get("setup_complete", 0): + if not frappe.is_setup_complete(): frappe.db.delete("Desktop Icon", {"standard": 0}) # set standard as blocked and hidden if setting first active domain diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 641bf60eb6..1dde537a5d 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -25,7 +25,7 @@ def get_notifications(): "open_count_doctype": {}, "targets": {}, } - if frappe.flags.in_install or not frappe.get_system_settings("setup_complete"): + if frappe.flags.in_install or not frappe.is_setup_complete(): return out config = get_notification_config() diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 2183b8f82e..676b8da9dd 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -63,6 +63,13 @@ frappe.pages["setup-wizard"].on_page_show = function () { }; frappe.setup.on("before_load", function () { + if ( + frappe.boot.setup_wizard_completed_apps?.length && + frappe.boot.setup_wizard_completed_apps.includes("frappe") + ) { + return; + } + // load slides frappe.setup.slides_settings.forEach((s) => { if (!(s.name === "user" && frappe.boot.developer_mode)) { @@ -207,7 +214,12 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } setTimeout(function () { // Reload - window.location.href = frappe.boot.apps_data.default_path || "/app"; + let current_route = localStorage.current_route; + + localStorage.current_route = ""; + localStorage.current_app = ""; + + window.location.href = current_route || frappe.boot.apps_data.default_path || "/app"; }, 2000); } diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index a17c7d32bb..26c21e107d 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -5,6 +5,7 @@ import json import frappe from frappe import _ +from frappe.core.doctype.installed_applications.installed_applications import get_setup_wizard_completed_apps from frappe.geo.country_info import get_country_info from frappe.permissions import AUTOMATIC_ROLES from frappe.translate import send_translations, set_default_language @@ -22,7 +23,12 @@ def get_setup_stages(args): # nosemgrep "status": _("Updating global settings"), "fail_msg": _("Failed to update global settings"), "tasks": [ - {"fn": update_global_settings, "args": args, "fail_msg": "Failed to update global settings"} + { + "fn": update_global_settings, + "args": args, + "fail_msg": "Failed to update global settings", + "app_name": "frappe", + } ], } ] @@ -47,18 +53,18 @@ def setup_complete(args): and clears cache. If wizard breaks, calls `setup_wizard_exception` hook""" # Setup complete: do not throw an exception, let the user continue to desk - if cint(frappe.db.get_single_value("System Settings", "setup_complete")): + if frappe.is_setup_complete(): return {"status": "ok"} - args = parse_args(sanitize_input(args)) - stages = get_setup_stages(args) + kwargs = parse_args(sanitize_input(args)) + stages = get_setup_stages(kwargs) is_background_task = frappe.conf.get("trigger_site_setup_in_background") if is_background_task: - process_setup_stages.enqueue(stages=stages, user_input=args, is_background_task=True, at_front=True) + process_setup_stages.enqueue(stages=stages, user_input=kwargs, is_background_task=True, at_front=True) return {"status": "registered"} else: - return process_setup_stages(stages, args) + return process_setup_stages(stages, kwargs) @frappe.whitelist() @@ -87,6 +93,8 @@ def initialize_system_settings_and_user(system_settings_data, user_data): def process_setup_stages(stages, user_input, is_background_task=False): from frappe.utils.telemetry import capture + setup_wizard_completed_apps = get_setup_wizard_completed_apps() + capture("initated_server_side", "setup") try: frappe.flags.in_setup_wizard = True @@ -100,7 +108,18 @@ def process_setup_stages(stages, user_input, is_background_task=False): for task in stage.get("tasks"): current_task = task + if task.get("app_name") and task.get("app_name") in setup_wizard_completed_apps: + continue + + if "frappe" in setup_wizard_completed_apps: + set_missing_values(task) + task.get("fn")(task.get("args")) + + if task.get("app_name"): + enable_setup_wizard_complete(task.get("app_name")) + else: + enable_setup_wizard_complete("frappe") except Exception: handle_setup_exception(user_input) message = current_task.get("fail_msg") if current_task else "Failed to complete setup" @@ -123,6 +142,23 @@ def process_setup_stages(stages, user_input, is_background_task=False): frappe.flags.in_setup_wizard = False +def set_missing_values(task): + if task and task.get("args"): + doc = frappe.get_doc("System Settings") + task["args"].update( + { + "country": doc.country, + "time_zone": doc.time_zone, + "time_format": doc.time_format, + "currency": doc.currency, + } + ) + + +def enable_setup_wizard_complete(app_name): + frappe.db.set_value("Installed Application", {"app_name": app_name}, "is_setup_complete", 1) + + def update_global_settings(args): # nosemgrep if args.language and args.language != "English": set_default_language(get_language_code(args.lang)) @@ -166,6 +202,7 @@ def get_setup_complete_hooks(args): # nosemgrep "fn": frappe.get_attr(method), "args": args, "fail_msg": "Failed to execute method", + "app_name": method.split(".")[0], } ], } @@ -183,6 +220,9 @@ def handle_setup_exception(args): # nosemgrep def update_system_settings(args): # nosemgrep + if not args.get("country"): + return + number_format = get_country_info(args.get("country")).get("number_format", "#,###.##") # replace these as float number formats, as they have 0 precision @@ -310,8 +350,6 @@ def disable_future_access(): # Enable onboarding after install frappe.db.set_single_value("System Settings", "enable_onboarding", 1) - frappe.db.set_single_value("System Settings", "setup_complete", 1) - @frappe.whitelist() def load_messages(language): diff --git a/frappe/installer.py b/frappe/installer.py index ed24ac7c80..481c2c5a2b 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -344,6 +344,8 @@ def add_to_installed_apps(app_name, rebuild_website=True): if frappe.flags.in_install: post_install(rebuild_website) + frappe.get_single("Installed Applications").update_versions() + def remove_from_installed_apps(app_name): installed_apps = frappe.get_installed_apps() diff --git a/frappe/patches.txt b/frappe/patches.txt index fbf893d40b..3ae67c9092 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -1,4 +1,5 @@ [pre_model_sync] +frappe.patches.v16_0.enable_setup_complete frappe.patches.v15_0.remove_implicit_primary_key frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3 execute:frappe.utils.global_search.setup_global_search_table() diff --git a/frappe/patches/v16_0/enable_setup_complete.py b/frappe/patches/v16_0/enable_setup_complete.py new file mode 100644 index 0000000000..7dc9c0acee --- /dev/null +++ b/frappe/patches/v16_0/enable_setup_complete.py @@ -0,0 +1,24 @@ +import frappe + + +def execute(): + frappe.reload_doc("core", "doctype", "installed_application") + frappe.reload_doc("core", "doctype", "installed_applications") + + is_setup_complete = frappe.db.get_single_value("System Settings", "setup_complete") + for app_name in frappe.get_all("Installed Application", pluck="app_name"): + has_setup_wizard = 0 + if app_name == "frappe": + has_setup_wizard = 1 + elif frappe.get_hooks(app_name=app_name).get("setup_wizard_stages"): + has_setup_wizard = 1 + + if has_setup_wizard: + frappe.db.set_value( + "Installed Application", + {"app_name": app_name}, + { + "has_setup_wizard": 1, + "is_setup_complete": is_setup_complete, + }, + ) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 9ebaaf61ac..e4fe8d1573 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -380,6 +380,7 @@ frappe.Application = class Application { if (r.exc) { return; } + me.redirect_to_login(); }, }); diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 2ce1982e59..ae132ae7cc 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -129,7 +129,12 @@ frappe.router = { if (!frappe.app) return; let sub_path = this.get_sub_path(); - if (frappe.boot.setup_complete) { + let current_app = localStorage.current_app; + + if ( + frappe.boot.setup_complete || + (current_app && frappe.boot.setup_wizard_not_required_apps?.includes(current_app)) + ) { !frappe.re_route["setup-wizard"] && (frappe.re_route["setup-wizard"] = "app"); } else if (!sub_path.startsWith("setup-wizard")) { frappe.re_route["setup-wizard"] && delete frappe.re_route["setup-wizard"]; diff --git a/frappe/sessions.py b/frappe/sessions.py index 8404405148..3e3553c8b2 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -170,7 +170,7 @@ def get(): bootinfo["lang"] = frappe.translate.get_user_lang() bootinfo["disable_async"] = frappe.conf.disable_async - bootinfo["setup_complete"] = cint(frappe.get_system_settings("setup_complete")) + bootinfo["setup_complete"] = frappe.is_setup_complete() bootinfo["apps_data"] = { "apps": get_apps() or [], "is_desk_apps": 1 if bool(is_desk_apps(get_apps())) else 0, diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 30810f8b41..9f0ae2b567 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -814,7 +814,7 @@ def get_site_info(): "country": system_settings.country, "language": system_settings.language or "english", "time_zone": system_settings.time_zone, - "setup_complete": cint(system_settings.setup_complete), + "setup_complete": frappe.is_setup_complete(), "scheduler_enabled": system_settings.enable_scheduler, # usage "emails_sent": get_emails_sent_this_month(), diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 32856bc8fa..9287e0fe1e 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -45,7 +45,6 @@ def after_install(): # only set home_page if the value doesn't exist in the db if not frappe.db.get_default("desktop:home_page"): frappe.db.set_default("desktop:home_page", "setup-wizard") - frappe.db.set_single_value("System Settings", "setup_complete", 0) # clear test log from frappe.tests.utils.generators import _clear_test_log @@ -136,7 +135,7 @@ def before_tests(): frappe.clear_cache() # complete setup if missing - if not cint(frappe.db.get_single_value("System Settings", "setup_complete")): + if not frappe.is_setup_complete(): complete_setup_wizard() frappe.db.set_single_value("Website Settings", "disable_signup", 0) diff --git a/frappe/www/apps.html b/frappe/www/apps.html index ae2ac3f3b1..dcc3b1ad60 100644 --- a/frappe/www/apps.html +++ b/frappe/www/apps.html @@ -11,8 +11,8 @@ endblock -%} {%- block footer -%} {%- endblock -%} {% block content %} {% set appsCount = apps|length if apps|length <= 6 else 6 %}
{% for app in apps %} - -