From d1612e426a897bb4a6d3c200f692d56a00d0659d Mon Sep 17 00:00:00 2001 From: Nicanor Romero Venier Date: Thu, 3 Sep 2015 17:07:33 +0200 Subject: [PATCH 01/23] Added hook for recieved lines --- src/octoprint/util/comm.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 44eae0ba..21f0b7ff 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -269,6 +269,7 @@ class MachineCom(object): sending=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sending"), sent=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sent") ) + self._recieved_message_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.recieved") self._printer_action_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.action") self._gcodescript_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.scripts") @@ -1399,9 +1400,6 @@ class MachineCom(object): self._errorValue = get_exception_string() self.close(is_error=True) return None - if ret == '': - #self._log("Recv: TIMEOUT") - return '' try: self._log("Recv: %s" % sanitize_ascii(ret)) @@ -1409,6 +1407,14 @@ class MachineCom(object): self._log("WARN: While reading last line: %s" % e) self._log("Recv: %r" % ret) + for name, hook in self._recieved_message_hooks.items(): + try: + ret = hook(self, ret) + except: + self._logger.exception("Error while processing hook {name}:".format(**locals())) + if ret is None: + return "" + return ret def _getNext(self): From ced18c86e0ecc2a33afa69722611220c4db80bda Mon Sep 17 00:00:00 2001 From: Nicanor Romero Venier Date: Thu, 3 Sep 2015 18:43:07 +0200 Subject: [PATCH 02/23] Added docs for the gcode.recieved hook --- docs/plugins/hooks.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index 36a6e2c3..c208babe 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -159,6 +159,30 @@ This describes actually four hooks: :param str gcode: Parsed GCODE command, e.g. ``G0`` or ``M110``, may also be None if no known command could be parsed :return: None, 1-tuple, 2-tuple or string, see the description above for details. +.. _sec-plugins-hook-comm-protocol-gcode-recieved: + +octoprint.comm.protocol.gcode.recieved +------------------------------- + +.. py:function:: hook(comm_instance, line, *args, **kwargs) + + Get the returned lines sent by the printer. Handlers should return the recieved line or in any case, the modified + version of it. + + **Example:** + + Looks for the response of a M115, which contains information about the MACHINE_TYPE, among other things. + + .. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/read_m115_response.py + :linenos: + :tab-width: 4 + :caption: `read_m115_response.py `_ + + :param MachineCom comm_instance: The :class:`~octoprint.util.comm.MachineCom` instance which triggered the hook. + :param str line: The line recieved from the printer. + :return: The recieved line or in any case, a modified version of it. + :rtype: str + .. _sec-plugins-hook-comm-protocol-scripts: octoprint.comm.protocol.scripts From fa95507177e5054725d0c20b0ff4ddd6f5103322 Mon Sep 17 00:00:00 2001 From: Nicanor Romero Venier Date: Mon, 7 Sep 2015 10:04:06 +0200 Subject: [PATCH 03/23] Included an early return if ret is None Also corrected typos, received instead of recieved. --- docs/plugins/hooks.rst | 12 ++++++------ src/octoprint/util/comm.py | 9 +++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index c208babe..303a5e12 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -159,14 +159,14 @@ This describes actually four hooks: :param str gcode: Parsed GCODE command, e.g. ``G0`` or ``M110``, may also be None if no known command could be parsed :return: None, 1-tuple, 2-tuple or string, see the description above for details. -.. _sec-plugins-hook-comm-protocol-gcode-recieved: +.. _sec-plugins-hook-comm-protocol-gcode-received: -octoprint.comm.protocol.gcode.recieved -------------------------------- +octoprint.comm.protocol.gcode.received +-------------------------------------- .. py:function:: hook(comm_instance, line, *args, **kwargs) - Get the returned lines sent by the printer. Handlers should return the recieved line or in any case, the modified + Get the returned lines sent by the printer. Handlers should return the received line or in any case, the modified version of it. **Example:** @@ -179,8 +179,8 @@ octoprint.comm.protocol.gcode.recieved :caption: `read_m115_response.py `_ :param MachineCom comm_instance: The :class:`~octoprint.util.comm.MachineCom` instance which triggered the hook. - :param str line: The line recieved from the printer. - :return: The recieved line or in any case, a modified version of it. + :param str line: The line received from the printer. + :return: The received line or in any case, a modified version of it. :rtype: str .. _sec-plugins-hook-comm-protocol-scripts: diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 21f0b7ff..645be0d8 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -269,7 +269,7 @@ class MachineCom(object): sending=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sending"), sent=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sent") ) - self._recieved_message_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.recieved") + self._received_message_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.received") self._printer_action_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.action") self._gcodescript_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.scripts") @@ -1407,13 +1407,14 @@ class MachineCom(object): self._log("WARN: While reading last line: %s" % e) self._log("Recv: %r" % ret) - for name, hook in self._recieved_message_hooks.items(): + for name, hook in self._received_message_hooks.items(): try: ret = hook(self, ret) except: self._logger.exception("Error while processing hook {name}:".format(**locals())) - if ret is None: - return "" + else: + if ret is None: + return "" return ret From 74b0056095c7a4d0bfa9409f537a9458514dc3cf Mon Sep 17 00:00:00 2001 From: Nicanor Romero Venier Date: Mon, 7 Sep 2015 10:52:03 +0200 Subject: [PATCH 04/23] Added info to the hook's docs --- docs/plugins/hooks.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index 303a5e12..add3d78b 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -167,7 +167,9 @@ octoprint.comm.protocol.gcode.received .. py:function:: hook(comm_instance, line, *args, **kwargs) Get the returned lines sent by the printer. Handlers should return the received line or in any case, the modified - version of it. + version of it. If the the handler returns None, processing will be aborted and the communication layer will get an + empty string as the received line. Note that Python functions will also automatically return ``None`` if an empty + ``return`` statement is used or just nothing is returned explicitely from the handler. **Example:** From 38dabfc1c0e9057c22827c09abc2e5659da567ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 7 Sep 2015 11:34:09 +0200 Subject: [PATCH 05/23] Only try to convert sorting_value to int if it's not None --- src/octoprint/plugin/core.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 028ba436..ce202600 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -1077,11 +1077,12 @@ class PluginManager(object): except: self.logger.exception("Error while trying to retrieve sorting order for plugin {}".format(impl[0])) - try: - int(sorting_value) - except ValueError: - self.logger.warn("The order value returned by {} for sorting context {} is not a valid integer, ignoring it".format(impl[0], sorting_context)) - sorting_value = None + if sorting_value is not None: + try: + int(sorting_value) + except ValueError: + self.logger.warn("The order value returned by {} for sorting context {} is not a valid integer, ignoring it".format(impl[0], sorting_context)) + sorting_value = None return sorting_value is None, sorting_value, impl[0] From c1880a7006083e73c05b8f1499de7af941b11404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 7 Sep 2015 13:57:10 +0200 Subject: [PATCH 06/23] New UiPlugin for serving custom UIs on / depending on request --- src/octoprint/plugin/__init__.py | 3 +- src/octoprint/plugin/types.py | 9 ++ src/octoprint/server/util/flask.py | 4 +- src/octoprint/server/views.py | 127 +++++++++++++++++++---------- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 1df93a9b..686c7836 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -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: diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 9fdbe024..9af9c617 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -485,6 +485,15 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): return os.path.join(self._basefolder, "templates") +class UiPlugin(OctoPrintPlugin, SortablePlugin): + + def will_handle_ui(self, request): + return False + + def on_ui_render(self, now, request, render_kwargs): + return None + + class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin): """ The ``WizardPlugin`` mixin allows plugins to report to OctoPrint whether diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index e14ab1ba..5c6c1199 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -281,11 +281,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 diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index be79b626..942c3bfb 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -22,21 +22,93 @@ 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: + # 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(response) + 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 + enable_accesscontrol = userManager is not None + 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 + + wizard = bool(templates["wizard"]["order"]) + 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) + + 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 +399,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 +490,8 @@ def robotsTxt(): @app.route("/i18n//.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 From 42d765400991cfcc727f6f44f784ce3f37c6450f Mon Sep 17 00:00:00 2001 From: Mark Walker Date: Mon, 7 Sep 2015 11:51:54 -0700 Subject: [PATCH 07/23] Fix issue #1047 rotate 90 degrees on iOS Safari The iOS version of webkit still requires the browser prefixes for transform, see http://stackoverflow.com/a/27304061/4414220 --- src/octoprint/static/less/octoprint.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/octoprint/static/less/octoprint.less b/src/octoprint/static/less/octoprint.less index ab276cb5..28ec2a82 100644 --- a/src/octoprint/static/less/octoprint.less +++ b/src/octoprint/static/less/octoprint.less @@ -914,6 +914,7 @@ textarea.block { } .rotate90 { + -webkit-transform: rotate(-90deg); transform: rotate(-90deg); } From d3abeed3a9ecdc538f11fc8c8b12909fecf5e21f Mon Sep 17 00:00:00 2001 From: Mark Walker Date: Mon, 7 Sep 2015 17:19:16 -0700 Subject: [PATCH 08/23] Update changelog for 42d754 fix for #1047 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b19a999..1cf331cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ * It's not possible anymore to select files that are not machinecode files (e.g. GCODE) for printing on the file API. * Changes to a user's personal settings via the UI now propagate across sessions. +* [#1047](https://github.com/foosel/OctoPrint/issues/1047) - Fixed 90 degree + webcam rotation for iOS Safari. ## 1.2.6 (2015-09-02) From b7c9949caf871df5394b7a2464b3e509fb8e5e31 Mon Sep 17 00:00:00 2001 From: Mark Bastiaans Date: Tue, 8 Sep 2015 11:35:53 +0200 Subject: [PATCH 09/23] Fixed checks with os.path.realpath() for systems with symlinked paths --- src/octoprint/settings.py | 2 +- src/octoprint/slicing/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index d3cc45c8..bde8b1cd 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -1072,7 +1072,7 @@ class Settings(object): def saveScript(self, script_type, name, script): script_folder = self.getBaseFolder("scripts") filename = os.path.realpath(os.path.join(script_folder, script_type, name)) - if not filename.startswith(script_folder): + if not filename.startswith(os.path.realpath(script_folder)): # oops, jail break, that shouldn't happen raise ValueError("Invalid script path to save to: {filename} (from {script_type}:{name})".format(**locals())) diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index 64a34ba1..7812fec5 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -585,7 +585,7 @@ class SlicingManager(object): name = self._sanitize(name) path = os.path.join(self.get_slicer_profile_path(slicer), "{name}.profile".format(name=name)) - if not os.path.realpath(path).startswith(self._profile_path): + if not os.path.realpath(path).startswith(os.path.realpath(self._profile_path)): raise IOError("Path to profile {name} tried to break out of allows sub path".format(**locals())) if must_exist and not (os.path.exists(path) and os.path.isfile(path)): raise UnknownProfile(slicer, name) From 406fe75bc483d943c34366636ed6d0c0b1ebb12f Mon Sep 17 00:00:00 2001 From: Mark Bastiaans Date: Tue, 8 Sep 2015 13:14:43 +0200 Subject: [PATCH 10/23] Added name to authors --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 0309fdf5..62af6002 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -53,6 +53,7 @@ date of first contribution): * [Andrew Erickson](https://github.com/aerickson) * [Nicanor Romero Venier](https://github.com/nicanor-romero) * [Thomas Hou](https://github.com/masterhou) + * [Mark Bastiaans](https://github.com/markbastiaans) OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by [Daid Braam](https://github.com/daid). Parts of its communication layer and From 8aeac51124b4e69cba904cf6cbb61754855b34d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 9 Sep 2015 16:13:10 +0200 Subject: [PATCH 11/23] Fixed an issue that cause user sessions to not be properly associated Sessions could get duplicated, wrongly saved etc. The reason was not persisting the actual user object to the internal session map (but the LocalProxy instead). That could lead to multiple sessions being created for one login, or the session user being set to an anonymous user, or various other odd effects depending on timing. --- src/octoprint/server/util/flask.py | 6 ++++-- src/octoprint/users.py | 24 +++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index e14ab1ba..4dfa3200 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -227,8 +227,10 @@ def passive_login(): user = flask.ext.login.current_user if user is not None and not user.is_anonymous(): - flask.g.user = user flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id())) + if hasattr(user, "get_session"): + flask.session["usersession.id"] = user.get_session() + flask.g.user = user return flask.jsonify(user.asDict()) elif settings().getBoolean(["accessControl", "autologinLocal"]) \ and settings().get(["accessControl", "autologinAs"]) is not None \ @@ -252,7 +254,7 @@ def passive_login(): logger = logging.getLogger(__name__) logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks)) - return ("", 204) + return "", 204 #~~ cache decorator for cacheable views diff --git a/src/octoprint/users.py b/src/octoprint/users.py index 6d844612..8e48f5b0 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -28,13 +28,18 @@ class UserManager(object): def login_user(self, user): self._cleanup_sessions() - if user is None \ - or (isinstance(user, LocalProxy) and not isinstance(user._get_current_object(), User)) \ - or (not isinstance(user, LocalProxy) and not isinstance(user, User)): + if user is None: + return + + if isinstance(user, LocalProxy): + user = user._get_current_object() + + if not isinstance(user, User): return None if not isinstance(user, SessionUser): user = SessionUser(user) + self._session_users_by_session[user.get_session()] = user if not user.get_name() in self._session_users_by_username: @@ -49,6 +54,9 @@ class UserManager(object): if user is None: return + if isinstance(user, LocalProxy): + user = user._get_current_object() + if not isinstance(user, SessionUser): return @@ -146,12 +154,10 @@ class UserManager(object): del self._session_users_by_username[username] def findUser(self, username=None, session=None): - if session is not None: - for session in self._session_users_by_session: - user = self._session_users_by_session[session] - if username is None or username == user.get_id(): - return user - break + if session is not None and session in self._session_users_by_session: + user = self._session_users_by_session[session] + if username is None or username == user.get_id(): + return user return None From 93da761918bacd65528ef694cef441493c06b0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 10 Sep 2015 15:43:08 +0200 Subject: [PATCH 12/23] Divided Core UI specific render_kwargs and general kwargs Also refactored version kwarg into dictionary with all three version information pieces. --- src/octoprint/server/views.py | 32 ++++++++++++----------- src/octoprint/templates/initscript.jinja2 | 6 ++--- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index 942c3bfb..961ba445 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -53,11 +53,25 @@ def index(): 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(response) + r = util.flask.add_non_caching_response_headers(r) return r cached = get_cached_view("_default", make_default_ui) @@ -71,31 +85,19 @@ def index(): def _get_render_kwargs(templates, plugin_names, plugin_vars, now): #~~ a bunch of settings - enable_accesscontrol = userManager is not None 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 - wizard = bool(templates["wizard"]["order"]) 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"]), + firstRun=first_run, + version=dict(number=VERSION, display=DISPLAY_VERSION, branch=BRANCH), uiApiKey=UI_API_KEY, templates=templates, pluginNames=plugin_names, locales=locales, - wizard=wizard, - now=now ) render_kwargs.update(plugin_vars) diff --git a/src/octoprint/templates/initscript.jinja2 b/src/octoprint/templates/initscript.jinja2 index 28bf8eb5..e239de95 100644 --- a/src/octoprint/templates/initscript.jinja2 +++ b/src/octoprint/templates/initscript.jinja2 @@ -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 }}; From 19c8b8e905a66cee1e56759a35656e54d1887d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 10 Sep 2015 15:43:30 +0200 Subject: [PATCH 13/23] Documented UiPlugin type and enhanced existing docs --- docs/plugins/mixins.rst | 20 ++++ src/octoprint/plugin/core.py | 32 ++++++- src/octoprint/plugin/types.py | 174 +++++++++++++++++++++++++++++++++- 3 files changed, 224 insertions(+), 2 deletions(-) diff --git a/docs/plugins/mixins.rst b/docs/plugins/mixins.rst index b57c012e..791d2aa3 100644 --- a/docs/plugins/mixins.rst +++ b/docs/plugins/mixins.rst @@ -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: diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index ce202600..30577575 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -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): diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 9af9c617..bbd06f54 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -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 ` hook. + + ``TemplatePlugin`` is a :class:`~octoprint.plugin.core.ReloadNeedingPlugin`. """ def get_template_configs(self): @@ -486,11 +502,163 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): 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 `_ 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 `_ + + .. 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 `_ + + Try installing the above plugin ``dummy_mobile_ui`` (also available in the + `plugin examples repository `_) + 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 `_ + 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 `_ 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 `_ + 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 @@ -555,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): @@ -859,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 From 2efc5c4fdbd2e5b760dd7e20755afd29c8ba467f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 11 Sep 2015 08:14:35 +0200 Subject: [PATCH 14/23] Ignore update definitions that are lacking the type Caused a KeyError so far, update definitions that are broken like that will now just be ignored instead. Closes #1057 --- .../plugins/softwareupdate/__init__.py | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 14f277a8..2bb0b72f 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -416,14 +416,13 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if not target in check_targets: continue - populated_check = self._populated_check(target, check) - try: + populated_check = self._populated_check(target, check) target_information, target_update_available, target_update_possible = self._get_current_version(target, populated_check, force=force) if target_information is None: target_information = dict() except exceptions.UnknownCheckType: - self._logger.warn("Unknown update check type for %s" % target) + self._logger.warn("Unknown update check type for target {}".format(target)) continue target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown")), target_information) @@ -669,6 +668,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, raise exceptions.RestartFailed() def _populated_check(self, target, check): + if not "type" in check: + raise exceptions.UnknownCheckType() + result = dict(check) if target == "octoprint": @@ -703,30 +705,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, def _send_client_message(self, message_type, data=None): self._plugin_manager.send_plugin_message(self._identifier, dict(type=message_type, data=data)) - def _populated_check(self, target, check): - result = dict(check) - - if target == "octoprint": - from flask.ext.babel import gettext - result["displayName"] = check.get("displayName", gettext("OctoPrint")) - result["displayVersion"] = check.get("displayVersion", "{octoprint_version}") - - from octoprint._version import get_versions - versions = get_versions() - if check["type"] == "github_commit": - result["current"] = versions.get("full-revisionid", versions.get("full", "unknown")) - else: - result["current"] = versions["version"] - else: - result["displayName"] = check.get("displayName", target) - result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown")) - if check["type"] in ("github_commit"): - result["current"] = check.get("current", None) - else: - result["current"] = check.get("current", check.get("displayVersion", None)) - - return result - def _get_version_checker(self, target, check): """ Retrieves the version checker to use for given target and check configuration. Will raise an UnknownCheckType From 7021b9fe89439a97240e4d6d68e9563dfdb359ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 11 Sep 2015 08:34:05 +0200 Subject: [PATCH 15/23] User user id, not user name, for all user operations --- src/octoprint/server/__init__.py | 6 ++--- src/octoprint/users.py | 40 +++++++++++++++++--------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index a64b5109..d1be80d0 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -82,7 +82,7 @@ def on_identity_loaded(sender, identity): if user is None: return - identity.provides.add(UserNeed(user.get_name())) + identity.provides.add(UserNeed(user.get_id())) if user.is_user(): identity.provides.add(RoleNeed("user")) if user.is_admin(): @@ -99,9 +99,9 @@ def load_user(id): if userManager is not None: if sessionid: - return userManager.findUser(username=id, session=sessionid) + return userManager.findUser(userid=id, session=sessionid) else: - return userManager.findUser(username=id) + return userManager.findUser(userid=id) return users.DummyUser() diff --git a/src/octoprint/users.py b/src/octoprint/users.py index 8e48f5b0..250f9bc9 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -23,7 +23,7 @@ class UserManager(object): def __init__(self): self._logger = logging.getLogger(__name__) self._session_users_by_session = dict() - self._session_users_by_username = dict() + self._session_users_by_userid = dict() def login_user(self, user): self._cleanup_sessions() @@ -42,9 +42,10 @@ class UserManager(object): self._session_users_by_session[user.get_session()] = user - if not user.get_name() in self._session_users_by_username: - self._session_users_by_username[user.get_name()] = [] - self._session_users_by_username[user.get_name()].append(user) + userid = user.get_id() + if not userid in self._session_users_by_userid: + self._session_users_by_userid[userid] = [] + self._session_users_by_userid[userid].append(user) self._logger.debug("Logged in user: %r" % user) @@ -60,11 +61,12 @@ class UserManager(object): if not isinstance(user, SessionUser): return - if user.get_name() in self._session_users_by_username: - users_by_username = self._session_users_by_username[user.get_name()] - for u in users_by_username: + userid = user.get_id() + if userid in self._session_users_by_userid: + users_by_userid = self._session_users_by_userid[userid] + for u in users_by_userid: if u.get_session() == user.get_session(): - users_by_username.remove(u) + users_by_userid.remove(u) break if user.get_session() in self._session_users_by_session: @@ -145,18 +147,18 @@ class UserManager(object): pass def removeUser(self, username): - if username in self._session_users_by_username: - users = self._session_users_by_username[username] + if username in self._session_users_by_userid: + users = self._session_users_by_userid[username] sessions = [user.get_session() for user in users if isinstance(user, SessionUser)] for session in sessions: if session in self._session_users_by_session: del self._session_users_by_session[session] - del self._session_users_by_username[username] + del self._session_users_by_userid[username] - def findUser(self, username=None, session=None): + def findUser(self, userid=None, session=None): if session is not None and session in self._session_users_by_session: user = self._session_users_by_session[session] - if username is None or username == user.get_id(): + if userid is None or userid == user.get_id(): return user return None @@ -351,16 +353,16 @@ class FilebasedUserManager(UserManager): self._dirty = True self._save() - def findUser(self, username=None, apikey=None, session=None): - user = UserManager.findUser(self, username=username, session=session) + def findUser(self, userid=None, apikey=None, session=None): + user = UserManager.findUser(self, userid=userid, session=session) if user is not None: return user - if username is not None: - if username not in self._users.keys(): + if userid is not None: + if userid not in self._users.keys(): return None - return self._users[username] + return self._users[userid] elif apikey is not None: for user in self._users.values(): @@ -419,7 +421,7 @@ class User(UserMixin): return self._passwordHash == passwordHash def get_id(self): - return self._username + return self.get_name() def get_name(self): return self._username From b6053c14f99bc9695e286916799db69ba5f47d25 Mon Sep 17 00:00:00 2001 From: Marcel Hellwig <1hellwig@informatik.uni-hamburg.de> Date: Fri, 11 Sep 2015 11:20:41 +0200 Subject: [PATCH 16/23] add errorhandler decorator to blueprintplugin This will allow you to add an errorhandler for your blueprint easily. --- src/octoprint/plugin/types.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index bbd06f54..66c8bcff 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -1055,6 +1055,24 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin): return f return decorator + @staticmethod + def errorhandler(code_or_exception): + """ + A decorator to mark errorhandlings methods in your BlueprintPlugin subclass. Works just the same as Flask's + own ``errorhandler`` decorator available on blueprints. + + See `the documentation for flask.Blueprint.errorhandler `_ + and `the documentation for flask.Flask.errorhandler `_ for more + information. + """ + from collections import defaultdict + def decorator(f): + if not hasattr(f, "_blueprint_error_handler") or f._blueprint_error_handler is None: + f._blueprint_error_handler = defaultdict(list) + f._blueprint_error_handler[f.__name__].append(code_or_exception) + return f + return decorator + def get_blueprint(self): """ Creates and returns the blueprint for your plugin. Override this if you want to define and handle your blueprint yourself. @@ -1073,6 +1091,9 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin): 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) + if hasattr(f, "_blueprint_error_handler") and member in f._blueprint_error_handler: + for code_or_exception in f._blueprint_error_handler[member]: + blueprint.errorhandler(code_or_exception)(f) return blueprint def get_blueprint_kwargs(self): From 2c0eed266ceadeb3cd9e263d2170b9e118ce726a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 11 Sep 2015 11:48:38 +0200 Subject: [PATCH 17/23] Fixed some wrong indentation that somehow made it through editorconfig --- src/octoprint/plugin/types.py | 2 +- .../plugins/virtual_printer/__init__.py | 50 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 66c8bcff..c7acb288 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -80,7 +80,7 @@ class OctoPrintPlugin(Plugin): and if not creating it before returning it. Injected by the plugin core system upon initialization of the implementation. - .. automethod:: get_plugin_data_folder + .. automethod:: get_plugin_data_folder """ def get_plugin_data_folder(self): diff --git a/src/octoprint/plugins/virtual_printer/__init__.py b/src/octoprint/plugins/virtual_printer/__init__.py index ab66badd..e915aa00 100644 --- a/src/octoprint/plugins/virtual_printer/__init__.py +++ b/src/octoprint/plugins/virtual_printer/__init__.py @@ -7,25 +7,34 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms import octoprint.plugin + class VirtualPrinterPlugin(octoprint.plugin.SettingsPlugin): + def virtual_printer_factory(self, comm_instance, port, baudrate, + read_timeout): + if not port == "VIRTUAL": + return None - def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout): - if not port == "VIRTUAL": - return None + if not self._settings.global_get_boolean( + ["devel", "virtualPrinter", "enabled"]): + return None - if not self._settings.global_get_boolean(["devel", "virtualPrinter", "enabled"]): - return None + import logging + import logging.handlers - import logging - import logging.handlers + seriallog_handler = logging.handlers.RotatingFileHandler( + self._settings.get_plugin_logfile_path(postfix="serial"), + maxBytes=2 * 1024 * 1024) + seriallog_handler.setFormatter( + logging.Formatter("%(asctime)s %(message)s")) + seriallog_handler.setLevel(logging.DEBUG) - seriallog_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="serial"), maxBytes=2*1024*1024) - seriallog_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) - seriallog_handler.setLevel(logging.DEBUG) + from . import virtual + + serial_obj = virtual.VirtualPrinter( + seriallog_handler=seriallog_handler, + read_timeout=float(read_timeout)) + return serial_obj - from . import virtual - serial_obj = virtual.VirtualPrinter(seriallog_handler=seriallog_handler, read_timeout=float(read_timeout)) - return serial_obj __plugin_name__ = "Virtual Printer" __plugin_author__ = "Gina Häußge, based on work by Daid Braam" @@ -33,13 +42,14 @@ __plugin_homepage__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Virtual- __plugin_license__ = "AGPLv3" __plugin_description__ = "Provides a virtual printer via a virtual serial port for development and testing purposes" + def __plugin_load__(): - plugin = VirtualPrinterPlugin() + plugin = VirtualPrinterPlugin() - global __plugin_implementation__ - __plugin_implementation__ = plugin + global __plugin_implementation__ + __plugin_implementation__ = plugin - global __plugin_hooks__ - __plugin_hooks__ = { - "octoprint.comm.transport.serial.factory": plugin.virtual_printer_factory - } + global __plugin_hooks__ + __plugin_hooks__ = { + "octoprint.comm.transport.serial.factory": plugin.virtual_printer_factory + } From ef06c511543639faa6cb37f4fc610e2d6b0480e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 11 Sep 2015 11:53:33 +0200 Subject: [PATCH 18/23] Added @punkkeks to AUTHORS.md --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 62af6002..412f9077 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -54,6 +54,7 @@ date of first contribution): * [Nicanor Romero Venier](https://github.com/nicanor-romero) * [Thomas Hou](https://github.com/masterhou) * [Mark Bastiaans](https://github.com/markbastiaans) + * [Marcel Hellwig](https://github.com/punkkeks) OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by [Daid Braam](https://github.com/daid). Parts of its communication layer and From 8af8b8f79a505b14fe8dc78a6e52878e7689d4c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 12 Sep 2015 11:09:28 +0200 Subject: [PATCH 19/23] SWU: Track check origins, ignore if from unavailable plugin There was a problem with software update checks configurations stored in config.yaml for which the providing plugin was then removed, since those check definitions then lacked their default values to be merged on whatever was stored in config.yaml, causing incomplete check configurations as a consequence over which the plugin tripped. This patch fixes that in that it tracks which check config keys are provided by plugins and only returns those as the active check configurations that belong to plugins that are still in the system. TODO: This is only half of the solution. Check configurations of plugins that are being uninstalled should be removed from the config if the user decides to remove any settings by the plugin too. We need some adjustments in the lifecycle tracking in order to make this possible however, so for now this must suffice to at least prevent any errors from occuring when incomplete configs are encountered. --- .../plugins/softwareupdate/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 2bb0b72f..33451e57 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -75,6 +75,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._refresh_configured_checks = False self._configured_checks = self._settings.get(["checks"], merged=True) update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config") + check_providers = self._settings.get(["check_providers"], merged=True) for name, hook in update_check_hooks.items(): try: hook_checks = hook() @@ -82,9 +83,23 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals())) else: for key, data in hook_checks.items(): + check_providers[key] = name if key in self._configured_checks: data = dict_merge(data, self._configured_checks[key]) self._configured_checks[key] = data + self._settings.set(["check_providers"], check_providers) + self._settings.save() + + # we only want to process checks that came from plugins for + # which the plugins are still installed and enabled + config_checks = self._settings.get(["checks"]) + plugin_and_not_enabled = lambda k: k in check_providers and \ + not check_providers[k] in self._plugin_manager.enabled_plugins + obsolete_plugin_checks = filter(plugin_and_not_enabled, + config_checks.keys()) + for key in obsolete_plugin_checks: + self._logger.debug("Check for key {} was provided by plugin {} that's no longer available, ignoring it".format(key, check_providers[key])) + del self._configured_checks[key] return self._configured_checks @@ -150,6 +165,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, }, }, "pip_command": None, + "check_providers": {}, "cache_ttl": 24 * 60, } @@ -422,7 +438,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if target_information is None: target_information = dict() except exceptions.UnknownCheckType: - self._logger.warn("Unknown update check type for target {}".format(target)) + self._logger.warn("Unknown update check type for target {}: {}".format(target, check.get("type", ""))) continue target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown")), target_information) From 0253f525b0a57433fceefcd108614eb4f08c12ce Mon Sep 17 00:00:00 2001 From: Bryan Mayland Date: Sat, 12 Sep 2015 11:32:52 -0400 Subject: [PATCH 20/23] Include a time estimate for gcode which uses firmware retract, if retract settings are seen in gcode --- src/octoprint/util/gcodeInterpreter.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/octoprint/util/gcodeInterpreter.py b/src/octoprint/util/gcodeInterpreter.py index b0b69b66..7ac7508f 100644 --- a/src/octoprint/util/gcodeInterpreter.py +++ b/src/octoprint/util/gcodeInterpreter.py @@ -54,6 +54,9 @@ class gcode(object): absoluteE = True scale = 1.0 posAbs = True + fwretractTime = 0 + fwretractDist = 0 + fwrecoverTime = 0 feedRateXY = min(printer_profile["axes"]["x"]["speed"], printer_profile["axes"]["y"]["speed"]) if feedRateXY == 0: # some somewhat sane default if axes speeds are insane... @@ -172,6 +175,10 @@ class gcode(object): P = getCodeFloat(line, 'P') if P is not None: totalMoveTimeMinute += P / 60.0 / 1000.0 + elif G == 10: #Firmware retract + totalMoveTimeMinute += fwretractTime + elif G == 11: #Firmware retract recover + totalMoveTimeMinute += fwrecoverTime elif G == 20: #Units are inches scale = 25.4 elif G == 21: #Units are mm @@ -214,6 +221,15 @@ class gcode(object): absoluteE = True elif M == 83: #Relative E absoluteE = False + elif M == 207 or M == 208: #Firmware retract settings + s = getCodeFloat(line, 'S') + f = getCodeFloat(line, 'F') + if s is not None and f is not None: + if M == 207: + fwretractTime = s / f + fwretractDist = s + else: + fwrecoverTime = (fwretractDist + s) / f elif T is not None: if T > settings().getInt(["gcodeAnalysis", "maxExtruders"]): From e46e7b3ee2ec4dad6346decbacd504ac814863b4 Mon Sep 17 00:00:00 2001 From: Marcel Hellwig <1hellwig@informatik.uni-hamburg.de> Date: Thu, 17 Sep 2015 18:04:14 +0200 Subject: [PATCH 21/23] changed magic line from python->python2 Since python is sometimes (and for me on arch linux) a symlink to the python3 utils, you should be more explicit with that. --- run | 2 +- setup.py | 2 +- src/octoprint/__init__.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/run b/run index 0c51da39..1145a24f 100755 --- a/run +++ b/run @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 import os import sys diff --git a/setup.py b/setup.py index fc2a74d6..5130635b 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # coding=utf-8 from setuptools import setup, find_packages diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py index 67baa4f7..883acc5d 100644 --- a/src/octoprint/__init__.py +++ b/src/octoprint/__init__.py @@ -1,4 +1,5 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 + import sys from octoprint.daemon import Daemon from octoprint.server import Server From 119f3ba45bf59e4d7304098731d546b2ea3ce44d Mon Sep 17 00:00:00 2001 From: Andres Date: Sun, 20 Sep 2015 17:44:49 -0300 Subject: [PATCH 22/23] Fixed wrong urls --- docs/api/job.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api/job.rst b/docs/api/job.rst index 3bcc1fde..eeb3b071 100644 --- a/docs/api/job.rst +++ b/docs/api/job.rst @@ -37,7 +37,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -54,7 +54,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -71,7 +71,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -88,7 +88,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... From d2e2cb814f3901a50803dd3adc6bc53e2a6f793b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 21 Sep 2015 08:41:28 +0200 Subject: [PATCH 23/23] M400 and proper M114 in virtual printer --- src/octoprint/plugins/virtual_printer/virtual.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/octoprint/plugins/virtual_printer/virtual.py b/src/octoprint/plugins/virtual_printer/virtual.py index a6d5399e..e9becc5b 100644 --- a/src/octoprint/plugins/virtual_printer/virtual.py +++ b/src/octoprint/plugins/virtual_printer/virtual.py @@ -235,7 +235,7 @@ class VirtualPrinter(object): self._deleteSdFile(filename) elif "M114" in data: # send dummy position report - output = "C: X:10.00 Y:3.20 Z:5.20 E:1.24" + output = "C: X:{} Y:{} Z:{} E:{}".format(self._lastX, self._lastY, self._lastZ, self._lastE) if not self._okBeforeCommandOutput: output = "ok " + output self._send(output) @@ -243,6 +243,8 @@ class VirtualPrinter(object): elif "M117" in data: # we'll just use this to echo a message, to allow playing around with pause triggers self._send("echo:%s" % re.search("M117\s+(.*)", data).group(1)) + elif "M400" in data: + self.buffered.join() elif "M999" in data: # mirror Marlin behaviour self._send("Resend: 1") @@ -674,6 +676,7 @@ class VirtualPrinter(object): continue self._performMove(line) + self.buffered.task_done() def write(self, data): if self._debug_drop_connection: