Merge branch 'maintenance' into devel

# Conflicts:
#	src/octoprint/server/__init__.py
#	src/octoprint/util/jinja.py
This commit is contained in:
Gina Häußge 2016-09-07 17:19:50 +02:00
commit 64d484bd09
18 changed files with 3080 additions and 1187 deletions

View file

@ -795,7 +795,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
else:
result["current"] = VERSION
if check["type"] == "github_release" and (check["prerelease"] or BRANCH != stable_branch):
if check["type"] == "github_release" and (check.get("prerelease", None) or BRANCH != stable_branch):
# we are tracking github releases and are either also tracking prerelease OR are currently installed
# from something that is not the stable (master) branch => we need to change some parameters

View file

@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import uuid
from sockjs.tornado import SockJSRouter
from flask import Flask, g, request, session, Blueprint
from flask import Flask, g, request, session, Blueprint, Request, Response
from flask.ext.login import LoginManager, current_user
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
from flask.ext.babel import Babel, gettext, ngettext
@ -169,7 +169,7 @@ class Server(object):
util.flask.enable_additional_translations(additional_folders=[self._settings.getBaseFolder("translations")])
# setup app
self._setup_app()
self._setup_app(app)
# setup i18n
self._setup_i18n(app)
@ -235,6 +235,7 @@ class Server(object):
components.update(dict(printer=printer))
def octoprint_plugin_inject_factory(name, implementation):
"""Factory for injections for all OctoPrintPlugins"""
if not isinstance(implementation, octoprint.plugin.OctoPrintPlugin):
return None
props = dict()
@ -245,6 +246,7 @@ class Server(object):
return props
def settings_plugin_inject_factory(name, implementation):
"""Factory for additional injections depending on plugin type"""
plugin_settings = octoprint.plugin.plugin_settings_for_settings_plugin(name, implementation)
if plugin_settings is None:
return
@ -252,6 +254,8 @@ class Server(object):
return dict(settings=plugin_settings)
def settings_plugin_config_migration_and_cleanup(name, implementation):
"""Take care of migrating and cleaning up any old settings"""
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
return
@ -293,6 +297,8 @@ class Server(object):
# setup jinja2
self._setup_jinja2()
# make sure plugin lifecycle events relevant for jinja2 are taken care of
def template_enabled(name, plugin):
if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.TemplatePlugin):
return
@ -315,25 +321,6 @@ class Server(object):
if self._debug:
events.DebugEventListener()
app.wsgi_app = util.ReverseProxied(
app.wsgi_app,
self._settings.get(["server", "reverseProxy", "prefixHeader"]),
self._settings.get(["server", "reverseProxy", "schemeHeader"]),
self._settings.get(["server", "reverseProxy", "hostHeader"]),
self._settings.get(["server", "reverseProxy", "prefixFallback"]),
self._settings.get(["server", "reverseProxy", "schemeFallback"]),
self._settings.get(["server", "reverseProxy", "hostFallback"])
)
secret_key = self._settings.get(["server", "secretKey"])
if not secret_key:
import string
from random import choice
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
secret_key = "".join(choice(chars) for _ in range(32))
self._settings.set(["server", "secretKey"], secret_key)
self._settings.save()
app.secret_key = secret_key
loginManager = LoginManager()
loginManager.session_protection = "strong"
loginManager.user_callback = load_user
@ -342,18 +329,16 @@ class Server(object):
principals.identity_loaders.appendleft(users.dummy_identity_loader)
loginManager.init_app(app)
if self._host is None:
self._host = self._settings.get(["server", "host"])
if self._port is None:
self._port = self._settings.getInt(["server", "port"])
app.debug = self._debug
# register API blueprint
self._setup_blueprints()
## Tornado initialization starts here
if self._host is None:
self._host = self._settings.get(["server", "host"])
if self._port is None:
self._port = self._settings.getInt(["server", "port"])
ioloop = IOLoop()
ioloop.install()
@ -370,7 +355,7 @@ class Server(object):
allow_client_caching=False
)
additional_mime_types=dict(mime_type_guesser=mime_type_guesser)
admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))
admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not octoprint.util.is_hidden_path(path), status_code=404))
def joined_dict(*dicts):
@ -407,6 +392,8 @@ class Server(object):
(r"/online.gif", util.tornado.StaticDataHandler, dict(data=bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")),
content_type="image/gif"))
]
# fetch additional routes from plugins
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
try:
result = hook(list(server_routes))
@ -460,10 +447,13 @@ class Server(object):
self._stop_intermediary_server()
# initialize and bind the server
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=self._settings.getInt(["server", "maxSize"]))
self._server.listen(self._port, address=self._host)
eventManager.fire(events.Events.STARTUP)
# auto connect
if self._settings.getBoolean(["serial", "autoconnect"]):
(port, baudrate) = self._settings.get(["serial", "port"]), self._settings.getInt(["serial", "baudrate"])
printer_profile = printerProfileManager.get_default()
@ -559,7 +549,8 @@ class Server(object):
def _create_socket_connection(self, session):
global printer, fileManager, analysisQueue, userManager, eventManager
return util.sockjs.PrinterStateConnection(printer, fileManager, analysisQueue, userManager, eventManager, pluginManager, session)
return util.sockjs.PrinterStateConnection(printer, fileManager, analysisQueue, userManager,
eventManager, pluginManager, session)
def _check_for_root(self):
if "geteuid" in dir(os) and os.geteuid() == 0:
@ -586,7 +577,41 @@ class Server(object):
return Locale.parse(request.accept_languages.best_match(LANGUAGES))
def _setup_app(self):
def _setup_app(self, app):
from octoprint.server.util.flask import ReverseProxiedEnvironment, OctoPrintFlaskRequest, OctoPrintFlaskResponse
s = settings()
app.debug = self._debug
secret_key = s.get(["server", "secretKey"])
if not secret_key:
import string
from random import choice
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
secret_key = "".join(choice(chars) for _ in range(32))
s.set(["server", "secretKey"], secret_key)
s.save()
app.secret_key = secret_key
reverse_proxied = ReverseProxiedEnvironment(
header_prefix=s.get(["server", "reverseProxy", "prefixHeader"]),
header_scheme=s.get(["server", "reverseProxy", "schemeHeader"]),
header_host=s.get(["server", "reverseProxy", "hostHeader"]),
header_server=s.get(["server", "reverseProxy", "serverHeader"]),
header_port=s.get(["server", "reverseProxy", "portHeader"]),
prefix=s.get(["server", "reverseProxy", "prefixFallback"]),
scheme=s.get(["server", "reverseProxy", "schemeFallback"]),
host=s.get(["server", "reverseProxy", "hostFallback"]),
server=s.get(["server", "reverseProxy", "serverFallback"]),
port=s.get(["server", "reverseProxy", "portFallback"])
)
OctoPrintFlaskRequest.environment_wrapper = reverse_proxied
app.request_class = OctoPrintFlaskRequest
app.response_class = OctoPrintFlaskResponse
@app.before_request
def before_request():
g.locale = self._get_locale()

View file

@ -155,97 +155,3 @@ def get_plugin_hash():
plugin_hash = hashlib.sha1()
plugin_hash.update(",".join(ui_plugins))
return plugin_hash.hexdigest()
#~~ reverse proxy compatible WSGI middleware
class ReverseProxied(object):
"""
Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.
In nginx:
.. code-block:: none
location /myprefix {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /myprefix;
}
Alternatively define prefix and scheme via config.yaml:
.. code-block:: yaml
server:
baseUrl: /myprefix
scheme: http
:param app: the WSGI application
:param header_script_name: the HTTP header in the wsgi environment from which to determine the prefix
:param header_scheme: the HTTP header in the wsgi environment from which to determine the scheme
:param header_host: the HTTP header in the wsgi environment from which to determine the host for which to generate external URLs
:param base_url: the prefix to use as fallback if headers are not set
:param scheme: the scheme to use as fallback if headers are not set
:param host: the host to use as fallback if headers are not set
"""
def __init__(self, app, header_prefix="x-script-name", header_scheme="x-scheme", header_host="x-forwarded-host", base_url="", scheme="", host=""):
self.app = app
# headers for prefix & scheme & host, converted to conform to WSGI format
to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_")
self._header_prefix = to_wsgi_format(header_prefix)
self._header_scheme = to_wsgi_format(header_scheme)
self._header_host = to_wsgi_format(header_host)
# fallback prefix & scheme & host from config
self._fallback_prefix = base_url
self._fallback_scheme = scheme
self._fallback_host = host
def __call__(self, environ, start_response):
# determine prefix
prefix = environ.get(self._header_prefix, "")
if not prefix:
prefix = self._fallback_prefix
# rewrite SCRIPT_NAME and if necessary also PATH_INFO based on prefix
if prefix:
environ["SCRIPT_NAME"] = prefix
path_info = environ["PATH_INFO"]
if path_info.startswith(prefix):
environ["PATH_INFO"] = path_info[len(prefix):]
# determine scheme
scheme = environ.get(self._header_scheme, "")
if scheme and "," in scheme:
# Scheme might be something like "https,https" if doubly-reverse-proxied
# without stripping original scheme header first, make sure to only use
# the first entry in such a case. See #1391.
scheme, _ = map(lambda x: x.strip(), scheme.split(",", 1))
if not scheme:
scheme = self._fallback_scheme
# rewrite wsgi.url_scheme based on scheme
if scheme:
environ["wsgi.url_scheme"] = scheme
# determine host
host = environ.get(self._header_host, "")
if not host:
host = self._fallback_host
# rewrite host header based on host
if host:
environ["HTTP_HOST"] = host
# call wrapped app with rewritten environment
return self.app(environ, start_response)

View file

@ -219,6 +219,193 @@ def fix_webassets_filtertool():
FilterTool._wrap_cache = fixed_wrap_cache
#~~ WSGI environment wrapper for reverse proxying
class ReverseProxiedEnvironment(object):
@staticmethod
def to_header_candidates(values):
if values is None:
return []
if not isinstance(values, (list, tuple)):
values = [values]
to_wsgi_format = lambda header: "HTTP_" + header.upper().replace("-", "_")
return map(to_wsgi_format, values)
def __init__(self,
header_prefix=None,
header_scheme=None,
header_host=None,
header_server=None,
header_port=None,
prefix=None,
scheme=None,
host=None,
server=None,
port=None):
# sensible defaults
if header_prefix is None:
header_prefix = ["x-script-name"]
if header_scheme is None:
header_scheme = ["x-forwarded-proto", "x-scheme"]
if header_host is None:
header_host = ["x-forwarded-host"]
if header_server is None:
header_server = ["x-forwarded-server"]
if header_port is None:
header_port = ["x-forwarded-port"]
# header candidates
self._headers_prefix = self.to_header_candidates(header_prefix)
self._headers_scheme = self.to_header_candidates(header_scheme)
self._headers_host = self.to_header_candidates(header_host)
self._headers_server = self.to_header_candidates(header_server)
self._headers_port = self.to_header_candidates(header_port)
# fallback prefix & scheme & host from config
self._fallback_prefix = prefix
self._fallback_scheme = scheme
self._fallback_host = host
self._fallback_server = server
self._fallback_port = port
def __call__(self, environ):
def retrieve_header(header_type):
candidates = getattr(self, "_headers_" + header_type, [])
fallback = getattr(self, "_fallback_" + header_type, None)
for candidate in candidates:
value = environ.get(candidate, None)
if value is not None:
return value
else:
return fallback
def host_to_server_and_port(host, scheme):
if host is None:
return None, None
if ":" in host:
server, port = host.split(":", 1)
else:
server = host
port = "443" if scheme == "https" else "80"
return server, port
# determine prefix
prefix = retrieve_header("prefix")
if prefix is not None:
environ["SCRIPT_NAME"] = prefix
path_info = environ["PATH_INFO"]
if path_info.startswith(prefix):
environ["PATH_INFO"] = path_info[len(prefix):]
# determine scheme
scheme = retrieve_header("scheme")
if scheme is not None and "," in scheme:
# Scheme might be something like "https,https" if doubly-reverse-proxied
# without stripping original scheme header first, make sure to only use
# the first entry in such a case. See #1391.
scheme, _ = map(lambda x: x.strip(), scheme.split(",", 1))
if scheme is not None:
environ["wsgi.url_scheme"] = scheme
# determine host
url_scheme = environ["wsgi.url_scheme"]
host = retrieve_header("host")
if host is not None:
# if we have a host, we take server_name and server_port from it
server, port = host_to_server_and_port(host, url_scheme)
environ["HTTP_HOST"] = host
environ["SERVER_NAME"] = server
environ["SERVER_PORT"] = port
else:
# else we take a look at the server and port headers and if we have
# something there we derive the host from it
# determine server - should usually not be used
server = retrieve_header("server")
if server is not None:
environ["SERVER_NAME"] = server
# determine port - should usually not be used
port = retrieve_header("port")
if port is not None:
environ["SERVER_PORT"] = port
# make sure HTTP_HOST matches SERVER_NAME and SERVER_PORT
expected_server, expected_port = host_to_server_and_port(environ.get("HTTP_HOST", None), url_scheme)
if expected_server != environ["SERVER_NAME"] or expected_port != environ["SERVER_PORT"]:
# there's a difference, fix it!
if url_scheme == "http" and environ["SERVER_PORT"] == "80" or url_scheme == "https" and environ["SERVER_PORT"] == "443":
# default port for scheme, can be skipped
environ["HTTP_HOST"] = environ["SERVER_NAME"]
else:
environ["HTTP_HOST"] = environ["SERVER_NAME"] + ":" + environ["SERVER_PORT"]
# call wrapped app with rewritten environment
return environ
#~~ request and response versions
from werkzeug.wrappers import cached_property
class OctoPrintFlaskRequest(flask.Request):
environment_wrapper = staticmethod(lambda x: x)
def __init__(self, environ, *args, **kwargs):
# apply environment wrapper to provided WSGI environment
flask.Request.__init__(self, self.environment_wrapper(environ), *args, **kwargs)
@cached_property
def cookies(self):
# strip cookie_suffix from all cookies in the request, return result
cookies = flask.Request.cookies.__get__(self)
def cookie_name_converter(key):
return key[:-len(self.cookie_suffix)] if key.endswith(self.cookie_suffix) else key
return dict((cookie_name_converter(key), value) for key, value in cookies.items())
@cached_property
def server_name(self):
"""Short cut to the request's server name header"""
return self.environ.get("SERVER_NAME")
@cached_property
def server_port(self):
"""Short cut to the request's server port header"""
return self.environ.get("SERVER_PORT")
@cached_property
def cookie_suffix(self):
"""
Request specific suffix for set and read cookies
We need this because cookies are not port-specific and we don't want to overwrite our
session and other cookies from one OctoPrint instance on our machine with those of another
one who happens to listen on the same address albeit a different port.
"""
return "_P" + self.server_port
class OctoPrintFlaskResponse(flask.Response):
def set_cookie(self, key, *args, **kwargs):
# restrict cookie path to script root
kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")
# add request specific cookie suffix to name
flask.Response.set_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)
def delete_cookie(self, key, *args, **kwargs):
# restrict cookie path to script root
kwargs["path"] = flask.request.script_root + kwargs.get("path", "/")
# add request specific cookie suffix to name
flask.Response.delete_cookie(self, key + flask.request.cookie_suffix, *args, **kwargs)
#~~ passive login helper
def passive_login():

View file

@ -104,18 +104,22 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
true
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.path"
Content-Type: text/plain; charset=utf-8
/tmp/tmpzupkro
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.name"
Content-Type: text/plain; charset=utf-8
test.gcode
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.content_type"
Content-Type: text/plain; charset=utf-8
application/octet-stream
------WebKitFormBoundarypYiSUx63abAmhT5C
Content-Disposition: form-data; name="file.size"
Content-Type: text/plain; charset=utf-8
349182
------WebKitFormBoundarypYiSUx63abAmhT5C--
@ -272,9 +276,19 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
header = header[header_check:]
# convert to dict
header = tornado.httputil.HTTPHeaders.parse(header.decode("utf-8"))
try:
header = tornado.httputil.HTTPHeaders.parse(header.decode("utf-8"))
except UnicodeDecodeError:
try:
header = tornado.httputil.HTTPHeaders.parse(header.decode("iso-8859-1"))
except:
# looks like we couldn't decode something here neither as UTF-8 nor ISO-8859-1
self._logger.warn("Could not decode multipart headers in request, should be either UTF-8 or ISO-8859-1")
self.send_error(400)
return
disp_header = header.get("Content-Disposition", "")
disposition, disp_params = tornado.httputil._parse_header(disp_header)
disposition, disp_params = _parse_header(disp_header, strip_quotes=False)
if disposition != "form-data":
self._logger.warn("Got a multipart header without form-data content disposition, ignoring that one")
@ -283,7 +297,22 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
self._logger.warn("Got a multipart header without name, ignoring that one")
return
self._current_part = self._on_part_start(disp_params["name"], header.get("Content-Type", None), filename=disp_params["filename"] if "filename" in disp_params else None)
filename = disp_params.get("filename*", None) # RFC 5987 header present?
if filename is not None:
try:
filename = _extended_header_value(filename)
except:
# parse error, this is not RFC 5987 compliant after all
self._logger.warn("extended filename* value {!r} is not RFC 5987 compliant")
self.send_error(400)
return
else:
# no filename* header, just strip quotes from filename header then and be done
filename = _strip_value_quotes(disp_params.get("filename", None))
self._current_part = self._on_part_start(_strip_value_quotes(disp_params["name"]),
header.get("Content-Type", None),
filename=filename)
def _on_part_start(self, name, content_type, filename=None):
"""
@ -376,6 +405,7 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
key = name + "." + n
self._new_body += b"--%s\r\n" % self._multipart_boundary
self._new_body += b"Content-Disposition: form-data; name=\"%s\"\r\n" % key
self._new_body += b"Content-Type: text/plain; charset=utf-8\r\n"
self._new_body += b"\r\n"
self._new_body += b"%s\r\n" % p
elif "data" in part:
@ -430,6 +460,47 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
options = _handle_method
def _parse_header(line, strip_quotes=True):
parts = tornado.httputil._parseparam(';' + line)
key = next(parts)
pdict = {}
for p in parts:
i = p.find('=')
if i >= 0:
name = p[:i].strip().lower()
value = p[i + 1:].strip()
if strip_quotes:
value = _strip_value_quotes(value)
pdict[name] = value
return key, pdict
def _strip_value_quotes(value):
if not value:
return value
if len(value) >= 2 and value[0] == value[-1] == '"':
value = value[1:-1]
value = value.replace('\\\\', '\\').replace('\\"', '"')
return value
def _extended_header_value(value):
if not value:
return value
if value.lower().startswith("iso-8859-1'") or value.lower().startswith("utf-8'"):
# RFC 5987 section 3.2
from urllib import unquote
encoding, _, value = value.split("'", 2)
return unquote(value).decode(encoding)
else:
# no encoding provided, strip potentially present quotes and call it a day
return _strip_value_quotes(value)
class WsgiInputContainer(object):
"""
A WSGI container for use with Tornado that allows supplying the request body to be used for ``wsgi.input`` in the

View file

@ -119,12 +119,16 @@ default_settings = {
"seenWizards": {},
"secretKey": None,
"reverseProxy": {
"prefixHeader": "X-Script-Name",
"schemeHeader": "X-Scheme",
"hostHeader": "X-Forwarded-Host",
"prefixFallback": "",
"schemeFallback": "",
"hostFallback": ""
"prefixHeader": None,
"schemeHeader": None,
"hostHeader": None,
"serverHeader": None,
"portHeader": None,
"prefixFallback": None,
"schemeFallback": None,
"hostFallback": None,
"serverFallback": None,
"portFallback": None
},
"uploads": {
"maxSize": 1 * 1024 * 1024 * 1024, # 1GB

View file

@ -606,12 +606,12 @@ $(function() {
}
}
}
output += gettext("Estimated Print Time") + ": " + formatFuzzyPrintTime(data["gcodeAnalysis"]["estimatedPrintTime"]) + "<br>";
output += gettext("Estimated print time") + ": " + formatFuzzyPrintTime(data["gcodeAnalysis"]["estimatedPrintTime"]) + "<br>";
}
if (data["prints"] && data["prints"]["last"]) {
output += gettext("Last Printed") + ": " + formatTimeAgo(data["prints"]["last"]["date"]) + "<br>";
output += gettext("Last printed") + ": " + formatTimeAgo(data["prints"]["last"]["date"]) + "<br>";
if (data["prints"]["last"]["lastPrintTime"]) {
output += gettext("Last Print Time") + ": " + formatDuration(data["prints"]["last"]["lastPrintTime"]);
output += gettext("Last print time") + ": " + formatDuration(data["prints"]["last"]["lastPrintTime"]);
}
}
return output;

View file

@ -473,17 +473,17 @@ $(function() {
var output = [];
output.push(gettext("Layer number") + ": " + (layer.number + 1));
output.push(gettext("Layer height") + " (mm): " + layer.height);
output.push(gettext("GCODE commands in layer") + ": " + layer.commands);
output.push(gettext("GCODE commands") + ": " + layer.commands);
if (layer.filament != undefined) {
if (layer.filament.length == 1) {
output.push(gettext("Filament used by layer") + ": " + layer.filament[0].toFixed(2) + "mm");
output.push(gettext("Filament") + ": " + layer.filament[0].toFixed(2) + "mm");
} else {
for (var i = 0; i < layer.filament.length; i++) {
output.push(gettext("Filament used by layer") + " (" + gettext("Tool") + " " + i + "): " + layer.filament[i].toFixed(2) + "mm");
output.push(gettext("Filament") + " (" + gettext("Tool") + " " + i + "): " + layer.filament[i].toFixed(2) + "mm");
}
}
}
output.push(gettext("Print time for layer") + ": " + formatFuzzyPrintTime(layer.printTime));
output.push(gettext("Estimated print time") + ": " + formatDuration(layer.printTime));
self.ui_layerInfo(output.join("<br>"));

View file

@ -28,7 +28,7 @@
<h1>{{ _('Model info') }}</h1>
<p data-bind="html: ui_modelInfo"></p>
<h1>Layer info</h1>
<h1>{{ _('Layer info') }}</h1>
<p data-bind="html: ui_layerInfo"></p>
</div>
<div class="span5">
@ -73,7 +73,13 @@
</div>
<div class="muted">
<small>{{ _('Note that the time estimates in this tab are calculated by the GCODE viewer in your browser and might differ from the values calculated by the server that are displayed in the "State" and "Files" panels in the sidebar due to slightly different implementations.') }}</small>
<small>{% trans %}
Note that the time and usage values in this tab are <strong>estimated</strong> by the GCODE viewer in your
browser and might differ from the values <strong>estimated</strong> by the server that are displayed in the
"State" and "Files" panels in the sidebar due to slightly different implementations. Also note that these
<strong>estimated</strong> values may be inaccurate since they can also take information present in the
GCODE file into account.
{% endtrans %}</small>
</div>
</div>
<div data-bind="visible: waitForApproval">

File diff suppressed because it is too large Load diff

11
tests/server/__init__.py Normal file
View file

@ -0,0 +1,11 @@
# coding=utf-8
"""
Unit tests for ``octoprint.server``.
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"

View file

@ -0,0 +1,10 @@
# coding=utf-8
"""
Unit tests for ``octoprint.server.util``.
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"

411
tests/server/util/flask.py Normal file
View file

@ -0,0 +1,411 @@
# coding=utf-8
"""
Unit tests for ``octoprint.server.util.flask``.
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
import unittest
import mock
from ddt import ddt, data, unpack
from octoprint.server.util.flask import ReverseProxiedEnvironment, OctoPrintFlaskRequest, OctoPrintFlaskResponse
standard_environ = {
"HTTP_HOST": "localhost:5000",
"SERVER_NAME": "localhost",
"SERVER_PORT": "5000",
"SCRIPT_NAME": "",
"PATH_INFO": "/",
"wsgi.url_scheme": "http"
}
@ddt
class ReverseProxiedEnvironmentTest(unittest.TestCase):
@data(
# defaults
({},
{}),
# prefix set, path info not prefixed
({
"HTTP_X_SCRIPT_NAME": "/octoprint",
"PATH_INFO": "/static/online.gif"
}, {
"SCRIPT_NAME": "/octoprint"
}),
# prefix set, path info prefixed
({
"HTTP_X_SCRIPT_NAME": "/octoprint",
"PATH_INFO": "/octoprint/static/online.gif",
}, {
"SCRIPT_NAME": "/octoprint",
"PATH_INFO": "/static/online.gif"
}),
# host set
({
"HTTP_X_FORWARDED_HOST": "example.com"
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "80"
}),
# host set with port
({
"HTTP_X_FORWARDED_HOST": "example.com:1234"
}, {
"HTTP_HOST": "example.com:1234",
"SERVER_NAME": "example.com",
"SERVER_PORT": "1234"
}),
# host and scheme set
({
"HTTP_X_FORWARDED_HOST": "example.com",
"HTTP_X_FORWARDED_PROTO": "https"
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "443",
"wsgi.url_scheme": "https"
}),
# host and scheme 2 set
({
"HTTP_X_FORWARDED_HOST": "example.com",
"HTTP_X_SCHEME": "https"
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "443",
"wsgi.url_scheme": "https"
}),
# host, server and port headers set -> only host wins
({
"HTTP_X_FORWARDED_HOST": "example.com",
"HTTP_X_FORWARDED_SERVER": "example2.com",
"HTTP_X_FORWARDED_PORT": "444",
"HTTP_X_FORWARDED_PROTO": "https"
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "443",
"wsgi.url_scheme": "https"
}),
# server and port headers set -> host derived with port
({
"HTTP_X_FORWARDED_SERVER": "example2.com",
"HTTP_X_FORWARDED_PORT": "444",
"HTTP_X_FORWARDED_PROTO": "https"
}, {
"HTTP_HOST": "example2.com:444",
"SERVER_NAME": "example2.com",
"SERVER_PORT": "444",
"wsgi.url_scheme": "https"
}),
# server and port headers set, standard port -> host derived, no port
({
"HTTP_X_FORWARDED_SERVER": "example.com",
"HTTP_X_FORWARDED_PORT": "80",
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "80",
}),
# multiple scheme entries -> only use first one
({
"HTTP_X_FORWARDED_PROTO": "https,http",
}, {
"wsgi.url_scheme": "https"
}),
# host = none -> should never happen but you never know...
({
"HTTP_HOST": None,
"HTTP_X_FORWARDED_SERVER": "example.com",
"HTTP_X_FORWARDED_PORT": "80"
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "80"
})
)
@unpack
def test_stock(self, environ, expected):
reverse_proxied = ReverseProxiedEnvironment()
merged_environ = dict(standard_environ)
merged_environ.update(environ)
actual = reverse_proxied(merged_environ)
merged_expected = dict(standard_environ)
merged_expected.update(environ)
merged_expected.update(expected)
self.assertDictEqual(merged_expected, actual)
@data(
# prefix overridden
({
"prefix": "fallback_prefix"
}, {
}, {
"SCRIPT_NAME": "fallback_prefix",
}),
# scheme overridden
({
"scheme": "https"
}, {
}, {
"wsgi.url_scheme": "https"
}),
# host overridden, default port
({
"host": "example.com"
}, {
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "80"
}),
# host overridden, included port
({
"host": "example.com:81"
}, {
}, {
"HTTP_HOST": "example.com:81",
"SERVER_NAME": "example.com",
"SERVER_PORT": "81"
}),
# server overridden
({
"server": "example.com"
}, {
}, {
"HTTP_HOST": "example.com:5000",
"SERVER_NAME": "example.com",
"SERVER_PORT": "5000"
}),
# port overridden, standard port
({
"port": "80"
}, {
}, {
"HTTP_HOST": "localhost",
"SERVER_PORT": "80"
}),
# port overridden, non standard port
({
"port": "81"
}, {
}, {
"HTTP_HOST": "localhost:81",
"SERVER_PORT": "81"
}),
# server and port overridden, default port
({
"server": "example.com",
"port": "80"
}, {
}, {
"HTTP_HOST": "example.com",
"SERVER_NAME": "example.com",
"SERVER_PORT": "80"
}),
# server and port overridden, non default port
({
"server": "example.com",
"port": "81"
}, {
}, {
"HTTP_HOST": "example.com:81",
"SERVER_NAME": "example.com",
"SERVER_PORT": "81"
}),
# prefix not really overridden
({
"prefix": "/octoprint"
}, {
"HTTP_X_SCRIPT_NAME": ""
}, {
}),
# scheme not really overridden
({
"scheme": "https"
}, {
"HTTP_X_FORWARDED_PROTO": "http"
}, {
}),
# scheme 2 not really overridden
({
"scheme": "https"
}, {
"HTTP_X_SCHEME": "http"
}, {
}),
# host not really overridden
({
"host": "example.com:444"
}, {
"HTTP_X_FORWARDED_HOST": "localhost:5000"
}, {
}),
# server not really overridden
({
"server": "example.com"
}, {
"HTTP_X_FORWARDED_SERVER": "localhost"
}, {
}),
# port not really overridden
({
"port": "444"
}, {
"HTTP_X_FORWARDED_PORT": "5000"
}, {
})
)
@unpack
def test_fallbacks(self, fallbacks, environ, expected):
reverse_proxied = ReverseProxiedEnvironment(**fallbacks)
merged_environ = dict(standard_environ)
merged_environ.update(environ)
actual = reverse_proxied(merged_environ)
merged_expected = dict(standard_environ)
merged_expected.update(environ)
merged_expected.update(expected)
self.assertDictEqual(merged_expected, actual)
def test_header_config_ok(self):
result = ReverseProxiedEnvironment.to_header_candidates(["prefix-header1", "prefix-header2"])
self.assertEquals(result, ["HTTP_PREFIX_HEADER1", "HTTP_PREFIX_HEADER2"])
def test_header_config_string(self):
result = ReverseProxiedEnvironment.to_header_candidates("prefix-header")
self.assertEquals(result, ["HTTP_PREFIX_HEADER"])
def test_header_config_none(self):
result = ReverseProxiedEnvironment.to_header_candidates(None)
self.assertEquals(result, [])
##~~
class OctoPrintFlaskRequestTest(unittest.TestCase):
def setUp(self):
self.orig_environment_wrapper = OctoPrintFlaskRequest.environment_wrapper
def tearDown(self):
OctoPrintFlaskRequest.environment_wrapper = staticmethod(self.orig_environment_wrapper)
def test_environment_wrapper(self):
def environment_wrapper(environ):
environ.update({
"TEST": "yes"
})
return environ
OctoPrintFlaskRequest.environment_wrapper = staticmethod(environment_wrapper)
request = OctoPrintFlaskRequest(standard_environ)
self.assertTrue("TEST" in request.environ)
def test_server_name(self):
request = OctoPrintFlaskRequest(standard_environ)
self.assertEquals(request.server_name, "localhost")
def test_server_port(self):
request = OctoPrintFlaskRequest(standard_environ)
self.assertEquals(request.server_port, "5000")
def test_cookie_suffix(self):
request = OctoPrintFlaskRequest(standard_environ)
self.assertEquals(request.cookie_suffix, "_P5000")
def test_cookies(self):
environ = dict(standard_environ)
environ["HTTP_COOKIE"] = "postfixed_P5000=postfixed_value; " \
"postfixed_wrong_P5001=postfixed_wrong_value; " \
"unpostfixed=unpostfixed_value"
request = OctoPrintFlaskRequest(environ)
cookies = request.cookies
self.assertDictEqual(cookies, {"postfixed": "postfixed_value",
"postfixed_wrong_P5001": "postfixed_wrong_value",
"unpostfixed": "unpostfixed_value"})
##~~
@ddt
class OctoPrintFlaskResponseTest(unittest.TestCase):
@data([None, None],
["/subfolder/", None],
[None, "/some/other/script/root"],
["/subfolder/", "/some/other/script/root"])
@unpack
def test_cookie_set_and_delete(self, path, scriptroot):
environ = dict(standard_environ)
if scriptroot is not None:
environ.update(dict(SCRIPT_NAME=scriptroot))
request = OctoPrintFlaskRequest(environ)
if path:
expected_path = path
else:
expected_path = "/"
if scriptroot:
expected_path = scriptroot + expected_path
if path is not None:
kwargs = dict(path=path)
else:
kwargs = dict()
with mock.patch("flask.request", new=request):
response = OctoPrintFlaskResponse()
# test set_cookie
with mock.patch("flask.Response.set_cookie") as set_cookie_mock:
response.set_cookie("some_key", "some_value", **kwargs)
set_cookie_mock.assert_called_once_with(response, "some_key_P5000", "some_value", path=expected_path)
# test delete_cookie
with mock.patch("flask.Response.delete_cookie") as delete_cookie_mock:
response.delete_cookie("some_key", "some_value", **kwargs)
delete_cookie_mock.assert_called_once_with(response, "some_key_P5000", "some_value", path=expected_path)

View file

@ -0,0 +1,90 @@
# coding=utf-8
"""
Unit tests for ``octoprint.server.util.tornado``.
"""
from __future__ import absolute_import
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License"
import unittest
import mock
from ddt import ddt, data, unpack
##~~ _parse_header
@ddt
class ParseHeaderTest(unittest.TestCase):
@data(
("form-data; filename=test.gco", "form-data", dict(filename="test.gco")),
("form-data; filename=\"test.gco\"", "form-data", dict(filename="test.gco")),
("form-data; filename=test\\\\.gco", "form-data", dict(filename="test\\\\.gco")),
("form-data; filename=\"test\\\\.gco\"", "form-data", dict(filename="test\\.gco"))
)
@unpack
def test_parse_header_strip_quotes(self, value, expected_key, expected_dict):
from octoprint.server.util.tornado import _parse_header
actual_key, actual_dict = _parse_header(value)
self.assertEqual(expected_key, actual_key)
self.assertDictEqual(expected_dict, actual_dict)
@data(
("form-data; filename=test.gco", "form-data", dict(filename="test.gco")),
("form-data; filename=\"test.gco\"", "form-data", dict(filename="\"test.gco\"")),
("form-data; filename=test\\\\.gco", "form-data", dict(filename="test\\\\.gco")),
("form-data; filename=\"test\\\\.gco\"", "form-data", dict(filename="\"test\\\\.gco\"")),
("form-data; filename=iso-8859-1'en'test.gco", "form-data", dict(filename="iso-8859-1'en'test.gco"))
)
@unpack
def test_parse_header_leave_quotes(self, value, expected_key, expected_dict):
from octoprint.server.util.tornado import _parse_header
actual_key, actual_dict = _parse_header(value, strip_quotes=False)
self.assertEqual(expected_key, actual_key)
self.assertDictEqual(expected_dict, actual_dict)
##~~ _strip_value_quotes
@ddt
class StripValueQuotesTest(unittest.TestCase):
@data(
("", ""),
(None, None),
('"test.gco"', "test.gco"),
('"test".gco', '"test".gco'),
("test\\\\.gco", "test\\\\.gco"),
('"test\\\\.gco"', "test\\.gco")
)
@unpack
def test_strip_value_quotes(self, value, expected):
from octoprint.server.util.tornado import _strip_value_quotes
actual = _strip_value_quotes(value)
self.assertEqual(expected, actual)
##~~ _extended_header_value
@ddt
class ExtendedHeaderValueTest(unittest.TestCase):
@data(
("", u""),
(None, None),
('"quoted-string"', u"quoted-string"),
("iso-8859-1'en'%A3%20rates", u"£ rates"),
("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", u"£ and € rates")
)
@unpack
def test_extended_header_value(self, value, expected):
from octoprint.server.util.tornado import _extended_header_value
actual = _extended_header_value(value)
self.assertEqual(expected, actual)

File diff suppressed because it is too large Load diff

View file

@ -6,9 +6,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: OctoPrint 1.2.14.dev70+gf671006\n"
"Project-Id-Version: OctoPrint 1.2.16.dev36+g2256a1e\n"
"Report-Msgid-Bugs-To: i18n@octoprint.org\n"
"POT-Creation-Date: 2016-07-28 11:50+0200\n"
"POT-Creation-Date: 2016-09-07 12:06+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -655,24 +655,24 @@ msgstr ""
#: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:239
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2:26
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:101
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:107
#: src/octoprint/templates/dialogs/confirmation.jinja2:11
#: src/octoprint/templates/dialogs/slicing.jinja2:50
#: src/octoprint/templates/sidebar/state.jinja2:24
#: src/octoprint/templates/sidebar/state.jinja2:25
msgid "Cancel"
msgstr ""
#: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:240
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:102
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:108
msgid "Save"
msgstr ""
#: src/octoprint/plugins/softwareupdate/__init__.py:394
#: src/octoprint/plugins/softwareupdate/__init__.py:445
msgid "Software Update"
msgstr ""
#: src/octoprint/plugins/softwareupdate/__init__.py:700
#: src/octoprint/server/views.py:165
#: src/octoprint/plugins/softwareupdate/__init__.py:752
#: src/octoprint/server/views.py:217
#: src/octoprint/static/js/app/viewmodels/appearance.js:11
#: src/octoprint/static/js/app/viewmodels/appearance.js:13
#: src/octoprint/static/js/app/viewmodels/appearance.js:18
@ -681,150 +681,150 @@ msgstr ""
msgid "OctoPrint"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:29
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:117
msgid "Release"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:30
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:118
msgid "Commit"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:132
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:152
#, python-format
msgid "%(name)s: %(version)s"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:135
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:155
msgid "unknown"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:165
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:185
msgid "There are updates available for the following components:"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:173
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:193
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate.jinja2:14
msgid "Release Notes"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:179
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:199
msgid ""
"Those components marked with <i class=\"icon-ok\"></i> can be updated "
"directly."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:184
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:204
msgid "Update Available"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:195
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:215
msgid "Ignore"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:199
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:219
msgid ""
"You can make this message display again via \"Settings\" > \"Software "
"Update\" > \"Check for update now\""
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:203
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:223
msgid "Update now"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:220
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:240
msgid "Everything is up-to-date"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:285
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:305
msgid "Updating..."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:286
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:306
msgid "Now updating, please wait."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:312
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:332
msgid "Update not started!"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:313
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:333
msgid ""
"The update could not be started. Is it already active? Please consult the"
" log for details."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:333
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:353
msgid "Can't update while printing"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:334
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:354
msgid ""
"A print job is currently in progress. Updating will be prevented until it"
" is done."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:387
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:407
#, python-format
msgid "Now updating %(name)s to %(version)s"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:395
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:415
msgid "Update successful, restarting!"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:396
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:416
msgid "The update finished successfully and the server will now be restarted."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:407
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:449
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:427
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:469
msgid "Restart failed"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:408
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:450
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:428
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:470
msgid ""
"The server apparently did not restart by itself, you'll have to do it "
"manually. Please consult the log file on what went wrong."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:424
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:444
msgid "The update finished successfully, please restart OctoPrint now."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:426
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:446
msgid "The update finished successfully, please reboot the server now."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:430
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:450
msgid "Update successful, restart required!"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:443
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:463
msgid ""
"Restarting OctoPrint failed, please restart it manually. You might also "
"want to consult the log file on what went wrong here."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:445
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:465
msgid ""
"Rebooting the server failed, please reboot it manually. You might also "
"want to consult the log file on what went wrong here."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:463
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:483
msgid "Update successful!"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:464
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:484
msgid "The update finished successfully."
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:476
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:496
msgid "Update failed!"
msgstr ""
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:477
#: src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js:497
msgid ""
"The update did not finish successfully. Please consult the log for "
"details."
@ -932,116 +932,121 @@ msgid "OctoPrint version tracking"
msgstr ""
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:90
msgid "OctoPrint Release Channel"
msgstr ""
#: src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2:96
msgid "Version cache TTL"
msgstr ""
#: src/octoprint/server/views.py:73
#: src/octoprint/server/views.py:125
msgid "Plugins"
msgstr ""
#: src/octoprint/server/views.py:131
#: src/octoprint/server/views.py:183
msgid "Connection"
msgstr ""
#: src/octoprint/server/views.py:132
#: src/octoprint/server/views.py:184
#: src/octoprint/templates/sidebar/state.jinja2:1
msgid "State"
msgstr ""
#: src/octoprint/server/views.py:133
#: src/octoprint/server/views.py:185
msgid "Files"
msgstr ""
#: src/octoprint/server/views.py:139
#: src/octoprint/server/views.py:191
msgid "Temperature"
msgstr ""
#: src/octoprint/server/views.py:140
#: src/octoprint/server/views.py:192
msgid "Control"
msgstr ""
#: src/octoprint/server/views.py:141
#: src/octoprint/server/views.py:193
msgid "Terminal"
msgstr ""
#: src/octoprint/server/views.py:144
#: src/octoprint/server/views.py:196
msgid "GCode Viewer"
msgstr ""
#: src/octoprint/server/views.py:146
#: src/octoprint/server/views.py:198
#: src/octoprint/templates/sidebar/state.jinja2:4
msgid "Timelapse"
msgstr ""
#: src/octoprint/server/views.py:151
#: src/octoprint/server/views.py:203
msgid "Printer"
msgstr ""
#: src/octoprint/server/views.py:153
#: src/octoprint/server/views.py:205
msgid "Serial Connection"
msgstr ""
#: src/octoprint/server/views.py:154
#: src/octoprint/server/views.py:206
#: src/octoprint/templates/dialogs/settings/printerprofiles.jinja2:1
msgid "Printer Profiles"
msgstr ""
#: src/octoprint/server/views.py:155
#: src/octoprint/server/views.py:207
msgid "Temperatures"
msgstr ""
#: src/octoprint/server/views.py:156
#: src/octoprint/server/views.py:208
msgid "Terminal Filters"
msgstr ""
#: src/octoprint/server/views.py:157
#: src/octoprint/server/views.py:209
msgid "GCODE Scripts"
msgstr ""
#: src/octoprint/server/views.py:159 src/octoprint/server/views.py:161
#: src/octoprint/server/views.py:211 src/octoprint/server/views.py:213
msgid "Features"
msgstr ""
#: src/octoprint/server/views.py:162
#: src/octoprint/server/views.py:214
msgid "Webcam"
msgstr ""
#: src/octoprint/server/views.py:163
#: src/octoprint/server/views.py:215
msgid "API"
msgstr ""
#: src/octoprint/server/views.py:167
#: src/octoprint/server/views.py:219
#: src/octoprint/templates/dialogs/settings/folders.jinja2:2
msgid "Folders"
msgstr ""
#: src/octoprint/server/views.py:168
#: src/octoprint/server/views.py:220
msgid "Appearance"
msgstr ""
#: src/octoprint/server/views.py:169
#: src/octoprint/server/views.py:221
#: src/octoprint/templates/dialogs/settings/logs.jinja2:2
msgid "Logs"
msgstr ""
#: src/octoprint/server/views.py:170
#: src/octoprint/server/views.py:222
msgid "Server"
msgstr ""
#: src/octoprint/server/views.py:173
#: src/octoprint/server/views.py:225
msgid "Access Control"
msgstr ""
#: src/octoprint/server/views.py:179
#: src/octoprint/server/views.py:231
msgid "Access"
msgstr ""
#: src/octoprint/server/views.py:180
#: src/octoprint/server/views.py:232
msgid "Interface"
msgstr ""
#: src/octoprint/static/js/app/dataupdater.js:96
#: src/octoprint/static/js/app/dataupdater.js:131
#: src/octoprint/static/js/app/helpers.js:451
#: src/octoprint/static/js/app/helpers.js:562
#: src/octoprint/templates/overlays/offline.jinja2:6
msgid "Server is offline"
msgstr ""
@ -1085,12 +1090,82 @@ msgstr ""
msgid "%(hour)02d:%(minute)02d:%(second)02d"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:392
#: src/octoprint/static/js/app/helpers.js:429
#: src/octoprint/static/js/app/helpers.js:436
#, python-format
msgid "%(days)d days"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:431
#, python-format
msgid "%(days)d.5 days"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:434
#, python-format
msgid "%(days)d day"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:445
#, python-format
msgid "%(hours)d hour"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:447
#: src/octoprint/static/js/app/helpers.js:455
#: src/octoprint/static/js/app/helpers.js:466
#, python-format
msgid "%(hours)d hours"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:451
#, python-format
msgid "%(hours)d.5 hours"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:460
msgid "1 day"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:473
msgid "a minute"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:475
msgid "2 minutes"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:481
#, python-format
msgid "%(minutes)d minutes"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:483
msgid "40 minutes"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:485
msgid "50 minutes"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:487
msgid "1 hour"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:492
msgid "a couple of seconds"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:494
msgid "less than a minute"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:503
msgid "YYYY-MM-DD HH:mm"
msgstr ""
#: src/octoprint/static/js/app/helpers.js:410
#: src/octoprint/static/js/app/helpers.js:415
#: src/octoprint/static/js/app/helpers.js:521
#: src/octoprint/static/js/app/helpers.js:526
msgid "off"
msgstr ""
@ -1178,19 +1253,22 @@ msgstr ""
#: src/octoprint/static/js/app/viewmodels/files.js:343
#: src/octoprint/static/js/app/viewmodels/files.js:348
#: src/octoprint/static/js/app/viewmodels/gcode.js:468
#: src/octoprint/static/js/app/viewmodels/gcode.js:471
msgid "Filament"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/files.js:352
msgid "Estimated Print Time"
#: src/octoprint/static/js/app/viewmodels/gcode.js:475
msgid "Estimated print time"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/files.js:355
msgid "Last Printed"
msgid "Last printed"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/files.js:357
msgid "Last Print Time"
msgid "Last print time"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/files.js:460
@ -1305,16 +1383,7 @@ msgid "Layer height"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/gcode.js:465
msgid "GCODE commands in layer"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/gcode.js:468
#: src/octoprint/static/js/app/viewmodels/gcode.js:471
msgid "Filament used by layer"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/gcode.js:475
msgid "Print time for layer"
msgid "GCODE commands"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/loginstate.js:22
@ -1474,11 +1543,13 @@ msgid "Pauses the print job"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/printerstate.js:81
msgid "Calculating..."
msgid "Still stabilizing..."
msgstr ""
#: src/octoprint/static/js/app/viewmodels/printerstate.js:91
msgid "Based on a linear approximation (accuracy highly dependent on the model)"
msgid ""
"Based on a linear approximation (very low accuracy, especially at the "
"beginning of the print)"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/printerstate.js:94
@ -1510,7 +1581,7 @@ msgid "Continue"
msgstr ""
#: src/octoprint/static/js/app/viewmodels/printerstate.js:146
#: src/octoprint/templates/sidebar/state.jinja2:23
#: src/octoprint/templates/sidebar/state.jinja2:24
msgid "Pause"
msgstr ""
@ -2095,7 +2166,7 @@ msgstr ""
#: src/octoprint/templates/dialogs/settings/folders.jinja2:47
#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:69
#: src/octoprint/templates/dialogs/settings/serialconnection.jinja2:90
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:69
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:75
#: src/octoprint/templates/tabs/timelapse.jinja2:13
msgid "Warning"
msgstr ""
@ -2721,42 +2792,68 @@ msgid "Release SD card"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:1
msgid "Machine State"
msgid "Current printer state"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:3
msgid "Name of file currently selected for printing"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:3
msgid "File"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:4
msgid "Current timelapse configuration"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:8
msgid "Estimated total print time base on statical analysis or past prints"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:8
msgid "Approx. Total Print Time"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:10
msgid "Total print time so far"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:10
msgid "Print Time"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:11
msgid ""
"Estimated time until the print job is done. This is only an estimate and "
"accuracy depends heavily on various factors!"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:11
msgid "Print Time Left"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:12
msgid "Bytes printed vs total bytes of file"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:12
msgid "Printed"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:22
#: src/octoprint/templates/sidebar/state.jinja2:23
msgid "Restart"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:22
#: src/octoprint/templates/sidebar/state.jinja2:23
msgid "Print"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:23
#: src/octoprint/templates/sidebar/state.jinja2:24
msgid "Resume"
msgstr ""
#: src/octoprint/templates/sidebar/state.jinja2:24
#: src/octoprint/templates/sidebar/state.jinja2:25
msgid "Cancels the print job"
msgstr ""
@ -2838,6 +2935,10 @@ msgstr ""
msgid "Model info"
msgstr ""
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:20
msgid "Layer info"
msgstr ""
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:24
msgid "Renderer options"
msgstr ""
@ -2876,13 +2977,20 @@ msgstr ""
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:65
msgid ""
"Note that the time estimates in this tab are calculated by the GCODE "
"viewer in your browser and might differ from the values calculated by the"
" server that are displayed in the \"State\" and \"Files\" panels in the "
"sidebar due to slightly different implementations."
"\n"
" Note that the time and usage values in this tab are "
"<strong>estimated</strong> by the GCODE viewer in your\n"
" browser and might differ from the values "
"<strong>estimated</strong> by the server that are displayed in the\n"
" \"State\" and \"Files\" panels in the sidebar due to slightly"
" different implementations. Also note that these\n"
" <strong>estimated</strong> values may be inaccurate since "
"they can also take information present in the\n"
" GCODE file into account.\n"
" "
msgstr ""
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:70
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:76
msgid ""
"<p>\n"
" You've selected <strong data-bind=\"text: "
@ -2899,7 +3007,7 @@ msgid ""
" </p>"
msgstr ""
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:81
#: src/octoprint/templates/tabs/gcodeviewer.jinja2:87
#, python-format
msgid "Yes, please visualize %(name)s regardless of its size"
msgstr ""