The plugin system now also injects self._settings into SettingsPlugins and create the blueprint for BlueprintPlugins

Also adjusted a lot of documentation for that stuff and continued writing the Getting Started guide for plugin development.
This commit is contained in:
Gina Häußge 2015-01-30 13:12:25 +01:00
parent e1e47abfbe
commit d3bd990009
9 changed files with 480 additions and 98 deletions

View file

@ -27,6 +27,8 @@ OctoPrint found that plugin in the folder and took a look into it. The name and
entry it got from the ``__plugin_name__`` and ``__plugin_version__`` lines. It also read the description from
``__plugin_description__`` and stored it in an internal data structure, but we'll just ignore this for now.
.. _sec-plugins-gettingstarted-sayinghello:
Saying hello: How to make the plugin actually do something
----------------------------------------------------------
@ -55,11 +57,11 @@ and restart OctoPrint. You now get this output in the log::
2015-01-27 11:17:10,792 - octoprint.plugins.helloworld - INFO - Hello World!
Neat, isn't it? We added a custom class that subclasses one of OctoPrint's :ref:`plugin mixins <sec-plugins-mixins>`
with :class:`StartupPlugin` and another control property, ``__plugin_implementations__``, that instantiates
our plugin class and tells OctoPrint about it. Taking a look at the documentation of :class:`StartupPlugin` we see that
this mixin offers two methods that get called by OctoPrint during startup of the server, ``on_startup`` and
``on_after_startup``. We decided to add our logging output by overriding ``on_after_startup``, but we could also have
used ``on_startup`` instead, in which case our logging statement would be executed before the server was done starting
with :class:`~octoprint.plugin.StartupPlugin` and another control property, ``__plugin_implementations__``, that instantiates
our plugin class and tells OctoPrint about it. Taking a look at the documentation of :class:`~octoprint.plugin.StartupPlugin` we see that
this mixin offers two methods that get called by OctoPrint during startup of the server, :func:`~octoprint.plugin.StartupPlugin.on_startup` and
:func:`~octoprint.plugin.StartupPlugin.on_after_startup`. We decided to add our logging output by overriding :func:`~octoprint.plugin.StartupPlugin.on_after_startup`, but we could also have
used :func:`~octoprint.plugin.StartupPlugin.on_startup` instead, in which case our logging statement would be executed before the server was done starting
up and ready to serve requests.
You'll also note that we are using ``self._logger`` for logging. Where did that one come from? OctoPrint's plugin system
@ -68,6 +70,8 @@ one of those being a fully instantiated `python logger <https://docs.python.org/
used by your plugin. As you can see in the log output above, that logger uses the namespace ``octoprint.plugins.helloworld``
for our little plugin here, or more generally ``octoprint.plugins.<plugin identifier>``.
.. _sec-plugins-gettingstarted-growingup:
Growing up: How to make it distributable
----------------------------------------
@ -86,9 +90,11 @@ install directly via Python's standard package manager ``pip`` or alternatively
So let's begin. First checkout the `Plugin Skeleton <https://github.com/OctoPrint/OctoPrint-PluginSkeleton>`_ and rename
the ``octoprint_skeleton`` folder to something better suited to our "Hello World" plugin::
git clone https://github.com/OctoPrint/OctoPrint-PluginSkeleton.git OctoPrint-HelloWorld
cd OctoPrint-HelloWorld
mv octoprint_skeleton octoprint_helloworld
$ git clone https://github.com/OctoPrint/OctoPrint-PluginSkeleton.git OctoPrint-HelloWorld
Cloning into 'OctoPrint-HelloWorld'...
[...]
$ cd OctoPrint-HelloWorld
$ mv octoprint_skeleton octoprint_helloworld
Then edit the configuration in the ``setup.py`` file to mirror our own "Hello World" plugin. The configuration should
look something like this:
@ -143,7 +149,8 @@ of information now defined twice:
plugin_description = "A quick \"Hello World\" example plugin for OctoPrint"
The nice thing about our plugin now being a proper python package is that OctoPrint can and will access the metadata defined
within ``setup.py``! So, we don't really need to define all this data twice. Remove both ``plugin_name`` and ``plugin_version``:
within ``setup.py``! So, we don't really need to define all this data twice. Remove ``__plugin_name__``, ``__plugin_version__``
and ``__plugin_description__``:
.. code-block:: python
@ -163,7 +170,7 @@ and restart OctoPrint::
2015-01-27 13:46:33,786 - octoprint.plugin.core - INFO - Found 3 plugin(s): OctoPrint-HelloWorld (1.0), CuraEngine (0.1), Discovery (0.1)
Our "Hello World" Plugin still gets detected fine, but it's now listed under the same name it's installed under,
"OctoPrint-HelloWorld". That's a bit ugly, so we'll override that bit via ``__plugin_name__`` again:
"OctoPrint-HelloWorld". That's a bit redundant and squashed, so we'll override that bit via ``__plugin_name__`` again:
.. code-block:: python
:emphasize-lines: 10
@ -196,25 +203,29 @@ already publish your plugin on Github and it would be directly installable by ot
But let's add some more features instead.
Frontend or get out: How to add functionality to OctoPrint's web interface
--------------------------------------------------------------------------
.. _sec-plugins-gettingstarted-templates:
Frontend fun: How to add functionality to OctoPrint's web interface
-------------------------------------------------------------------
Outputting a log line upon server startup is all nice and well, but we want to greet not only the administrator of
our OctoPrint instance but actually everyone that opens OctoPrint in their browser. Therefore, we need to modify
OctoPrint's web interface itself.
We can do this using the :class:`TemplatePlugin` mixin. For now, let's start with a little "Hello World!" in OctoPrint's
navigation bar right at the top. For this we'll first add the :class:`TemplatePlugin` to our ``HelloWorldPlugin`` class:
navigation bar right at the top that links to the Wikipedia node about "Hello World" programs. For this we'll first
add the :class:`TemplatePlugin` to our ``HelloWorldPlugin`` class:
.. code-block:: python
:emphasize-lines: 6
:emphasize-lines: 7
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin):
class HelloWorldPlugin(octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin):
def on_after_startup(self):
self._logger.info("Hello World!")
@ -230,13 +241,13 @@ Next, we'll create a sub folder ``templates`` underneath our ``octoprint_hellowo
Our plugin's directory structure should now look like this::
|-+ octoprint_helloworld
| |-+ templates
| | `- helloworld_navbar.jinja2
| `- __init__.py
|- README.md
|- requirements.txt
`- setup.py
octoprint_helloworld/
templates/
helloworld_navbar.jinja2
__init__.py
README.md
requirements.txt
setup.py
Restart OctoPrint and open the web interface in your browser (make sure to clear your browser's cache!).
@ -245,4 +256,206 @@ Restart OctoPrint and open the web interface in your browser (make sure to clear
:align: center
:alt: Our "Hello World" navigation bar element in action
Now look at that!
Now look at that!
.. _sec-plugins-gettingstarted-settings:
Settings Galore: How to make parts of your plugin user adjustable
-----------------------------------------------------------------
Remember that Wikipedia link we added to our little link in the navigation bar? It links to the english Wikipedia. But
what if we want to allow our users to adjust that according to their wishes, e.g. to link to the german language node
about "Hello World" programs instead?
To allow your users to customized the behaviour of your plugin you'll need to implement the :class:`~octoprint.plugin.SettingsPlugin`
mixin and override it's :func:`~octoprint.plugin.SettingsPlugin.get_settings_defaults` method. We'll save the URL to
inject into the link under the key ``url`` in our plugin's settings and set it to the old value by default. We'll therefore
return just a single key in our default settings dictionary. To be able to quickly see if we've done that right we'll
extend our little startup message to also log the current setting to the console. We can access that via ``self._settings``,
which is a little settings manager OctoPrint conveniently injects into our Plugin when we include the :class:`~octoprint.plugin.SettingsPlugin`
mixin.
Let's take a look at how all that would look in our plugin's ``__init__.py``:
.. code-block:: python
:emphasize-lines: 8, 10, 12-13
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.SettingsPlugin):
def on_after_startup(self):
self._logger.info("Hello World! (more: %s)" % self._settings.get(["url"]))
def get_settings_defaults(self):
return dict(url="https://en.wikipedia.org/wiki/Hello_world")
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
Restart OctoPrint. You should see something like this::
2015-01-30 11:41:06,058 - octoprint.plugins.helloworld - INFO - Hello World! (more: https://en.wikipedia.org/wiki/Hello_world)
So far so good. But how do we now get that value into our template? We have two options, the
static one using so called template variables and a dynamic one which retrieves that data from the backend and binds it
into the template using `Knockout data bindings <http://knockoutjs.com/documentation/introduction.html>`_. First let's
take a look at the static version using template variables. We already have the :class:`~octoprint.plugin.TemplatePlugin`
mixin included in our plugin, we just need to override its method :func:`~octoprint.plugin.TemplatePlugin.get_template_vars`
to add our URL as a template variable.
Adjust your plugin's ``__init__.py`` like this:
.. code-block:: python
:emphasize-lines: 15-16
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.SettingsPlugin):
def on_after_startup(self):
self._logger.info("Hello World! (more: %s)" % self._settings.get(["url"]))
def get_settings_defaults(self):
return dict(url="https://en.wikipedia.org/wiki/Hello_world")
def get_template_vars(self):
return dict(url=self._settings.get(["url"]))
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
Also adjust your plugin's ``templates/helloworld_navbar.jinja2`` like this:
.. code-block:: html
<a href="{{ plugin_helloworld_url|escape }}">Hello World!</a>
OctoPrint injects the template variables that your plugin defines prefixed with ``plugin_<plugin identifier>_`` into
the template renderer, so your ``url`` got turned into ``plugin_helloworld_url`` which you can now use as a simple
`Jinja2 Variable <http://jinja.pocoo.org/docs/dev/templates/#variables>`_ in your plugin's template.
Restart OctoPrint and shift-reload the page in your browser (to make sure you really get a fresh copy). The link should
still work and point to the URL we defined as default.
Let's change the URL! Open up your OctoPrint instance's ``config.yaml`` file and add the following to it (if a ``plugins``
section doesn't yet exist in the file, create it):
.. code-block:: yaml
:emphasize-lines: 3-4
# [...]
plugins:
helloworld:
url: https://de.wikipedia.org/wiki/Hallo-Welt-Programm
# [...]
Restart OctoPrint. Not only should the URL displayed in the log file have changed, but also the link should now (after
a proper shift-reload) point to the german Wikipedia node about "Hello World" programs::
2015-01-30 11:47:18,634 - octoprint.plugins.helloworld - INFO - Hello World! (more: https://de.wikipedia.org/wiki/Hallo-Welt-Programm)
Nice! But not very user friendly. We don't have any way yet to edit the URL from within OctoPrint and have to restart
the server and reload the page every time we want a value change to take effect. Let's try adding a little settings dialog
for our plugin in which we can edit the URL and take any changes take immediate effect.
First of all, we'll create the settings dialog. You might already have guessed that we'll need another template for that.
So in your plugin's ``templates`` folder create a new file ``helloworld_settings.jinja2`` and put the following content
into it:
.. code-block:: html
<form class="form-horizontal">
<div class="control-group">
<label class="control-label">{{ _('URL') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.plugins.helloworld.url">
</div>
</div>
</form>
Note how we access our plugin's property via ``settings.plugins.helloworld.url``. The ``settings`` observable is made
available in the ``SettingsViewModel`` and holds the exact data structure returned from the server for all of
OctoPrint's settings. Accessing plugin settings hence works by following the path under which they are stored in
OctoPrint's internal settings data model (made public via the ``config.yaml``), ``plugins.<plugin identifier>.<configuration key>``.
We'll bind our own settings dialog to the existing ``SettingsViewModel``, so this will be the way we'll access our
property.
Now adjust your ``templates/helloworld_navbar.jinja2`` file to use a ``data-bind`` attribute to set the value from the
settings view model into the ``href`` attribute of the link tag:
.. code-block:: html
<a href="#" data-bind="attr: {href: settings.settings.plugins.helloworld.url}">Hello World!</a>
You might have noticed the quite ugly way to access our plugin's ``url`` property here: ``settings.settings.plugins.helloworld.url``.
The reason for this is that we'll make our plugin use the existing ``NavigationViewModel`` which holds the
``SettingsViewModel`` as a property called ``settings``. So to get to the ``settings`` property of the ``SettingsViewModel``
from the ``NavigationViewModel``, we'll need to first "switch" to the ``SettingsViewModel`` using its property name. Hence
the ugly access string.
If you were now to restart OctoPrint and reload the web interface, you'll get the settings dialog placed just fine
in OctoPrint's settings, and the link would also still show up in the navigation bar, but both the input field of the
settings dialog as well as the link's ``href`` attribute would not show our link. The reason for this is that OctoPrint
by default assumes that you'll want to bind your own view models to your templates and hence "unbinds" the included
templates from the templates that are in place at the injected location already. In order to tell OctoPrint to please
don't do this here (since we *do* want to use both ``NavigationViewModel`` and ``SettingsViewModel``), we'll need to
override the default template configuration using the :class:`~octoprint.plugin.TemplatePlugin`s
:func:`~octoprint.plugin.TemplatePlugin.get_template_configs` method. We'll tell OctoPrint to use no custom bindings
for both our ``navbar`` and our ``settings`` plugin. We'll also remove the override of :func:`octoprint.plugin.TemplatePlugin.get_template_vars`
again since we don't use that anymore:
.. code-block:: python
:emphasize-lines: 15-19
# coding=utf-8
from __future__ import absolute_import
import octoprint.plugin
class HelloWorldPlugin(octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.SettingsPlugin):
def on_after_startup(self):
self._logger.info("Hello World! (more: %s)" % self._settings.get(["url"]))
def get_settings_defaults(self):
return dict(url="https://en.wikipedia.org/wiki/Hello_world")
def get_template_configs(self):
return [
dict(type="navbar", custom_bindings=False),
dict(type="settings", custom_bindings=False)
]
__plugin_name__ = "Hello World"
__plugin_implementations__ = [HelloWorldPlugin()]
Restart OctoPrint and shift-reload your browser. Your link in the navigation bar should still point to the URL we
defined in ``config.yaml`` earlier. Open the "Settings" and click on the new "Hello World" entry that shows up under
"Plugins".
.. _fig-plugins-gettingstarted-helloworld_settings:
.. figure:: ../images/plugins_gettingstarted_helloworld_settings.png
:align: center
:alt: Our "Hello World" navigation bar element in action
Nice! Edit the value, then click "Save". Your link in the navigation bar should now have been updated as well.
.. note::
The way we've done our data binding and how OctoPrint currently works, your link's target will update immediately
when you update the value in the settings dialog. Even if you click Cancel instead of Save, the change will still
be reflected in the UI but will be overwritten again by the stored data upon a reload. This is caused by OctoPrint
not storing a copy of the settings data while it is being edited, which might be changed in the future to
prevent this unexpected behaviour from occurring.
Congratulations, you've just made your Plugin configurable :)

View file

@ -50,6 +50,8 @@ Injected Properties
``self._logger``
A `python logger instance <https://docs.python.org/2/library/logging.html>`_ logging to the log target
``octoprint.plugin.<plugin identifier>``.
``self._settings``
The plugin's personalized settings manager, injected only into plugins that include the :class:`SettingsPlugin` mixin.
``self._plugin_manager``
OctoPrint's plugin manager.
``self._printer_profile_manager``

View file

@ -3,6 +3,7 @@
Available plugin mixins
=======================
.. automodule:: octoprint.plugin.types
:members:
.. automodule:: octoprint.plugin
:members: AppPlugin, AssetPlugin, BlueprintPlugin, EventHandlerPlugin, ProgressPlugin, SettingsPlugin, ShutdownPlugin, SimpleApiPlugin, SlicerPlugin, StartupPlugin, TemplatePlugin
:undoc-members:

View file

@ -41,7 +41,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
if "enabled" in all_plugin_settings[key] and not all_plugin_settings[key]:
plugin_disabled_list.append(key)
_instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points, plugin_disabled_list=plugin_disabled_list)
_instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points, logging_prefix="octoprint.plugins.", plugin_disabled_list=plugin_disabled_list)
else:
raise ValueError("Plugin Manager not initialized yet")
return _instance

View file

@ -72,7 +72,7 @@ class PluginInfo(object):
@property
def name(self):
return self._get_instance_attribute(self.__class__.attr_name, default=self._name)
return self._get_instance_attribute(self.__class__.attr_name, default=(self._name, self.key))
@property
def description(self):
@ -116,15 +116,23 @@ class PluginInfo(object):
def _get_instance_attribute(self, attr, default=None):
if not hasattr(self.instance, attr):
return default
if isinstance(default, (tuple, list)):
for value in default:
if value is not None:
return value
return None
else:
return default
return getattr(self.instance, attr)
class PluginManager(object):
def __init__(self, plugin_folders, plugin_types, plugin_entry_points, plugin_disabled_list=None):
def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None):
self.logger = logging.getLogger(__name__)
if logging_prefix is None:
logging_prefix = ""
if plugin_disabled_list is None:
plugin_disabled_list = []
@ -132,6 +140,7 @@ class PluginManager(object):
self.plugin_types = plugin_types
self.plugin_entry_points = plugin_entry_points
self.plugin_disabled_list = plugin_disabled_list
self.logging_prefix = logging_prefix
self.plugins = dict()
self.plugin_hooks = defaultdict(list)
@ -290,21 +299,35 @@ class PluginManager(object):
self.log_registered_plugins()
def initialize_implementations(self, additional_injects=None):
def initialize_implementations(self, additional_injects=None, additional_inject_factories=None):
if additional_injects is None:
additional_injects = dict()
if additional_inject_factories is None:
additional_inject_factories = []
for name, implementations in self.plugin_implementations.items():
plugin = self.plugins[name]
for implementation in implementations:
kwargs = dict(additional_injects)
for factory in additional_inject_factories:
try:
return_value = factory(name, implementation)
except:
self.logger.exception("Exception while executing injection factory %r" % factory)
else:
if return_value is not None:
if isinstance(return_value, dict):
kwargs.update(return_value)
kwargs.update(dict(
identifier=name,
plugin_name=plugin.name,
plugin_version=plugin.version,
basefolder=os.path.realpath(plugin.location),
logger=logging.getLogger("octoprint.plugins." + name),
logger=logging.getLogger(self.logging_prefix + name),
))
try:
implementation.pre_initialize(**kwargs)
except:

View file

@ -21,10 +21,10 @@ class StartupPlugin(Plugin):
will listen on. Note that the ``host`` may be ``0.0.0.0`` if it will listen on all interfaces, so you can't just
blindly use this for constructing publicly reachable URLs. Also note that when this method is called, the server
is not actually up yet and none of your plugin's APIs or blueprints will be reachable yet. If you need to be
externally reachable, use ``on_after_startup`` instead or additionally.
externally reachable, use :func:`on_after_startup` instead or additionally.
:param host: the host the server will listen on, may be ``0.0.0.0``
:param port: the port the server will listen on
:param string host: the host the server will listen on, may be ``0.0.0.0``
:param int port: the port the server will listen on
"""
pass
@ -40,7 +40,7 @@ class StartupPlugin(Plugin):
class ShutdownPlugin(Plugin):
"""
The ``ShutdownPlugin`` allows hooking into the shutdown of OctoPrint. It's usually used in conjunction with the
``StartupPlugin`` mixin, to cleanly shut down additional services again that where started by the ``StartupPlugin``
:class:`StartupPlugin` mixin, to cleanly shut down additional services again that where started by the :class:`StartupPlugin`
part of the plugin.
"""
@ -57,16 +57,16 @@ class AssetPlugin(Plugin):
be automatically embedded into the pages delivered by the server to be used within the client sided part of
the plugin.
A typical usage of the ``AssetPlugin`` functionality is to embed a custom view model to be used on the settings page
of a ``SettingsPlugin``.
A typical usage of the ``AssetPlugin`` functionality is to embed a custom view model to be used by templates injected
through :class:`TemplatePlugin`s.
"""
def get_asset_folder(self):
"""
Defines the folder where the plugin stores its static assets as defined in ``get_assets``. Override this if
Defines the folder where the plugin stores its static assets as defined in :func:`get_assets`. Override this if
your plugin stores its assets at some other place than the ``static`` sub folder in the plugin base directory.
:return: the absolute path to the folder where the plugin stores its static assets
:return string: the absolute path to the folder where the plugin stores its static assets
"""
import os
return os.path.join(self._basefolder, "static")
@ -84,7 +84,7 @@ class AssetPlugin(Plugin):
LESS files with additional styles, will be embedded into delivered pages when running in LESS mode.
The expected format to be returned is a dictionary mapping one or more of these keys to a list of files of that
type, the files being represented as relative paths from the asset folder as defined via ``get_asset_folder``.
type, the files being represented as relative paths from the asset folder as defined via :func:`get_asset_folder`.
Example:
.. code-block:: python
@ -96,10 +96,10 @@ class AssetPlugin(Plugin):
less=['less/my_styles.less']
)
The assets will be made available by OctoPrint under the URL ``/plugin_assets/<plugin name>/<path>``, with
``plugin_name`` being the plugin's name and ``path`` being the path as defined in the asset dictionary.
The assets will be made available by OctoPrint under the URL ``/plugin_assets/<plugin identifier>/<path>``, with
``plugin identifier`` being the plugin's identifier and ``path`` being the path as defined in the asset dictionary.
:return: a dictionary describing the static assets to publish for the plugin
:return dict: a dictionary describing the static assets to publish for the plugin
"""
return dict()
@ -115,22 +115,22 @@ class TemplatePlugin(Plugin):
with the current implementation, plugins will always be located *to the left* of the existing links.
The included template must be called ``<pluginname>_navbar.jinja2`` (e.g. ``myplugin_navbar.jinja2``) unless
overridden by the configuration supplied through ``get_template_config``.
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 structure will have all additional classes and styles applied as specified via the configuration supplied
through ``get_template_config``.
through :func:`get_template_configs`.
Sidebar
The left side bar containing Connection, State and Files sections can be enriched with additional sections. Note
that with the current implementations, plugins will always be located *beneath* the existing sections.
The included template must be called ``<pluginname>_sidebar.jinja2`` (e.g. ``myplugin_sidebar.jinja2``) unless
overridden by the configuration supplied through ``get_template_config``.
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 divs for both the whole box as well as the content pane will have all additional classes and styles applied
as specified via the configuration supplied through ``get_template_config``.
as specified via the configuration supplied through :func:`get_template_configs`.
Tabs
The available tabs of the main part of the interface may be extended with additional tabs originating from within
@ -138,11 +138,11 @@ class TemplatePlugin(Plugin):
tabs.
The included template must be called ``<pluginname>_tab.jinja2`` (e.g. ``myplugin_tab.jinja2``) unless
overridden by the configuration supplied through ``get_template_config``.
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 navigation will have the additional classes and styles applied as specified via the
configuration supplied through ``get_template_config``.
configuration supplied through :func:`get_template_configs`.
Settings
Plugins may inject a dialog into the existing settings view. Note that with the current implementations, plugins
@ -150,11 +150,11 @@ class TemplatePlugin(Plugin):
their displayed name.
The included template must be called ``<pluginname>_settings.jinja2`` (e.g. ``myplugin_settings.jinja2``) unless
overridden by the configuration supplied through ``get_template_config``.
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 navigation will have the additional classes and styles applied as defined via the
supplied configuration supplied through ``get_template_config``.
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
@ -360,15 +360,16 @@ class TemplatePlugin(Plugin):
those, since the implicit default template will only be included automatically if no other templates of that
type are defined.
:return: a list containing the configuration options for the plugin's injected templates
:return list: a list containing the configuration options for the plugin's injected templates
"""
return []
def get_template_vars(self):
"""
Defines additional template variables to include into the template renderer.
Defines additional template variables to include into the template renderer. Variable names will be prefixed
with ``plugin_<plugin identifier>_``.
:return: a dictionary containing any additional template variables to include in the renderer
:return dict: a dictionary containing any additional template variables to include in the renderer
"""
return dict()
@ -377,7 +378,7 @@ class TemplatePlugin(Plugin):
Defines the folder where the plugin stores its templates. Override this if your plugin stores its templates at
some other place than the ``templates`` sub folder in the plugin base directory.
:return: the absolute path to the folder where the plugin stores its jinja2 templates
:return string: the absolute path to the folder where the plugin stores its jinja2 templates
"""
import os
return os.path.join(self._basefolder, "templates")
@ -397,63 +398,194 @@ class SimpleApiPlugin(Plugin):
class BlueprintPlugin(Plugin):
"""
The ``BlueprintPlugin`` mixin allows plugins to define their own full fledged endpoints for whatever purpose,
be it a more sophisticated API than what is possible via the ``SimpleApiPlugin`` or a custom web frontend.
be it a more sophisticated API than what is possible via the :class:`SimpleApiPlugin` or a custom web frontend.
The mechanism at work here is `Flask's <http://flask.pocoo.org/>`_ own `Blueprint mechanism <http://flask.pocoo.org/docs/0.10/blueprints/>`_.
Your plugin should define a blueprint like this:
The mixin automatically creates a blueprint for you that will be registered under ``/plugin/<plugin identifier>/``.
All you need to do is decorate all of your view functions with the :func:`route` decorator,
which behaves exactly the same like Flask's regular ``route`` decorators. Example:
.. code-block:: python
:linenos:
import octoprint.plugin
import flask
class MyBlueprintPlugin(octoprint.plugin.BlueprintPlugin):
@octoprint.plugin.BlueprintPlugin.route("/echo", methods=["GET"])
def myEcho(self):
if not "text" in flask.request.values:
return flask.make_response("Expected a text to echo back.", 400)
return flask.request.values["text"]
__plugin_implementations__ = [MyPlugin()]
Your blueprint will be published by OctoPrint under the base URL ``/plugin/<plugin identifier>/``, so the above
example of a plugin with the identifier "myblueprintplugin" would be reachable under
``/plugin/myblueprintplugin/echo``.
Just like with regular blueprints you'll be able to create URLs via ``url_for``, just use the prefix
``plugin.<plugin identifier>``, e.g.:
.. code-block:: python
template_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
blueprint = flask.Blueprint("plugin.myplugin", __name__, template_folder=template_folder)
flask.url_for("plugin.myblueprintplugin.echo") # will return "/plugin/myblueprintplugin/echo"
Use your blueprint just like any other Flask blueprint for defining your own endpoints, e.g.
.. code-block:: python
@blueprint.route("/echo", methods=["GET"])
def myEcho():
if not "text" in flask.request.values:
return flask.make_response("Expected a text to echo back.", 400)
return flask.request.values["text"]
Your blueprint will be published by OctoPrint under the URL ``/plugin/<pluginname>/``, so the above example
would be reachable under ``/plugin/myplugin/echo`` (given that you named your plugin "myplugin"). You'll be able
to create URLs via ``url_for`` under the prefix that you've chosen when constructing your blueprint, ``plugin.myplugin``
in the above example:
.. code-block:: python
flask.url_for("plugin.myplugin.echo") # will return "/plugin/myplugin/echo"
OctoPrint Blueprint plugins should always follow the naming scheme ``plugin.<plugin name>`` here
to avoid conflicts.
"""
@staticmethod
def route(rule, **options):
"""
A decorator to mark view methods in your BlueprintPlugin subclass. Works just the same as Flask's
own ``route`` decorator available on blueprints.
See `the documentation for flask.Blueprint.route <http://flask.pocoo.org/docs/0.10/api/#flask.Blueprint.route>`_
and `the documentation for flask.Flask.route <http://flask.pocoo.org/docs/0.10/api/#flask.Flask.route>`_ for more
information.
"""
from collections import defaultdict
def decorator(f):
# We attach the decorator parameters directly to the function object, because that's the only place
# we can access right now.
# This neat little trick was adapter from the Flask-Classy project: https://pythonhosted.org/Flask-Classy/
if not hasattr(f, "_blueprint_rules") or f._blueprint_rules is None:
f._blueprint_rules = defaultdict(list)
f._blueprint_rules[f.__name__].append((rule, options))
return f
return decorator
def get_blueprint(self):
"""
Retrieves the blueprint as defined by your plugin.
Creates and returns the blueprint for your plugin. Override this if you want to define and handle your blueprint yourself.
This method will only be called once during server initialization.
:return: the blueprint ready to be registered with Flask
"""
return None
import flask
blueprint = flask.Blueprint("plugin." + self._identifier, self._identifier)
for member in [member for member in dir(self) if not member.startswith("_")]:
f = getattr(self, member)
if hasattr(f, "_blueprint_rules") and member in f._blueprint_rules:
for blueprint_rule in f._blueprint_rules[member]:
rule, options = blueprint_rule
blueprint.add_url_rule(rule, options.pop("endpoint", f.__name__), view_func=f, **options)
return blueprint
def is_blueprint_protected(self):
"""
Whether the blueprint is supposed to be protected by API key (the default) or not.
Whether a valid API key is needed to access the blueprint (the default) or not.
"""
return True
class SettingsPlugin(Plugin):
"""
Including the ``SettingsPlugin`` mixin allows plugins to store and retrieve their own settings within OctoPrint's
configuration.
Plugins including the mixing will get injected an additional property ``self._settings`` which is an instance of
:class:`PluginSettingsManager` already properly initialized for use by the plugin. In order for the manager to
know about the available settings structure and default values upon initialization, implementing plugins will need
to provide a dictionary with the plugin's default settings through overriding the method :func:`get_settings_defaults`.
The defined structure will then be available to access through the settings manager available as ``self._settings``.
If your plugin needs to react to the change of specific configuration values on the fly, e.g. to adjust the log level
of a logger when the user changes a corresponding flag via the settings dialog, you can override the
:func:`on_settings_save` method and wrap the call to the implementation from the parent class with retrieval of the
old and the new value and react accordingly.
Example:
.. code-block:: python
import octoprint.plugin
class MySettingsPlugin(octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin):
def get_settings_defaults(self):
return dict(
some_setting="foo",
some_value=23,
sub=dict(
some_flag=True
)
)
def on_settings_save(self, data):
old_flag = self._settings.getBoolean(["sub", "some_flag"])
super(MySettingsPlugin, self).on_settings_save(data)
new_flag = self._settings.getBoolean(["sub", "some_flag"])
if old_flag != new_flag:
self._logger.info("sub.some_flag changed from {old_flag} to {new_flag}".format(**locals()))
def on_after_startup(self):
some_setting = self._settings.get(["some_setting"])
some_value = self._settings.getInt(["some_value"])
some_flag = self._settings.getBoolean(["sub", "some_flag"])
self._logger.info("some_setting = {some_setting}, some_value = {some_value}, sub.some_flag = {some_flag}".format(**locals())
__plugin_implementations__ = [MySettingsPlugin()]
Of course, you are always free to completely override both :func:`on_settings_load` and :func:`on_settings_save` if the
default implementations do not fit your requirements.
"""
def on_settings_load(self):
return None
"""
Loads the settings for the plugin, called by the Settings API view in order to retrieve all settings from
all plugins. Override this if you want to inject additional settings properties that are not stored within
OctoPrint's configuration.
.. note::
The default implementation will return your plugin's settings as is, so just in the structure and in the types
that are currently stored in OctoPrint's configuration.
If you need more granular control here, e.g. over the used data types, you'll need to override this method
and iterate yourself over all your settings, using the proper retriever methods on the settings manager
to retrieve the data in the correct format.
:return: the current settings of the plugin, as a dictionary
"""
return self._settings.get([], asdict=True, merged=True)
def on_settings_save(self, data):
pass
"""
Saves the settings for the plugin, called by the Settings API view in order to persist all settings
from all plugins. Override this if you need to directly react to settings changes or want to extract
additional settings properties that are not stored within OctoPrint's configuration.
.. note::
The default implementation will persist your plugin's settings as is, so just in the structure and in the
types that were received by the Settings API view.
If you need more granular control here, e.g. over the used data types, you'll need to override this method
and iterate yourself over all your settings, retrieving them (if set) from the supplied received ``data``
and using the proper setter methods on the settings manager to persist the data in the correct format.
:param dict data: the settings dictionary to be saved for the plugin
"""
import octoprint.util
current = self._settings.get([], asdict=True, merged=True)
data = octoprint.util.dict_merge(current, data)
self._settings.set([], data)
def get_settings_defaults(self):
"""
Retrieves the plugin's default settings with which the plugin's settings manager will be initialized.
Override this in your plugin's implementation and return a dictionary defining your settings data structure
with included default values.
"""
return dict()
class EventHandlerPlugin(Plugin):
@ -502,9 +634,9 @@ class ProgressPlugin(Plugin):
"""
Called by OctoPrint on minimally 1% increments during a running print job.
:param location string: Location of the file
:param path string: Path of the file
:param progress int: Current progress as a value between 0 and 100
:param string location: Location of the file
:param string path: Path of the file
:param int progress: Current progress as a value between 0 and 100
"""
pass
@ -512,12 +644,12 @@ class ProgressPlugin(Plugin):
"""
Called by OctoPrint on minimally 1% increments during a running slicing job.
:param slicer string: Key of the slicer reporting the progress
:param source_location string: Location of the source file
:param source_path string: Path of the source file
:param destination_location string: Location the destination file
:param destination_path string: Path of the destination file
:param progress int: Current progress as a value between 0 and 100
:param string slicer: Key of the slicer reporting the progress
:param string source_location: Location of the source file
:param string source_path: Path of the source file
:param string destination_location: Location the destination file
:param string destination_path: Path of the destination file
:param int progress: Current progress as a value between 0 and 100
"""
pass

View file

@ -467,7 +467,14 @@ class Server():
printer = Printer(fileManager, analysisQueue, printerProfileManager)
appSessionManager = util.flask.AppSessionManager()
pluginManager.initialize_implementations(dict(
def plugin_settings_factory(name, implementation):
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
return None
default_settings = implementation.get_settings_defaults()
plugin_settings = octoprint.plugin.plugin_settings(name, defaults=default_settings)
return dict(settings=plugin_settings)
pluginManager.initialize_implementations(additional_injects=dict(
plugin_manager=pluginManager,
printer_profile_manager=printerProfileManager,
event_bus=eventManager,
@ -475,8 +482,9 @@ class Server():
slicing_manager=slicingManager,
file_manager=fileManager,
printer=printer,
app_session_manager=appSessionManager,
))
app_session_manager=appSessionManager
), additional_inject_factories=[plugin_settings_factory])
slicingManager.initialize()
# configure additional template folders for jinja2
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)

View file

@ -55,11 +55,13 @@ class SlicingManager(object):
self._slicers = dict()
self._slicer_names = dict()
self._load_slicers()
self._progress_callbacks = []
self._last_progress_report = None
def initialize(self):
self._load_slicers()
def register_progress_callback(self, callback):
self._progress_callbacks.append(callback)

View file

@ -3,6 +3,7 @@ function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsV
self.loginState = loginStateViewModel;
self.appearance = appearanceViewModel;
self.settings = settingsViewModel;
self.systemActions = settingsViewModel.system_actions;
self.users = usersViewModel;