New corewizard plugin taking care of first run setup

Also introduced wizard finish backend method and remove old
firstrun dialog remnants.

TODO: bound check won't work this way, needs to be redone
differently. Goal is to only call onWizardFinish for wizards which
are actually involved...
This commit is contained in:
Gina Häußge 2015-08-18 17:08:51 +02:00
parent 1bba5ca788
commit b1e4a1d9dc
11 changed files with 257 additions and 98 deletions

View file

@ -0,0 +1,137 @@
# coding=utf-8
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
import octoprint.plugin
class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.WizardPlugin,
octoprint.plugin.SettingsPlugin,
octoprint.plugin.BlueprintPlugin):
#~~ TemplatePlugin API
def get_template_configs(self):
required = self._get_subwizard_attrs("_is_", "_wizard_required")
names = self._get_subwizard_attrs("_get_", "_wizard_name")
additional = self._get_subwizard_attrs("_get_", "_additional_wizard_template_data")
result = list()
for key, method in required.iteritems():
if not method():
continue
if not key in names:
continue
name = names[key]()
if not name:
continue
config = dict(type="wizard", name=name, template="corewizard_{}_wizard.jinja2".format(key), div="wizard_plugin_corewizard_{}".format(key))
if key in additional:
additional_result = additional[key]()
if additional_result:
config.update(additional_result)
result.append(config)
return result
#~~ AssetPlugin API
def get_assets(self):
return dict(
js=["js/corewizard.js"]
)
#~~ WizardPlugin API
def is_wizard_required(self):
methods = self._get_subwizard_attrs("_is_", "_wizard_required")
return self._settings.global_get(["server", "firstRun"]) and any(map(lambda m: m(), methods.values()))
def get_wizard_details(self):
result = dict()
def add_result(key, method):
result[key] = method()
self._get_subwizard_attrs("_get_", "_wizard_details", add_result)
return result
#~~ ACL subwizard
def _is_acl_wizard_required(self):
return self._user_manager is not None and not self._user_manager.hasBeenCustomized()
def _get_acl_wizard_details(self):
return dict()
def _get_acl_wizard_name(self):
return "Access Control"
@octoprint.plugin.BlueprintPlugin.route("/acl", methods=["POST"])
def acl_wizard_api(self):
from flask import request
from octoprint.server.api import valid_boolean_trues
if "ac" in request.values and request.values["ac"] in valid_boolean_trues and \
"user" in request.values.keys() and "pass1" in request.values.keys() and \
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
# configure access control
self._settings.global_set_boolean(["accessControl", "enabled"], True)
octoprint.server.userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"], overwrite=True)
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
# disable access control
self._settings.global_set_boolean(["accessControl", "enabled"], False)
octoprint.server.loginManager.anonymous_user = octoprint.users.DummyUser
octoprint.server.principals.identity_loaders.appendleft(octoprint.users.dummy_identity_loader)
self._settings.save()
#~~ Webcam subwizard
def _is_webcam_wizard_required(self):
webcam_snapshot_url = self._settings.global_get(["webcam", "snapshotUrl"])
webcam_stream_url = self._settings.global_get(["webcam", "streamUrl"])
ffmpeg_path = self._settings.global_get(["webcam", "ffmpeg"])
return not (webcam_snapshot_url and webcam_stream_url and ffmpeg_path)
def _get_webcam_wizard_details(self):
return dict()
def _get_webcam_wizard_name(self):
return "Webcam & Timelapse"
#~~ helpers
def _get_subwizard_attrs(self, start, end, callback=None):
result = dict()
for item in dir(self):
if not item.startswith(start) or not item.endswith(end):
continue
key = item[len(start):-len(end)]
if not key:
continue
attr = getattr(self, item)
if callable(callback):
callback(key, attr)
result[key] = attr
return result
__plugin_name__ = "Core Wizard"
__plugin_description__ = "Provides wizard dialogs for core components"
__plugin_implementation__ = CoreWizardPlugin()

View file

@ -1,5 +1,5 @@
$(function() {
function AclWizardViewModel() {
function CoreWizardAclViewModel() {
var self = this;
self.username = ko.observable(undefined);
@ -52,7 +52,7 @@ $(function() {
self._sendData = function(data, callback) {
$.ajax({
url: API_BASEURL + "setup",
url: API_BASEURL + "plugin/corewizard/acl",
type: "POST",
dataType: "json",
data: data,
@ -65,7 +65,7 @@ $(function() {
};
self.onWizardTabChange = function(current, next) {
if (!current || !_.startsWith(current, "wizard_firstrun_acl") || self.setup()) {
if (!current || !_.startsWith(current, "wizard_plugin_corewizard_acl_") || self.setup()) {
return true;
}
showMessageDialog({
@ -75,26 +75,37 @@ $(function() {
return false;
};
self.onWizardFinished = function() {
self.onWizardFinish = function() {
if (!self.decision()) {
return "reload";
}
};
}
function WebcamWizardViewModel(parameters) {
function CoreWizardWebcamViewModel(parameters) {
var self = this;
self.settingsViewModel = parameters[0];
self.onWizardFinish = function() {
if (self.unbound) return;
self.settingsViewModel.enqueueForSaving({
webcam: {
streamUrl: self.settingsViewModel.webcam_streamUrl(),
snapshotUrl: self.settingsViewModel.webcam_snapshotUrl()
}
});
}
}
OCTOPRINT_VIEWMODELS.push([
AclWizardViewModel,
CoreWizardAclViewModel,
[],
"#wizard_firstrun_acl"
"#wizard_plugin_corewizard_acl"
], [
WebcamWizardViewModel,
CoreWizardWebcamViewModel,
["settingsViewModel"],
"#wizard_firstrun_webcam"
"#wizard_plugin_corewizard_webcam"
]);
});

View file

@ -4,13 +4,13 @@
<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
OctoPrint by default 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
It looks like you haven't configured access control yet. Please <strong>set up a 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 %}

View file

@ -0,0 +1,41 @@
<h3>{{ _('Webcam &amp; Timelapse Recordings') }}</h3>
{% trans %}<p>
If you have a webcam, you may now configure it here.
</p>{% endtrans %}
<h4>{{ _('Webcam') }}</h4>
{% trans %}<p>
The <strong>Stream URL</strong> is the URL OctoPrint uses to embed the
actual webcam stream (which should be an MJPG stream) into the web
interface. This needs to be reachable <strong>from your browser</strong>.
</p><p>
It may be
<ul>
<li>a fully qualified URL (<code>http://host:port/path</code>),</li>
<li>a URL defaulting to the protocol used for accessing the web
interface (e.g. <code>//host:port/path</code>),</li>
<li>an absolute path on the same host as OctoPrint (e.g. <code>/absolute/path</code>) or</li>
<li>a relative path on the same host as OctoPrint (e.g. <code>relative/path</code>)</li>
</ul>
</p><p>
The <strong>Snapshot URL</strong> is the URL OctoPrint uses to fetch single
images from the webcam for creating timelapse recordings. This needs to be
reachable <strong>from the OctoPrint server</strong>.
</p>{% endtrans %}
<form class="form-horizontal" data-bind="with: settingsViewModel">
{% include "dialogs/_snippets/configurewebcamurls.jinja2" %}
</form>
<h4>{{ _('Timelapse Recordings') }}</h4>
{% trans %}<p>
To render the snapshots into timelapse recordings, OctoPrint also needs to
know the correct <strong>path to FFMPEG</strong>.
</p>{% endtrans %}
<form class="form-horizontal" data-bind="with: settingsViewModel">
{% include "dialogs/_snippets/configureffmpeg.jinja2" %}
</form>

View file

@ -100,7 +100,7 @@ def pluginCommand(name):
@api.route("/setup/wizard", methods=["GET"])
def wizardState():
if not s().getBoolean(["server", "firstRun"]) and not admin_permission.can():
if not admin_permission.can():
abort(403)
result = dict()
@ -117,29 +117,28 @@ def wizardState():
return jsonify(result)
@api.route("/setup", methods=["POST"])
def firstRunSetup():
if not s().getBoolean(["server", "firstRun"]):
@api.route("/setup/wizard", methods=["POST"])
def wizardFinish():
if not admin_permission.can():
abort(403)
if "ac" in request.values.keys() and request.values["ac"] in valid_boolean_trues and \
"user" in request.values.keys() and "pass1" in request.values.keys() and \
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
# configure access control
s().setBoolean(["accessControl", "enabled"], True)
octoprint.server.userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"], overwrite=True)
s().setBoolean(["server", "firstRun"], False)
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
# disable access control
s().setBoolean(["accessControl", "enabled"], False)
if s().getBoolean(["server", "firstRun"]):
s().setBoolean(["server", "firstRun"], False)
octoprint.server.loginManager.anonymous_user = octoprint.users.DummyUser
octoprint.server.principals.identity_loaders.appendleft(octoprint.users.dummy_identity_loader)
wizard_plugins = octoprint.server.pluginManager.get_implementations(octoprint.plugin.WizardPlugin)
for implementation in wizard_plugins:
name = implementation._identifier
try:
implementation.on_wizard_finish()
except:
logging.getLogger(__name__).exceptino("There was an error finishing the wizard for {}, ignoring".format(name))
s().save()
return NO_CONTENT
#~~ system state

View file

@ -613,7 +613,6 @@ def collect_plugin_assets(enable_gcodeviewer=True, enable_timelapse=True, prefer
'js/app/viewmodels/appearance.js',
'js/app/viewmodels/connection.js',
'js/app/viewmodels/control.js',
'js/app/viewmodels/firstrun_wizard.js',
'js/app/viewmodels/files.js',
'js/app/viewmodels/loginstate.js',
'js/app/viewmodels/navigation.js',

View file

@ -182,7 +182,6 @@ def index():
templates["wizard"]["entries"] = dict(
firstrunstart=(gettext("Start"), dict(template="dialogs/wizard/firstrun_start.jinja2", _div="wizard_firstrun_start")),
firstrunend=(gettext("Finish"), dict(template="dialogs/wizard/firstrun_end.jinja2", _div="wizard_firstrun_end")),
access=(gettext("Access Control"), dict(template="dialogs/wizard/firstrun_acl.jinja2", _div="wizard_firstrun_acl", custom_bindings=True))
)
# extract data from template plugins

View file

@ -446,12 +446,14 @@ $(function() {
if (object == undefined || !object.length) {
log.info("Did not bind view model", viewModel.constructor.name, "to target", target, "since it does not exist");
viewModel.unbound = true;
return;
}
var element = object.get(0);
if (element == undefined) {
log.info("Did not bind view model", viewModel.constructor.name, "to target", target, "since it does not exist");
viewModel.unbound = true;
return;
}
@ -460,6 +462,7 @@ $(function() {
log.debug("View model", viewModel.constructor.name, "bound to", target);
} catch (exc) {
log.error("Could not bind view model", viewModel.constructor.name, "to target", target, ":", (exc.stack || exc));
viewModel.unbound = true;
}
});
}

View file

@ -9,6 +9,8 @@ $(function() {
self.allViewModels = undefined;
self.finishing = false;
self.isDialogActive = function() {
return self.wizardDialog.is(":visible");
};
@ -18,17 +20,19 @@ $(function() {
self.getWizardDetails(function(response) {
_.each(self.allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onWizardDetails")) {
if (!viewModel.unbound && viewModel.hasOwnProperty("onWizardDetails")) {
viewModel.onWizardDetails(response);
}
});
self.wizardDialog.modal({
minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); }
}).css({
width: 'auto',
'margin-left': function() { return -($(this).width() /2); }
});
if (!self.isDialogActive()) {
self.wizardDialog.modal({
minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); }
}).css({
width: 'auto',
'margin-left': function() { return -($(this).width() /2); }
});
}
});
};
@ -73,7 +77,7 @@ $(function() {
var active = tab[0].id;
if (active != undefined) {
_.each(allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onAfterWizardTabChange")) {
if (!viewModel.unbound && viewModel.hasOwnProperty("onAfterWizardTabChange")) {
viewModel.onAfterWizardTabChange(active);
}
});
@ -96,7 +100,7 @@ $(function() {
if (current != undefined && next != undefined) {
var result = true;
_.each(allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onWizardTabChange")) {
if (!viewModel.unbound && viewModel.hasOwnProperty("onWizardTabChange")) {
result = result && (viewModel.onWizardTabChange(current, next) !== false);
}
});
@ -106,19 +110,20 @@ $(function() {
onFinish: function(tab, navigation, index) {
var closeDialog = true;
_.each(allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onBeforeWizardFinish")) {
if (!viewModel.unbound && viewModel.hasOwnProperty("onBeforeWizardFinish")) {
closeDialog = closeDialog && (viewModel.onBeforeWizardFinish() !== false);
}
});
if (closeDialog) {
_.each(allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onWizardFinish")) {
if (!viewModel.unbound && viewModel.hasOwnProperty("onWizardFinish")) {
viewModel.onWizardFinish();
}
});
self.settingsViewModel.saveEnqueued();
self.closeDialog();
self.finishWizard(function() {
self.closeDialog();
});
}
}
});
@ -136,8 +141,28 @@ $(function() {
});
};
self.finishWizard = function(callback) {
self.finishing = true;
self.settingsViewModel.saveEnqueued();
$.ajax({
url: API_BASEURL + "setup/wizard",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
success: function() {
self.finishing = false;
callback();
},
failure: function() {
self.finishing = false;
}
})
};
self.onSettingsPreventRefresh = function() {
if (self.isDialogActive() && hasDataChanged(self.settingsViewModel.getLocalData(), self.settingsViewModel.lastReceivedSettings)) {
if (!self.finishing && self.isDialogActive()
&& hasDataChanged(self.settingsViewModel.getLocalData(), self.settingsViewModel.lastReceivedSettings)) {
// we have local changes, show update dialog
self.settingsViewModel.settingsUpdatedDialog.modal("show");
return true;

View file

@ -1,54 +0,0 @@
<div id="first_run_dialog" class="modal hide fade" data-backdrop="static" data-keyboard="false">
<div class="modal-header">
<h3><i class="icon-warning-sign"></i> {{ _('Configure Access Control') }}</h3>
</div>
<div class="modal-body">
{% 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>
<div class="modal-footer">
<a href="#" class="btn btn-danger" data-bind="click: disableAccessControl">{{ _('Disable Access Control') }}</a>
<a href="#" class="btn btn-primary" data-bind="click: keepAccessControl, enable: validData(), css: {disabled: !validData()}">{{ _('Keep Access Control Enabled') }}</a>
</div>
</div>

View file

@ -124,7 +124,6 @@
</div>
<!-- Dialogs -->
{% include 'dialogs/firstrun.jinja2' %}
{% include 'dialogs/settings.jinja2' %}
{% include 'dialogs/slicing.jinja2' %}
{% include 'dialogs/usersettings.jinja2' %}