Merge branch 'dev/uiPlugin' into devel

This commit is contained in:
Gina Häußge 2015-09-10 16:25:34 +02:00
commit f0ab517857
7 changed files with 324 additions and 53 deletions

View file

@ -18,6 +18,7 @@ StartupPlugin
.. autoclass:: octoprint.plugin.StartupPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-shutdownplugin:
@ -26,6 +27,7 @@ ShutdownPlugin
.. autoclass:: octoprint.plugin.ShutdownPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-settingsplugin:
@ -34,6 +36,7 @@ SettingsPlugin
.. autoclass:: octoprint.plugin.SettingsPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-assetplugin:
@ -42,6 +45,7 @@ AssetPlugin
.. autoclass:: octoprint.plugin.AssetPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-templateplugin:
@ -50,6 +54,7 @@ TemplatePlugin
.. autoclass:: octoprint.plugin.TemplatePlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-wizardplugin:
@ -58,6 +63,16 @@ WizardPlugin
.. autoclass:: octoprint.plugin.WizardPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-uiplugin:
UiPlugin
--------
.. autoclass:: octoprint.plugin.UiPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-simpleapiplugin:
@ -66,6 +81,7 @@ SimpleApiPlugin
.. autoclass:: octoprint.plugin.SimpleApiPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-blueprintplugin:
@ -74,6 +90,7 @@ BlueprintPlugin
.. autoclass:: octoprint.plugin.BlueprintPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-eventhandlerplugin:
@ -82,6 +99,7 @@ EventHandlerPlugin
.. autoclass:: octoprint.plugin.EventHandlerPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-progressplugin:
@ -90,6 +108,7 @@ ProgressPlugin
.. autoclass:: octoprint.plugin.ProgressPlugin
:members:
:show-inheritance:
.. _sec-plugins-mixins-slicerplugin:
@ -98,4 +117,5 @@ SlicerPlugin
.. autoclass:: octoprint.plugin.SlicerPlugin
:members:
:show-inheritance:

View file

@ -104,7 +104,8 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
SlicerPlugin,
AppPlugin,
ProgressPlugin,
WizardPlugin]
WizardPlugin,
UiPlugin]
if plugin_entry_points is None:
plugin_entry_points = "octoprint.plugin"
if plugin_disabled_list is None:

View file

@ -12,6 +12,12 @@ way and could be extracted into a separate Python module in the future.
.. autoclass:: Plugin
:members:
.. autoclass:: RestartNeedingPlugin
:members:
.. autoclass:: SortablePlugin
:members:
"""
from __future__ import absolute_import
@ -1280,10 +1286,34 @@ class Plugin(object):
pass
class RestartNeedingPlugin(Plugin):
pass
"""
Mixin for plugin types that need a restart in order to be enabled.
"""
class SortablePlugin(Plugin):
"""
Mixin for plugin types that are sortable.
"""
def get_sorting_key(self, context=None):
"""
Returns the sorting key to use for the implementation in the specified ``context``.
May return ``None`` if order is irrelevant.
Implementations returning None will be ordered by plugin identifier
after all implementations which did return a sorting key value that was
not None sorted by that.
Arguments:
context (str): The sorting context for which to provide the
sorting key value.
Returns:
int or None: An integer signifying the sorting key value of the plugin
(sorting will be done ascending), or None if the implementation
doesn't care about calling order.
"""
return None
class PluginNeedsRestart(Exception):

View file

@ -9,6 +9,9 @@ Please note that the plugin implementation types are documented in the section
.. autoclass:: OctoPrintPlugin
:show-inheritance:
.. autoclass:: ReloadNeedingPlugin
:show-inheritance:
"""
from __future__ import absolute_import
@ -97,12 +100,18 @@ class OctoPrintPlugin(Plugin):
class ReloadNeedingPlugin(Plugin):
pass
"""
Mixin for plugin types that need a reload of the UI in order to become usable.
"""
class StartupPlugin(OctoPrintPlugin, SortablePlugin):
"""
The ``StartupPlugin`` allows hooking into the startup of OctoPrint. It can be used to start up additional services
on or just after the startup of the server.
``StartupPlugin`` is a :class:`~octoprint.plugin.core.SortablePlugin`. The
relevant sorting context for :meth:`on_startup` is ``StartupPlugin.on_startup``,
the one for :meth:`on_after_startup` will be ``StartupPlugin.on_after_startup``.
"""
def on_startup(self, host, port):
@ -132,6 +141,9 @@ class ShutdownPlugin(OctoPrintPlugin, SortablePlugin):
The ``ShutdownPlugin`` allows hooking into the shutdown of OctoPrint. It's usually used in conjunction with the
:class:`StartupPlugin` mixin, to cleanly shut down additional services again that where started by the :class:`StartupPlugin`
part of the plugin.
``ShutdownPlugin`` is a :class:`~octoprint.plugin.core.SortablePlugin`.
The relevant sorting context will be ``ShutdownPlugin.on_shutdown``.
"""
def on_shutdown(self):
@ -149,6 +161,8 @@ class AssetPlugin(OctoPrintPlugin, RestartNeedingPlugin):
A typical usage of the ``AssetPlugin`` functionality is to embed a custom view model to be used by templates injected
through a :class:`TemplatePlugin`.
``AssetPlugin`` is a :class:`~octoprint.plugins.core.RestartNeedingPlugin`.
"""
def get_asset_folder(self):
@ -297,6 +311,8 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin):
responsibility to ensure that all core functionality is still maintained.
Plugins can also add additional template types by implementing the :ref:`octoprint.ui.web.templatetypes <sec-plugins-hook-ui-web-templatetypes>` hook.
``TemplatePlugin`` is a :class:`~octoprint.plugin.core.ReloadNeedingPlugin`.
"""
def get_template_configs(self):
@ -485,6 +501,167 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin):
return os.path.join(self._basefolder, "templates")
class UiPlugin(OctoPrintPlugin, SortablePlugin):
"""
The ``UiPlugin`` mixin allows plugins to completely replace the UI served
by OctoPrint when requesting the main page hosted at `/`.
OctoPrint will query whether your mixin implementation will handle a
provided request by calling :meth:`~octoprint.plugin.UiPlugin.will_handle_ui` with the Flask
`Request <http://flask.pocoo.org/docs/0.10/api/#flask.Request>`_ object as
parameter. If you plugin returns `True` here, OctoPrint will next call
:meth:`~octoprint.plugin.UiPlugin.on_ui_render` with a couple of parameters like
- again - the Flask Request object and the render keyword arguments as
used by the default OctoPrint web interface. For more information see below.
There are two methods used in order to allow for caching of the actual
response sent to the client. Whatever a plugin implementation returns
from the call to its :meth:`~octoprint.plugin.UiPlugin.on_ui_render` method
will be cached server side. The cache will be emptied in case of explicit
no-cache headers sent by the client, or if the ``_refresh`` query parameter
on the request exists and is set to ``true``. To prevent caching of the
response altogether, a plugin may set no-cache headers on the returned
response as well.
``UiPlugin`` is a :class:`~octoprint.plugin.core.SortablePlugin`. The
relevant sorting context when acting as a UiPlugin is ``UiPlugin.will_handle_ui``.
The first plugin to return ``True`` will be the one whose ui will be used,
no further calls to :meth:`~octoprint.plugin.UiPlugin.on_ui_render` will be performed.
If implementations want to serve custom templates in the :meth:`~octoprint.plugin.UiPlugin.on_ui_render`
method it is recommended to also implement the :class:`~octoprint.plugin.TemplatePlugin`
mixin.
**Example**
What follows is a very simple example that renders a different (non functional and
only exemplary) UI if the requesting client has a UserAgent string hinting
at it being a mobile device:
.. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/dummy_mobile_ui/__init__.py
:linenos:
:tab-width: 4
:caption: `dummy_mobile_ui/__init__.py <https://github.com/OctoPrint/Plugin-Examples/blob/master/dummy_mobile_ui/__init__.py>`_
.. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/dummy_mobile_ui/templates/dummy_mobile_ui_index.jinja2
:linenos:
:tab-width: 4
:caption: `dummy_mobile_ui/templates/dummy_mobile_ui_index.jinja2 <https://github.com/OctoPrint/Plugin-Examples/blob/master/dummy_mobile_ui/templates/dummy_mobile_ui_index.jinja2>`_
Try installing the above plugin ``dummy_mobile_ui`` (also available in the
`plugin examples repository <https://github.com/OctoPrint/Plugin-Examples/blob/master/dummy_mobile_ui>`_)
into your OctoPrint instance. If you access it from a regular desktop browser,
you should still see the default UI. However if you access it from a mobile
device (make sure to not have that request the desktop version of pages!)
you should see the very simple dummy page defined above.
"""
def will_handle_ui(self, request):
"""
Called by OctoPrint to determine if the mixin implementation will be
able to handle the ``request`` provided as a parameter.
Return ``True`` here to signal that your implementation will handle
the request and that the result of its :meth:`~octoprint.plugin.UiPlugin.on_ui_render` method
is what should be served to the user.
Arguments:
request (flask.Request): A Flask `Request <http://flask.pocoo.org/docs/0.10/api/#flask.Request>`_
object.
Returns:
bool: ``True`` if the the implementation will serve the request,
``False`` otherwise.
"""
return False
def on_ui_render(self, now, request, render_kwargs):
"""
Called by OctoPrint to retrieve the response to send to the client
for the ``request`` to ``/``. Only called if :meth:`~octoprint.plugin.UiPlugin.will_handle_ui`
returned ``True``.
``render_kwargs`` will be a dictionary (whose contents are cached) which
will contain the following key and value pairs (note that not all
key value pairs contained in the dictionary are listed here, only
those you should depend on as a plugin developer at the current time):
.. list-table::
:widths: 5 95
* - debug
- ``True`` if debug mode is enabled, ``False`` otherwise.
* - firstRun
- ``True`` if the server is being run for the first time (not
configured yet), ``False`` otherwise.
* - version
- OctoPrint's version information. This is a ``dict`` with the
following keys:
.. list-table::
:widths: 5 95
* - number
- The version number (e.g. ``x.y.z``)
* - branch
- The GIT branch from which the OctoPrint instance was built
(e.g. ``master``)
* - display
- The full human readable version string, including the
branch information (e.g. ``x.y.z (master branch)``
* - uiApiKey
- The UI API key to use for unauthorized API requests. This is
freshly generated on every server restart.
* - templates
- Template data to render in the UI. Will be a ``dict`` containing entries
for all known template types.
The sub structure for each key will be as follows:
.. list-table::
:widths: 5 95
* - order
- A list of template names in the order they should appear
in the final rendered page
* - entries
- The template entry definitions to render. Depending on the
template type those are either 2-tuples of a name and a ``dict``
or directly ``dicts`` with information regarding the
template to render.
For the possible contents of the data ``dicts`` see the
:class:`~octoprint.plugin.TemplatePlugin` mixin.
* - pluginNames
- A list of names of :class:`~octoprint.plugin.TemplatePlugin`
implementation that were enabled when creating the ``templates``
value.
* - locales
- The locales for which there are translations available.
On top of that all additional template variables as provided by :meth:`~octoprint.plugin.TemplatePlugin.get_template_vars`
will be contained in the dictionary as well.
Arguments:
now (datetime.datetime): The datetime instance representing "now"
for this request, in case your plugin implementation needs this
information.
request (flask.Request): A Flask `Request <http://flask.pocoo.org/docs/0.10/api/#flask.Request>`_ object.
render_kwargs (dict): The (cached) render keyword arguments that
would usually be provided to the core UI render function.
Returns:
flask.Response: Should return a Flask `Response <http://flask.pocoo.org/docs/0.10/api/#flask.Response>`_
object that can be served to the requesting client directly. May be
created with ``flask.make_response`` combined with something like
``flask.render_template``.
"""
return None
class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin):
"""
The ``WizardPlugin`` mixin allows plugins to report to OctoPrint whether
@ -546,6 +723,8 @@ class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin):
def get_wizard_version(self):
return 1
``WizardPlugin`` is a :class:`~octoprint.plugin.core.ReloadNeedingPlugin`.
"""
def is_wizard_required(self):
@ -850,6 +1029,8 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin):
flask.url_for("plugin.myblueprintplugin.myEcho") # will return "/plugin/myblueprintplugin/echo"
``BlueprintPlugin`` implements :class:`~octoprint.plugins.core.RestartNeedingPlugin`.
"""
@staticmethod

View file

@ -283,11 +283,11 @@ def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=No
if not callable(refreshif) or not refreshif():
rv = _cache.get(cache_key)
if rv is not None:
logger.debug("Serving entry for {path} from cache".format(path=flask.request.path))
logger.debug("Serving entry for {path} from cache (key: {key})".format(path=flask.request.path, key=cache_key))
return rv
# get value from wrapped function
logger.debug("No cache entry or refreshing cache for {path}, calling wrapped function".format(path=flask.request.path))
logger.debug("No cache entry or refreshing cache for {path} (key: {key}), calling wrapped function".format(path=flask.request.path, key=cache_key))
rv = f(*args, **kwargs)
# do not store if the "unless_response" condition is true

View file

@ -22,21 +22,95 @@ from . import util
import logging
_logger = logging.getLogger(__name__)
@app.route("/")
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values,
key=lambda: "view/%s/%s" % (request.path, g.locale),
unless_response=util.flask.cache_check_response_headers)
def index():
_templates = None
_plugin_names = None
_plugin_vars = None
@app.route("/")
def index():
force_refresh = util.flask.cache_check_headers() or "_refresh" in request.values
global _templates, _plugin_names, _plugin_vars
if force_refresh or _templates is None or _plugin_names is None or _plugin_vars is None:
_templates, _plugin_names, _plugin_vars = _process_templates()
now = datetime.datetime.utcnow()
render_kwargs = _get_render_kwargs(_templates, _plugin_names, _plugin_vars, now)
def get_cached_view(key, view):
return util.flask.cached(refreshif=lambda: force_refresh,
key=lambda: "ui:{}:{}".format(key, g.locale),
unless_response=util.flask.cache_check_response_headers)(view)
ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render")
for plugin in ui_plugins:
if plugin.will_handle_ui(request):
# plugin claims responsibility, let it render the UI
cached = get_cached_view(plugin._identifier, plugin.on_ui_render)
response = cached(now, request, render_kwargs)
if response is not None:
break
else:
wizard = bool(_templates["wizard"]["order"])
enable_accesscontrol = userManager is not None
render_kwargs.update(dict(
webcamStream=settings().get(["webcam", "stream"]),
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
enableAccessControl=enable_accesscontrol,
enableSdSupport=settings().get(["feature", "sdSupport"]),
gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
wizard=wizard,
now=now,
))
# no plugin took an interest, we'll use the default UI
def make_default_ui():
r = make_response(render_template("index.jinja2", **render_kwargs))
if bool(render_kwargs["templates"]["wizard"]["order"]):
r = util.flask.add_non_caching_response_headers(r)
return r
cached = get_cached_view("_default", make_default_ui)
response = cached()
response.headers["Last-Modified"] = now
return response
def _get_render_kwargs(templates, plugin_names, plugin_vars, now):
#~~ a bunch of settings
first_run = settings().getBoolean(["server", "firstRun"])
locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)
#~~ prepare full set of template vars for rendering
render_kwargs = dict(
debug=debug,
firstRun=first_run,
version=dict(number=VERSION, display=DISPLAY_VERSION, branch=BRANCH),
uiApiKey=UI_API_KEY,
templates=templates,
pluginNames=plugin_names,
locales=locales,
)
render_kwargs.update(plugin_vars)
return render_kwargs
def _process_templates():
enable_accesscontrol = userManager is not None
first_run = settings().getBoolean(["server", "firstRun"])
enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"])
enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"]))
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0
enable_accesscontrol = userManager is not None
preferred_stylesheet = settings().get(["devel", "stylesheet"])
locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)
##~~ prepare templates
@ -327,43 +401,7 @@ def index():
templates[t]["entries"].update(template_sorting[t]["custom_insert_entries"](sorted_missing))
templates[t]["order"] = template_sorting[t]["custom_insert_order"](templates[t]["order"], sorted_missing)
#~~ prepare full set of template vars for rendering
wizard = bool(templates["wizard"]["order"])
now = datetime.datetime.utcnow()
render_kwargs = dict(
webcamStream=settings().get(["webcam", "stream"]),
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
enableAccessControl=enable_accesscontrol,
enableSdSupport=settings().get(["feature", "sdSupport"]),
firstRun=first_run,
debug=debug,
version=VERSION,
display_version=DISPLAY_VERSION,
branch=BRANCH,
gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
uiApiKey=UI_API_KEY,
templates=templates,
pluginNames=plugin_names,
locales=locales,
wizard=wizard,
now=now
)
render_kwargs.update(plugin_vars)
#~~ render!
response = make_response(render_template(
"index.jinja2",
**render_kwargs
))
response.headers["Last-Modified"] = now
if wizard:
response = util.flask.add_non_caching_response_headers(response)
return response
return templates, plugin_names, plugin_vars
def _process_template_configs(name, implementation, configs, rules):
@ -454,7 +492,8 @@ def robotsTxt():
@app.route("/i18n/<string:locale>/<string:domain>.js")
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale))
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values,
key=lambda: "{}:{}".format(request.path, g.locale))
def localeJs(locale, domain):
messages = dict()
plural_expr = None

View file

@ -25,9 +25,9 @@
var SOCKJS_CLOSE_NORMAL = 1000;
var UI_API_KEY = "{{ uiApiKey }}";
var VERSION = "{{ version }}";
var DISPLAY_VERSION = "{{ display_version }}";
var BRANCH = "{{ branch }}";
var VERSION = "{{ version.number }}";
var DISPLAY_VERSION = "{{ version.display }}";
var BRANCH = "{{ version.branch }}";
var LOCALE = "{{ g.locale }}";
var AVAILABLE_LOCALES = {{ locales|tojson }};