diff --git a/cura/Machines/Models/IntentModel.py b/cura/Machines/Models/IntentModel.py index da872f1723..951be7ab2d 100644 --- a/cura/Machines/Models/IntentModel.py +++ b/cura/Machines/Models/IntentModel.py @@ -114,7 +114,10 @@ class IntentModel(ListModel): Logger.log("w", "Could not find the variant %s", active_variant_name) continue active_variant_node = machine_node.variants[active_variant_name] - active_material_node = active_variant_node.materials[extruder.material.getMetaDataEntry("base_file")] + active_material_node = active_variant_node.materials.get(extruder.material.getMetaDataEntry("base_file")) + if active_material_node is None: + Logger.log("w", "Could not find the material %s", extruder.material.getMetaDataEntry("base_file")) + continue nodes.add(active_material_node) return nodes diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index 780adf54aa..0e4e491e46 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -1,13 +1,18 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import threading -from typing import Optional, Callable, Any, TYPE_CHECKING +from typing import Any, Callable, Optional, TYPE_CHECKING from UM.Logger import Logger -from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer -from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler +got_server_type = False +try: + from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer + from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler + got_server_type = True +except PermissionError: # Bug in http.server: Can't access MIME types. This will prevent the user from logging in. See Sentry bug Cura-3Q. + Logger.error("Can't start a server due to a PermissionError when starting the http.server.") if TYPE_CHECKING: from cura.OAuth2.Models import AuthenticationResponse @@ -50,15 +55,16 @@ class LocalAuthorizationServer: Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port) # Create the server and inject the callback and code. - self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler) - self._web_server.setAuthorizationHelpers(self._auth_helpers) - self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) - self._web_server.setVerificationCode(verification_code) - self._web_server.setState(state) + if got_server_type: + self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler) + self._web_server.setAuthorizationHelpers(self._auth_helpers) + self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) + self._web_server.setVerificationCode(verification_code) + self._web_server.setState(state) - # Start the server on a new thread. - self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) - self._web_server_thread.start() + # Start the server on a new thread. + self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) + self._web_server_thread.start() ## Stops the web server if it was running. It also does some cleanup. def stop(self) -> None: diff --git a/cura_app.py b/cura_app.py index d1f7ad1c46..ab52404b4d 100755 --- a/cura_app.py +++ b/cura_app.py @@ -31,10 +31,11 @@ known_args = vars(parser.parse_known_args()[0]) if with_sentry_sdk: sentry_env = "unknown" # Start off with a "IDK" if hasattr(sys, "frozen"): - sentry_env = "production" # A frozen build is a "real" distribution. - elif ApplicationMetadata.CuraVersion == "master": - sentry_env = "development" - elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion: + sentry_env = "production" # A frozen build has the posibility to be a "real" distribution. + + if ApplicationMetadata.CuraVersion == "master": + sentry_env = "development" # Master is always a development version. + elif ApplicationMetadata.CuraVersion in ["beta", "BETA"]: sentry_env = "beta" try: if ApplicationMetadata.CuraVersion.split(".")[2] == "99": diff --git a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py index c89bd31e21..8a183c25f4 100644 --- a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py @@ -47,7 +47,10 @@ class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): def checkRemovableDrives(self): drives = {} + # The currently available disk drives, e.g.: bitmask = ...1100 <-- ...DCBA bitmask = ctypes.windll.kernel32.GetLogicalDrives() + # Since we are ignoring drives A and B, the bitmask has has to shift twice to the right + bitmask >>= 2 # Check possible drive letters, from C to Z # Note: using ascii_uppercase because we do not want this to change with locale! # Skip A and B, since those drives are typically reserved for floppy disks. diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index 14db1a992d..f848f818d7 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -83,7 +83,7 @@ class CloudPackageChecker(QObject): package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages)) if package_discrepancy: self._model.addDiscrepancies(package_discrepancy) - self._model.initialize(subscribed_packages_payload) + self._model.initialize(self._package_manager, subscribed_packages_payload) self._handlePackageDiscrepancies() def _handlePackageDiscrepancies(self) -> None: diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py index 5d70fe7d29..778a36fbde 100644 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ b/plugins/Toolbox/src/CloudSync/LicensePresenter.py @@ -4,6 +4,7 @@ from typing import Dict, Optional, List, Any from PyQt5.QtCore import QObject, pyqtSlot +from UM.Logger import Logger from UM.PackageManager import PackageManager from UM.Signal import Signal from cura.CuraApplication import CuraApplication @@ -12,12 +13,19 @@ from UM.i18n import i18nCatalog from .LicenseModel import LicenseModel -## Call present() to show a licenseDialog for a set of packages -# licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages class LicensePresenter(QObject): + """Presents licenses for a set of packages for the user to accept or reject. + + Call present() exactly once to show a licenseDialog for a set of packages + Before presenting another set of licenses, create a new instance using resetCopy(). + + licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages. + """ def __init__(self, app: CuraApplication) -> None: super().__init__() + self._presented = False + """Whether present() has been called and state is expected to be initialized""" self._catalog = i18nCatalog("cura") self._dialog = None # type: Optional[QObject] self._package_manager = app.getPackageManager() # type: PackageManager @@ -39,6 +47,10 @@ class LicensePresenter(QObject): # \param plugin_path: Root directory of the Toolbox plugin # \param packages: Dict[package id, file path] def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: + if self._presented: + Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) + return + path = os.path.join(plugin_path, self._compatibility_dialog_path) self._initState(packages) @@ -56,6 +68,14 @@ class LicensePresenter(QObject): } self._dialog = self._app.createQmlComponent(path, context_properties) self._presentCurrentPackage() + self._presented = True + + def resetCopy(self) -> "LicensePresenter": + """Clean up and return a new copy with the same settings such as app""" + if self._dialog: + self._dialog.close() + self.licenseAnswers.disconnectAll() + return LicensePresenter(self._app) @pyqtSlot() def onLicenseAccepted(self) -> None: diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index 614d397d91..db16c5ea84 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -2,9 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import Qt, pyqtProperty, pyqtSlot + +from UM.PackageManager import PackageManager from UM.Qt.ListModel import ListModel +from UM.Version import Version + from cura import ApplicationMetadata -from UM.Logger import Logger from typing import List, Dict, Any @@ -46,7 +49,7 @@ class SubscribedPackagesModel(ListModel): def getIncompatiblePackages(self) -> List[str]: return [package["package_id"] for package in self._items if not package["is_compatible"]] - def initialize(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None: + def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None: self._items.clear() for item in subscribed_packages_payload: if item["package_id"] not in self._discrepancies: @@ -59,15 +62,13 @@ class SubscribedPackagesModel(ListModel): "md5_hash": item["md5_hash"], "is_dismissed": False, } - if self._sdk_version not in item["sdk_versions"]: - package.update({"is_compatible": False}) - else: - package.update({"is_compatible": True}) + + compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"]) + package.update({"is_compatible": compatible}) + try: package.update({"icon_url": item["icon_url"]}) except KeyError: # There is no 'icon_url" in the response payload for this package package.update({"icon_url": ""}) self._items.append(package) self.setItems(self._items) - - diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py index aed8c3f836..fc3dfaea38 100644 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -72,6 +72,8 @@ class SyncOrchestrator(Extension): self._showErrorMessage(message) plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) + self._license_presenter = self._license_presenter.resetCopy() + self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers) self._license_presenter.present(plugin_path, success_items) # Called when user has accepted / declined all licenses for the downloaded packages