refactor: track completed app setup wizards and re-run the setup wizard upon new app installation. (#32640)

This commit is contained in:
rohitwaghchaure 2025-06-03 12:36:22 +05:30 committed by GitHub
parent 2baedc18c6
commit 5c6b2b5bec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 301 additions and 24 deletions

View file

@ -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"

View file

@ -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

View file

@ -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:

View file

@ -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):

View file

@ -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
}
}

View file

@ -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

View file

@ -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",
)

View file

@ -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

View file

@ -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()

View file

@ -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);
}

View file

@ -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):

View file

@ -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()

View file

@ -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()

View file

@ -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,
},
)

View file

@ -380,6 +380,7 @@ frappe.Application = class Application {
if (r.exc) {
return;
}
me.redirect_to_login();
},
});

View file

@ -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"];

View file

@ -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,

View file

@ -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(),

View file

@ -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)

View file

@ -11,8 +11,8 @@ endblock -%} {%- block footer -%} {%- endblock -%} {% block content %}
{% set appsCount = apps|length if apps|length <= 6 else 6 %}
<div class="apps" style="grid-template-columns: repeat({{ appsCount }}, 1fr);">
{% for app in apps %}
<a href="{{ app.route }}" class="app-icon">
<div class="app-logo">
<a class="app-icon" style="cursor: pointer;">
<div class="app-logo app-name-cls" app-name="{{ app.name }}" app-route="{{ app.route }}">
<img src="{{ app.logo }}" />
<div
app-name="{{ app.name }}"
@ -73,7 +73,25 @@ endblock -%} {%- block footer -%} {%- endblock -%} {% block content %}
},
});
});
$(".app-name-cls").on("click", function (e) {
localStorage.current_app = $(this).attr("app-name");
localStorage.current_route = $(this).attr("app-route");
frappe.call({
method: "frappe.apps.get_incomplete_setup_route",
args: { current_app: localStorage.current_app, app_route:localStorage.current_route },
callback: function (r) {
window.location.href = r.message;
},
})
});
$(".logout-btn").on("click", function () {
localStorage.current_app = "";
localStorage.current_route = "";
frappe.call({
method: "logout",
callback: function () {

View file

@ -17,6 +17,6 @@ def get_boot():
"site_name": frappe.local.site,
"read_only_mode": frappe.flags.read_only,
"csrf_token": frappe.sessions.get_csrf_token(),
"setup_complete": cint(frappe.get_system_settings("setup_complete")),
"setup_complete": frappe.is_setup_complete(),
}
)