* setup complete stages * [setup] better setup-in-progress card * restructure setup exception flow * use setup_stages hook * Add message for non-dev mode, fail instead of error * message to not include commits in app setup stagesversion-14
@@ -182,39 +182,72 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { | |||
} | |||
action_on_complete() { | |||
var me = this; | |||
if (!this.current_slide.set_values()) return; | |||
this.update_values(); | |||
this.show_working_state(); | |||
this.disable_keyboard_nav(); | |||
this.listen_for_setup_stages(); | |||
return frappe.call({ | |||
method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete", | |||
args: {args: this.values}, | |||
callback: function() { | |||
me.show_setup_complete_state(); | |||
if(frappe.setup.welcome_page) { | |||
localStorage.setItem("session_last_route", frappe.setup.welcome_page); | |||
callback: (r) => { | |||
if(r.message.status === 'ok') { | |||
this.post_setup_success(); | |||
} else if(r.message.fail !== undefined) { | |||
this.abort_setup(r.message.fail); | |||
} | |||
setTimeout(function() { | |||
// Reload | |||
window.location.href = ''; | |||
}, 2000); | |||
setTimeout(()=> { | |||
$('body').removeClass('setup-state'); | |||
}, 20000); | |||
}, | |||
error: function() { | |||
var d = frappe.msgprint(__("There were errors.")); | |||
d.custom_onhide = function() { | |||
$(me.parent).find('.page-card-container').remove(); | |||
$('body').removeClass('setup-state'); | |||
me.container.show(); | |||
frappe.set_route(me.page_name, me.slides.length - 1); | |||
}; | |||
} | |||
error: this.abort_setup.bind(this, "Error in setup", true) | |||
}); | |||
} | |||
post_setup_success() { | |||
this.set_setup_complete_message(__("Setup Complete"), __("Refreshing...")); | |||
if(frappe.setup.welcome_page) { | |||
localStorage.setItem("session_last_route", frappe.setup.welcome_page); | |||
} | |||
setTimeout(function() { | |||
// Reload | |||
window.location.href = ''; | |||
}, 2000); | |||
} | |||
abort_setup(fail_msg, error=false) { | |||
this.$working_state.find('.state-icon-container').html(''); | |||
fail_msg = fail_msg ? fail_msg : __("Failed to complete setup"); | |||
if(error && !frappe.boot.developer_mode) { | |||
frappe.msgprint(`Don't worry. It's not you, it's us. We've | |||
received the issue details and will get back to you on the solution. | |||
Please feel free to contact us on support@erpnext.com in the meantime.`); | |||
} | |||
this.update_setup_message('Could not start up: ' + fail_msg); | |||
this.$working_state.find('.title').html('Setup failed'); | |||
this.$abort_btn.show(); | |||
} | |||
listen_for_setup_stages() { | |||
frappe.realtime.on("setup_task", (data) => { | |||
// console.log('data', data); | |||
if(data.stage_status) { | |||
// .html('Process '+ data.progress[0] + ' of ' + data.progress[1] + ': ' + data.stage_status); | |||
this.update_setup_message(data.stage_status); | |||
this.set_setup_load_percent((data.progress[0]+1)/data.progress[1] * 100); | |||
} | |||
if(data.fail_msg) { | |||
this.abort_setup(data.fail_msg); | |||
} | |||
}) | |||
} | |||
update_setup_message(message) { | |||
this.$working_state.find('.setup-message').html(message); | |||
} | |||
get_setup_slides_filtered_by_domain() { | |||
var filtered_slides = []; | |||
frappe.setup.slides.forEach(function(slide) { | |||
@@ -233,51 +266,56 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { | |||
show_working_state() { | |||
this.container.hide(); | |||
$('body').addClass('setup-state'); | |||
frappe.set_route(this.page_name); | |||
this.working_state_message = this.get_message( | |||
__("Setting Up"), | |||
__("Sit tight while your system is being setup. This may take a few moments."), | |||
true | |||
).appendTo(this.parent); | |||
this.$working_state = this.get_message( | |||
__("Setting up your system"), | |||
__("Starting Frappé ...")).appendTo(this.parent); | |||
this.attach_abort_button(); | |||
this.current_id = this.slides.length; | |||
this.current_slide = null; | |||
this.completed_state_message = this.get_message( | |||
__("Setup Complete"), | |||
__("Refreshing...") | |||
); | |||
} | |||
show_setup_complete_state() { | |||
this.working_state_message.hide(); | |||
this.completed_state_message.appendTo(this.parent); | |||
attach_abort_button() { | |||
this.$abort_btn = $(`<button class='btn btn-default btn-xs text-muted' | |||
style="margin-bottom: 30px;">${__('Retry')}</button>`); | |||
this.$working_state.find('.content').append(this.$abort_btn); | |||
this.$abort_btn.on('click', () => { | |||
$(this.parent).find('.setup-in-progress').remove(); | |||
this.container.show(); | |||
frappe.set_route(this.page_name, this.slides.length - 1); | |||
}); | |||
this.$abort_btn.hide(); | |||
} | |||
get_message(title, message="", loading=false) { | |||
const loading_html = loading | |||
? '<div style="width:100%;height:100%" class="lds-rolling state-icon"><div></div></div>' | |||
: `<div style="width:100%;height:100%" class="state-icon"> | |||
<i class="fa fa-check-circle text-success" | |||
style="font-size: 64px; margin-top: -8px;"></i> | |||
</div>`; | |||
return $(`<div class="page-card-container" data-state="setup"> | |||
<div class="page-card"> | |||
<div class="page-card-head"> | |||
${loading | |||
? `<span class="indicator orange">${title}</span>` | |||
: `<span class="indicator green">${title}</span>` | |||
} | |||
</div> | |||
<p>${message}</p> | |||
<div class="state-icon-container"> | |||
${loading_html} | |||
</div> | |||
get_message(title, message="") { | |||
const loading_html = `<div class="progress-chart" style ="width: 150px;"> | |||
<div class="progress" style="margin-top: 70px; margin-bottom: 0px"> | |||
<div class="progress-bar" style="width: 2%; background-color: #5e64ff;"></div> | |||
</div> | |||
</div>`; | |||
return $(`<div class="slides-wrapper setup-wizard-slide setup-in-progress"> | |||
<div class="content text-center"> | |||
<p class="title lead">${title}</p> | |||
<div class="state-icon-container">${loading_html}</div> | |||
<p class="setup-message text-muted" style="margin: 30px 0px;">${message}</p> | |||
</div> | |||
</div>`); | |||
} | |||
set_setup_complete_message(title, message) { | |||
this.$working_state.find('.title').html(title); | |||
this.$working_state.find('.setup-message').html(message); | |||
} | |||
set_setup_load_percent(percent) { | |||
this.$working_state.find('.progress-bar').css({"width": percent + "%"}); | |||
} | |||
}; | |||
frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide { | |||
@@ -13,50 +13,117 @@ from werkzeug.useragents import UserAgent | |||
from . import install_fixtures | |||
from six import string_types | |||
def get_setup_stages(args): | |||
# App setup stage functions should not include frappe.db.commit | |||
# That is done by frappe after successful completion of all stages | |||
stages = [ | |||
{ | |||
'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' | |||
} | |||
] | |||
} | |||
] | |||
stages += get_stages_hooks(args) + get_setup_complete_hooks(args) | |||
stages.append({ | |||
# post executing hooks | |||
'status': 'Wrapping up', | |||
'fail_msg': 'Failed to complete setup', | |||
'tasks': [ | |||
{ | |||
'fn': run_post_setup_complete, | |||
'args': args, | |||
'fail_msg': 'Failed to complete setup' | |||
} | |||
] | |||
}) | |||
return stages | |||
@frappe.whitelist() | |||
def setup_complete(args): | |||
"""Calls hooks for `setup_wizard_complete`, sets home page as `desktop` | |||
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')): | |||
# do not throw an exception if setup is already complete | |||
# let the user continue to desk | |||
return | |||
#frappe.throw(_('Setup already complete')) | |||
args = process_args(args) | |||
args = parse_args(args) | |||
try: | |||
if args.language and args.language != "english": | |||
set_default_language(get_language_code(args.language)) | |||
stages = get_setup_stages(args) | |||
frappe.clear_cache() | |||
# update system settings | |||
update_system_settings(args) | |||
update_user_name(args) | |||
for method in frappe.get_hooks("setup_wizard_complete"): | |||
frappe.get_attr(method)(args) | |||
try: | |||
current_task = None | |||
for idx, stage in enumerate(stages): | |||
frappe.publish_realtime('setup_task', {"progress": [idx, len(stages)], | |||
"stage_status": stage.get('status')}, user=frappe.session.user) | |||
for task in stage.get('tasks'): | |||
current_task = task | |||
task.get('fn')(task.get('args')) | |||
except Exception: | |||
handle_setup_exception(args) | |||
return {'status': 'fail', 'fail': current_task.get('fail_msg')} | |||
else: | |||
run_setup_success(args) | |||
return {'status': 'ok'} | |||
disable_future_access() | |||
def update_global_settings(args): | |||
if args.language and args.language != "english": | |||
set_default_language(get_language_code(args.lang)) | |||
frappe.clear_cache() | |||
frappe.db.commit() | |||
frappe.clear_cache() | |||
except: | |||
frappe.db.rollback() | |||
if args: | |||
traceback = frappe.get_traceback() | |||
for hook in frappe.get_hooks("setup_wizard_exception"): | |||
frappe.get_attr(hook)(traceback, args) | |||
update_system_settings(args) | |||
update_user_name(args) | |||
raise | |||
def run_post_setup_complete(args): | |||
disable_future_access() | |||
frappe.db.commit() | |||
frappe.clear_cache() | |||
else: | |||
for hook in frappe.get_hooks("setup_wizard_success"): | |||
frappe.get_attr(hook)(args) | |||
install_fixtures.install() | |||
def run_setup_success(args): | |||
for hook in frappe.get_hooks("setup_wizard_success"): | |||
frappe.get_attr(hook)(args) | |||
install_fixtures.install() | |||
def get_stages_hooks(args): | |||
stages = [] | |||
for method in frappe.get_hooks("setup_wizard_stages"): | |||
stages += frappe.get_attr(method)(args) | |||
return stages | |||
def get_setup_complete_hooks(args): | |||
stages = [] | |||
for method in frappe.get_hooks("setup_wizard_complete"): | |||
stages.append({ | |||
'status': 'Executing method', | |||
'fail_msg': 'Failed to execute method', | |||
'tasks': [ | |||
{ | |||
'fn': frappe.get_attr(method), | |||
'args': args, | |||
'fail_msg': 'Failed to execute method' | |||
} | |||
] | |||
}) | |||
return stages | |||
def handle_setup_exception(args): | |||
frappe.db.rollback() | |||
if args: | |||
traceback = frappe.get_traceback() | |||
for hook in frappe.get_hooks("setup_wizard_exception"): | |||
frappe.get_attr(hook)(traceback, args) | |||
def update_system_settings(args): | |||
number_format = get_country_info(args.get("country")).get("number_format", "#,###.##") | |||
@@ -126,7 +193,7 @@ def update_user_name(args): | |||
if args.get('name'): | |||
add_all_roles_to(args.get("name")) | |||
def process_args(args): | |||
def parse_args(args): | |||
if not args: | |||
args = frappe.local.form_dict | |||
if isinstance(args, string_types): | |||
@@ -234,14 +301,6 @@ def email_setup_wizard_exception(traceback, args): | |||
user_agent = frappe._dict() | |||
message = """ | |||
#### Basic Information | |||
- **Site:** {site} | |||
- **User:** {user} | |||
- **Browser:** {user_agent.platform} {user_agent.browser} version: {user_agent.version} language: {user_agent.language} | |||
- **Browser Languages**: `{accept_languages}` | |||
--- | |||
#### Traceback | |||
@@ -257,7 +316,16 @@ def email_setup_wizard_exception(traceback, args): | |||
#### Request Headers | |||
<pre>{headers}</pre>""".format( | |||
<pre>{headers}</pre> | |||
--- | |||
#### Basic Information | |||
- **Site:** {site} | |||
- **User:** {user} | |||
- **Browser:** {user_agent.platform} {user_agent.browser} version: {user_agent.version} language: {user_agent.language} | |||
- **Browser Languages**: `{accept_languages}`""".format( | |||
site=frappe.local.site, | |||
traceback=traceback, | |||
args="\n".join(pretty_args), | |||
@@ -268,14 +336,13 @@ def email_setup_wizard_exception(traceback, args): | |||
frappe.sendmail(recipients=frappe.local.conf.setup_wizard_exception_email, | |||
sender=frappe.session.user, | |||
subject="Exception in Setup Wizard - {}".format(frappe.local.site), | |||
subject="Setup failed: {}".format(frappe.local.site), | |||
message=message, | |||
delayed=False) | |||
def get_language_code(lang): | |||
return frappe.db.get_value('Language', {'language_name':lang}) | |||
def enable_twofactor_all_roles(): | |||
all_role = frappe.get_doc('Role',{'role_name':'All'}) | |||
all_role.two_factor_auth = True | |||
@@ -279,6 +279,9 @@ select.input-sm { | |||
opacity: 1; | |||
cursor: pointer; | |||
} | |||
.setup-wizard-slide .progress-bar { | |||
background-color: #5e64ff; | |||
} | |||
.page-card-container { | |||
padding: 70px; | |||
} | |||
@@ -312,49 +315,3 @@ select.input-sm { | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
@keyframes lds-rolling { | |||
0% { | |||
-webkit-transform: translate(-50%, -50%) rotate(0deg); | |||
transform: translate(-50%, -50%) rotate(0deg); | |||
} | |||
100% { | |||
-webkit-transform: translate(-50%, -50%) rotate(360deg); | |||
transform: translate(-50%, -50%) rotate(360deg); | |||
} | |||
} | |||
@-webkit-keyframes lds-rolling { | |||
0% { | |||
-webkit-transform: translate(-50%, -50%) rotate(0deg); | |||
transform: translate(-50%, -50%) rotate(0deg); | |||
} | |||
100% { | |||
-webkit-transform: translate(-50%, -50%) rotate(360deg); | |||
transform: translate(-50%, -50%) rotate(360deg); | |||
} | |||
} | |||
.lds-rolling { | |||
-webkit-transform: translate(-100px, -100px) scale(1) translate(100px, 100px); | |||
transform: translate(-100px, -100px) scale(1) translate(100px, 100px); | |||
} | |||
.lds-rolling div { | |||
position: absolute; | |||
width: 60px; | |||
height: 60px; | |||
border: 3px solid #d1d8dd; | |||
border-top-color: transparent; | |||
border-radius: 50%; | |||
-webkit-animation: lds-rolling 1s linear infinite; | |||
animation: lds-rolling 1s linear infinite; | |||
top: 50px; | |||
left: 50px; | |||
} | |||
.lds-rolling div:after { | |||
position: absolute; | |||
width: 60px; | |||
height: 60px; | |||
border: 3px solid #d1d8dd; | |||
border-top-color: transparent; | |||
border-radius: 50%; | |||
-webkit-transform: rotate(90deg); | |||
transform: rotate(90deg); | |||
} |
@@ -335,6 +335,10 @@ select.input-sm { | |||
cursor: pointer; | |||
} | |||
} | |||
.progress-bar { | |||
background-color: #5e64ff; | |||
} | |||
} | |||
.page-card-container { | |||
@@ -376,50 +380,4 @@ select.input-sm { | |||
align-items: center; | |||
} | |||
@keyframes lds-rolling { | |||
0% { | |||
-webkit-transform: translate(-50%, -50%) rotate(0deg); | |||
transform: translate(-50%, -50%) rotate(0deg); | |||
} | |||
100% { | |||
-webkit-transform: translate(-50%, -50%) rotate(360deg); | |||
transform: translate(-50%, -50%) rotate(360deg); | |||
} | |||
} | |||
@-webkit-keyframes lds-rolling { | |||
0% { | |||
-webkit-transform: translate(-50%, -50%) rotate(0deg); | |||
transform: translate(-50%, -50%) rotate(0deg); | |||
} | |||
100% { | |||
-webkit-transform: translate(-50%, -50%) rotate(360deg); | |||
transform: translate(-50%, -50%) rotate(360deg); | |||
} | |||
} | |||
.lds-rolling { | |||
-webkit-transform: translate(-100px, -100px) scale(1) translate(100px, 100px); | |||
transform: translate(-100px, -100px) scale(1) translate(100px, 100px); | |||
div { | |||
position: absolute; | |||
width: 60px; | |||
height: 60px; | |||
border: 3px solid #d1d8dd; | |||
border-top-color: transparent; | |||
border-radius: 50%; | |||
-webkit-animation: lds-rolling 1s linear infinite; | |||
animation: lds-rolling 1s linear infinite; | |||
top: 50px; | |||
left: 50px; | |||
&:after { | |||
position: absolute; | |||
width: 60px; | |||
height: 60px; | |||
border: 3px solid #d1d8dd; | |||
border-top-color: transparent; | |||
border-radius: 50%; | |||
-webkit-transform: rotate(90deg); | |||
transform: rotate(90deg); | |||
} | |||
} | |||
} |