Merge branch 'maintenance' into devel
This commit is contained in:
commit
30e962b447
31 changed files with 567 additions and 157 deletions
|
|
@ -77,6 +77,7 @@ date of first contribution):
|
|||
* [J-J Heinonen](https://github.com/jammi)
|
||||
* [Noah Martin](https://github.com/noahsmartin)
|
||||
* [Eyal Soha](https://github.com/eyal0)
|
||||
* [Greg Hulands](https://github.com/ghulands)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
* [appdirs](http://github.com/ActiveState/appdirs): MIT
|
||||
* [Awesome-Slugify](https://pypi.python.org/pypi/awesome-slugify): GPLv3
|
||||
* [chainmap](https://bitbucket.org/jeunice/chainmap): Python
|
||||
* [Click](http://click.pocoo.org/): BSD
|
||||
* [feedparser](https://github.com/kurtmckee/feedparser): BSD
|
||||
* [Flask](http://flask.pocoo.org/): BSD
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
* [Flask-Login](https://github.com/maxcountryman/flask-login): MIT
|
||||
* [Flask-Markdown](http://github.com/dcolish/flask-markdown): BSD
|
||||
* [Flask-Principal](http://packages.python.org/Flask-Principal/): MIT
|
||||
* [future](https://python-future.org/): MIT
|
||||
* [netaddr](https://github.com/drkjam/netaddr/): BSD
|
||||
* [netifaces](https://bitbucket.org/al45tair/netifaces): MIT
|
||||
* [pkginfo](http://pypi.python.org/pypi/pkginfo/): Python
|
||||
|
|
@ -52,7 +54,19 @@
|
|||
* [requests](http://python-requests.org/): Apache License 2.0
|
||||
* [rsa](http://stuvel.eu/rsa): Apache License 2.0
|
||||
* [sarge](http://sarge.readthedocs.org/): BSD
|
||||
* [scandir](https://github.com/benhoyt/scandir): BSD
|
||||
* [semantic_version](https://github.com/rbarrois/python-semanticversion): BSD
|
||||
* [SockJS-Tornado](http://github.com/mrjoes/sockjs-tornado/): MIT
|
||||
* [Tornado](http://www.tornadoweb.org/): Apache License 2.0
|
||||
* [watchdog](http://github.com/gorakhargosh/watchdog): Apache License 2.0
|
||||
* [websocket-client](https://github.com/liris/websocket-client): LGPLv3
|
||||
|
||||
## Development (testing, documentation generation, etc)
|
||||
|
||||
* [ddt](https://github.com/txels/ddt): MIT
|
||||
* [mock](https://github.com/testing-cabal/mock): BSD
|
||||
* [nose](http://pythonhosted.org/nose/): LGPL
|
||||
* [pypandoc](https://github.com/bebraw/pypandoc): MIT
|
||||
* [Sphinx](http://sphinx-doc.org/): BSD
|
||||
* [sphinxcontrib-httpdomain](https://bitbucket.org/birkenfeld/sphinx-contrib/src/default/httpdomain/): BSD
|
||||
* [sphinx_rtd_theme](https://github.com/snide/sphinx_rtd_theme/): BSD
|
||||
|
|
|
|||
|
|
@ -268,9 +268,14 @@ Slicer
|
|||
- ``string``
|
||||
- Identifier of the slicer
|
||||
* - ``displayName``
|
||||
- 0..1
|
||||
- 1
|
||||
- ``string``
|
||||
- Display name of the slicer
|
||||
* - ``sameDevice``
|
||||
- 1
|
||||
- ``boolean``
|
||||
- Whether the slicer runs on the same device as OctoPrint (``true``) and hence can't be used while printing,
|
||||
or not (``false``)
|
||||
* - ``default``
|
||||
- 1
|
||||
- ``boolean``
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ class FatalStartupError(BaseException):
|
|||
def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
|
||||
logging_config=None, debug=False, verbosity=0, uncaught_logger=None,
|
||||
uncaught_handler=None, safe_mode=False, after_preinit_logging=None,
|
||||
after_settings=None, after_logging=None, after_safe_mode=None):
|
||||
after_settings=None, after_logging=None, after_safe_mode=None,
|
||||
after_plugin_manager=None):
|
||||
kwargs = dict()
|
||||
|
||||
logger, recorder = preinit_logging(debug, verbosity, uncaught_logger, uncaught_handler)
|
||||
|
|
@ -94,6 +95,11 @@ def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
|
|||
after_safe_mode(**kwargs)
|
||||
|
||||
plugin_manager = init_pluginsystem(settings, safe_mode=safe_mode)
|
||||
kwargs["plugin_manager"] = plugin_manager
|
||||
|
||||
if callable(after_plugin_manager):
|
||||
after_plugin_manager(**kwargs)
|
||||
|
||||
return settings, logger, safe_mode, plugin_manager
|
||||
|
||||
|
||||
|
|
@ -176,7 +182,7 @@ def init_logging(settings, use_logging_file=True, logging_file=None, default_con
|
|||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
"file": {
|
||||
"class": "octoprint.logging.handlers.CleaningTimedRotatingFileHandler",
|
||||
"class": "octoprint.logging.handlers.OctoPrintLogHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "simple",
|
||||
"when": "D",
|
||||
|
|
@ -221,6 +227,7 @@ def init_logging(settings, use_logging_file=True, logging_file=None, default_con
|
|||
if verbosity > 2:
|
||||
default_config["root"]["level"] = "DEBUG"
|
||||
|
||||
config = default_config
|
||||
if use_logging_file:
|
||||
# further logging configuration from file...
|
||||
if logging_file is None:
|
||||
|
|
@ -233,9 +240,8 @@ def init_logging(settings, use_logging_file=True, logging_file=None, default_con
|
|||
config_from_file = yaml.safe_load(f)
|
||||
|
||||
# we merge that with the default config
|
||||
config = dict_merge(default_config, config_from_file)
|
||||
else:
|
||||
config = default_config
|
||||
if config_from_file is not None and isinstance(config_from_file, dict):
|
||||
config = dict_merge(default_config, config_from_file)
|
||||
|
||||
# configure logging globally
|
||||
return set_logging_config(config, debug, verbosity, uncaught_logger, uncaught_handler)
|
||||
|
|
|
|||
|
|
@ -17,20 +17,24 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
|
|||
from octoprint import init_platform, __display_version__, FatalStartupError
|
||||
|
||||
def log_startup(recorder=None, safe_mode=None, **kwargs):
|
||||
from octoprint.logging import get_divider_line
|
||||
|
||||
logger = logging.getLogger("octoprint.server")
|
||||
|
||||
logger.info(get_divider_line("*"))
|
||||
logger.info("Starting OctoPrint {}".format(__display_version__))
|
||||
if safe_mode:
|
||||
logger.info("Starting in SAFE MODE. Third party plugins will be disabled!")
|
||||
|
||||
if recorder and len(recorder):
|
||||
logger.info("--- Logged during platform initialization: ---")
|
||||
logger.info(get_divider_line("-", "Logged during platform initialization:"))
|
||||
|
||||
from octoprint.logging.handlers import CombinedLogHandler
|
||||
handler = CombinedLogHandler(*logging.getLogger().handlers)
|
||||
recorder.setTarget(handler)
|
||||
recorder.flush()
|
||||
|
||||
logger.info("----------------------------------------------")
|
||||
logger.info(get_divider_line("-"))
|
||||
|
||||
from octoprint import urllib3_ssl
|
||||
if not urllib3_ssl:
|
||||
|
|
@ -41,6 +45,30 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
|
|||
"update to a Python version >= 2.7.9 or alternatively "
|
||||
"install PyOpenSSL plus its dependencies. For details see "
|
||||
"https://urllib3.readthedocs.org/en/latest/security.html#openssl-pyopenssl")
|
||||
logger.info(get_divider_line("*"))
|
||||
|
||||
def log_register_rollover(safe_mode=None, plugin_manager=None, **kwargs):
|
||||
from octoprint.logging import get_handler, log_to_handler, get_divider_line
|
||||
from octoprint.logging.handlers import OctoPrintLogHandler
|
||||
|
||||
def rollover_callback():
|
||||
handler = get_handler("file")
|
||||
if handler is None:
|
||||
return
|
||||
|
||||
logger = logging.getLogger("octoprint.server")
|
||||
|
||||
def _log(message, level=logging.INFO):
|
||||
log_to_handler(logger, handler, level, message)
|
||||
|
||||
_log(get_divider_line("-", "Log roll over detected"))
|
||||
_log("OctoPrint {}".format(__display_version__))
|
||||
if safe_mode:
|
||||
_log("SAFE MODE is active. Third party plugins are disabled!")
|
||||
plugin_manager.log_all_plugins(only_to_handler=handler)
|
||||
_log(get_divider_line("-"))
|
||||
|
||||
OctoPrintLogHandler.registerRolloverCallback(rollover_callback)
|
||||
|
||||
try:
|
||||
settings, _, safe_mode, plugin_manager = init_platform(basedir,
|
||||
|
|
@ -50,7 +78,8 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
|
|||
verbosity=verbosity,
|
||||
uncaught_logger=__name__,
|
||||
safe_mode=safe_mode,
|
||||
after_safe_mode=log_startup)
|
||||
after_safe_mode=log_startup,
|
||||
after_plugin_manager=log_register_rollover)
|
||||
except FatalStartupError as e:
|
||||
click.echo(e.message, err=True)
|
||||
click.echo("There was a fatal error starting up OctoPrint.", err=True)
|
||||
|
|
|
|||
|
|
@ -332,24 +332,31 @@ class CommandTrigger(GenericEventListener):
|
|||
self._executeGcodeCommand(command, debug=debug)
|
||||
|
||||
def _executeSystemCommand(self, command, debug=False):
|
||||
def commandExecutioner(command):
|
||||
def commandExecutioner(cmd):
|
||||
if debug:
|
||||
self._logger.info("Executing system command: %s" % command)
|
||||
self._logger.info("Executing system command: {}".format(cmd))
|
||||
else:
|
||||
self._logger.info("Executing a system command")
|
||||
# we run this with shell=True since we have to trust whatever
|
||||
# our admin configured as command and since we want to allow
|
||||
# shell-alike handling here...
|
||||
subprocess.Popen(command, shell=True)
|
||||
subprocess.check_call(cmd, shell=True)
|
||||
|
||||
try:
|
||||
if isinstance(command, (list, tuple, set)):
|
||||
for c in command:
|
||||
commandExecutioner(c)
|
||||
else:
|
||||
commandExecutioner(command)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self._logger.warn("Command failed with return code %i: %s" % (e.returncode, str(e)))
|
||||
except:
|
||||
self._logger.exception("Command failed")
|
||||
def process():
|
||||
try:
|
||||
if isinstance(command, (list, tuple, set)):
|
||||
for c in command:
|
||||
commandExecutioner(c)
|
||||
else:
|
||||
commandExecutioner(command)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self._logger.warn("Command failed with return code %i: %s" % (e.returncode, str(e)))
|
||||
except:
|
||||
self._logger.exception("Command failed")
|
||||
|
||||
t = threading.Thread(target=process)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def _executeGcodeCommand(self, command, debug=False):
|
||||
commands = [command]
|
||||
|
|
@ -397,7 +404,7 @@ class CommandTrigger(GenericEventListener):
|
|||
|
||||
currentData = self._printer.get_current_data()
|
||||
|
||||
if "currentZ" in currentData.keys() and currentData["currentZ"] is not None:
|
||||
if "currentZ" in currentData and currentData["currentZ"] is not None:
|
||||
params["__currentZ"] = str(currentData["currentZ"])
|
||||
|
||||
if "job" in currentData and "file" in currentData["job"] and "name" in currentData["job"]["file"] \
|
||||
|
|
@ -405,9 +412,9 @@ class CommandTrigger(GenericEventListener):
|
|||
params["__filename"] = currentData["job"]["file"]["name"]
|
||||
params["__filepath"] = currentData["job"]["file"]["path"]
|
||||
params["__fileorigin"] = currentData["job"]["file"]["origin"]
|
||||
if "progress" in currentData.keys() and currentData["progress"] is not None \
|
||||
and "completion" in currentData["progress"].keys() and currentData["progress"]["completion"] is not None:
|
||||
params["__progress"] = str(round(currentData["progress"]["completion"] * 100))
|
||||
if "progress" in currentData and currentData["progress"] is not None \
|
||||
and "completion" in currentData["progress"] and currentData["progress"]["completion"] is not None:
|
||||
params["__progress"] = str(round(currentData["progress"]["completion"]))
|
||||
|
||||
# now add the payload keys as well
|
||||
if isinstance(payload, dict):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,137 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
from . import handlers
|
||||
from octoprint.logging import handlers
|
||||
|
||||
def log_to_handler(logger, handler, level, msg, exc_info=None, extra=None, *args):
|
||||
"""
|
||||
Logs to the provided handler only.
|
||||
|
||||
Arguments:
|
||||
logger: logger to log to
|
||||
handler: handler to restrict logging to
|
||||
level: level to log at
|
||||
msg: message to log
|
||||
exc_info: optional exception info
|
||||
extra: optional extra data
|
||||
*args: log args
|
||||
"""
|
||||
from logging import _srcfile
|
||||
import sys
|
||||
|
||||
# this is just the same as logging.Logger._log
|
||||
|
||||
if _srcfile:
|
||||
# IronPython doesn't track Python frames, so findCaller raises an
|
||||
# exception on some versions of IronPython. We trap it here so that
|
||||
# IronPython can use logging.
|
||||
try:
|
||||
fn, lno, func = logger.findCaller()
|
||||
except ValueError:
|
||||
fn, lno, func = "(unknown file)", 0, "(unknown function)"
|
||||
else:
|
||||
fn, lno, func = "(unknown file)", 0, "(unknown function)"
|
||||
if exc_info:
|
||||
if not isinstance(exc_info, tuple):
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
record = logger.makeRecord(logger.name, level, fn, lno, msg, args, exc_info, func, extra)
|
||||
|
||||
# and this is a mixture of logging.Logger.handle and logging.Logger.callHandlers
|
||||
|
||||
if (not logger.disabled) and logger.filter(record):
|
||||
if record.levelno >= handler.level:
|
||||
handler.handle(record)
|
||||
|
||||
|
||||
def get_handler(name, logger=None):
|
||||
"""
|
||||
Retrieves the handler named ``name``.
|
||||
|
||||
If optional ``logger`` is provided, search will be
|
||||
limited to that logger, otherwise the root logger will be
|
||||
searched.
|
||||
|
||||
Arguments:
|
||||
name: the name of the handler to look for
|
||||
logger: (optional) the logger to search in, root logger if not provided
|
||||
|
||||
Returns:
|
||||
the handler if it could be found, None otherwise
|
||||
"""
|
||||
import logging
|
||||
|
||||
if logger is None:
|
||||
logger = logging.getLogger()
|
||||
|
||||
for handler in logger.handlers:
|
||||
if handler.get_name() == name:
|
||||
return handler
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_divider_line(c, message=None, length=78, indent=3):
|
||||
"""
|
||||
Generate a divider line for logging, optionally with included message.
|
||||
|
||||
Examples:
|
||||
|
||||
>>> get_divider_line("-")
|
||||
'------------------------------------------------------------------------------'
|
||||
>>> get_divider_line("=", length=10)
|
||||
'=========='
|
||||
>>> get_divider_line("-", message="Hi", length=10)
|
||||
'--- Hi ---'
|
||||
>>> get_divider_line("-", message="A slightly longer text")
|
||||
'--- A slightly longer text ---------------------------------------------------'
|
||||
>>> get_divider_line("-", message="A slightly longer text", indent=5)
|
||||
'----- A slightly longer text -------------------------------------------------'
|
||||
>>> get_divider_line("-", message="Hello World!", length=10)
|
||||
'--- Hello World!'
|
||||
>>> get_divider_line(None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: c is not text
|
||||
>>> get_divider_line("´`")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: c is not a single character
|
||||
>>> get_divider_line("-", message=3)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: message is not text
|
||||
>>> get_divider_line("-", length="hello")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: length is not an int
|
||||
>>> get_divider_line("-", indent="hi")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: indent is not an int
|
||||
|
||||
Arguments:
|
||||
c: character to use for the line
|
||||
message: message to print in the line
|
||||
length: length of the line
|
||||
indent: indentation of message in line
|
||||
|
||||
Returns:
|
||||
formatted divider line
|
||||
"""
|
||||
|
||||
assert isinstance(c, (str, unicode, bytes)), "c is not text"
|
||||
assert len(c) == 1, "c is not a single character"
|
||||
assert isinstance(length, int), "length is not an int"
|
||||
assert isinstance(indent, int), "indent is not an int"
|
||||
|
||||
if message is None:
|
||||
return c * length
|
||||
|
||||
assert isinstance(message, (str, unicode, bytes)), "message is not text"
|
||||
|
||||
space = length - 2 * (indent + 1)
|
||||
if space >= len(message):
|
||||
return c * indent + " " + message + " " + c * (length - indent - 2 - len(message))
|
||||
else:
|
||||
return c * indent + " " + message
|
||||
|
|
|
|||
|
|
@ -17,6 +17,21 @@ class CleaningTimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler
|
|||
os.remove(s)
|
||||
|
||||
|
||||
class OctoPrintLogHandler(CleaningTimedRotatingFileHandler):
|
||||
rollover_callbacks = []
|
||||
|
||||
@classmethod
|
||||
def registerRolloverCallback(cls, callback, *args, **kwargs):
|
||||
cls.rollover_callbacks.append((callback, args, kwargs))
|
||||
|
||||
def doRollover(self):
|
||||
CleaningTimedRotatingFileHandler.doRollover(self)
|
||||
|
||||
for rcb in self.__class__.rollover_callbacks:
|
||||
callback, args, kwargs = rcb
|
||||
callback(*args, **kwargs)
|
||||
|
||||
|
||||
class SerialLogHandler(logging.handlers.RotatingFileHandler):
|
||||
|
||||
_do_rollover = False
|
||||
|
|
|
|||
|
|
@ -1062,21 +1062,30 @@ class PluginManager(object):
|
|||
return True
|
||||
|
||||
|
||||
def log_all_plugins(self, show_bundled=True, bundled_str=(" (bundled)", ""), show_location=True, location_str=" = {location}", show_enabled=True, enabled_str=(" ", "!")):
|
||||
def log_all_plugins(self, show_bundled=True, bundled_str=(" (bundled)", ""), show_location=True,
|
||||
location_str=" = {location}", show_enabled=True, enabled_str=(" ", "!"),
|
||||
only_to_handler=None):
|
||||
all_plugins = self.enabled_plugins.values() + self.disabled_plugins.values()
|
||||
|
||||
def _log(message, level=logging.INFO):
|
||||
if only_to_handler is not None:
|
||||
import octoprint.logging
|
||||
octoprint.logging.log_to_handler(self.logger, only_to_handler, level, message, [])
|
||||
else:
|
||||
self.logger.log(level, message)
|
||||
|
||||
if len(all_plugins) <= 0:
|
||||
self.logger.info("No plugins available")
|
||||
_log("No plugins available")
|
||||
else:
|
||||
self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join(
|
||||
map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
|
||||
bundled_strs=bundled_str,
|
||||
show_location=show_location,
|
||||
location_str=location_str,
|
||||
show_enabled=show_enabled,
|
||||
enabled_strs=enabled_str),
|
||||
sorted(self.plugins.values(), key=lambda x: str(x).lower()))
|
||||
)))
|
||||
formatted_plugins = "\n".join(map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
|
||||
bundled_strs=bundled_str,
|
||||
show_location=show_location,
|
||||
location_str=location_str,
|
||||
show_enabled=show_enabled,
|
||||
enabled_strs=enabled_str),
|
||||
sorted(self.plugins.values(), key=lambda x: str(x).lower())))
|
||||
_log("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins),
|
||||
plugins=formatted_plugins))
|
||||
|
||||
def get_plugin(self, identifier, require_enabled=True):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -746,6 +746,21 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin):
|
|||
"""
|
||||
return None
|
||||
|
||||
def get_ui_additional_etag(self, default_additional):
|
||||
"""
|
||||
Allows to provide a list of additional fields to use for ETag generation.
|
||||
|
||||
By default the same list will be returned that is also used in the stock UI (and injected
|
||||
via the parameter ``default_additional``).
|
||||
|
||||
Arguments:
|
||||
default_additional (list): The list of default fields added to the ETag of the default UI
|
||||
|
||||
Returns:
|
||||
(list): A list of additional fields for the ETag generation, or None
|
||||
"""
|
||||
return default_additional
|
||||
|
||||
def get_ui_custom_lastmodified(self):
|
||||
"""
|
||||
Allows to calculate the LastModified differently than using the most recent modification
|
||||
|
|
@ -806,6 +821,25 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin):
|
|||
"""
|
||||
return False
|
||||
|
||||
def get_ui_custom_template_filter(self, default_template_filter):
|
||||
"""
|
||||
Allows to specify a custom template filter to use for filtering the template contained in the
|
||||
``render_kwargs`` provided to the templating sub system.
|
||||
|
||||
Only relevant for UiPlugins that actually utilize the stock templates of OctoPrint.
|
||||
|
||||
By default simply returns the provided ``default_template_filter``.
|
||||
|
||||
Arguments:
|
||||
default_template_filter (callable): The default template filter used by the default UI
|
||||
|
||||
Returns:
|
||||
(callable) A filter function accepting the ``template_type`` and ``template_key`` of a template
|
||||
and returning ``True`` to keep it and ``False`` to filter it out. If ``None`` is returned, no
|
||||
filtering will take place.
|
||||
"""
|
||||
return default_template_filter
|
||||
|
||||
class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin):
|
||||
"""
|
||||
The ``WizardPlugin`` mixin allows plugins to report to OctoPrint whether
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from . import version_checks, updaters, exceptions, util, cli
|
|||
|
||||
from octoprint.server.util.flask import restricted_access, with_revalidation_checking, check_etag
|
||||
from octoprint.server import admin_permission, VERSION, REVISION, BRANCH
|
||||
from octoprint.util import dict_merge
|
||||
from octoprint.util import dict_merge, to_unicode
|
||||
import octoprint.settings
|
||||
|
||||
|
||||
|
|
@ -40,6 +40,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
self._configured_checks = None
|
||||
self._refresh_configured_checks = False
|
||||
|
||||
self._get_versions_mutex = threading.Lock()
|
||||
|
||||
self._version_cache = dict()
|
||||
self._version_cache_ttl = 0
|
||||
self._version_cache_path = None
|
||||
|
|
@ -70,6 +72,10 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
self._console_logger.setLevel(logging.DEBUG)
|
||||
self._console_logger.propagate = False
|
||||
|
||||
def on_after_startup(self):
|
||||
# refresh cache now if necessary so it's faster once the user connects to the instance
|
||||
self.get_current_versions()
|
||||
|
||||
def _get_configured_checks(self):
|
||||
with self._configured_checks_mutex:
|
||||
if self._refresh_configured_checks or self._configured_checks is None:
|
||||
|
|
@ -555,57 +561,70 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
update_possible = False
|
||||
information = dict()
|
||||
|
||||
for target, check in checks.items():
|
||||
if not target in check_targets:
|
||||
continue
|
||||
# we don't want to do the same work twice, so let's use a lock
|
||||
with self._get_versions_mutex:
|
||||
for target, check in checks.items():
|
||||
if not target in check_targets:
|
||||
continue
|
||||
|
||||
if not check:
|
||||
continue
|
||||
if not check:
|
||||
continue
|
||||
|
||||
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 target {}: {}".format(target, check.get("type", "<n/a>")))
|
||||
continue
|
||||
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 target {}: {}".format(target, check.get("type", "<n/a>")))
|
||||
continue
|
||||
|
||||
target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown", release_notes=None)), target_information)
|
||||
target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown", release_notes=None)), target_information)
|
||||
|
||||
update_available = update_available or target_update_available
|
||||
update_possible = update_possible or (target_update_possible and target_update_available)
|
||||
update_available = update_available or target_update_available
|
||||
update_possible = update_possible or (target_update_possible and target_update_available)
|
||||
|
||||
local_name = target_information["local"]["name"]
|
||||
local_value = target_information["local"]["value"]
|
||||
local_name = target_information["local"]["name"]
|
||||
local_value = target_information["local"]["value"]
|
||||
|
||||
release_notes = None
|
||||
if target_information and target_information["remote"] and target_information["remote"]["value"]:
|
||||
if "release_notes" in populated_check and populated_check["release_notes"]:
|
||||
release_notes = populated_check["release_notes"]
|
||||
elif "release_notes" in target_information["remote"]:
|
||||
release_notes = target_information["remote"]["release_notes"]
|
||||
release_notes = None
|
||||
if target_information and target_information["remote"] and target_information["remote"]["value"]:
|
||||
if "release_notes" in populated_check and populated_check["release_notes"]:
|
||||
release_notes = populated_check["release_notes"]
|
||||
elif "release_notes" in target_information["remote"]:
|
||||
release_notes = target_information["remote"]["release_notes"]
|
||||
|
||||
if release_notes:
|
||||
release_notes = release_notes.format(octoprint_version=VERSION,
|
||||
target_name=target_information["remote"]["name"],
|
||||
target_version=target_information["remote"]["value"])
|
||||
if release_notes:
|
||||
release_notes = release_notes.format(octoprint_version=VERSION,
|
||||
target_name=target_information["remote"]["name"],
|
||||
target_version=target_information["remote"]["value"])
|
||||
|
||||
information[target] = dict(updateAvailable=target_update_available,
|
||||
updatePossible=target_update_possible,
|
||||
information=target_information,
|
||||
displayName=populated_check["displayName"],
|
||||
displayVersion=populated_check["displayVersion"].format(octoprint_version=VERSION, local_name=local_name, local_value=local_value),
|
||||
check=populated_check,
|
||||
releaseNotes=release_notes)
|
||||
information[target] = dict(updateAvailable=target_update_available,
|
||||
updatePossible=target_update_possible,
|
||||
information=target_information,
|
||||
displayName=populated_check["displayName"],
|
||||
displayVersion=populated_check["displayVersion"].format(octoprint_version=VERSION, local_name=local_name, local_value=local_value),
|
||||
check=populated_check,
|
||||
releaseNotes=release_notes)
|
||||
|
||||
if self._version_cache_dirty:
|
||||
self._save_version_cache()
|
||||
if self._version_cache_dirty:
|
||||
self._save_version_cache()
|
||||
return information, update_available, update_possible
|
||||
|
||||
def _get_check_hash(self, check):
|
||||
def dict_to_sorted_repr(d):
|
||||
lines = []
|
||||
for key in sorted(d.keys()):
|
||||
value = d[key]
|
||||
if isinstance(value, dict):
|
||||
lines.append("{!r}: {}".format(key, dict_to_sorted_repr(value)))
|
||||
else:
|
||||
lines.append("{!r}: {!r}".format(key, value))
|
||||
|
||||
return "{" + ", ".join(lines) + "}"
|
||||
|
||||
hash = hashlib.md5()
|
||||
hash.update(repr(check))
|
||||
hash.update(dict_to_sorted_repr(check))
|
||||
return hash.hexdigest()
|
||||
|
||||
def _get_current_version(self, target, check, force=False):
|
||||
|
|
@ -846,15 +865,15 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
if target == "octoprint":
|
||||
from flask_babel import gettext
|
||||
|
||||
result["displayName"] = check.get("displayName")
|
||||
result["displayName"] = to_unicode(check.get("displayName"), errors="replace")
|
||||
if result["displayName"] is None:
|
||||
# displayName missing or set to None
|
||||
result["displayName"] = gettext("OctoPrint")
|
||||
result["displayName"] = to_unicode(gettext("OctoPrint"), errors="replace")
|
||||
|
||||
result["displayVersion"] = check.get("displayVersion")
|
||||
result["displayVersion"] = to_unicode(check.get("displayVersion"), errors="replace")
|
||||
if result["displayVersion"] is None:
|
||||
# displayVersion missing or set to None
|
||||
result["displayVersion"] = "{octoprint_version}"
|
||||
result["displayVersion"] = u"{octoprint_version}"
|
||||
|
||||
stable_branch = "master"
|
||||
release_branches = []
|
||||
|
|
@ -909,15 +928,15 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
result["release_compare"] = "python_unequal"
|
||||
|
||||
else:
|
||||
result["displayName"] = check.get("displayName")
|
||||
result["displayName"] = to_unicode(check.get("displayName"), errors="replace")
|
||||
if result["displayName"] is None:
|
||||
# displayName missing or None
|
||||
result["displayName"] = target
|
||||
result["displayName"] = to_unicode(target, errors="replace")
|
||||
|
||||
result["displayVersion"] = check.get("displayVersion", check.get("current"))
|
||||
result["displayVersion"] = to_unicode(check.get("displayVersion", check.get("current")), errors="replace")
|
||||
if result["displayVersion"] is None:
|
||||
# displayVersion AND current missing or None
|
||||
result["displayVersion"] = "unknown"
|
||||
result["displayVersion"] = u"unknown"
|
||||
|
||||
if check["type"] in ("github_commit",):
|
||||
result["current"] = check.get("current", None)
|
||||
|
|
|
|||
|
|
@ -1152,12 +1152,12 @@ class Server(object):
|
|||
if len(css_core) == 0:
|
||||
css_core_bundle = Bundle(*[])
|
||||
else:
|
||||
css_core_bundle = Bundle(*css_app, output="webassets/packed_core.css", filters="cssrewrite")
|
||||
css_core_bundle = Bundle(*css_core, output="webassets/packed_core.css", filters="cssrewrite")
|
||||
|
||||
if len(css_plugins) == 0:
|
||||
css_plugins_bundle = Bundle(*[])
|
||||
else:
|
||||
css_plugins_bundle = Bundle(*css_app, output="webassets/packed_plugins.css", filters="cssrewrite")
|
||||
css_plugins_bundle = Bundle(*css_plugins, output="webassets/packed_plugins.css", filters="cssrewrite")
|
||||
|
||||
if len(css_app) == 0:
|
||||
css_app_bundle = Bundle(*[])
|
||||
|
|
@ -1168,12 +1168,12 @@ class Server(object):
|
|||
if len(less_core) == 0:
|
||||
less_core_bundle = Bundle(*[])
|
||||
else:
|
||||
less_core_bundle = Bundle(*less_app, output="webassets/packed_core.less", filters="cssrewrite, less_importrewrite")
|
||||
less_core_bundle = Bundle(*less_core, output="webassets/packed_core.less", filters="cssrewrite, less_importrewrite")
|
||||
|
||||
if len(less_plugins) == 0:
|
||||
less_plugins_bundle = Bundle(*[])
|
||||
else:
|
||||
less_plugins_bundle = Bundle(*less_app, output="webassets/packed_plugins.less", filters="cssrewrite, less_importrewrite")
|
||||
less_plugins_bundle = Bundle(*less_plugins, output="webassets/packed_plugins.less", filters="cssrewrite, less_importrewrite")
|
||||
|
||||
if len(less_app) == 0:
|
||||
less_app_bundle = Bundle(*[])
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ def _etag(configured, lm=None):
|
|||
else:
|
||||
hash.update(repr(sorted(slicingManager.registered_slicers)))
|
||||
|
||||
hash.update("v2") # increment version if we change the API format
|
||||
|
||||
return hash.hexdigest()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,27 @@ def index():
|
|||
_templates[locale], _plugin_names, _plugin_vars = _process_templates()
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
render_kwargs = _get_render_kwargs(_templates[locale], _plugin_names, _plugin_vars, now)
|
||||
|
||||
enable_accesscontrol = userManager.enabled
|
||||
enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"])
|
||||
enable_timelapse = bool(settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"]))
|
||||
|
||||
def default_template_filter(template_type, template_key):
|
||||
if template_type == "navbar":
|
||||
return template_key != "login" or enable_accesscontrol
|
||||
elif template_type == "tab":
|
||||
return (template_key != "gcodeviewer" or enable_gcodeviewer) and \
|
||||
(template_key != "timelapse" or enable_timelapse)
|
||||
elif template_type == "settings":
|
||||
return template_key != "accesscontrol" or enable_accesscontrol
|
||||
elif template_type == "usersettings":
|
||||
return enable_accesscontrol
|
||||
else:
|
||||
return True
|
||||
|
||||
default_additional_etag = [enable_accesscontrol,
|
||||
enable_gcodeviewer,
|
||||
enable_timelapse]
|
||||
|
||||
def get_preemptively_cached_view(key, view, data=None, additional_request_data=None, additional_unless=None):
|
||||
if (data is None and additional_request_data is None) or g.locale is None:
|
||||
|
|
@ -190,7 +210,10 @@ def index():
|
|||
data=d,
|
||||
unless=unless)(view)
|
||||
|
||||
def get_cached_view(key, view, additional_key_data=None, additional_files=None, custom_files=None, custom_etag=None, custom_lastmodified=None):
|
||||
def get_cached_view(key, view, additional_key_data=None, additional_files=None, additional_etag=None, custom_files=None, custom_etag=None, custom_lastmodified=None):
|
||||
if additional_etag is None:
|
||||
additional_etag = []
|
||||
|
||||
def cache_key():
|
||||
return _cache_key(key, additional_key_data=additional_key_data)
|
||||
|
||||
|
|
@ -200,11 +223,11 @@ def index():
|
|||
lastmodified_ok = util.flask.check_lastmodified(lastmodified)
|
||||
etag_ok = util.flask.check_etag(compute_etag(files=files,
|
||||
lastmodified=lastmodified,
|
||||
additional=cache_key()))
|
||||
additional=[cache_key()] + additional_etag))
|
||||
return lastmodified_ok and etag_ok
|
||||
|
||||
def validate_cache(cached):
|
||||
etag_different = compute_etag(additional=cache_key()) != cached.get_etag()[0]
|
||||
etag_different = compute_etag(additional=[cache_key()] + additional_etag) != cached.get_etag()[0]
|
||||
return force_refresh or etag_different
|
||||
|
||||
def collect_files():
|
||||
|
|
@ -273,7 +296,7 @@ def index():
|
|||
if lastmodified:
|
||||
hash.update(lastmodified)
|
||||
for add in additional:
|
||||
hash.update(add)
|
||||
hash.update(str(add))
|
||||
return hash.hexdigest()
|
||||
|
||||
decorated_view = view
|
||||
|
|
@ -293,7 +316,8 @@ def index():
|
|||
additional_files=p.get_ui_additional_tracked_files,
|
||||
custom_files=p.get_ui_custom_tracked_files,
|
||||
custom_etag=p.get_ui_custom_etag,
|
||||
custom_lastmodified=p.get_ui_custom_lastmodified)
|
||||
custom_lastmodified=p.get_ui_custom_lastmodified,
|
||||
additional_etag=p.get_ui_additional_etag(default_additional_etag))
|
||||
|
||||
if preemptive_cache_enabled and p.get_ui_preemptive_caching_enabled():
|
||||
view = get_preemptively_cached_view(p._identifier,
|
||||
|
|
@ -304,12 +328,30 @@ def index():
|
|||
else:
|
||||
view = cached
|
||||
|
||||
template_filter = p.get_ui_custom_template_filter(default_template_filter)
|
||||
if template_filter is not None and callable(template_filter):
|
||||
filtered_templates = _filter_templates(_templates[locale], template_filter)
|
||||
else:
|
||||
filtered_templates = _templates[locale]
|
||||
|
||||
render_kwargs = _get_render_kwargs(filtered_templates,
|
||||
_plugin_names,
|
||||
_plugin_vars,
|
||||
now)
|
||||
|
||||
return view(now, request, render_kwargs)
|
||||
|
||||
def default_view():
|
||||
wizard = wizard_active(_templates[locale])
|
||||
enable_accesscontrol = userManager.enabled
|
||||
filtered_templates = _filter_templates(_templates[locale], default_template_filter)
|
||||
|
||||
wizard = wizard_active(filtered_templates)
|
||||
accesscontrol_active = enable_accesscontrol and userManager.hasBeenCustomized()
|
||||
|
||||
render_kwargs = _get_render_kwargs(filtered_templates,
|
||||
_plugin_names,
|
||||
_plugin_vars,
|
||||
now)
|
||||
|
||||
render_kwargs.update(dict(
|
||||
webcamStream=settings().get(["webcam", "stream"]),
|
||||
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
|
||||
|
|
@ -331,7 +373,8 @@ def index():
|
|||
return r
|
||||
|
||||
cached = get_cached_view("_default",
|
||||
make_default_ui)
|
||||
make_default_ui,
|
||||
additional_etag=default_additional_etag)
|
||||
preemptively_cached = get_preemptively_cached_view("_default",
|
||||
cached,
|
||||
dict(),
|
||||
|
|
@ -396,12 +439,7 @@ def _get_render_kwargs(templates, plugin_names, plugin_vars, now):
|
|||
|
||||
|
||||
def _process_templates():
|
||||
enable_accesscontrol = userManager.enabled
|
||||
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
|
||||
preferred_stylesheet = settings().get(["devel", "stylesheet"])
|
||||
|
||||
##~~ prepare templates
|
||||
|
||||
|
|
@ -473,12 +511,10 @@ def _process_templates():
|
|||
# navbar
|
||||
|
||||
templates["navbar"]["entries"] = dict(
|
||||
settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin")
|
||||
settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin"),
|
||||
systemmenu=dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False),
|
||||
login=dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False),
|
||||
)
|
||||
if enable_accesscontrol:
|
||||
templates["navbar"]["entries"]["login"] = dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False)
|
||||
if enable_systemmenu:
|
||||
templates["navbar"]["entries"]["systemmenu"] = dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False)
|
||||
|
||||
# sidebar
|
||||
|
||||
|
|
@ -493,12 +529,10 @@ def _process_templates():
|
|||
templates["tab"]["entries"] = dict(
|
||||
temperature=(gettext("Temperature"), dict(template="tabs/temperature.jinja2", _div="temp")),
|
||||
control=(gettext("Control"), dict(template="tabs/control.jinja2", _div="control")),
|
||||
gcodeviewer=(gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode")),
|
||||
terminal=(gettext("Terminal"), dict(template="tabs/terminal.jinja2", _div="term")),
|
||||
timelapse=(gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse"))
|
||||
)
|
||||
if enable_gcodeviewer:
|
||||
templates["tab"]["entries"]["gcodeviewer"] = (gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode"))
|
||||
if enable_timelapse:
|
||||
templates["tab"]["entries"]["timelapse"] = (gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse"))
|
||||
|
||||
# settings dialog
|
||||
|
||||
|
|
@ -520,21 +554,19 @@ def _process_templates():
|
|||
|
||||
section_octoprint=(gettext("OctoPrint"), None),
|
||||
|
||||
accesscontrol=(gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False)),
|
||||
folders=(gettext("Folders"), dict(template="dialogs/settings/folders.jinja2", _div="settings_folders", custom_bindings=False)),
|
||||
appearance=(gettext("Appearance"), dict(template="dialogs/settings/appearance.jinja2", _div="settings_appearance", custom_bindings=False)),
|
||||
logs=(gettext("Logs"), dict(template="dialogs/settings/logs.jinja2", _div="settings_logs")),
|
||||
server=(gettext("Server"), dict(template="dialogs/settings/server.jinja2", _div="settings_server", custom_bindings=False)),
|
||||
)
|
||||
if enable_accesscontrol:
|
||||
templates["settings"]["entries"]["accesscontrol"] = (gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False))
|
||||
|
||||
# user settings dialog
|
||||
|
||||
if enable_accesscontrol:
|
||||
templates["usersettings"]["entries"] = dict(
|
||||
access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)),
|
||||
interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)),
|
||||
)
|
||||
templates["usersettings"]["entries"] = dict(
|
||||
access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)),
|
||||
interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)),
|
||||
)
|
||||
|
||||
# wizard
|
||||
|
||||
|
|
@ -784,6 +816,19 @@ def _process_template_config(name, implementation, rule, config=None, counter=1)
|
|||
return data
|
||||
|
||||
|
||||
def _filter_templates(templates, template_filter):
|
||||
filtered_templates = dict()
|
||||
for template_type, template_collection in templates.items():
|
||||
filtered_entries = dict()
|
||||
for template_key, template_entry in template_collection["entries"].items():
|
||||
if template_filter(template_type, template_key):
|
||||
filtered_entries[template_key] = template_entry
|
||||
filtered_templates[template_type] = dict(order=filter(lambda x: x in filtered_entries,
|
||||
template_collection["order"]),
|
||||
entries=filtered_entries)
|
||||
return filtered_templates
|
||||
|
||||
|
||||
@app.route("/robots.txt")
|
||||
@util.flask.cached(timeout=-1)
|
||||
def robotsTxt():
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ default_settings = {
|
|||
"showFahrenheitAlso": False,
|
||||
"components": {
|
||||
"order": {
|
||||
"navbar": ["settings", "systemmenu", "login", "plugin_announcements"],
|
||||
"navbar": ["settings", "systemmenu", "plugin_announcements", "login"],
|
||||
"sidebar": ["connection", "state", "files"],
|
||||
"tab": ["temperature", "control", "gcodeviewer", "terminal", "timelapse"],
|
||||
"settings": [
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -158,6 +158,10 @@ $(function() {
|
|||
|
||||
PNotify.prototype.options.styling = "bootstrap2";
|
||||
PNotify.prototype.options.mouse_reset = false;
|
||||
PNotify.prototype.options.stack.firstpos1 = 40 + 20; // navbar + 20
|
||||
PNotify.prototype.options.stack.firstpos2 = 20;
|
||||
PNotify.prototype.options.stack.spacing1 = 20;
|
||||
PNotify.prototype.options.stack.spacing2 = 20;
|
||||
|
||||
PNotify.singleButtonNotify = function(options) {
|
||||
if (!options.confirm || !options.confirm.buttons || !options.confirm.buttons.length) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ $(function() {
|
|||
self.colorTransparent = parameters[0].appearance_colorTransparent;
|
||||
|
||||
self.brand = ko.pureComputed(function() {
|
||||
if (self.name())
|
||||
return self.name();
|
||||
else
|
||||
return gettext("OctoPrint");
|
||||
});
|
||||
|
||||
self.fullbrand = ko.pureComputed(function() {
|
||||
if (self.name())
|
||||
return gettext("OctoPrint") + ": " + self.name();
|
||||
else
|
||||
|
|
|
|||
|
|
@ -113,13 +113,10 @@ $(function() {
|
|||
},
|
||||
"model": function(data) {
|
||||
return data["type"] && (data["type"] == "model" || data["type"] == "folder");
|
||||
},
|
||||
"emptyFolder": function(data) {
|
||||
return data["type"] && (data["type"] != "folder" || data["weight"] > 0);
|
||||
}
|
||||
},
|
||||
"name",
|
||||
["emptyFolder"],
|
||||
[],
|
||||
[["sd", "local"], ["machinecode", "model"]],
|
||||
0
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,14 @@ $(function() {
|
|||
}
|
||||
});
|
||||
|
||||
self.userMenuTitle = ko.pureComputed(function() {
|
||||
if (self.loggedIn()) {
|
||||
return _.sprintf(gettext("Logged in as %(name)s"), {name: self.username()});
|
||||
} else {
|
||||
return gettext("Login");
|
||||
}
|
||||
});
|
||||
|
||||
self.reloadUser = function() {
|
||||
if (self.currentUser() == undefined) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ $(function() {
|
|||
|
||||
self.loginState = parameters[0];
|
||||
self.printerProfiles = parameters[1];
|
||||
self.printerState = parameters[2];
|
||||
|
||||
self.file = ko.observable(undefined);
|
||||
self.target = undefined;
|
||||
|
|
@ -23,6 +24,8 @@ $(function() {
|
|||
self.profiles = ko.observableArray();
|
||||
self.printerProfile = ko.observable();
|
||||
|
||||
self.slicerSameDevice = ko.observable();
|
||||
|
||||
self.allViewModels = undefined;
|
||||
|
||||
self.slicersForFile = function(file) {
|
||||
|
|
@ -73,6 +76,19 @@ $(function() {
|
|||
self.profile(undefined);
|
||||
};
|
||||
|
||||
self.metadataForSlicer = function(key) {
|
||||
if (key == undefined || !self.data.hasOwnProperty(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var slicer = self.data[key];
|
||||
self.slicerSameDevice(slicer.sameDevice);
|
||||
};
|
||||
|
||||
self.resetMetadata = function() {
|
||||
self.slicerSameDevice(true);
|
||||
};
|
||||
|
||||
self.configuredSlicers = ko.pureComputed(function() {
|
||||
return _.filter(self.slicers(), function(slicer) {
|
||||
return slicer.configured;
|
||||
|
|
@ -136,8 +152,10 @@ $(function() {
|
|||
self.slicer.subscribe(function(newValue) {
|
||||
if (newValue === undefined) {
|
||||
self.resetProfiles();
|
||||
self.resetMetadata();
|
||||
} else {
|
||||
self.profilesForSlicer(newValue);
|
||||
self.metadataForSlicer(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -153,7 +171,20 @@ $(function() {
|
|||
return self.destinationFilename() != undefined
|
||||
&& self.destinationFilename().trim() != ""
|
||||
&& self.slicer() != undefined
|
||||
&& self.profile() != undefined;
|
||||
&& self.profile() != undefined
|
||||
&& (!(self.printerState.isPrinting() || self.printerState.isPaused()) || !self.slicerSameDevice());
|
||||
});
|
||||
|
||||
self.sliceButtonTooltip = ko.pureComputed(function() {
|
||||
if (!self.enableSliceButton()) {
|
||||
if ((self.printerState.isPrinting() || self.printerState.isPaused()) && self.slicerSameDevice()) {
|
||||
return gettext("Cannot slice on the same device while printing");
|
||||
} else {
|
||||
return gettext("Cannot slice, not all parameters specified");
|
||||
}
|
||||
} else {
|
||||
return gettext("Start the slicing process");
|
||||
}
|
||||
});
|
||||
|
||||
self.requestData = function() {
|
||||
|
|
@ -218,6 +249,10 @@ $(function() {
|
|||
};
|
||||
|
||||
self.slice = function() {
|
||||
if (!self.enableSliceButton()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var destinationFilename = self._sanitize(self.destinationFilename());
|
||||
|
||||
var destinationExtensions = self.data[self.slicer()] && self.data[self.slicer()].extensions && self.data[self.slicer()].extensions.destination
|
||||
|
|
@ -275,7 +310,7 @@ $(function() {
|
|||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
SlicingViewModel,
|
||||
["loginStateViewModel", "printerProfilesViewModel"],
|
||||
["loginStateViewModel", "printerProfilesViewModel", "printerStateViewModel"],
|
||||
"#slicing_configuration_dialog"
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -98,6 +98,12 @@ body {
|
|||
// invert for dropdown
|
||||
.navbar-background-color(fade(@bottom, @gradientalpha), fade(@top, @gradientalpha), @image);
|
||||
}
|
||||
li>a:hover {
|
||||
// darken for hover
|
||||
@darkenedTop: darken(@top, 5%);
|
||||
@darkenedBottom: darken(@bottom, 5%);
|
||||
.navbar-background-color(fade(@darkenedTop, @gradientalpha), fade(@darkenedBottom, @gradientalpha), @image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,14 +180,37 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.brand span {
|
||||
background-size: 20px 20px;
|
||||
background-position: left center;
|
||||
padding-left: 24px;
|
||||
background-repeat: no-repeat;
|
||||
.brand {
|
||||
padding: 10px 20px 6px;
|
||||
|
||||
span {
|
||||
padding-left: 26px;
|
||||
|
||||
// background properties left standalone intentionally, otherwise we overwrite the image
|
||||
background-size: 20px 20px;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
display: inline-block;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
line-height: 20px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#navbar_login a.dropdown-toggle span {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.octoprint-container {
|
||||
margin-top: 20px;
|
||||
|
||||
|
|
@ -371,9 +400,7 @@ table {
|
|||
#temperature-graph {
|
||||
height: 350px;
|
||||
width: 100%;
|
||||
background-image: url("../img/graph-background.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background: url("../img/graph-background.png") no-repeat center;
|
||||
}
|
||||
|
||||
.tab-content, .tab-pane {
|
||||
|
|
@ -553,6 +580,16 @@ ul.dropdown-menu li a {
|
|||
animation: highlightframes 2s;
|
||||
}
|
||||
}
|
||||
|
||||
.back {
|
||||
.back-path {
|
||||
white-space: nowrap;
|
||||
span {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-buttons {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@
|
|||
<select data-bind="options: matchingSlicers, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicer...') }}', value: slicer, valueAllowUnset: true"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Runs locally') }}</label>
|
||||
<div class="controls">
|
||||
<strong data-bind="text: slicerSameDevice() ? gettext('Yes') : gettext('No')"></strong>
|
||||
<span class="help-block"><small>{{ _('For performance reasons locally run slicers are disabled while printing') }}</small></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Slicing Profile') }}</label>
|
||||
<div class="controls">
|
||||
|
|
@ -48,6 +55,6 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</a>
|
||||
<a href="#" class="btn btn-primary" data-bind="click: function() { if ($root.enableSliceButton()) { $root.slice() } }, enabled: enableSliceButton, css: {disabled: !$root.enableSliceButton()}">{{ _('Slice') }}</a>
|
||||
<a href="#" class="btn btn-primary" data-bind="click: function() { if ($root.enableSliceButton()) { $root.slice() } }, enabled: enableSliceButton, css: {disabled: !$root.enableSliceButton()}, attr: {title: $root.sliceButtonTooltip}">{{ _('Slice') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<div id="navbar" class="navbar navbar-static-top">
|
||||
<div class="navbar-inner" data-bind="css: appearanceClasses">
|
||||
<div class="container">
|
||||
<a class="brand" href="#"> <span data-bind="text: appearance.brand">OctoPrint</span></a>
|
||||
<a class="brand" href="javascript:void(0)" data-bind="attr: {title: appearance.fullbrand}"><span data-bind="text: appearance.brand">OctoPrint</span></a>
|
||||
<div class="nav-collapse">
|
||||
<!-- Navbar -->
|
||||
<ul class="nav pull-right">
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
</div>
|
||||
<div class="footer">
|
||||
<ul class="pull-left muted">
|
||||
<li><small>{{ _('Version') }}: <span class="version">{{ display_version|e }}</span></small></li>
|
||||
<li><small>{{ _('OctoPrint') }} <span class="version">{{ display_version|e }}</span></small></li>
|
||||
</ul>
|
||||
<ul class="pull-right">
|
||||
<li><a href="http://octoprint.org" target="_blank" rel="noreferrer noopener"><i class="icon-home"></i> {{ _('Homepage') }}</a></li>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<a href="#" class="dropdown-toggle">
|
||||
<i class="icon-user"></i> <span data-bind="text: loginState.userMenuText">{{ _('Login') }}</span>
|
||||
<a href="javascript:void(0)" class="dropdown-toggle">
|
||||
<i class="icon-user"></i> <span data-bind="text: loginState.userMenuText, attr: {title: loginState.userMenuTitle}">{{ _('Login') }}</span>
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<div id="login_dropdown_loggedout" style="padding: 15px" class="dropdown-menu" data-bind="css: {hide: loginState.loggedIn(), 'dropdown-menu': !loginState.loggedIn()}">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog" data-bind="click: function() { $root.show(); }">
|
||||
<i class="icon-wrench"></i> {{ _('Settings') }}
|
||||
<a id="navbar_show_settings" title="{{ _('Settings') }}" class="pull-right" href="#settings_dialog" data-bind="click: function() { $root.show(); }">
|
||||
<i class="icon-wrench"></i>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-bind="visible: system.systemActions().length > 0">
|
||||
<i class="icon-off"></i> {{ _('System') }}
|
||||
<a href="javascript:void(0)" title="{{ _('System') }}" class="dropdown-toggle" data-toggle="dropdown" data-bind="visible: system.systemActions().length > 0">
|
||||
<i class="icon-off"></i>
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu" data-bind="foreach: system.systemActions">
|
||||
|
|
@ -7,6 +7,6 @@
|
|||
<li class="divider"></li>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: action != "divider" -->
|
||||
<li><a href="#" data-bind="click: $root.system.triggerCommand, text: name"></a></li>
|
||||
<li><a href="javascript:void(0)" data-bind="click: $root.system.triggerCommand, text: name"></a></li>
|
||||
<!-- /ko -->
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
<input type="text" class="input-block search-query" data-bind="value: searchQuery, valueUpdate: 'input'" placeholder="{{ _('Search...') }}">
|
||||
</form>
|
||||
<div class="gcode_files">
|
||||
<div class="entry back clickable" data-bind="visible: currentPath() != '', click: function() { $root.navigateUp(); }" style="display: none"><i class="icon-arrow-left"></i> {{ _('Back') }}</div>
|
||||
<div class="entry back clickable" data-bind="visible: currentPath() != '', click: function() { $root.navigateUp(); }" style="display: none">
|
||||
<div class="back-arrow"><i class="icon-arrow-left"></i> {{ _('Back') }}</div>
|
||||
<div class="back-path"><small class="muted">{{ _('Currently in') }} <span data-bind="text: currentPath"></span></small></div>
|
||||
</div>
|
||||
|
||||
<!-- ko slimScrolledForeach: filesAndFolders -->
|
||||
<div class="entry" data-bind="attr: { id: $root.getEntryId($data) }, template: { name: $root.templateFor($data), data: $data }, css: $data.type"></div>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,4 @@
|
|||
<input type="text" data-bind="value: model">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Color') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="value: color, options: availableColors, optionsText: 'name', optionsValue: 'key'">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -627,10 +627,10 @@ class ZTimelapse(Timelapse):
|
|||
|
||||
def _on_z_change(self, event, payload):
|
||||
if self._retraction_zhop != 0 and payload["old"] is not None and payload["new"] is not None:
|
||||
# check if height difference equals z-hop or is negative, if so don't take a picture
|
||||
diff = round(payload["new"] - payload["old"], 3)
|
||||
# check if height difference equals z-hop, if so don't take a picture
|
||||
diff = round(abs(payload["new"] - payload["old"]), 3)
|
||||
zhop = round(self._retraction_zhop, 3)
|
||||
if diff == zhop or diff <= 0:
|
||||
if diff == zhop:
|
||||
return
|
||||
|
||||
self.capture_image()
|
||||
|
|
|
|||
|
|
@ -1196,7 +1196,7 @@ class MachineCom(object):
|
|||
|
||||
# temperature processing
|
||||
elif ' T:' in line or line.startswith('T:') or ' T0:' in line or line.startswith('T0:') or ((' B:' in line or line.startswith('B:')) and not 'A:' in line):
|
||||
if not disable_external_heatup_detection and not line.strip().startswith("ok") and not self._heating:
|
||||
if not disable_external_heatup_detection and not line.strip().startswith("ok") and not self._heating and self._firmwareInfoReceived:
|
||||
self._logger.debug("Externally triggered heatup detected")
|
||||
self._heating = True
|
||||
self._heatupWaitStartTime = time.time()
|
||||
|
|
|
|||
Loading…
Reference in a new issue