Merge branch 'devel' into dev/gcodeScripts
This commit is contained in:
commit
8da9f55b05
20 changed files with 282 additions and 139 deletions
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "2.7"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -e .[develop]
|
||||
script:
|
||||
- nosetests
|
||||
- nosetests tests/
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-napoleon
|
||||
sphinx_rtd_theme
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
# Testing dependencies
|
||||
mock>=1.0.1
|
||||
nose>=1.3.0
|
||||
sphinxcontrib-httpdomain
|
||||
sphinx_rtd_theme
|
||||
po2json
|
||||
ddt
|
||||
|
||||
# Documentation dependencies
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-napoleon
|
||||
sphinx_rtd_theme
|
||||
|
||||
# Translation dependencies
|
||||
po2json
|
||||
|
|
|
|||
15
setup.py
15
setup.py
|
|
@ -250,6 +250,10 @@ def get_cmdclass():
|
|||
return cmdclass
|
||||
|
||||
|
||||
def requirements(filename):
|
||||
return filter(lambda line: line and not line.startswith("#"), map(lambda line: line.strip(), open(filename).read().split("\n")))
|
||||
|
||||
|
||||
def params():
|
||||
name = "OctoPrint"
|
||||
version = versioneer.get_version()
|
||||
|
|
@ -287,7 +291,16 @@ def params():
|
|||
|
||||
include_package_data = True
|
||||
zip_safe = False
|
||||
install_requires = open("requirements.txt").read().split("\n")
|
||||
install_requires = requirements("requirements.txt")
|
||||
extras_require = dict(
|
||||
develop=requirements("requirements-dev.txt")
|
||||
)
|
||||
|
||||
if os.environ.get('READTHEDOCS', None) == 'True':
|
||||
# we can't tell read the docs to please perform a pip install -e .[develop], so we help
|
||||
# it a bit here by explicitely adding the development dependencies, which include our
|
||||
# documentation dependencies
|
||||
install_requires = install_requires + extras_require['develop']
|
||||
|
||||
entry_points = {
|
||||
"console_scripts": [
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
import logging
|
||||
import os
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
from octoprint.events import eventManager, Events
|
||||
from octoprint.plugin import plugin_manager, ProgressPlugin
|
||||
|
||||
from .destinations import FileDestinations
|
||||
from .analysis import QueueEntry, AnalysisQueue
|
||||
|
|
@ -114,7 +115,7 @@ class FileManager(object):
|
|||
self._slicing_progress_callbacks = []
|
||||
self._last_slicing_progress = None
|
||||
|
||||
self._progress_plugins = plugin_manager().get_implementations(ProgressPlugin)
|
||||
self._progress_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.ProgressPlugin)
|
||||
|
||||
for storage_type, storage_manager in self._storage_managers.items():
|
||||
self._determine_analysis_backlog(storage_type, storage_manager)
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ def uploadGcodeFile(target):
|
|||
futureFilename = fileManager.sanitize_name(FileDestinations.LOCAL, upload.filename)
|
||||
except:
|
||||
futureFilename = None
|
||||
if futureFilename is None or not (slicingManager.slicing_enabled or octoprint.filemanager.valid_file_type(futureFilename, type="gcode")):
|
||||
if futureFilename is None:
|
||||
return make_response("Can not upload file %s, wrong format?" % upload.filename, 415)
|
||||
|
||||
# prohibit overwriting currently selected file while it's being printed
|
||||
|
|
|
|||
|
|
@ -12,19 +12,28 @@ from octoprint.server import slicingManager
|
|||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server.api import api, NO_CONTENT
|
||||
|
||||
from octoprint.settings import settings as s
|
||||
from octoprint.settings import settings as s, valid_boolean_trues
|
||||
|
||||
from octoprint.slicing import SlicerNotConfigured
|
||||
|
||||
|
||||
@api.route("/slicing", methods=["GET"])
|
||||
def slicingListAll():
|
||||
default_slicer = s().get(["slicing", "defaultSlicer"])
|
||||
|
||||
if "configured" in request.values and request.values["configured"] in valid_boolean_trues:
|
||||
slicers = slicingManager.configured_slicers
|
||||
else:
|
||||
slicers = slicingManager.registered_slicers
|
||||
|
||||
result = dict()
|
||||
for slicer in slicingManager.registered_slicers:
|
||||
for slicer in slicers:
|
||||
slicer_impl = slicingManager.get_slicer(slicer, require_configured=False)
|
||||
result[slicer] = dict(
|
||||
key=slicer,
|
||||
displayName=slicingManager.get_slicer(slicer).get_slicer_properties()["name"],
|
||||
displayName=slicer_impl.get_slicer_properties()["name"],
|
||||
default=default_slicer == slicer,
|
||||
configured = slicer_impl.is_slicer_configured(),
|
||||
profiles=_getSlicingProfilesData(slicer)
|
||||
)
|
||||
|
||||
|
|
@ -35,7 +44,13 @@ def slicingListSlicerProfiles(slicer):
|
|||
if not slicer in slicingManager.registered_slicers:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
return jsonify(_getSlicingProfilesData(slicer))
|
||||
configured = False
|
||||
if "configured" in request.values and request.values["configured"] in valid_boolean_trues:
|
||||
if not slicer in slicingManager.configured_slicers:
|
||||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
configured = True
|
||||
|
||||
return jsonify(_getSlicingProfilesData(slicer, require_configured=configured))
|
||||
|
||||
@api.route("/slicing/<string:slicer>/profiles/<string:name>", methods=["GET"])
|
||||
def slicingGetSlicerProfile(slicer, name):
|
||||
|
|
@ -57,7 +72,7 @@ def slicingAddSlicerProfile(slicer, name):
|
|||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return None, None, make_response("Expected content-type JSON", 400)
|
||||
return make_response("Expected content-type JSON", 400)
|
||||
|
||||
try:
|
||||
json_data = request.json
|
||||
|
|
@ -88,7 +103,7 @@ def slicingPatchSlicerProfile(slicer, name):
|
|||
return make_response("Unknown slicer {slicer}".format(**locals()), 404)
|
||||
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return None, None, make_response("Expected content-type JSON", 400)
|
||||
return make_response("Expected content-type JSON", 400)
|
||||
|
||||
profile = slicingManager.load_profile(slicer, name)
|
||||
if not profile:
|
||||
|
|
@ -130,9 +145,10 @@ def slicingDelSlicerProfile(slicer, name):
|
|||
slicingManager.delete_profile(slicer, name)
|
||||
return NO_CONTENT
|
||||
|
||||
def _getSlicingProfilesData(slicer):
|
||||
profiles = slicingManager.all_profiles(slicer)
|
||||
if not profiles:
|
||||
def _getSlicingProfilesData(slicer, require_configured=False):
|
||||
try:
|
||||
profiles = slicingManager.all_profiles(slicer, require_configured=require_configured)
|
||||
except SlicerNotConfigured:
|
||||
return dict()
|
||||
|
||||
result = dict()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import octoprint.plugin
|
|||
import octoprint.events
|
||||
from octoprint.settings import settings
|
||||
|
||||
from .exceptions import *
|
||||
|
||||
|
||||
class SlicingProfile(object):
|
||||
def __init__(self, slicer, name, data, display_name=None, description=None):
|
||||
|
|
@ -44,10 +46,6 @@ class TemporaryProfile(object):
|
|||
pass
|
||||
|
||||
|
||||
class SlicingCancelled(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
class SlicingManager(object):
|
||||
def __init__(self, profile_path, printer_profile_manager):
|
||||
self._profile_path = profile_path
|
||||
|
|
@ -71,8 +69,7 @@ class SlicingManager(object):
|
|||
def _load_slicers(self):
|
||||
plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin)
|
||||
for name, plugin in plugins.items():
|
||||
if plugin.is_slicer_configured():
|
||||
self._slicers[plugin.get_slicer_properties()["type"]] = plugin
|
||||
self._slicers[plugin.get_slicer_properties()["type"]] = plugin
|
||||
|
||||
@property
|
||||
def slicing_enabled(self):
|
||||
|
|
@ -89,13 +86,15 @@ class SlicingManager(object):
|
|||
@property
|
||||
def default_slicer(self):
|
||||
slicer_name = settings().get(["slicing", "defaultSlicer"])
|
||||
if slicer_name in self.configured_slicers:
|
||||
if slicer_name in self.registered_slicers:
|
||||
return slicer_name
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_slicer(self, slicer, require_configured=True):
|
||||
return self._slicers[slicer] if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()) else None
|
||||
if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()):
|
||||
return self._slicers[slicer]
|
||||
raise SlicerNotConfigured(slicer)
|
||||
|
||||
def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None, printer_profile_id=None, position=None):
|
||||
if callback_args is None:
|
||||
|
|
@ -106,11 +105,13 @@ class SlicingManager(object):
|
|||
if not slicer_name in self.configured_slicers:
|
||||
if not slicer_name in self.registered_slicers:
|
||||
error = "No such slicer: {slicer_name}".format(**locals())
|
||||
exc = UnknownSlicer(slicer_name)
|
||||
else:
|
||||
error = "Slicer not configured: {slicer_name}".format(**locals())
|
||||
callback_kwargs.update(dict(_error=error))
|
||||
exc = SlicerNotConfigured(slicer_name)
|
||||
callback_kwargs.update(dict(_error=error, _exc=exc))
|
||||
callback(*callback_args, **callback_kwargs)
|
||||
return False, error
|
||||
raise exc
|
||||
|
||||
slicer = self.get_slicer(slicer_name)
|
||||
|
||||
|
|
@ -154,23 +155,24 @@ class SlicingManager(object):
|
|||
|
||||
def cancel_slicing(self, slicer_name, source_path, dest_path):
|
||||
if not slicer_name in self.registered_slicers:
|
||||
return
|
||||
raise UnknownSlicer(slicer_name)
|
||||
|
||||
slicer = self.get_slicer(slicer_name)
|
||||
slicer.cancel_slicing(dest_path)
|
||||
|
||||
def load_profile(self, slicer, name):
|
||||
def load_profile(self, slicer, name, require_configured=True):
|
||||
if not slicer in self.registered_slicers:
|
||||
return None
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
try:
|
||||
path = self.get_profile_path(slicer, name, must_exist=True)
|
||||
except IOError:
|
||||
return None
|
||||
return self._load_profile_from_path(slicer, path)
|
||||
return self._load_profile_from_path(slicer, path, require_configured=require_configured)
|
||||
|
||||
def save_profile(self, slicer, name, profile, overrides=None, allow_overwrite=True, display_name=None, description=None):
|
||||
if not slicer in self.registered_slicers:
|
||||
return
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
if not isinstance(profile, SlicingProfile):
|
||||
if isinstance(profile, dict):
|
||||
|
|
@ -191,7 +193,7 @@ class SlicingManager(object):
|
|||
|
||||
def temporary_profile(self, slicer, name=None, overrides=None):
|
||||
if not slicer in self.registered_slicers:
|
||||
return None
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
profile = self._get_default_profile(slicer)
|
||||
if name:
|
||||
|
|
@ -207,16 +209,21 @@ class SlicingManager(object):
|
|||
|
||||
def delete_profile(self, slicer, name):
|
||||
if not slicer in self.registered_slicers:
|
||||
return None
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
if not name:
|
||||
raise ValueError("name must be set")
|
||||
|
||||
path = self.get_profile_path(slicer, name)
|
||||
if not os.path.exists(path) or not os.path.isfile(path):
|
||||
return
|
||||
os.remove(path)
|
||||
|
||||
def all_profiles(self, slicer):
|
||||
def all_profiles(self, slicer, require_configured=False):
|
||||
if not slicer in self.registered_slicers:
|
||||
return None
|
||||
raise UnknownSlicer(slicer)
|
||||
if require_configured and not slicer in self.configured_slicers:
|
||||
raise SlicerNotConfigured(slicer)
|
||||
|
||||
profiles = dict()
|
||||
slicer_profile_path = self.get_slicer_profile_path(slicer)
|
||||
|
|
@ -228,12 +235,12 @@ class SlicingManager(object):
|
|||
path = os.path.join(slicer_profile_path, entry)
|
||||
profile_name = entry[:-len(".profile")]
|
||||
|
||||
profiles[profile_name] = self._load_profile_from_path(slicer, path)
|
||||
profiles[profile_name] = self._load_profile_from_path(slicer, path, require_configured=require_configured)
|
||||
return profiles
|
||||
|
||||
def get_slicer_profile_path(self, slicer):
|
||||
if not slicer in self.registered_slicers:
|
||||
return None
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
path = os.path.join(self._profile_path, slicer)
|
||||
if not os.path.exists(path):
|
||||
|
|
@ -242,10 +249,10 @@ class SlicingManager(object):
|
|||
|
||||
def get_profile_path(self, slicer, name, must_exist=False):
|
||||
if not slicer in self.registered_slicers:
|
||||
return None
|
||||
raise UnknownSlicer(slicer)
|
||||
|
||||
if not name:
|
||||
return None
|
||||
raise ValueError("name must be set")
|
||||
|
||||
name = self._sanitize(name)
|
||||
|
||||
|
|
@ -269,11 +276,11 @@ class SlicingManager(object):
|
|||
sanitized_name = sanitized_name.replace(" ", "_")
|
||||
return sanitized_name
|
||||
|
||||
def _load_profile_from_path(self, slicer, path):
|
||||
return self.get_slicer(slicer).get_slicer_profile(path)
|
||||
def _load_profile_from_path(self, slicer, path, require_configured=False):
|
||||
return self.get_slicer(slicer, require_configured=require_configured).get_slicer_profile(path)
|
||||
|
||||
def _save_profile_to_path(self, slicer, path, profile, allow_overwrite=True, overrides=None):
|
||||
self.get_slicer(slicer).save_slicer_profile(path, profile, allow_overwrite=allow_overwrite, overrides=overrides)
|
||||
def _save_profile_to_path(self, slicer, path, profile, allow_overwrite=True, overrides=None, require_configured=False):
|
||||
self.get_slicer(slicer, require_configured=require_configured).save_slicer_profile(path, profile, allow_overwrite=allow_overwrite, overrides=overrides)
|
||||
|
||||
def _get_default_profile(self, slicer):
|
||||
default_profiles = settings().get(["slicing", "defaultProfiles"])
|
||||
|
|
|
|||
28
src/octoprint/slicing/exceptions.py
Normal file
28
src/octoprint/slicing/exceptions.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# coding=utf-8
|
||||
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) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
class SlicingException(BaseException):
|
||||
pass
|
||||
|
||||
class SlicingCancelled(SlicingException):
|
||||
pass
|
||||
|
||||
class SlicerException(SlicingException):
|
||||
def __init__(self, slicer, *args, **kwargs):
|
||||
super(SlicingException, self).__init__(*args, **kwargs)
|
||||
self.slicer = slicer
|
||||
|
||||
class SlicerNotConfigured(SlicerException):
|
||||
def __init__(self, slicer, *args, **kwargs):
|
||||
super(SlicerException, self).__init__(slicer, *args, **kwargs)
|
||||
self.message = "Slicer not configured: {slicer}".format(slicer=slicer)
|
||||
|
||||
class UnknownSlicer(SlicerException):
|
||||
def __init__(self, slicer, *args, **kwargs):
|
||||
super(SlicerException, self).__init__(slicer, *args, **kwargs)
|
||||
self.message = "No such slicer: {slicer}".format(slicer=slicer)
|
||||
|
|
@ -167,25 +167,7 @@ function DataUpdater(allViewModels) {
|
|||
|
||||
log.debug("Got event " + type + " with payload: " + JSON.stringify(payload));
|
||||
|
||||
if (type == "UpdatedFiles") {
|
||||
_.each(self.allViewModels, function (viewModel) {
|
||||
if (viewModel.hasOwnProperty("onUpdatedFiles")) {
|
||||
viewModel.onUpdatedFiles(payload);
|
||||
}
|
||||
});
|
||||
} else if (type == "MetadataStatisticsUpdated") {
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onMetadataStatisticsUpdated")) {
|
||||
viewModel.onMetadataStatisticsUpdated(payload);
|
||||
}
|
||||
})
|
||||
} else if (type == "MetadataAnalysisFinished") {
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onMetadataAnalysisFinished")) {
|
||||
viewModel.onMetadataAnalysisFinished(payload);
|
||||
}
|
||||
});
|
||||
} else if (type == "MovieRendering") {
|
||||
if (type == "MovieRendering") {
|
||||
new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)});
|
||||
} else if (type == "MovieDone") {
|
||||
new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)});
|
||||
|
|
@ -207,21 +189,10 @@ function DataUpdater(allViewModels) {
|
|||
gcodeUploadProgressBar.css("width", "0%");
|
||||
gcodeUploadProgressBar.text("");
|
||||
new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload), type: "success"});
|
||||
|
||||
_.each(self.allViewModels, function (viewModel) {
|
||||
if (viewModel.hasOwnProperty("onSlicingDone")) {
|
||||
viewModel.onSlicingDone(payload);
|
||||
}
|
||||
});
|
||||
} else if (type == "SlicingCancelled") {
|
||||
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
||||
gcodeUploadProgressBar.css("width", "0%");
|
||||
gcodeUploadProgressBar.text("");
|
||||
_.each(self.allViewModels, function (viewModel) {
|
||||
if (viewModel.hasOwnProperty("onSlicingCancelled")) {
|
||||
viewModel.onSlicingCancelled(payload);
|
||||
}
|
||||
});
|
||||
} else if (type == "SlicingFailed") {
|
||||
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
||||
gcodeUploadProgressBar.css("width", "0%");
|
||||
|
|
@ -229,11 +200,6 @@ function DataUpdater(allViewModels) {
|
|||
|
||||
html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload);
|
||||
new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false});
|
||||
_.each(self.allViewModels, function (viewModel) {
|
||||
if (viewModel.hasOwnProperty("onSlicingFailed")) {
|
||||
viewModel.onSlicingFailed(payload);
|
||||
}
|
||||
});
|
||||
} else if (type == "TransferStarted") {
|
||||
gcodeUploadProgress.addClass("progress-striped").addClass("active");
|
||||
gcodeUploadProgressBar.css("width", "100%");
|
||||
|
|
@ -249,6 +215,26 @@ function DataUpdater(allViewModels) {
|
|||
});
|
||||
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
|
||||
}
|
||||
|
||||
var legacyEventHandlers = {
|
||||
"UpdatedFiles": "onUpdatedFiles",
|
||||
"MetadataStatisticsUpdated": "onMetadataStatisticsUpdated",
|
||||
"MetadataAnalysisFinished": "onMetadataAnalysisFinished",
|
||||
"SlicingDone": "onSlicingDone",
|
||||
"SlicingCancelled": "onSlicingCancelled",
|
||||
"SlicingFailed": "onSlicingFailed"
|
||||
};
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onEvent" + type)) {
|
||||
viewModel["onEvent" + type](payload);
|
||||
} else if (legacyEventHandlers.hasOwnProperty(type) && viewModel.hasOwnProperty(legacyEventHandlers[type])) {
|
||||
// there might still be code that uses the old callbacks, make sure those still get called
|
||||
// but log a warning
|
||||
log.warn("View model " + viewModel.name + " is using legacy event handler " + legacyEventHandlers[type] + ", new handler is called " + legacyEventHandlers[type]);
|
||||
viewModel[legacyEventHandlers[type]](payload);
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "feedbackCommandOutput": {
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ $(function() {
|
|||
self.sliceFile = function(file) {
|
||||
if (!file) return;
|
||||
|
||||
self.slicing.show(file.origin, file.name);
|
||||
self.slicing.show(file.origin, file.name, true);
|
||||
};
|
||||
|
||||
self.initSdCard = function() {
|
||||
|
|
@ -265,7 +265,7 @@ $(function() {
|
|||
};
|
||||
|
||||
self.enableSlicing = function(data) {
|
||||
return self.loginState.isUser() && !(self.isPrinting() || self.isPaused());
|
||||
return self.loginState.isUser() && self.slicing.enableSlicingDialog();
|
||||
};
|
||||
|
||||
self.enableAdditionalData = function(data) {
|
||||
|
|
@ -379,7 +379,7 @@ $(function() {
|
|||
}
|
||||
|
||||
function gcode_upload_fail(e, data) {
|
||||
var error = "<p>" + gettext("Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\" and slicing support is enabled and configured.") + "</p>";
|
||||
var error = "<p>" + gettext("Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\".") + "</p>";
|
||||
error += pnotifyAdditionalInfo("<pre>" + data.jqXHR.responseText + "</pre>");
|
||||
new PNotify({
|
||||
title: "Upload failed",
|
||||
|
|
@ -551,21 +551,21 @@ $(function() {
|
|||
self.requestData();
|
||||
};
|
||||
|
||||
self.onUpdatedFiles = function(payload) {
|
||||
self.onEventUpdatedFiles = function(payload) {
|
||||
if (payload.type == "gcode") {
|
||||
self.requestData();
|
||||
}
|
||||
};
|
||||
|
||||
self.onSlicingDone = function(payload) {
|
||||
self.onEventSlicingDone = function(payload) {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onMetadataAnalysisFinished = function(payload) {
|
||||
self.onEventMetadataAnalysisFinished = function(payload) {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onMetadataStatisticsUpdated = function(payload) {
|
||||
self.onEventMetadataStatisticsUpdated = function(payload) {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ $(function() {
|
|||
self.profiles = ko.observableArray();
|
||||
self.printerProfile = ko.observable();
|
||||
|
||||
self.configured_slicers = ko.computed(function() {
|
||||
return _.filter(self.slicers(), function(slicer) {
|
||||
return slicer.configured;
|
||||
});
|
||||
});
|
||||
|
||||
self.afterSlicingOptions = [
|
||||
{"value": "none", "text": gettext("Do nothing")},
|
||||
{"value": "select", "text": gettext("Select for printing")},
|
||||
|
|
@ -28,7 +34,11 @@ $(function() {
|
|||
];
|
||||
self.afterSlicing = ko.observable("none");
|
||||
|
||||
self.show = function(target, file) {
|
||||
self.show = function(target, file, force) {
|
||||
if (!self.enableSlicingDialog() && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.requestData();
|
||||
self.target = target;
|
||||
self.file = file;
|
||||
|
|
@ -43,6 +53,10 @@ $(function() {
|
|||
self.profilesForSlicer(newValue);
|
||||
});
|
||||
|
||||
self.enableSlicingDialog = ko.computed(function() {
|
||||
return self.configured_slicers().length > 0;
|
||||
});
|
||||
|
||||
self.enableSliceButton = ko.computed(function() {
|
||||
return self.gcodeFilename() != undefined
|
||||
&& self.gcodeFilename().trim() != ""
|
||||
|
|
@ -75,13 +89,14 @@ $(function() {
|
|||
name = slicer.key;
|
||||
}
|
||||
|
||||
if (slicer.default) {
|
||||
if (slicer.default && slicer.configured) {
|
||||
selectedSlicer = slicer.key;
|
||||
}
|
||||
|
||||
self.slicers.push({
|
||||
key: slicer.key,
|
||||
name: name
|
||||
name: name,
|
||||
configured: slicer.configured
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -170,6 +185,10 @@ $(function() {
|
|||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
};
|
||||
|
||||
self.onEventSettingsUpdated = function(payload) {
|
||||
self.requestData();
|
||||
};
|
||||
}
|
||||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
|
|
|
|||
|
|
@ -4,42 +4,47 @@
|
|||
<h3 data-bind="text: title"></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ _('Please configure which slicer and which slicing profile to use and name the GCode file to slice to below, or click "Cancel" if you do not wish to slice the file now.') }}</p>
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Slicer') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: slicers, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicer...') }}', value: slicer, valueAllowUnset: true"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Slicing Profile') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: profiles, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicing profile...') }}', value: profile, valueAllowUnset: true"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Printer Profile') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: printerProfiles.profiles.items, optionsText: 'name', optionsValue: 'id', value: printerProfile, optionsCaption: '{{ _('Select a printer profile...') }}'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('GCode Filename') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="text" data-bind="value: gcodeFilename">
|
||||
<span class="add-on">.gco</span>
|
||||
<div data-bind="visible: !enableSlicingDialog()">
|
||||
<p>{{ _('Slicing is currently disabled since no slicer has been configured yet. Please configure a slicer under "Settings".') }}</p>
|
||||
</div>
|
||||
<div data-bind="visible: enableSlicingDialog()">
|
||||
<p>{{ _('Please configure which slicer and which slicing profile to use and name the GCode file to slice to below, or click "Cancel" if you do not wish to slice the file now.') }}</p>
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Slicer') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: configured_slicers, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicer...') }}', value: slicer, valueAllowUnset: true"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('After slicing...') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: afterSlicingOptions, optionsText: 'text', optionsValue: 'value', value: afterSlicing"></select>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Slicing Profile') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: profiles, optionsText: 'name', optionsValue: 'key', optionsCaption: '{{ _('Select a slicing profile...') }}', value: profile, valueAllowUnset: true"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Printer Profile') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: printerProfiles.profiles.items, optionsText: 'name', optionsValue: 'id', value: printerProfile, optionsCaption: '{{ _('Select a printer profile...') }}'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('GCode Filename') }}</label>
|
||||
<div class="controls">
|
||||
<div class="input-append">
|
||||
<input type="text" data-bind="value: gcodeFilename">
|
||||
<span class="add-on">.gco</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('After slicing...') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="options: afterSlicingOptions, optionsText: 'text', optionsValue: 'value', value: afterSlicing"></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</a>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
<div class="uploaded">{{ _('Uploaded') }}: <span data-bind="text: formatTimeAgo(date)"></span></div>
|
||||
<div class="size">{{ _('Size') }}: <span data-bind="text: formatSize(size)"></span></div>
|
||||
<div class="btn-group action-buttons">
|
||||
<a class="btn btn-mini" data-bind="attr: {href: $root.downloadLink($data), css: {disabled: !$root.downloadLink($data)}}"><i class="icon-download-alt" title="{{ _('Download') }}"></i></a>
|
||||
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"><i class="icon-trash" title="{{ _('Remove') }}"></i></div>
|
||||
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSlicing($data)) { $root.sliceFile($data); } else { return; } }, css: {disabled: !$root.enableSlicing($data)}"><i class="icon-magic" title="{{ _('Slice') }}"></i></div>
|
||||
</div>
|
||||
|
|
|
|||
11
tests/filemanager/__init__.py
Normal file
11
tests/filemanager/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.filemanager.``.
|
||||
"""
|
||||
|
||||
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"
|
||||
|
||||
|
|
@ -26,6 +26,10 @@ class FileManagerTest(unittest.TestCase):
|
|||
event_manager.return_value.fire = mock.MagicMock()
|
||||
self.fire_event = event_manager.return_value.fire
|
||||
|
||||
# mock plugin manager
|
||||
self.plugin_manager_patcher = mock.patch("octoprint.plugin.plugin_manager")
|
||||
self.plugin_manager = self.plugin_manager_patcher.start()
|
||||
|
||||
self.analysis_queue = mock.MagicMock(spec=octoprint.filemanager.AnalysisQueue)
|
||||
|
||||
self.slicing_manager = mock.MagicMock(spec=octoprint.slicing.SlicingManager)
|
||||
|
|
@ -42,12 +46,13 @@ class FileManagerTest(unittest.TestCase):
|
|||
|
||||
def cleanUp(self):
|
||||
self.event_manager_patcher.stop()
|
||||
self.plugin_manager_patcher.stop()
|
||||
|
||||
def test_add_file(self):
|
||||
wrapper = object()
|
||||
|
||||
self.local_storage.add_file.return_value = ("", "test.file")
|
||||
self.local_storage.get_absolute_path.return_value = "prefix/test.file"
|
||||
self.local_storage.path_on_disk.return_value = "prefix/test.file"
|
||||
|
||||
test_profile = dict(id="_default", name="My Default Profile")
|
||||
self.printer_profile_manager.get_current_or_default.return_value = test_profile
|
||||
|
|
@ -126,7 +131,7 @@ class FileManagerTest(unittest.TestCase):
|
|||
self.printer_profile_manager.get.return_value = None
|
||||
|
||||
# mock get_absolute_path method on local storage
|
||||
def get_absolute_path(path):
|
||||
def path_on_disk(path):
|
||||
if isinstance(path, tuple):
|
||||
import os
|
||||
joined_path = ""
|
||||
|
|
@ -134,7 +139,7 @@ class FileManagerTest(unittest.TestCase):
|
|||
joined_path = os.path.join(joined_path, part)
|
||||
path = joined_path
|
||||
return "prefix/" + path
|
||||
self.local_storage.get_absolute_path.side_effect = get_absolute_path
|
||||
self.local_storage.path_on_disk.side_effect = path_on_disk
|
||||
|
||||
# mock split_path method on local storage
|
||||
def split_path(path):
|
||||
|
|
@ -148,12 +153,14 @@ class FileManagerTest(unittest.TestCase):
|
|||
self.local_storage.add_file.side_effect = add_file
|
||||
|
||||
# mock slice method on slicing manager
|
||||
def slice(slicer_name, source_path, dest_path, profile, done_cb, printer_profile_id=None, callback_args=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
def slice(slicer_name, source_path, dest_path, profile, done_cb, printer_profile_id=None, position=None, callback_args=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
self.assertEquals("some_slicer", slicer_name)
|
||||
self.assertEquals("prefix/source.file", source_path)
|
||||
self.assertEquals("tmp.file", dest_path)
|
||||
self.assertIsNone(profile)
|
||||
self.assertIsNone(overrides)
|
||||
self.assertIsNone(printer_profile_id)
|
||||
self.assertIsNone(position)
|
||||
self.assertIsNotNone(on_progress)
|
||||
self.assertIsNotNone(on_progress_args)
|
||||
self.assertTupleEqual(("some_slicer", octoprint.filemanager.FileDestinations.LOCAL, "source.file", octoprint.filemanager.FileDestinations.LOCAL, "dest.file"), on_progress_args)
|
||||
|
|
@ -202,8 +209,8 @@ class FileManagerTest(unittest.TestCase):
|
|||
temp_file.name = "tmp.file"
|
||||
mocked_tempfile.return_value = temp_file
|
||||
|
||||
# mock get_absolute_path method on local storage
|
||||
def get_absolute_path(path):
|
||||
# mock path_on_disk method on local storage
|
||||
def path_on_disk(path):
|
||||
if isinstance(path, tuple):
|
||||
import os
|
||||
joined_path = ""
|
||||
|
|
@ -211,15 +218,17 @@ class FileManagerTest(unittest.TestCase):
|
|||
joined_path = os.path.join(joined_path, part)
|
||||
path = joined_path
|
||||
return "prefix/" + path
|
||||
self.local_storage.get_absolute_path.side_effect = get_absolute_path
|
||||
self.local_storage.path_on_disk.side_effect = path_on_disk
|
||||
|
||||
# mock slice method on slicing manager
|
||||
def slice(slicer_name, source_path, dest_path, profile, done_cb, printer_profile_id=None, callback_args=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
def slice(slicer_name, source_path, dest_path, profile, done_cb, printer_profile_id=None, position=None, callback_args=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
self.assertEquals("some_slicer", slicer_name)
|
||||
self.assertEquals("prefix/source.file", source_path)
|
||||
self.assertEquals("tmp.file", dest_path)
|
||||
self.assertIsNone(profile)
|
||||
self.assertIsNone(overrides)
|
||||
self.assertIsNone(printer_profile_id)
|
||||
self.assertIsNone(position)
|
||||
self.assertIsNotNone(on_progress)
|
||||
self.assertIsNotNone(on_progress_args)
|
||||
self.assertTupleEqual(("some_slicer", octoprint.filemanager.FileDestinations.LOCAL, "source.file", octoprint.filemanager.FileDestinations.LOCAL, "dest.file"), on_progress_args)
|
||||
|
|
|
|||
10
tests/plugin/__init__.py
Normal file
10
tests/plugin/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.plugin``.
|
||||
"""
|
||||
|
||||
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"
|
||||
10
tests/printer/__init__.py
Normal file
10
tests/printer/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.printer``.
|
||||
"""
|
||||
|
||||
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"
|
||||
10
tests/slicing/__init__.py
Normal file
10
tests/slicing/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Unit tests for ``octoprint.slicing``.
|
||||
"""
|
||||
|
||||
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"
|
||||
|
|
@ -20,18 +20,23 @@ class TestSlicingManager(unittest.TestCase):
|
|||
|
||||
self.slicer_plugin = mock.MagicMock()
|
||||
self.slicer_plugin.get_slicer_properties.return_value = dict(type="mock", name="Mock", same_device=True)
|
||||
self.slicer_plugin.is_slicer_configured.return_value = True
|
||||
|
||||
# mock plugin manager
|
||||
self.plugin_manager_patcher = mock.patch("octoprint.plugin.plugin_manager")
|
||||
self.plugin_manager = self.plugin_manager_patcher.start()
|
||||
self._mock_slicer_plugins(self.slicer_plugin)
|
||||
|
||||
# mock profile manager
|
||||
self.printer_profile_manager = mock.MagicMock(spec=octoprint.printer.profile.PrinterProfileManager)
|
||||
|
||||
# mock settings
|
||||
self.settings_patcher = mock.patch("octoprint.slicing.settings")
|
||||
settings = self.settings_patcher.start()
|
||||
self.settings = settings.return_value
|
||||
|
||||
self.slicing_manager = octoprint.slicing.SlicingManager(self.profile_path)
|
||||
self.slicing_manager = octoprint.slicing.SlicingManager(self.profile_path, self.printer_profile_manager)
|
||||
self.slicing_manager.initialize()
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
|
|
@ -114,28 +119,37 @@ class TestSlicingManager(unittest.TestCase):
|
|||
# mock slicing
|
||||
self.slicer_plugin.do_slice.return_value = True, None
|
||||
|
||||
# mock printer profile manager
|
||||
printer_profile = dict(_id="mock_printer", _name="Mock Printer Profile")
|
||||
def get_printer_profile(printer_profile_id):
|
||||
self.assertEquals("mock_printer", printer_profile_id)
|
||||
return printer_profile
|
||||
self.printer_profile_manager.get.side_effect = get_printer_profile
|
||||
|
||||
##~~ call tested method
|
||||
slicer_name = "mock"
|
||||
source_path = "prefix/source.file"
|
||||
dest_path = "prefix/dest.file"
|
||||
profile_name = "dummy_profile"
|
||||
printer_profile_id = "mock_printer"
|
||||
position = dict(x=10, y=20)
|
||||
callback = mock.MagicMock()
|
||||
callback_args = ("one", "two", "three")
|
||||
callback_kwargs = dict(foo="bar")
|
||||
overrides = dict(layer_height=0.5)
|
||||
|
||||
self.slicing_manager.slice(slicer_name, source_path, dest_path, profile_name, callback, callback_args=callback_args, callback_kwargs=callback_kwargs, overrides=overrides)
|
||||
self.slicing_manager.slice(slicer_name, source_path, dest_path, profile_name, callback, printer_profile_id=printer_profile_id, position=position, callback_args=callback_args, callback_kwargs=callback_kwargs, overrides=overrides)
|
||||
|
||||
# assert that temporary profile was created properly
|
||||
self.slicer_plugin.save_slicer_profile.assert_called_once_with("tmp.file", default_profile, overrides=overrides)
|
||||
|
||||
# assert that slicing thread was created properly
|
||||
mocked_thread.assert_called_once_with(target=mock.ANY, args=(self.slicer_plugin, source_path, dest_path, profile_name, overrides, callback, callback_args, callback_kwargs))
|
||||
mocked_thread.assert_called_once_with(target=mock.ANY, args=(self.slicer_plugin, source_path, dest_path, profile_name, overrides, printer_profile, position, callback, callback_args, callback_kwargs))
|
||||
self.assertTrue(mock_thread.mock.daemon)
|
||||
mock_thread.mock.start.assert_called_once()
|
||||
|
||||
# assert that slicer was called correctly
|
||||
self.slicer_plugin.do_slice.assert_called_once_with("prefix/source.file", machinecode_path="prefix/dest.file", profile_path="tmp.file", on_progress=None, on_progress_args=None, on_progress_kwargs=None)
|
||||
self.slicer_plugin.do_slice.assert_called_once_with(source_path, printer_profile, machinecode_path=dest_path, profile_path="tmp.file", position=position, on_progress=None, on_progress_args=None, on_progress_kwargs=None)
|
||||
|
||||
# assert that temporary profile was deleted again
|
||||
mocked_os_remove.assert_called_once_with("tmp.file")
|
||||
|
|
|
|||
Loading…
Reference in a new issue