Merge branch 'devel' of github.com:foosel/OctoPrint into dev/folderSupport
This commit is contained in:
commit
070fd4a47f
29 changed files with 344 additions and 65 deletions
|
|
@ -56,6 +56,10 @@
|
|||
* Documentation improvements
|
||||
* Test buttons for webcam snapshot & stream URL, ffmpeg path and some other settings
|
||||
(see also [#183](https://github.com/foosel/OctoPrint/issues/183)).
|
||||
* Temperature graph automatically adjusts its Y axis range if necessary to
|
||||
accomodate the plotted data (see also [#632](https://github.com/foosel/OctoPrint/issues/632)).
|
||||
* "Fan on" command now always sends `S255` parameter for better compatibility
|
||||
across firmwares.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ from .analysis import QueueEntry, AnalysisQueue
|
|||
from .storage import LocalFileStorage
|
||||
from .util import AbstractFileWrapper, StreamWrapper, DiskFileWrapper
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
ContentTypeMapping = namedtuple("ContentTypeMapping", "extensions, content_type")
|
||||
ContentTypeDetector = namedtuple("ContentTypeDetector", "extensions, detector")
|
||||
|
||||
extensions = dict(
|
||||
)
|
||||
|
||||
|
|
@ -25,11 +30,11 @@ def full_extension_tree():
|
|||
result = dict(
|
||||
# extensions for 3d model files
|
||||
model=dict(
|
||||
stl=["stl"]
|
||||
stl=ContentTypeMapping(["stl"], "application/sla")
|
||||
),
|
||||
# extensions for printable machine code
|
||||
machinecode=dict(
|
||||
gcode=["gcode", "gco", "g"]
|
||||
gcode=ContentTypeMapping(["gcode", "gco", "g"], "text/plain")
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -68,8 +73,12 @@ def get_all_extensions(subtree=None):
|
|||
for key, value in subtree.items():
|
||||
if isinstance(value, dict):
|
||||
result += get_all_extensions(value)
|
||||
elif isinstance(value, (ContentTypeMapping, ContentTypeDetector)):
|
||||
result += value.extensions
|
||||
elif isinstance(value, (list, tuple)):
|
||||
result += value
|
||||
elif isinstance(subtree, (ContentTypeMapping, ContentTypeDetector)):
|
||||
result = subtree.extensions
|
||||
elif isinstance(subtree, (list, tuple)):
|
||||
result = subtree
|
||||
return result
|
||||
|
|
@ -79,7 +88,9 @@ def get_path_for_extension(extension, subtree=None):
|
|||
subtree = full_extension_tree()
|
||||
|
||||
for key, value in subtree.items():
|
||||
if isinstance(value, (list, tuple)) and extension in value:
|
||||
if isinstance(value, (ContentTypeMapping, ContentTypeDetector)) and extension in value.extensions:
|
||||
return [key]
|
||||
elif isinstance(value, (list, tuple)) and extension in value:
|
||||
return [key]
|
||||
elif isinstance(value, dict):
|
||||
path = get_path_for_extension(extension, subtree=value)
|
||||
|
|
@ -88,6 +99,23 @@ def get_path_for_extension(extension, subtree=None):
|
|||
|
||||
return None
|
||||
|
||||
def get_content_type_mapping_for_extension(extension, subtree=None):
|
||||
if not subtree:
|
||||
subtree = full_extension_tree()
|
||||
|
||||
for key, value in subtree.items():
|
||||
content_extension_matches = isinstance(value, (ContentTypeMapping, ContentTypeDetector)) and extension in value. extensions
|
||||
list_extension_matches = isinstance(value, (list, tuple)) and extension in value
|
||||
|
||||
if content_extension_matches or list_extension_matches:
|
||||
return value
|
||||
elif isinstance(value, dict):
|
||||
result = get_content_type_mapping_for_extension(extension, subtree=value)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
def valid_extension(extension, type=None):
|
||||
if not type:
|
||||
return extension in get_all_extensions()
|
||||
|
|
@ -106,6 +134,19 @@ def get_file_type(filename):
|
|||
extension = extension[1:].lower()
|
||||
return get_path_for_extension(extension)
|
||||
|
||||
def get_mime_type(filename):
|
||||
_, extension = os.path.splitext(filename)
|
||||
extension = extension[1:].lower()
|
||||
mapping = get_content_type_mapping_for_extension(extension)
|
||||
if mapping:
|
||||
if isinstance(mapping, ContentTypeMapping) and mapping.content_type is not None:
|
||||
return mapping.content_type
|
||||
elif isinstance(mapping, ContentTypeDetector) and callable(mapping.detector):
|
||||
result = mapping.detector(filename)
|
||||
if result is not None:
|
||||
return result
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
class NoSuchStorage(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms
|
|||
|
||||
import io
|
||||
|
||||
from octoprint.util import atomic_write
|
||||
|
||||
class AbstractFileWrapper(object):
|
||||
"""
|
||||
Wrapper for file representations to save to storages.
|
||||
|
|
@ -85,7 +87,7 @@ class StreamWrapper(AbstractFileWrapper):
|
|||
"""
|
||||
import shutil
|
||||
|
||||
with open(path, "wb") as dest:
|
||||
with atomic_write(path, "wb") as dest:
|
||||
with self.stream() as source:
|
||||
shutil.copyfileobj(source, dest)
|
||||
|
||||
|
|
|
|||
|
|
@ -84,13 +84,17 @@ class CoreWizardPlugin(octoprint.plugin.AssetPlugin,
|
|||
from flask import request
|
||||
from octoprint.server.api import valid_boolean_trues, NO_CONTENT
|
||||
|
||||
if "ac" in request.values and request.values["ac"] in valid_boolean_trues and \
|
||||
"user" in request.values.keys() and "pass1" in request.values.keys() and \
|
||||
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
|
||||
data = request.values
|
||||
if hasattr(request, "json") and request.json:
|
||||
data = request.json
|
||||
|
||||
if "ac" in data and data["ac"] in valid_boolean_trues and \
|
||||
"user" in data.keys() and "pass1" in data.keys() and \
|
||||
"pass2" in data.keys() and data["pass1"] == data["pass2"]:
|
||||
# configure access control
|
||||
self._settings.global_set_boolean(["accessControl", "enabled"], True)
|
||||
octoprint.server.userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"], overwrite=True)
|
||||
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
|
||||
octoprint.server.userManager.addUser(data["user"], data["pass1"], True, ["user", "admin"], overwrite=True)
|
||||
elif "ac" in data.keys() and not data["ac"] in valid_boolean_trues:
|
||||
# disable access control
|
||||
self._settings.global_set_boolean(["accessControl", "enabled"], False)
|
||||
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
|
||||
def _save_profile(self, path, profile, allow_overwrite=True):
|
||||
import yaml
|
||||
with open(path, "wb") as f:
|
||||
with octoprint.util.atomic_write(path, "wb") as f:
|
||||
yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
|
||||
def _convert_to_engine(self, profile, new_engine):
|
||||
|
|
|
|||
|
|
@ -557,7 +557,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
|
||||
try:
|
||||
import json
|
||||
with open(self._repository_cache_path, "w+b") as f:
|
||||
with octoprint.util.atomic_write(self._repository_cache_path, "wb") as f:
|
||||
json.dump(repo_data, f)
|
||||
except Exception as e:
|
||||
self._logger.exception("Error while saving repository data to {}: {}".format(self._repository_cache_path, str(e)))
|
||||
|
|
|
|||
|
|
@ -691,7 +691,7 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
# Use a string for mtime because it could be float and the
|
||||
# javascript needs to exact match
|
||||
if not sd:
|
||||
date = int(os.stat(path_on_disk).st_ctime)
|
||||
date = int(os.stat(path_on_disk).st_mtime)
|
||||
|
||||
try:
|
||||
fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk)
|
||||
|
|
|
|||
|
|
@ -332,13 +332,34 @@ class Server(object):
|
|||
|
||||
upload_suffixes = dict(name=s.get(["server", "uploads", "nameSuffix"]), path=s.get(["server", "uploads", "pathSuffix"]))
|
||||
|
||||
def mime_type_guesser(path):
|
||||
from octoprint.filemanager import get_mime_type
|
||||
return get_mime_type(path)
|
||||
|
||||
download_handler_kwargs = dict(
|
||||
as_attachment=True,
|
||||
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))
|
||||
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))
|
||||
|
||||
def joined_dict(*dicts):
|
||||
if not len(dicts):
|
||||
return dict()
|
||||
|
||||
joined = dict()
|
||||
for d in dicts:
|
||||
joined.update(d)
|
||||
return joined
|
||||
|
||||
server_routes = self._router.urls + [
|
||||
# various downloads
|
||||
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("timelapse"), as_attachment=True)),
|
||||
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))),
|
||||
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))),
|
||||
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("timelapse")), download_handler_kwargs, no_hidden_files_validator)),
|
||||
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("uploads")), download_handler_kwargs, no_hidden_files_validator, additional_mime_types)),
|
||||
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("logs")), download_handler_kwargs, admin_validator)),
|
||||
# camera snapshot
|
||||
(r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
|
||||
(r"/downloads/camera/current", util.tornado.UrlProxyHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
|
||||
# generated webassets
|
||||
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets")))
|
||||
]
|
||||
|
|
|
|||
|
|
@ -768,6 +768,8 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
|
|||
:class:``~tornado.web.StaticFileHandler`` as the ``default_filename`` keyword parameter). Defaults to ``None``.
|
||||
as_attachment (bool): Whether to serve requested files with ``Content-Disposition: attachment`` header (``True``)
|
||||
or not. Defaults to ``False``.
|
||||
allow_client_caching (bool): Whether to allow the client to cache (by not setting any ``Cache-Control`` or
|
||||
``Expires`` headers on the response) or not.
|
||||
access_validation (function): Callback to call in the ``get`` method to validate access to the resource. Will
|
||||
be called with ``self.request`` as parameter which contains the full tornado request object. Should raise
|
||||
a ``tornado.web.HTTPError`` if access is not allowed in which case the request will not be further processed.
|
||||
|
|
@ -776,13 +778,22 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
|
|||
with the requested path as parameter. Should raise a ``tornado.web.HTTPError`` (e.g. an 404) if the requested
|
||||
path does not pass validation in which case the request will not be further processed.
|
||||
Defaults to ``None`` and hence no path validation being performed.
|
||||
etag_generator (function): Callback to call for generating the value of the ETag response header. Will be
|
||||
called with the response handler as parameter. May return ``None`` to prevent the ETag response header
|
||||
from being set. If not provided the last modified time of the file in question will be used as returned
|
||||
by ``get_content_version``.
|
||||
"""
|
||||
|
||||
def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None, path_validation=None):
|
||||
def initialize(self, path, default_filename=None, as_attachment=False, allow_client_caching=True,
|
||||
access_validation=None, path_validation=None, etag_generator=None,
|
||||
mime_type_guesser=None):
|
||||
tornado.web.StaticFileHandler.initialize(self, os.path.abspath(path), default_filename)
|
||||
self._as_attachment = as_attachment
|
||||
self._allow_client_caching = allow_client_caching
|
||||
self._access_validation = access_validation
|
||||
self._path_validation = path_validation
|
||||
self._etag_generator = etag_generator
|
||||
self._mime_type_guesser = mime_type_guesser
|
||||
|
||||
def get(self, path, include_body=True):
|
||||
if self._access_validation is not None:
|
||||
|
|
@ -800,6 +811,24 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
|
|||
if self._as_attachment:
|
||||
self.set_header("Content-Disposition", "attachment; filename=%s" % os.path.basename(path))
|
||||
|
||||
if not self._allow_client_caching:
|
||||
self.set_header("Cache-Control", "max-age=0, must-revalidate, private")
|
||||
self.set_header("Expires", "-1")
|
||||
|
||||
def compute_etag(self):
|
||||
if self._etag_generator is not None:
|
||||
return self._etag_generator(self)
|
||||
else:
|
||||
return self.get_content_version(self.absolute_path)
|
||||
|
||||
def get_content_type(self):
|
||||
if self._mime_type_guesser is not None:
|
||||
type = self._mime_type_guesser(self.absolute_path)
|
||||
if type is not None:
|
||||
return type
|
||||
|
||||
return tornado.web.StaticFileHandler.get_content_type(self)
|
||||
|
||||
@classmethod
|
||||
def get_content_version(cls, abspath):
|
||||
import os
|
||||
|
|
@ -809,7 +838,7 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
|
|||
##~~ URL Forward Handler for forwarding requests to a preconfigured static URL
|
||||
|
||||
|
||||
class UrlForwardHandler(tornado.web.RequestHandler):
|
||||
class UrlProxyHandler(tornado.web.RequestHandler):
|
||||
"""
|
||||
`tornado.web.RequestHandler <http://tornado.readthedocs.org/en/branch4.0/web.html#request-handlers>`_ that proxies
|
||||
requests to a preconfigured url and returns the response. Allows delivery of the requested content as attachment
|
||||
|
|
@ -819,7 +848,7 @@ class UrlForwardHandler(tornado.web.RequestHandler):
|
|||
for making the request to the configured endpoint and return the body of the client response with the status code
|
||||
from the client response and the following headers:
|
||||
|
||||
* ``Date``, ``Cache-Control``, ``Server``, ``Content-Type`` and ``Location`` will be copied over.
|
||||
* ``Date``, ``Cache-Control``, ``Expires``, ``ETag``, ``Server``, ``Content-Type`` and ``Location`` will be copied over.
|
||||
* If ``as_attachment`` is set to True, ``Content-Disposition`` will be set to ``attachment``. If ``basename`` is
|
||||
set including the attachement's ``filename`` attribute will be set to the base name followed by the extension
|
||||
guessed based on the MIME type from the ``Content-Type`` header of the response. If no extension can be guessed
|
||||
|
|
@ -869,7 +898,7 @@ class UrlForwardHandler(tornado.web.RequestHandler):
|
|||
filename = None
|
||||
|
||||
self.set_status(response.code)
|
||||
for name in ("Date", "Cache-Control", "Server", "Content-Type", "Location"):
|
||||
for name in ("Date", "Cache-Control", "Server", "Content-Type", "Location", "Expires", "ETag"):
|
||||
value = response.headers.get(name)
|
||||
if value:
|
||||
self.set_header(name, value)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ from octoprint.server import app, userManager, pluginManager, gettext, \
|
|||
debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY, BRANCH
|
||||
from octoprint.settings import settings
|
||||
|
||||
import re
|
||||
|
||||
from . import util
|
||||
|
||||
import logging
|
||||
|
|
@ -26,12 +28,21 @@ _templates = None
|
|||
_plugin_names = None
|
||||
_plugin_vars = None
|
||||
|
||||
_valid_id_re = re.compile("[a-z_]+")
|
||||
_valid_div_re = re.compile("[a-zA-Z_-]+")
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
force_refresh = util.flask.cache_check_headers() or "_refresh" in request.values
|
||||
|
||||
global _templates, _plugin_names, _plugin_vars
|
||||
|
||||
# helper to check if wizards are active
|
||||
def wizard_active(templates):
|
||||
return templates is not None and bool(templates["wizard"]["order"])
|
||||
|
||||
# we force a refresh if the client forces one or if we have wizards cached
|
||||
force_refresh = util.flask.cache_check_headers() or "_refresh" in request.values or wizard_active(_templates)
|
||||
|
||||
# if we need to refresh our template cache or it's not yet set, process it
|
||||
if force_refresh or _templates is None or _plugin_names is None or _plugin_vars is None:
|
||||
_templates, _plugin_names, _plugin_vars = _process_templates()
|
||||
|
||||
|
|
@ -53,9 +64,8 @@ def index():
|
|||
break
|
||||
|
||||
else:
|
||||
wizard = bool(_templates["wizard"]["order"])
|
||||
wizard = wizard_active(_templates)
|
||||
enable_accesscontrol = userManager is not None
|
||||
|
||||
render_kwargs.update(dict(
|
||||
webcamStream=settings().get(["webcam", "stream"]),
|
||||
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
|
||||
|
|
@ -70,7 +80,8 @@ def index():
|
|||
# no plugin took an interest, we'll use the default UI
|
||||
def make_default_ui():
|
||||
r = make_response(render_template("index.jinja2", **render_kwargs))
|
||||
if bool(render_kwargs["templates"]["wizard"]["order"]):
|
||||
if wizard:
|
||||
# if we have active wizard dialogs, set non caching headers
|
||||
r = util.flask.add_non_caching_response_headers(r)
|
||||
return r
|
||||
|
||||
|
|
@ -466,6 +477,9 @@ def _process_template_config(name, implementation, rule, config=None, counter=1)
|
|||
data["_div"] = rule["div"](name)
|
||||
if "suffix" in data:
|
||||
data["_div"] = data["_div"] + data["suffix"]
|
||||
if not _valid_div_re.match(data["_div"]):
|
||||
_logger.warn("Template config {} contains invalid div identifier {}, skipping it".format(name, data["_div"]))
|
||||
return None
|
||||
|
||||
if not "template" in data:
|
||||
data["template"] = rule["template"](name)
|
||||
|
|
@ -477,6 +491,7 @@ def _process_template_config(name, implementation, rule, config=None, counter=1)
|
|||
data_bind = "allowBindings: true"
|
||||
if "data_bind" in data:
|
||||
data_bind = data_bind + ", " + data["data_bind"]
|
||||
data_bind = data_bind.replace("\"", "\\\"")
|
||||
data["data_bind"] = data_bind
|
||||
|
||||
data["_key"] = "plugin_" + name
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import logging
|
|||
import re
|
||||
import uuid
|
||||
|
||||
from octoprint.util import atomic_write
|
||||
|
||||
_APPNAME = "OctoPrint"
|
||||
|
||||
_instance = None
|
||||
|
|
@ -1079,7 +1081,7 @@ class Settings(object):
|
|||
path, _ = os.path.split(filename)
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
with open(filename, "w+") as f:
|
||||
with atomic_write(filename, "wb") as f:
|
||||
f.write(script)
|
||||
|
||||
def _default_basedir(applicationName):
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,6 @@ function DataUpdater(allViewModels) {
|
|||
self._configHash = undefined;
|
||||
|
||||
self.reloadOverlay = $("#reloadui_overlay");
|
||||
$("#reloadui_overlay_reload").click(function() { location.reload(true); });
|
||||
|
||||
self.connect = function() {
|
||||
OctoPrint.socket.connect({debug: !!SOCKJS_DEBUG});
|
||||
|
|
|
|||
|
|
@ -705,7 +705,7 @@ $(function() {
|
|||
data = getOnlyChangedData(self.getLocalData(), self.lastReceivedSettings);
|
||||
}
|
||||
|
||||
OctoPrint.settings.save(data)
|
||||
return OctoPrint.settings.save(data)
|
||||
.done(function(data, status, xhr) {
|
||||
self.receiving(true);
|
||||
self.sending(false);
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@ $(function() {
|
|||
var heaterOptions = self.heaterOptions();
|
||||
if (!heaterOptions) return;
|
||||
|
||||
var maxTemps = [310/1.1];
|
||||
|
||||
_.each(_.keys(heaterOptions), function(type) {
|
||||
if (type == "bed" && !self.hasBed()) {
|
||||
return;
|
||||
|
|
@ -258,12 +260,31 @@ $(function() {
|
|||
color: pusher.color(heaterOptions[type].color).tint(0.5).html(),
|
||||
data: targets
|
||||
});
|
||||
|
||||
maxTemps.push(self.getMaxTemp(actuals, targets));
|
||||
});
|
||||
|
||||
self.plotOptions.yaxis.max = Math.max.apply(null, maxTemps) * 1.1;
|
||||
$.plot(graph, data, self.plotOptions);
|
||||
}
|
||||
};
|
||||
|
||||
self.getMaxTemp = function(actuals, targets) {
|
||||
var pair;
|
||||
var maxTemp = 0;
|
||||
actuals.forEach(function(pair) {
|
||||
if (pair[1] > maxTemp){
|
||||
maxTemp = pair[1];
|
||||
}
|
||||
});
|
||||
targets.forEach(function(pair) {
|
||||
if (pair[1] > maxTemp){
|
||||
maxTemp = pair[1];
|
||||
}
|
||||
});
|
||||
return maxTemp;
|
||||
}
|
||||
|
||||
self.setTarget = function(item) {
|
||||
var value = item.newTarget();
|
||||
if (!value) return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ $(function() {
|
|||
self.settingsViewModel = parameters[1];
|
||||
|
||||
self.wizardDialog = undefined;
|
||||
self.reloadOverlay = undefined;
|
||||
|
||||
self.allViewModels = undefined;
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ $(function() {
|
|||
|
||||
self.onStartup = function() {
|
||||
self.wizardDialog = $("#wizard_dialog");
|
||||
self.reloadOverlay = $("#reloadui_overlay");
|
||||
};
|
||||
|
||||
self.onUserLoggedIn = function() {
|
||||
|
|
@ -126,8 +128,7 @@ $(function() {
|
|||
.done(function() {
|
||||
self.closeDialog();
|
||||
if (reload) {
|
||||
log.info("Wizard requested reloading");
|
||||
location.reload(true);
|
||||
self.reloadOverlay.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -144,12 +145,27 @@ $(function() {
|
|||
};
|
||||
|
||||
self.finishWizard = function() {
|
||||
var deferred = $.Deferred();
|
||||
self.finishing = true;
|
||||
self.settingsViewModel.saveData();
|
||||
return OctoPrint.wizard.finish(self.wizards)
|
||||
.always(function() {
|
||||
self.finishing = false;
|
||||
|
||||
self.settingsViewModel.saveData()
|
||||
.done(function() {
|
||||
OctoPrint.wizard.finish(self.wizards)
|
||||
.done(function() {
|
||||
deferred.resolve(arguments);
|
||||
})
|
||||
.fail(function() {
|
||||
deferred.reject(arguments);
|
||||
})
|
||||
.always(function() {
|
||||
self.finishing = false;
|
||||
});
|
||||
})
|
||||
.fail(function() {
|
||||
deferred.reject(arguments);
|
||||
});
|
||||
|
||||
return deferred;
|
||||
};
|
||||
|
||||
self.onSettingsPreventRefresh = function() {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
class="{% if mark_active %}active{% set mark_active = False %}{% endif %} {% if "classes_link" in data %}{{ data.classes_link|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
|
||||
{% if "styles_link" in data %} style="{{ data.styles_link|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}" {% endif %}
|
||||
>
|
||||
<a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a>
|
||||
<a href="#{{ data._div }}" data-toggle="tab">{{ entry|e }}</a>
|
||||
</li>
|
||||
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- /ko -->{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
class="{% if mark_active %}active{% set mark_active = False %}{% endif %} {% if "classes_link" in data %}{{ data.classes_link|join(' ') }}{% elif "classes" in data %}{{ data.classes|join(' ') }}{% endif %}"
|
||||
{% if "styles_link" in data %} style="{{ data.styles_link|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}" {% endif %}
|
||||
>
|
||||
<a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a>
|
||||
<a href="#{{ data._div }}" data-toggle="tab">{{ entry|e }}</a>
|
||||
</li>
|
||||
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- /ko -->{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
>
|
||||
<div class="accordion-heading">
|
||||
<a class="accordion-toggle" data-toggle="collapse" data-target="#{{ data._div }}">
|
||||
{% if "icon" in data %}<i class="icon-{{ data.icon }}"></i> {% endif %}{{ entry }}
|
||||
{% if "icon" in data %}<i class="icon-{{ data.icon }}"></i> {% endif %}{{ entry|e }}
|
||||
</a>
|
||||
{% if "template_header" in data %}
|
||||
{% include data.template_header ignore missing %}
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
{% if "data_bind" in data %}data-bind="{{ data.data_bind }}"{% endif %}
|
||||
{% if "styles_link" in data %} style="{{ data.styles_link|join(', ') }}" {% elif "styles" in data %} style="{{ data.styles|join(', ') }}" {% endif %}
|
||||
>
|
||||
<a href="#{{ data._div }}" data-toggle="tab">{{ entry }}</a>
|
||||
<a href="#{{ data._div }}" data-toggle="tab">{{ entry|e }}</a>
|
||||
</li>
|
||||
{% if "custom_bindings" not in data or data["custom_bindings"] %}<!-- /ko -->{% endif %}
|
||||
{% endfor %}
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
</div>
|
||||
<div class="footer">
|
||||
<ul class="pull-left muted">
|
||||
<li><small>{{ _('Version') }}: <span class="version">{{ display_version }}</span></small></li>
|
||||
<li><small>{{ _('Version') }}: <span class="version">{{ display_version|e }}</span></small></li>
|
||||
</ul>
|
||||
<ul class="pull-right">
|
||||
<li><a href="http://octoprint.org"><i class="icon-home"></i> {{ _('Homepage') }}</a></li>
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@
|
|||
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
|
||||
var CONFIG_LOGFILESPERPAGE = 10;
|
||||
var CONFIG_USERSPERPAGE = 10;
|
||||
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
|
||||
var CONFIG_WEBCAM_STREAM = "{{ webcamStream|e }}";
|
||||
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
|
||||
var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %}
|
||||
var CONFIG_FIRST_RUN = {% if firstRun -%} true; {% else %} false; {%- endif %}
|
||||
var CONFIG_TEMPERATURE_GRAPH = {% if enableTemperatureGraph -%} true; {% else %} false; {%- endif %}
|
||||
var CONFIG_GCODE_SIZE_THRESHOLD = {{ gcodeThreshold }};
|
||||
var CONFIG_GCODE_MOBILE_SIZE_THRESHOLD = {{ gcodeMobileThreshold }};
|
||||
var CONFIG_GCODE_SIZE_THRESHOLD = {{ gcodeThreshold|e }};
|
||||
var CONFIG_GCODE_MOBILE_SIZE_THRESHOLD = {{ gcodeMobileThreshold|e }};
|
||||
var CONFIG_WIZARD = {% if wizard -%} true; {% else %} false; {%- endif %}
|
||||
|
||||
var SOCKJS_URI = "{{ url_for('index') }}" + "sockjs";
|
||||
|
|
@ -24,11 +24,11 @@
|
|||
// sockjs should define CLOSE_NORMAL for us, but they don't (from ws spec)
|
||||
var SOCKJS_CLOSE_NORMAL = 1000;
|
||||
|
||||
var UI_API_KEY = "{{ uiApiKey }}";
|
||||
var VERSION = "{{ version.number }}";
|
||||
var DISPLAY_VERSION = "{{ version.display }}";
|
||||
var BRANCH = "{{ version.branch }}";
|
||||
var LOCALE = "{{ g.locale }}";
|
||||
var UI_API_KEY = "{{ uiApiKey|e }}";
|
||||
var VERSION = "{{ version.number|e }}";
|
||||
var DISPLAY_VERSION = "{{ version.display|e }}";
|
||||
var BRANCH = "{{ version.branch|e }}";
|
||||
var LOCALE = "{{ g.locale|e }}";
|
||||
var AVAILABLE_LOCALES = {{ locales|tojson }};
|
||||
|
||||
var OCTOPRINT_VIEWMODELS = [];
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@
|
|||
<div class="container">
|
||||
<div class="hero-unit">
|
||||
<h1>{{ _('Please reload') }}</h1>
|
||||
<p>{{ _('There is a new version of the server active now, a reload of the user interface is needed. This will not interrupt any print jobs you might have ongoing. Please reload the web interface now by clicking the button below.') }}</p>
|
||||
<p>{% trans %}
|
||||
There is a new version of the server active now, a reload
|
||||
of the user interface is needed. This will not interrupt
|
||||
any print jobs you might have ongoing. Please reload the
|
||||
web interface now by clicking the button below.
|
||||
{% endtrans %}</p>
|
||||
<p>
|
||||
<a class="btn btn-primary btn-large" id="reloadui_overlay_reload">{{ _('Reload now') }}</a>
|
||||
<a class="btn btn-primary btn-large" title="{{ _('Reload now') }}" href="javascript:void(0)" onclick="location.reload(true); return false;">{{ _('Reload now') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@
|
|||
<h1>{{ _('General') }}</h1>
|
||||
<div>
|
||||
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }">{{ _('Motors off') }}</button>
|
||||
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">{{ _('Fan on') }}</button>
|
||||
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S255'}) }">{{ _('Fan on') }}</button>
|
||||
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">{{ _('Fan off') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
<label for="webcam_timelapse_mode">{{ _('Timelapse Mode') }}</label>
|
||||
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
|
||||
<option value="off">{{ _('Off') }}</option>
|
||||
<option value="zchange">{{ _('On Z Change') }}</option>
|
||||
<option value="timed">{{ _('Timed') }}</option>
|
||||
<option value="zchange">{{ _('On Z Change') }}</option>
|
||||
</select>
|
||||
<span class="help-block" data-bind="visible: timelapseType() == 'zchange'"><span class="label label-warning">{{ _('Warning') }}</span> {{ _('Do not use with spiralized ("Joris") vases or similar continuous Z models.') }}</span>
|
||||
<span class="help-block" data-bind="visible: timelapseType() == 'zchange'"><span class="label label-info">{{ _('Note') }}</span> {% trans %}Does not work when printing from the printer's SD Card (no way to detect the change in Z reliably). Use "Timed" mode for those prints instead.{% endtrans %}</span>
|
||||
|
||||
<label for="webcam_timelapse_fps">{{ _('Timelapse frame rate (in frames per second)') }}</label>
|
||||
<div class="input-append">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import logging
|
|||
|
||||
from octoprint.settings import settings
|
||||
|
||||
from octoprint.util import atomic_write
|
||||
|
||||
class UserManager(object):
|
||||
valid_roles = ["user", "admin"]
|
||||
|
||||
|
|
@ -217,7 +219,7 @@ class FilebasedUserManager(UserManager):
|
|||
"settings": user._settings
|
||||
}
|
||||
|
||||
with open(self._userfile, "wb") as f:
|
||||
with atomic_write(self._userfile, "wb") as f:
|
||||
yaml.safe_dump(data, f, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
self._dirty = False
|
||||
self._load()
|
||||
|
|
|
|||
|
|
@ -510,6 +510,29 @@ def atomic_write(filename, mode="w+b", prefix="tmp", suffix=""):
|
|||
shutil.move(temp_config.name, filename)
|
||||
|
||||
|
||||
def bom_aware_open(filename, encoding="ascii", mode="r", **kwargs):
|
||||
import codecs
|
||||
|
||||
codec = codecs.lookup(encoding)
|
||||
encoding = codec.name
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = dict()
|
||||
|
||||
potential_bom_attribute = "BOM_" + codec.name.replace("utf-", "utf").upper()
|
||||
if "r" in mode and hasattr(codecs, potential_bom_attribute):
|
||||
# these encodings might have a BOM, so let's see if there is one
|
||||
bom = getattr(codecs, potential_bom_attribute)
|
||||
|
||||
with open(filename, "rb") as f:
|
||||
header = f.read(4)
|
||||
|
||||
if header.startswith(bom):
|
||||
encoding += "-sig"
|
||||
|
||||
return codecs.open(filename, encoding=encoding, **kwargs)
|
||||
|
||||
|
||||
class RepeatedTimer(threading.Thread):
|
||||
"""
|
||||
This class represents an action that should be run repeatedly in an interval. It is similar to python's
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from octoprint.settings import settings, default_settings
|
|||
from octoprint.events import eventManager, Events
|
||||
from octoprint.filemanager import valid_file_type
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii, CountedEvent, RepeatedTimer
|
||||
from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii, CountedEvent, RepeatedTimer, to_unicode, bom_aware_open
|
||||
|
||||
try:
|
||||
import _winreg
|
||||
|
|
@ -548,7 +548,7 @@ class MachineCom(object):
|
|||
self._clear_to_send.set()
|
||||
|
||||
def sendCommand(self, cmd, cmd_type=None, processed=False, force=False):
|
||||
cmd = cmd.encode('ascii', 'replace')
|
||||
cmd = to_unicode(cmd, errors="replace")
|
||||
if not processed:
|
||||
cmd = process_gcode_line(cmd)
|
||||
if not cmd:
|
||||
|
|
@ -1631,10 +1631,11 @@ class MachineCom(object):
|
|||
command_allowing_checksum = gcode is not None or self._sendChecksumWithUnknownCommands
|
||||
checksum_enabled = self._alwaysSendChecksum or (self.isPrinting() and not self._neverSendChecksum)
|
||||
|
||||
command_to_send = command.encode("ascii", errors="replace")
|
||||
if command_requiring_checksum or (command_allowing_checksum and checksum_enabled):
|
||||
self._doIncrementAndSendWithChecksum(command)
|
||||
self._doIncrementAndSendWithChecksum(command_to_send)
|
||||
else:
|
||||
self._doSendWithoutChecksum(command)
|
||||
self._doSendWithoutChecksum(command_to_send)
|
||||
|
||||
# trigger "sent" phase and use up one "ok"
|
||||
self._process_command_phase("sent", command, command_type, gcode=gcode)
|
||||
|
|
@ -2039,7 +2040,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
|
|||
Opens the file for reading and determines the file size.
|
||||
"""
|
||||
PrintingFileInformation.start(self)
|
||||
self._handle = open(self._filename, "r")
|
||||
self._handle = bom_aware_open(self._filename, encoding="utf-8", errors="replace")
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
|
|
@ -2069,7 +2070,7 @@ class PrintingGcodeFileInformation(PrintingFileInformation):
|
|||
if self._handle is None:
|
||||
# file got closed just now
|
||||
return None
|
||||
line = self._handle.readline()
|
||||
line = to_unicode(self._handle.readline())
|
||||
if not line:
|
||||
self.close()
|
||||
processed = process_gcode_line(line, offsets=offsets, current_tool=current_tool)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ class gcode(object):
|
|||
if os.path.isfile(filename):
|
||||
self.filename = filename
|
||||
self._fileSize = os.stat(filename).st_size
|
||||
with open(filename, "r") as f:
|
||||
|
||||
import codecs
|
||||
with codecs.open(filename, encoding="utf-8", errors="replace") as f:
|
||||
self._load(f, printer_profile, throttle=throttle)
|
||||
|
||||
def abort(self):
|
||||
|
|
|
|||
|
|
@ -8,4 +8,3 @@ 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) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,98 @@ import mock
|
|||
import octoprint.filemanager
|
||||
import octoprint.filemanager.util
|
||||
|
||||
class FilemanagerMethodTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# mock plugin manager
|
||||
self.plugin_manager_patcher = mock.patch("octoprint.plugin.plugin_manager")
|
||||
self.plugin_manager_getter = self.plugin_manager_patcher.start()
|
||||
|
||||
self.plugin_manager = mock.MagicMock()
|
||||
|
||||
hook_extensions = dict(
|
||||
some_plugin=lambda: dict(dict(machinecode=dict(foo=["foo", "f"]))),
|
||||
other_plugin=lambda: dict(dict(model=dict(amf=["amf"]))),
|
||||
mime_map=lambda: dict(
|
||||
mime_map=dict(
|
||||
mime_map_yes=octoprint.filemanager.ContentTypeMapping(["mime_map_yes"], "application/mime_map_yes")
|
||||
)
|
||||
),
|
||||
mime_detect=lambda: dict(
|
||||
dict(
|
||||
machinecode=dict(
|
||||
mime_detect_yes=octoprint.filemanager.ContentTypeDetector(["mime_detect_yes"], lambda x: "application/mime_detect_yes"),
|
||||
mime_detect_no=octoprint.filemanager.ContentTypeDetector(["mime_detect_no"], lambda x: None)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
self.plugin_manager.get_hooks.return_value = hook_extensions
|
||||
|
||||
self.plugin_manager_getter.return_value = self.plugin_manager
|
||||
|
||||
def tearDown(self):
|
||||
self.plugin_manager_patcher.stop()
|
||||
|
||||
def test_full_extension_tree(self):
|
||||
full = octoprint.filemanager.full_extension_tree()
|
||||
self.assertTrue("machinecode" in full)
|
||||
self.assertTrue("gcode" in full["machinecode"])
|
||||
self.assertTrue(isinstance(full["machinecode"]["gcode"], octoprint.filemanager.ContentTypeMapping))
|
||||
self.assertItemsEqual(["gcode", "gco", "g"], full["machinecode"]["gcode"].extensions)
|
||||
self.assertTrue("foo" in full["machinecode"])
|
||||
self.assertTrue(isinstance(full["machinecode"]["foo"], list))
|
||||
self.assertItemsEqual(["f", "foo"], full["machinecode"]["foo"])
|
||||
|
||||
self.assertTrue("model" in full)
|
||||
self.assertTrue("stl" in full["model"])
|
||||
self.assertTrue(isinstance(full["model"]["stl"], octoprint.filemanager.ContentTypeMapping))
|
||||
self.assertItemsEqual(["stl"], full["model"]["stl"].extensions)
|
||||
self.assertTrue("amf" in full["model"])
|
||||
self.assertTrue(isinstance(full["model"]["amf"], list))
|
||||
self.assertItemsEqual(["amf"], full["model"]["amf"])
|
||||
|
||||
def test_get_mimetype(self):
|
||||
self.assertEquals(octoprint.filemanager.get_mime_type("foo.stl"), "application/sla")
|
||||
self.assertEquals(octoprint.filemanager.get_mime_type("foo.gcode"), "text/plain")
|
||||
self.assertEquals(octoprint.filemanager.get_mime_type("foo.unknown"), "application/octet-stream")
|
||||
self.assertEquals(octoprint.filemanager.get_mime_type("foo.mime_map_yes"), "application/mime_map_yes")
|
||||
self.assertEquals(octoprint.filemanager.get_mime_type("foo.mime_map_no"), "application/octet-stream")
|
||||
self.assertEquals(octoprint.filemanager.get_mime_type("foo.mime_detect_yes"), "application/mime_detect_yes")
|
||||
self.assertEquals(octoprint.filemanager.get_mime_type("foo.mime_detect_no"), "application/octet-stream")
|
||||
|
||||
def test_valid_file_type(self):
|
||||
self.assertTrue(octoprint.filemanager.valid_file_type("foo.stl", type="model"))
|
||||
self.assertTrue(octoprint.filemanager.valid_file_type("foo.stl", type="stl"))
|
||||
self.assertFalse(octoprint.filemanager.valid_file_type("foo.stl", type="machinecode"))
|
||||
self.assertTrue(octoprint.filemanager.valid_file_type("foo.foo", type="machinecode"))
|
||||
self.assertTrue(octoprint.filemanager.valid_file_type("foo.foo", type="foo"))
|
||||
self.assertTrue(octoprint.filemanager.valid_file_type("foo.foo"))
|
||||
self.assertTrue(octoprint.filemanager.valid_file_type("foo.mime_map_yes"))
|
||||
self.assertTrue(octoprint.filemanager.valid_file_type("foo.mime_detect_yes"))
|
||||
self.assertFalse(octoprint.filemanager.valid_file_type("foo.unknown"))
|
||||
|
||||
def test_get_file_type(self):
|
||||
self.assertEquals(["machinecode", "gcode"], octoprint.filemanager.get_file_type("foo.gcode"))
|
||||
self.assertEquals(["machinecode", "gcode"], octoprint.filemanager.get_file_type("foo.gco"))
|
||||
self.assertEquals(["machinecode", "foo"], octoprint.filemanager.get_file_type("foo.f"))
|
||||
self.assertEquals(["model", "stl"], octoprint.filemanager.get_file_type("foo.stl"))
|
||||
self.assertEquals(["model", "amf"], octoprint.filemanager.get_file_type("foo.amf"))
|
||||
self.assertIsNone(octoprint.filemanager.get_file_type("foo.unknown"))
|
||||
|
||||
def test_hook_failure(self):
|
||||
def hook():
|
||||
raise RuntimeError("Boo!")
|
||||
self.plugin_manager.get_hooks.return_value = dict(hook=hook)
|
||||
|
||||
with mock.patch("octoprint.filemanager.logging") as patched_logging:
|
||||
logger = mock.MagicMock()
|
||||
patched_logging.getLogger.return_value = logger
|
||||
|
||||
octoprint.filemanager.get_all_extensions()
|
||||
|
||||
self.assertEquals(1, len(logger.mock_calls))
|
||||
|
||||
class FileManagerTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -109,13 +201,13 @@ class FileManagerTest(unittest.TestCase):
|
|||
self.assertEquals(metadata, expected)
|
||||
self.local_storage.get_metadata.assert_called_once_with("test.file")
|
||||
|
||||
@mock.patch("__builtin__.open", new_callable=mock.mock_open)
|
||||
@mock.patch("octoprint.filemanager.util.atomic_write")
|
||||
@mock.patch("io.FileIO")
|
||||
@mock.patch("shutil.copyfileobj")
|
||||
@mock.patch("os.remove")
|
||||
@mock.patch("tempfile.NamedTemporaryFile")
|
||||
@mock.patch("time.time", side_effect=[1411979916.422, 1411979932.116])
|
||||
def test_slice(self, mocked_time, mocked_tempfile, mocked_os, mocked_shutil, mocked_fileio, mocked_open):
|
||||
def test_slice(self, mocked_time, mocked_tempfile, mocked_os, mocked_shutil, mocked_fileio, mocked_atomic_write):
|
||||
callback = mock.MagicMock()
|
||||
callback_args = ("one", "two", "three")
|
||||
|
||||
|
|
@ -187,8 +279,8 @@ class FileManagerTest(unittest.TestCase):
|
|||
self.local_storage.add_file.assert_called_once_with("dest.file", mock.ANY, printer_profile=expected_printer_profile, allow_overwrite=True, links=expected_links)
|
||||
|
||||
# assert that the generated gcode was manipulated as required
|
||||
expected_open_calls = [mock.call("prefix/dest.file", "wb")]
|
||||
self.assertEquals(mocked_open.call_args_list, expected_open_calls)
|
||||
expected_atomic_write_calls = [mock.call("prefix/dest.file", "wb")]
|
||||
self.assertEquals(mocked_atomic_write.call_args_list, expected_atomic_write_calls)
|
||||
#mocked_open.return_value.write.assert_called_once_with(";Generated from source.file aabbccddeeff\r")
|
||||
|
||||
# assert that shutil was asked to copy the concatenated multistream
|
||||
|
|
|
|||
Loading…
Reference in a new issue