From 6691cc00a56f310e785aa7fcc01fdd8e7db3b799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 20 Aug 2015 12:50:45 +0200 Subject: [PATCH] Documentation of the wizard plugins & slight refactoring --- docs/plugins/mixins.rst | 8 + src/octoprint/plugin/types.py | 191 +++++++++++++++++- src/octoprint/server/api/__init__.py | 6 +- src/octoprint/server/views.py | 2 +- .../static/js/app/viewmodels/wizard.js | 2 +- 5 files changed, 204 insertions(+), 5 deletions(-) diff --git a/docs/plugins/mixins.rst b/docs/plugins/mixins.rst index b4063fd7..b57c012e 100644 --- a/docs/plugins/mixins.rst +++ b/docs/plugins/mixins.rst @@ -51,6 +51,14 @@ TemplatePlugin .. autoclass:: octoprint.plugin.TemplatePlugin :members: +.. _sec-plugins-mixins-wizardplugin: + +WizardPlugin +------------ + +.. autoclass:: octoprint.plugin.WizardPlugin + :members: + .. _sec-plugins-mixins-simpleapiplugin: SimpleApiPlugin diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index f47cfc0f..f42770a0 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -249,6 +249,20 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): wrapper div and the link in the navigation will have the additional classes and styles applied as defined via the supplied configuration supplied through :func:`get_template_configs`. + Wizards + Plugins may define wizard dialogs to display to the user if necessary (e.g. in case of missing information that + needs to be queried from the user to make the plugin work). Note that with the current implementations, all + wizard dialogs will always be sorted alphabetically by their ``name``. A wizard dialog provided through a + plugin will only be displayed if the plugin reports the wizard as being required through :meth:`~octoprint.plugin.WizardPlugin.is_wizard_required`. + Please also refer to the :class:`~octoprint.plugin.WizardPlugin` mixin for further details on this. + + The included template must be called ``_wizard.jinja2`` (e.g. ``myplugin_wizard.jinja2``) unless + overridden by the configuration supplied through :func:`get_template_configs`. + + The template will be already wrapped into the necessary structure, plugins just need to supply the pure content. + The wrapper div and the link in the wizard navigation will have the additional classes and styles applied as defined + via the supplied configuration supplied through :func:`get_template_configs`. + Generic Plugins may also inject arbitrary templates into the page of the web interface itself, e.g. in order to add overlays or dialogs to be called from within the plugin's javascript code. @@ -451,18 +465,191 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin): + """ + The ``WizardPlugin`` mixin allows plugins to report to OctoPrint whether + the ``wizard`` templates they define via the :class:`~octoprint.plugin.TemplatePlugin` + should be displayed to the user, what details to provide to their respective + wizard frontend components and what to do when the wizard is finished + by the user. + + OctoPrint will only display such wizard dialogs to the user which belong + to plugins that + + * report ``True`` in their :func:`is_wizard_required` method and + * have not yet been shown to the user in the version currently being reported + by the :meth:`~octoprint.plugin.WizardPlugin.get_wizard_version` method + + Example: If a plugin with the identifier ``myplugin`` has a specific + setting ``some_key`` it needs to have filled by the user in order to be + able to work at all, it would probably test for that setting's value in + the :meth:`~octoprint.plugin.WizardPlugin.is_wizard_required` method and + return ``True`` if the value is unset: + + .. code-block:: python + + class MyPlugin(octoprint.plugin.SettingsPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.WizardPlugin): + + def get_default_settings(self): + return dict(some_key=None) + + def is_wizard_required(self): + return self._settings.get(["some_key"]) is None + + OctoPrint will then display the wizard dialog provided by the plugin through + the :class:`TemplatePlugin` mixin. Once the user finishes the wizard on the + frontend, OctoPrint will store that it already showed the wizard of ``myplugin`` + in the version reported by :meth:`~octoprint.plugin.WizardPlugin.get_wizard_version` + - here ``None`` since that is the default value returned by that function + and the plugin did not override it. + + If the plugin in a later version needs another setting from the user in order + to function, it will also need to change the reported version in order to + have OctoPrint reshow the dialog. E.g. + + .. code-block:: python + + class MyPlugin(octoprint.plugin.SettingsPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.WizardPlugin): + + def get_default_settings(self): + return dict(some_key=None, some_other_key=None) + + def is_wizard_required(self): + some_key_unset = self._settings.get(["some_key"]) is None + some_other_key_unset = self._settings.get(["some_other_key"]) is None + + return some_key_unset or some_other_key_unset + + def get_wizard_version(self): + return 1 + """ def is_wizard_required(self): + """ + Allows the plugin to report whether it needs to display a wizard to the + user or not. + + Defaults to ``False``. + + OctoPrint will only include those wizards from plugins which are reporting + their wizards as being required through this method returning ``True``. + Still, if OctoPrint already displayed that wizard in the same version + to the user once it won't be displayed again regardless whether this + method returns ``True`` or not. + """ return False + def get_wizard_version(self): + """ + The version of this plugin's wizard. OctoPrint will only display a wizard + of the same plugin and wizard version once to the user. After they + finish the wizard, OctoPrint will remember that it already showed this + wizard in this particular version and not reshow it. + + If a plugin needs to show its wizard to the user again (e.g. because + of changes in the required settings), increasing this value is the + way to notify OctoPrint of these changes. + + Returns: + int or None: an int signifying the current wizard version, should be incremented by plugins whenever there + are changes to the plugin that might necessitate reshowing the wizard if it is required. ``None`` + will also be accepted and lead to the wizard always be ignored unless it has never been finished + so far + """ + return None + def get_wizard_details(self): + """ + Called by OctoPrint when the wizard wrapper dialog is shown. Allows the plugin to return data + that will then be made available to the view models via the view model callback ``onWizardDetails``. + + Use this if your plugin's view model that handles your wizard dialog needs additional + data to perform its task. + + Returns: + dict: a dictionary containig additional data to provide to the frontend. Whatever the plugin + returns here will be made available on the wizard API under the plugin's identifier + """ return dict() def on_wizard_finish(self, handled): + """ + Called by OctoPrint whenever the user finishes a wizard session is finished. + + The ``handled`` parameter will indicate whether that plugin's wizard was + included in the wizard dialog presented to the user (so the plugin providing + it was reporting that the wizard was required and the wizard plus version was not + ignored/had already been seen). + + Use this to do any clean up tasks necessary after wizard completion. + + Arguments: + handled (bool): True if the plugin's wizard was previously reported as + required, not ignored and thus presented to the user, + False otherwise + """ pass - def get_wizard_version(self): - return None + @classmethod + def is_wizard_ignored(cls, seen_wizards, implementation): + """ + Determines whether the provided implementation is ignored based on the + provided information about already seen wizards and their versions or not. + + A wizard is ignored if + + * the current and seen versions are identical + * the current version is None and the seen version is not + * the current version is less or equal than the seen one + + .. code-block:: none + + | current | + | N | 1 | 2 | N = None + ----+---+---+---+ X = ignored + s N | X | | | + e --+---+---+---+ + e 1 | X | X | | + n --+---+---+---+ + 2 | X | X | X | + ----+---+---+---+ + + Arguments: + seen_wizards (dict): A dictionary with information about already seen + wizards and their versions. Mappings from the identifiers of + the plugin providing the wizard to the reported wizard + version (int or None) that was already seen by the user. + implementation (object): The plugin implementation to check. + + Returns: + bool: False if the provided ``implementation`` is either not a :class:`WizardPlugin` + or has not yet been seen (in this version), True otherwise + """ + + if not isinstance(implementation, cls): + return False + + name = implementation._identifier + if not name in seen_wizards: + return False + + seen = seen_wizards[name] + wizard_version = implementation.get_wizard_version() + + current = None + if wizard_version is not None: + try: + current = int(wizard_version) + except ValueError as e: + import logging + logging.getLogger(__name__).log("WizardPlugin {} returned invalid value {} for wizard version: {}".format(name, wizard_version, str(e))) + + return (current == seen) \ + or (current is None and seen is not None) \ + or (current <= seen) class SimpleApiPlugin(OctoPrintPlugin): diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index c40501a8..3508f2d8 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -103,6 +103,8 @@ def wizardState(): if not s().getBoolean(["server", "firstRun"]) and not admin_permission.can(): abort(403) + seen_wizards = s().get(["server", "seenWizards"]) + result = dict() wizard_plugins = octoprint.server.pluginManager.get_implementations(octoprint.plugin.WizardPlugin) for implementation in wizard_plugins: @@ -110,10 +112,12 @@ def wizardState(): try: required = implementation.is_wizard_required() details = implementation.get_wizard_details() + version = implementation.get_wizard_version() + ignored = octoprint.plugin.WizardPlugin.is_wizard_ignored(seen_wizards, implementation) except: logging.getLogger(__name__).exception("There was an error fetching wizard details for {}, ignoring".format(name)) else: - result[name] = dict(required=required, details=details) + result[name] = dict(required=required, details=details, version=version, ignored=ignored) return jsonify(result) diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index ce7e36c9..01a056ac 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -202,7 +202,7 @@ def index(): configs = implementation.get_template_configs() if isinstance(implementation, octoprint.plugin.WizardPlugin): wizard_required = implementation.is_wizard_required() - wizard_ignored = name in seen_wizards and seen_wizards[name] == implementation.get_wizard_version() + wizard_ignored = octoprint.plugin.WizardPlugin.is_wizard_ignored(seen_wizards, implementation) except: _logger.exception("Error while retrieving template data for plugin {}, ignoring it".format(name)) continue diff --git a/src/octoprint/static/js/app/viewmodels/wizard.js b/src/octoprint/static/js/app/viewmodels/wizard.js index 3c41b082..c15b11a3 100644 --- a/src/octoprint/static/js/app/viewmodels/wizard.js +++ b/src/octoprint/static/js/app/viewmodels/wizard.js @@ -142,7 +142,7 @@ $(function() { type: "GET", dataType: "json", success: function(response) { - self.wizards = _.filter(_.keys(response), function(key) { return response[key] && response[key]["required"]; }); + self.wizards = _.filter(_.keys(response), function(key) { return response[key] && response[key]["required"] && !response[key]["ignored"]; }); if (callback) { callback(response); }