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:
parent
e1e47abfbe
commit
d3bd990009
9 changed files with 480 additions and 98 deletions
|
|
@ -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 :)
|
||||
|
|
@ -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``
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue