diff --git a/src/octoprint/plugins/corewizard/__init__.py b/src/octoprint/plugins/corewizard/__init__.py new file mode 100644 index 00000000..8f94d535 --- /dev/null +++ b/src/octoprint/plugins/corewizard/__init__.py @@ -0,0 +1,137 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__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() diff --git a/src/octoprint/static/js/app/viewmodels/firstrun_wizard.js b/src/octoprint/plugins/corewizard/static/js/corewizard.js similarity index 78% rename from src/octoprint/static/js/app/viewmodels/firstrun_wizard.js rename to src/octoprint/plugins/corewizard/static/js/corewizard.js index 277a15d7..7dc318b4 100644 --- a/src/octoprint/static/js/app/viewmodels/firstrun_wizard.js +++ b/src/octoprint/plugins/corewizard/static/js/corewizard.js @@ -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" ]); }); diff --git a/src/octoprint/templates/dialogs/wizard/firstrun_acl.jinja2 b/src/octoprint/plugins/corewizard/templates/corewizard_acl_wizard.jinja2 similarity index 95% rename from src/octoprint/templates/dialogs/wizard/firstrun_acl.jinja2 rename to src/octoprint/plugins/corewizard/templates/corewizard_acl_wizard.jinja2 index 96540aa3..58cd4bfa 100644 --- a/src/octoprint/templates/dialogs/wizard/firstrun_acl.jinja2 +++ b/src/octoprint/plugins/corewizard/templates/corewizard_acl_wizard.jinja2 @@ -4,13 +4,13 @@ Please read the following, it is very important for your printer's health!

- 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 prevent strangers - possibly with malicious intent - to gain access to your printer 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).

- It looks like you haven't configured access control yet. Please set up an username and password for the + It looks like you haven't configured access control yet. Please set up a username and password 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":

{% endtrans %} diff --git a/src/octoprint/plugins/corewizard/templates/corewizard_webcam_wizard.jinja2 b/src/octoprint/plugins/corewizard/templates/corewizard_webcam_wizard.jinja2 new file mode 100644 index 00000000..052961df --- /dev/null +++ b/src/octoprint/plugins/corewizard/templates/corewizard_webcam_wizard.jinja2 @@ -0,0 +1,41 @@ +

{{ _('Webcam & Timelapse Recordings') }}

+ +{% trans %}

+ If you have a webcam, you may now configure it here. +

{% endtrans %} + +

{{ _('Webcam') }}

+ +{% trans %}

+ The Stream URL 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 from your browser. +

+ It may be +

+

+ The Snapshot URL is the URL OctoPrint uses to fetch single + images from the webcam for creating timelapse recordings. This needs to be + reachable from the OctoPrint server. +

{% endtrans %} + +
+ {% include "dialogs/_snippets/configurewebcamurls.jinja2" %} +
+ +

{{ _('Timelapse Recordings') }}

+ +{% trans %}

+ To render the snapshots into timelapse recordings, OctoPrint also needs to + know the correct path to FFMPEG. +

{% endtrans %} + +
+ {% include "dialogs/_snippets/configureffmpeg.jinja2" %} +
diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 170dbbac..53cbdf83 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -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 diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 91d41ebd..0337ffdd 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -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', diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 028aba9a..b22e828e 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -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 diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index cb518e0b..58b1e9e7 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -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; } }); } diff --git a/src/octoprint/static/js/app/viewmodels/wizard.js b/src/octoprint/static/js/app/viewmodels/wizard.js index c73d504b..00879c30 100644 --- a/src/octoprint/static/js/app/viewmodels/wizard.js +++ b/src/octoprint/static/js/app/viewmodels/wizard.js @@ -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; diff --git a/src/octoprint/templates/dialogs/firstrun.jinja2 b/src/octoprint/templates/dialogs/firstrun.jinja2 deleted file mode 100644 index 9c3360d4..00000000 --- a/src/octoprint/templates/dialogs/firstrun.jinja2 +++ /dev/null @@ -1,54 +0,0 @@ - diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index 3538f39e..415734d9 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -124,7 +124,6 @@ - {% include 'dialogs/firstrun.jinja2' %} {% include 'dialogs/settings.jinja2' %} {% include 'dialogs/slicing.jinja2' %} {% include 'dialogs/usersettings.jinja2' %}