From e702736455f565ecedc7880c5ade7687ac0597b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 2 Mar 2015 12:58:24 +0100 Subject: [PATCH 1/8] Updated/fixed unit tests and travis configuration --- .travis.yml | 6 +++--- tests/filemanager/__init__.py | 11 +++++++++++ tests/filemanager/test_filemanager.py | 25 +++++++++++++++++-------- tests/plugin/__init__.py | 10 ++++++++++ tests/printer/__init__.py | 10 ++++++++++ tests/slicing/__init__.py | 10 ++++++++++ tests/slicing/test_slicingmanager.py | 22 ++++++++++++++++++---- 7 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 tests/filemanager/__init__.py create mode 100644 tests/plugin/__init__.py create mode 100644 tests/printer/__init__.py create mode 100644 tests/slicing/__init__.py diff --git a/.travis.yml b/.travis.yml index 15d917c4..edc98c04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: python python: - - "2.7" +- "2.7" install: - - pip install -r requirements.txt +- python setup.py install script: - - nosetests \ No newline at end of file +- nosetests tests/ \ No newline at end of file diff --git a/tests/filemanager/__init__.py b/tests/filemanager/__init__.py new file mode 100644 index 00000000..65f33057 --- /dev/null +++ b/tests/filemanager/__init__.py @@ -0,0 +1,11 @@ +# coding=utf-8 +""" +Unit tests for ``octoprint.filemanager.``. +""" + +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__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" + diff --git a/tests/filemanager/test_filemanager.py b/tests/filemanager/test_filemanager.py index 8681ffb2..d517f6c0 100644 --- a/tests/filemanager/test_filemanager.py +++ b/tests/filemanager/test_filemanager.py @@ -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) diff --git a/tests/plugin/__init__.py b/tests/plugin/__init__.py new file mode 100644 index 00000000..43b44800 --- /dev/null +++ b/tests/plugin/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" +Unit tests for ``octoprint.plugin``. +""" + +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__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" diff --git a/tests/printer/__init__.py b/tests/printer/__init__.py new file mode 100644 index 00000000..8f164a2c --- /dev/null +++ b/tests/printer/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" +Unit tests for ``octoprint.printer``. +""" + +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__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" diff --git a/tests/slicing/__init__.py b/tests/slicing/__init__.py new file mode 100644 index 00000000..b202f2bb --- /dev/null +++ b/tests/slicing/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" +Unit tests for ``octoprint.slicing``. +""" + +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__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" diff --git a/tests/slicing/test_slicingmanager.py b/tests/slicing/test_slicingmanager.py index 9a209f3e..1b17a89d 100644 --- a/tests/slicing/test_slicingmanager.py +++ b/tests/slicing/test_slicingmanager.py @@ -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") From 19114d1073e5a3b4529189bad6cd6e92805f549b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 2 Mar 2015 13:01:13 +0100 Subject: [PATCH 2/8] Use fully qualified import for plugin manager in file manager to simplify unit testing --- src/octoprint/filemanager/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index 85e1931c..dfab1da8 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -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) From 778a38a668a740bac9cf22121f5639a503ff5b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 2 Mar 2015 13:15:00 +0100 Subject: [PATCH 3/8] Added [develop] extra to setup.py to install development dependencies Use pip install -e .[develop] to prime your environment for development, this will install all requirements both for running as well as for development tasks (such as running unit tests or compiling the documentation) and register OctoPrint as an editable python package as well. --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 17549a0e..6a875439 100644 --- a/setup.py +++ b/setup.py @@ -288,6 +288,9 @@ def params(): include_package_data = True zip_safe = False install_requires = open("requirements.txt").read().split("\n") + extras_require = dict( + develop=open("requirements-dev.txt").read().split("\n") + ) entry_points = { "console_scripts": [ From cef46830825c0ce59cebc2b22ea7292a055d2675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 2 Mar 2015 13:15:46 +0100 Subject: [PATCH 4/8] Travis should use the develop flavor for install so that tests can run --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index edc98c04..df4eb121 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,6 @@ language: python python: - "2.7" install: -- python setup.py install +- pip install -e .[develop] script: - nosetests tests/ \ No newline at end of file From db382e39a7525b03aa6b09ed7c76cf008fbcfb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 2 Mar 2015 15:17:53 +0100 Subject: [PATCH 5/8] setup.py: Always include develop dependencies on RTD --- docs/requirements.txt | 3 --- requirements-dev.txt | 12 +++++++++--- setup.py | 14 ++++++++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) delete mode 100644 docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 42469fd9..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinxcontrib-httpdomain -sphinxcontrib-napoleon -sphinx_rtd_theme diff --git a/requirements-dev.txt b/requirements-dev.txt index b3f117e0..cac003f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/setup.py b/setup.py index 6a875439..3d2b59a3 100644 --- a/setup.py +++ b/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,11 +291,17 @@ 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=open("requirements-dev.txt").read().split("\n") + 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": [ "octoprint = octoprint:main" From 36a837cd77d6106d5506e9f530f68c7dd5184c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 2 Mar 2015 15:30:03 +0100 Subject: [PATCH 6/8] Always allow uploading STL files, even if no slicer is configured yet --- src/octoprint/server/api/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/octoprint/server/api/files.py b/src/octoprint/server/api/files.py index bb14b9ed..8a1e3b7e 100644 --- a/src/octoprint/server/api/files.py +++ b/src/octoprint/server/api/files.py @@ -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 From befe2e7bee6e4e59cc21e9a4afde1df28949c543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 2 Mar 2015 15:34:29 +0100 Subject: [PATCH 7/8] Allow adding slicing profiles for unconfigured slicers and properly reload slicing view model upon configuration This should fix the problem where on a fresh setup it was impossible to upload slicing profiles for Cura before the path to the binary was configured. Also made the slicing dialog auto update available slicers when the settings are updated. The slicing button in the file list is now only active if a slicer is available. The slicing dialog will only show upon upload of an STL file if a slicer is available. Closes #795 --- src/octoprint/server/api/slicing.py | 34 ++++++--- src/octoprint/slicing/__init__.py | 61 +++++++++------- src/octoprint/slicing/exceptions.py | 28 ++++++++ src/octoprint/static/js/app/dataupdater.js | 56 ++++++--------- .../static/js/app/viewmodels/files.js | 14 ++-- .../static/js/app/viewmodels/slicing.js | 25 ++++++- .../templates/dialogs/slicing.jinja2 | 71 ++++++++++--------- 7 files changed, 175 insertions(+), 114 deletions(-) create mode 100644 src/octoprint/slicing/exceptions.py diff --git a/src/octoprint/server/api/slicing.py b/src/octoprint/server/api/slicing.py index 28ab35e9..978103a6 100644 --- a/src/octoprint/server/api/slicing.py +++ b/src/octoprint/server/api/slicing.py @@ -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//profiles/", 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() diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index 871e4673..8d4848e4 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -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"]) diff --git a/src/octoprint/slicing/exceptions.py b/src/octoprint/slicing/exceptions.py new file mode 100644 index 00000000..4eefe4ae --- /dev/null +++ b/src/octoprint/slicing/exceptions.py @@ -0,0 +1,28 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__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) diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index aa4ce6ac..d25a731b 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -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": { diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 019acb8e..cde25948 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -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 = "

" + 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.") + "

"; + var error = "

" + 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\".") + "

"; error += pnotifyAdditionalInfo("
" + data.jqXHR.responseText + "
"); 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(); }; } diff --git a/src/octoprint/static/js/app/viewmodels/slicing.js b/src/octoprint/static/js/app/viewmodels/slicing.js index 194bff3d..eba0581a 100644 --- a/src/octoprint/static/js/app/viewmodels/slicing.js +++ b/src/octoprint/static/js/app/viewmodels/slicing.js @@ -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([ diff --git a/src/octoprint/templates/dialogs/slicing.jinja2 b/src/octoprint/templates/dialogs/slicing.jinja2 index 3b0ad040..cdcf88b3 100644 --- a/src/octoprint/templates/dialogs/slicing.jinja2 +++ b/src/octoprint/templates/dialogs/slicing.jinja2 @@ -4,42 +4,47 @@