Merge branch 'maintenance' into devel

This commit is contained in:
Gina Häußge 2017-02-24 15:28:01 +01:00
commit 30e962b447
31 changed files with 567 additions and 157 deletions

View file

@ -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

View file

@ -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

View file

@ -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``

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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):
"""

View file

@ -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

View file

@ -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)

View file

@ -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(*[])

View file

@ -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()

View file

@ -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():

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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
);

View file

@ -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;

View file

@ -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"
]);
});

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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()}">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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()

View file

@ -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()