First work on a (first run) wizard, also usable by plugins

This commit is contained in:
Gina Häußge 2015-07-12 11:51:19 +02:00
parent 75992ef837
commit ac1e2e8ad4
14 changed files with 232 additions and 12 deletions

View file

@ -795,6 +795,7 @@ class Server():
"js/lib/jquery/jquery.fileupload.js",
"js/lib/jquery/jquery.slimscroll.min.js",
"js/lib/jquery/jquery.qrcode.min.js",
"js/lib/jquery/jquery.bootstrap.wizard.min.js",
"js/lib/moment-with-locales.min.js",
"js/lib/pusher.color.min.js",
"js/lib/detectmobilebrowser.js",

View file

@ -610,7 +610,8 @@ def collect_plugin_assets(enable_gcodeviewer=True, enable_timelapse=True, prefer
'js/app/viewmodels/terminal.js',
'js/app/viewmodels/users.js',
'js/app/viewmodels/log.js',
'js/app/viewmodels/usersettings.js'
'js/app/viewmodels/usersettings.js',
'js/app/viewmodels/wizard.js'
]
if enable_gcodeviewer:
assets["js"] += [

View file

@ -34,6 +34,7 @@ def index():
enable_accesscontrol = userManager is not None
preferred_stylesheet = settings().get(["devel", "stylesheet"])
locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)
first_run = settings().getBoolean(["server", "firstRun"])# and (userManager is None or not userManager.hasBeenCustomized())
##~~ prepare templates
@ -46,6 +47,7 @@ def index():
tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2", to_entry=lambda data: (data["name"], data)),
settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2", to_entry=lambda data: (data["name"], data)),
usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2", to_entry=lambda data: (data["name"], data)),
wizard=dict(div=lambda x: "wizard_plugin_" + x, template=lambda x: x + "_wizard.jinja2", to_entry=lambda data: (data["name"], data)),
generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data)
)
@ -56,6 +58,7 @@ def index():
tab=dict(add="append", key="name"),
settings=dict(add="custom_append", key="name", custom_add_entries=lambda missing: dict(section_plugins=(gettext("Plugins"), None)), custom_add_order=lambda missing: ["section_plugins"] + missing),
usersettings=dict(add="append", key="name"),
wizard=dict(add="append", key="name"),
generic=dict(add="append", key=None)
)
@ -162,6 +165,24 @@ def index():
interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)),
)
# wizard
if first_run:
def custom_insert_order(existing, missing):
if "firstrunstart" in missing:
missing.remove("firstrunstart")
if "firstrunend" in missing:
missing.remove("firstrunend")
return ["firstrunstart"] + existing + missing + ["firstrunend"]
template_sorting["wizard"] = dict(add="custom_insert", key="name", custom_insert_entries=lambda missing: dict(), custom_insert_order=custom_insert_order)
templates["wizard"]["entries"] = dict(
firstrunstart=(gettext("Start"), dict(template="dialogs/wizard/firstrunstart.jinja2", _div="firstrun_start")),
firstrunend=(gettext("Finish"), dict(template="dialogs/wizard/firstrunend.jinja2", _div="firstrun_end")),
access=(gettext("Access Control"), dict(template="dialogs/wizard/accesscontrol.jinja2", _div="firstrun_dialog", custom_bindings=True))
)
# extract data from template plugins
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
@ -243,10 +264,13 @@ def index():
elif template_sorting[t]["add"] == "custom_append" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
templates[t]["order"] += template_sorting[t]["custom_add_order"](sorted_missing)
elif template_sorting[t]["add"] == "custom_insert" and "custom_insert_entries" in template_sorting[t] and "custom_insert_order" in template_sorting[t]:
templates[t]["entries"].update(template_sorting[t]["custom_insert_entries"](sorted_missing))
templates[t]["order"] = template_sorting[t]["custom_insert_order"](templates[t]["order"], sorted_missing)
#~~ prepare full set of template vars for rendering
first_run = settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized())
wizard = bool(templates["wizard"]["order"])
render_kwargs = dict(
webcamStream=settings().get(["webcam", "stream"]),
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
@ -261,7 +285,8 @@ def index():
uiApiKey=UI_API_KEY,
templates=templates,
pluginNames=plugin_names,
locales=locales
locales=locales,
wizard=wizard
)
render_kwargs.update(plugin_vars)
@ -275,7 +300,7 @@ def index():
))
response.headers["Last-Modified"] = datetime.datetime.now()
if first_run:
if wizard:
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "-1"

View file

@ -191,6 +191,7 @@ default_settings = {
"section_octoprint", "folders", "appearance", "logs", "plugin_pluginmanager", "plugin_softwareupdate"
],
"usersettings": ["access", "interface"],
"wizard": ["access"],
"generic": []
},
"disabled": {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,54 @@
$(function() {
function WizardViewModel() {
var self = this;
self.wizardDialog = undefined;
self.showDialog = function() {
self.wizardDialog.modal({
minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); }
}).css({
width: 'auto',
'margin-left': function() { return -($(this).width() /2); }
});
};
self.closeDialog = function() {
self.wizardDialog.modal("hide");
};
self.onStartup = function() {
self.wizardDialog = $("#wizard_dialog");
};
self.onAllBound = function(allViewModels) {
if (CONFIG_WIZARD) {
self.wizardDialog.bootstrapWizard({
tabClass: "nav nav-list",
nextSelector: ".button-next",
previousSelector: ".button-previous",
finishSelector: ".button-finish",
onTabShow: function(tab, navigation, index) {
var total = navigation.find("li").length;
var current = index+1;
if (current >= total) {
self.wizardDialog.find(".button-next").hide();
self.wizardDialog.find(".button-finish").show().removeClass("disabled");
} else {
self.wizardDialog.find(".button-finish").hide();
self.wizardDialog.find(".button-next").show();
}
}
});
self.showDialog();
}
}
}
OCTOPRINT_VIEWMODELS.push([
WizardViewModel,
[],
"#wizard_dialog"
]);
});

View file

@ -0,0 +1,22 @@
/*!
* jQuery twitter bootstrap wizard plugin
* Examples and documentation at: http://github.com/VinceG/twitter-bootstrap-wizard
* version 1.0
* Requires jQuery v1.3.2 or later
* Supports Bootstrap 2.2.x, 2.3.x, 3.0
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
* Authors: Vadim Vincent Gabriel (http://vadimg.com), Jason Gill (www.gilluminate.com)
*/
(function(e){var n=function(d,k){d=e(d);var a=this,g=[],c=e.extend({},e.fn.bootstrapWizard.defaults,k),f=null,b=null;this.rebindClick=function(h,a){h.unbind("click",a).bind("click",a)};this.fixNavigationButtons=function(){f.length||(b.find("a:first").tab("show"),f=b.find('li:has([data-toggle="tab"]):first'));e(c.previousSelector,d).toggleClass("disabled",a.firstIndex()>=a.currentIndex());e(c.nextSelector,d).toggleClass("disabled",a.currentIndex()>=a.navigationLength());e(c.backSelector,d).toggleClass("disabled",
0==g.length);a.rebindClick(e(c.nextSelector,d),a.next);a.rebindClick(e(c.previousSelector,d),a.previous);a.rebindClick(e(c.lastSelector,d),a.last);a.rebindClick(e(c.firstSelector,d),a.first);a.rebindClick(e(c.backSelector,d),a.back);if(c.onTabShow&&"function"===typeof c.onTabShow&&!1===c.onTabShow(f,b,a.currentIndex()))return!1};this.next=function(h){if(d.hasClass("last")||c.onNext&&"function"===typeof c.onNext&&!1===c.onNext(f,b,a.nextIndex()))return!1;h=a.currentIndex();$index=a.nextIndex();$index>
a.navigationLength()||(g.push(h),b.find('li:has([data-toggle="tab"]):eq('+$index+") a").tab("show"))};this.previous=function(h){if(d.hasClass("first")||c.onPrevious&&"function"===typeof c.onPrevious&&!1===c.onPrevious(f,b,a.previousIndex()))return!1;h=a.currentIndex();$index=a.previousIndex();0>$index||(g.push(h),b.find('li:has([data-toggle="tab"]):eq('+$index+") a").tab("show"))};this.first=function(h){if(c.onFirst&&"function"===typeof c.onFirst&&!1===c.onFirst(f,b,a.firstIndex())||d.hasClass("disabled"))return!1;
g.push(a.currentIndex());b.find('li:has([data-toggle="tab"]):eq(0) a').tab("show")};this.last=function(h){if(c.onLast&&"function"===typeof c.onLast&&!1===c.onLast(f,b,a.lastIndex())||d.hasClass("disabled"))return!1;g.push(a.currentIndex());b.find('li:has([data-toggle="tab"]):eq('+a.navigationLength()+") a").tab("show")};this.back=function(){if(0==g.length)return null;var a=g.pop();if(c.onBack&&"function"===typeof c.onBack&&!1===c.onBack(f,b,a))return g.push(a),!1;d.find('li:has([data-toggle="tab"]):eq('+
a+") a").tab("show")};this.currentIndex=function(){return b.find('li:has([data-toggle="tab"])').index(f)};this.firstIndex=function(){return 0};this.lastIndex=function(){return a.navigationLength()};this.getIndex=function(a){return b.find('li:has([data-toggle="tab"])').index(a)};this.nextIndex=function(){return b.find('li:has([data-toggle="tab"])').index(f)+1};this.previousIndex=function(){return b.find('li:has([data-toggle="tab"])').index(f)-1};this.navigationLength=function(){return b.find('li:has([data-toggle="tab"])').length-
1};this.activeTab=function(){return f};this.nextTab=function(){return b.find('li:has([data-toggle="tab"]):eq('+(a.currentIndex()+1)+")").length?b.find('li:has([data-toggle="tab"]):eq('+(a.currentIndex()+1)+")"):null};this.previousTab=function(){return 0>=a.currentIndex()?null:b.find('li:has([data-toggle="tab"]):eq('+parseInt(a.currentIndex()-1)+")")};this.show=function(b){b=isNaN(b)?d.find('li:has([data-toggle="tab"]) a[href=#'+b+"]"):d.find('li:has([data-toggle="tab"]):eq('+b+") a");0<b.length&&
(g.push(a.currentIndex()),b.tab("show"))};this.disable=function(a){b.find('li:has([data-toggle="tab"]):eq('+a+")").addClass("disabled")};this.enable=function(a){b.find('li:has([data-toggle="tab"]):eq('+a+")").removeClass("disabled")};this.hide=function(a){b.find('li:has([data-toggle="tab"]):eq('+a+")").hide()};this.display=function(a){b.find('li:has([data-toggle="tab"]):eq('+a+")").show()};this.remove=function(a){var c="undefined"!=typeof a[1]?a[1]:!1;a=b.find('li:has([data-toggle="tab"]):eq('+a[0]+
")");c&&(c=a.find("a").attr("href"),e(c).remove());a.remove()};var l=function(d){var g=b.find('li:has([data-toggle="tab"])');d=g.index(e(d.currentTarget).parent('li:has([data-toggle="tab"])'));g=e(g[d]);if(c.onTabClick&&"function"===typeof c.onTabClick&&!1===c.onTabClick(f,b,a.currentIndex(),d,g))return!1},m=function(d){$element=e(d.target).parent();d=b.find('li:has([data-toggle="tab"])').index($element);if($element.hasClass("disabled")||c.onTabChange&&"function"===typeof c.onTabChange&&!1===c.onTabChange(f,
b,a.currentIndex(),d))return!1;f=$element;a.fixNavigationButtons()};this.resetWizard=function(){e('a[data-toggle="tab"]',b).off("click",l);e('a[data-toggle="tab"]',b).off("shown shown.bs.tab",m);b=d.find("ul:first",d);f=b.find('li:has([data-toggle="tab"]).active',d);e('a[data-toggle="tab"]',b).on("click",l);e('a[data-toggle="tab"]',b).on("shown shown.bs.tab",m);a.fixNavigationButtons()};b=d.find("ul:first",d);f=b.find('li:has([data-toggle="tab"]).active',d);b.hasClass(c.tabClass)||b.addClass(c.tabClass);
if(c.onInit&&"function"===typeof c.onInit)c.onInit(f,b,0);if(c.onShow&&"function"===typeof c.onShow)c.onShow(f,b,a.nextIndex());e('a[data-toggle="tab"]',b).on("click",l);e('a[data-toggle="tab"]',b).on("shown shown.bs.tab",m)};e.fn.bootstrapWizard=function(d){if("string"==typeof d){var k=Array.prototype.slice.call(arguments,1);1===k.length&&k.toString();return this.data("bootstrapWizard")[d](k)}return this.each(function(a){a=e(this);if(!a.data("bootstrapWizard")){var g=new n(a,d);a.data("bootstrapWizard",
g);g.fixNavigationButtons()}})};e.fn.bootstrapWizard.defaults={tabClass:"nav nav-pills",nextSelector:".wizard li.next",previousSelector:".wizard li.previous",firstSelector:".wizard li.first",lastSelector:".wizard li.last",backSelector:".wizard li.back",onShow:null,onInit:null,onNext:null,onPrevious:null,onLast:null,onFirst:null,onBack:null,onTabChange:null,onTabClick:null,onTabShow:null}})(jQuery);

View file

@ -685,13 +685,9 @@ ul.dropdown-menu li a {
}
}
/** Settings dialog */
#settings_dialog {
.modal-body {
#settings_dialog_menu {
margin-left: 0;
}
}
#settings_dialog_menu,
#wizard_dialog_menu {
margin-left: 0;
}

View file

@ -0,0 +1,56 @@
<div id="wizard_dialog" class="modal hide fade large" data-backdrop="static" data-keyboard="false">
<div class="modal-header">
<h3><i class="icon-magic"></i> {{ _('Setup Wizard') }}</h3>
</div>
<div class="modal-body">
<div class="full-sized-box">
<div class="tabbable row-fluid">
<div class="span3 scrollable" id="wizard_dialog_menu">
<ul class="nav nav-list" id="wizardTabs">
{% for key in templates.wizard.order %}
{% set entry, data = templates.wizard.entries[key] %}
{% if data is none %}
<li class="nav-header">{{ entry }}</li>
{% else %}
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<li id="{{ data._div }}_link"
{% if "data_bind" in data %}data-bind="{{ data.data_bind }}"{% endif %}
class="{% if "classes_link" in data %}{{ data.classes_link|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
{% if "styles_link" in data %} style="{{ data.styles_link|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}" {% endif %}
>
<a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a>
</li>
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- /ko -->{% endif %}
{% endif %}
{% endfor %}
</ul>
</div>
<div class="tab-content span9 scrollable" id="wizard_dialog_content">
{% for key in templates.wizard.order %}
{% set entry, data = templates.wizard.entries[key] %}
{% if data is not none %}
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- ko allowBindings: false -->{% endif %}
<div id="{{ data._div }}"
{% if "data_bind" in data %}data-bind="{{ data.data_bind }}"{% endif %}
class="tab-pane {% if classes_content in data %}{{ data.classes_content|join(' ') }}{% elif classes in data %}{{ data.classes|join(' ') }}{% endif %}"
{% if "styles_content" in data %} style="{{ data.styles_content|join(', ') }}" {% elif styles in data %} style="{{ data.styles|join(', ') }}" {% endif %}
>
{% include data.template ignore missing %}
</div>
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- /ko -->{% endif %}
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="pull-right">
<button class="btn btn-primary button-next" name="next">{{ _('Next') }}</button>
<button class="btn btn-primary button-finish" style="display: none" name="finish">{{ _('Finish') }}</button>
</div>
<div class="pull-left">
<button class="btn button-previous" name="previous">{{ _('Previous') }}</button>
</div>
</div>
</div>

View file

@ -0,0 +1,50 @@
<h3>{{ _('Access Control') }}</h3>
{% trans %}<p>
<strong>Please read the following, it is very important for your printer's health!</strong>
</p>
<p>
OctoPrint by default now ships with Access Control enabled, meaning you won't be able to do anything with the
printer unless you login first as a configured user. This is to <strong>prevent strangers - possibly with
malicious intent - to gain access to your printer</strong> via the internet or another untrustworthy network
and using it in such a way that it is damaged or worse (i.e. causes a fire).
</p>
<p>
It looks like you haven't configured access control yet. Please <strong>set up an username and password</strong> for the
initial administrator account who will have full access to both the printer and OctoPrint's settings, then click
on "Keep Access Control Enabled":
</p>{% endtrans %}
<form class="form-horizontal">
<div class="control-group" data-bind="css: {success: validUsername()}">
<label class="control-label" for="first_run_username">{{ _('Username') }}</label>
<div class="controls">
<input type="text" class="input-medium" data-bind="value: username, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {success: validPassword()}">
<label class="control-label" for="first_run_username">{{ _('Password') }}</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: password, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {error: passwordMismatch(), success: validPassword() && !passwordMismatch()}">
<label class="control-label" for="first_run_username">{{ _('Confirm Password') }}</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: confirmedPassword, valueUpdate: 'afterkeydown'">
<span class="help-inline" data-bind="visible: passwordMismatch()">{{ _('Passwords do not match') }}</span>
</div>
</div>
</form>
{% trans %}<p>
<strong>Note:</strong> In case that your OctoPrint installation is only accessible from within a trustworthy network and you don't
need Access Control for other reasons, you may alternatively disable Access Control. You should only
do this if you are absolutely certain that only people you know and trust will be able to connect to it.
</p>
<p>
<strong>Do NOT underestimate the risk of an unsecured access from the internet to your printer!</strong>
</p>{% endtrans %}
<div class="row-fluid">
<a href="#" class="btn btn-danger span6" data-bind="click: disableAccessControl">{{ _('Disable Access Control') }}</a>
<a href="#" class="btn btn-primary span6" data-bind="click: keepAccessControl, enable: validData(), css: {disabled: !validData()}">{{ _('Keep Access Control Enabled') }}</a>
</div>

View file

@ -0,0 +1,6 @@
<h3>{{ _('All Done!') }}</h3>
{% trans %}
Your OctoPrint installation is now all set up and ready to go. Happy
printing!
{% endtrans %}

View file

@ -0,0 +1,6 @@
<h3>{{ _('Hello!') }}</h3>
{% trans %}
Thank you for installing OctoPrint! This wizard will lead you through the
final steps to get your OctoPrint instance all setup and ready to go!
{% endtrans %}

View file

@ -129,6 +129,7 @@
{% include 'dialogs/settings.jinja2' %}
{% include 'dialogs/slicing.jinja2' %}
{% include 'dialogs/usersettings.jinja2' %}
{% include 'dialogs/wizard.jinja2' %}
<!-- End of dialogs -->
<!-- Overlays -->

View file

@ -17,6 +17,7 @@
var CONFIG_TEMPERATURE_GRAPH = {% if enableTemperatureGraph -%} true; {% else %} false; {%- endif %}
var CONFIG_GCODE_SIZE_THRESHOLD = {{ gcodeThreshold }};
var CONFIG_GCODE_MOBILE_SIZE_THRESHOLD = {{ gcodeMobileThreshold }};
var CONFIG_WIZARD = {% if wizard -%} true; {% else %} false; {%- endif %}
var SOCKJS_URI = "{{ url_for('index') }}" + "sockjs";
var SOCKJS_DEBUG = CONFIG_DEBUG;