From c529f2b31a6df05a6518f192904b995bf3db0182 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 15:33:46 +0200 Subject: [PATCH 01/21] Upon syncing, get account permissions from API This incurs another request every minute. But it's not provided by the log-in itself and can change, so that'll be necessary. Contributes to issue CURA-9220. --- cura/API/Account.py | 49 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index b63983e0cc..14b01a44c3 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -1,19 +1,25 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import enum from datetime import datetime +import json from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, pyqtEnum -from typing import Any, Optional, Dict, TYPE_CHECKING, Callable +from PyQt6.QtNetwork import QNetworkRequest +from typing import Any, Callable, Dict, Optional, Set, TYPE_CHECKING from UM.Logger import Logger from UM.Message import Message from UM.i18n import i18nCatalog +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.Models import OAuth2Settings, UserProfile from cura.UltimakerCloud import UltimakerCloudConstants +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope if TYPE_CHECKING: from cura.CuraApplication import CuraApplication + from PyQt6.QtNetwork import QNetworkReply i18n_catalog = i18nCatalog("cura") @@ -78,6 +84,7 @@ class Account(QObject): self._logged_in = False self._user_profile: Optional[UserProfile] = None self._additional_rights: Dict[str, Any] = {} + self._permissions: Set[str] = set() # Set of account permission keys, e.g. {"digital-factory.print-job.write"} self._sync_state = SyncState.IDLE self._manual_sync_enabled = False self._update_packages_enabled = False @@ -109,6 +116,7 @@ class Account(QObject): self._sync_services: Dict[str, int] = {} """contains entries "service_name" : SyncState""" + self.syncRequested.connect(self._updatePermissions) def initialize(self) -> None: self._authorization_service.initialize(self._application.getPreferences()) @@ -321,3 +329,40 @@ class Account(QObject): def additionalRights(self) -> Dict[str, Any]: """A dictionary which can be queried for additional account rights.""" return self._additional_rights + + def _updatePermissions(self) -> None: + """ + Update the set of permissions that the user has. + """ + def callback(reply: "QNetworkReply"): + status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) + if status_code is None: + Logger.error("Server did not respond to request to get list of permissions.") + return + if status_code >= 300: + Logger.error(f"Request to get list of permission resulted in HTTP error {status_code}") + return + + try: + reply_data = json.loads(bytes(reply.readAll()).decode("UTF-8")) + except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as e: + Logger.logException("e", f"Could not parse response to permission list request: {e}") + return + if "errors" in reply_data: + Logger.error(f"Request to get list of permission resulted in error response: {reply_data['errors']}") + return + + if "data" in reply_data and "permissions" in reply_data["data"]: + self._permissions = set(reply_data["data"]["permissions"]) + + def error_callback(reply: "QNetworkReply", error: "QNetworkReply.NetworkError"): + Logger.error(f"Request for user permissions list failed. Network error: {error}") + + HttpRequestManager.getInstance().get( + url = f"{self._oauth_root}/users/permissions", + scope = JsonDecoratorScope(UltimakerCloudScope(self._application)), + callback = callback, + error_callback = error_callback, + timeout = 10 + ) + From f3403ff8564cff7a089f353e32d4c49f8d0d46a0 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 15:57:51 +0200 Subject: [PATCH 02/21] Add property to get the list of permissions for the user The QML code can read this list and see if certain keys are in the list. Contributes to issue CURA-9220. --- cura/API/Account.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 14b01a44c3..e4bf8102a9 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -5,7 +5,7 @@ from datetime import datetime import json from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, pyqtEnum from PyQt6.QtNetwork import QNetworkRequest -from typing import Any, Callable, Dict, Optional, Set, TYPE_CHECKING +from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING from UM.Logger import Logger from UM.Message import Message @@ -84,7 +84,7 @@ class Account(QObject): self._logged_in = False self._user_profile: Optional[UserProfile] = None self._additional_rights: Dict[str, Any] = {} - self._permissions: Set[str] = set() # Set of account permission keys, e.g. {"digital-factory.print-job.write"} + self._permissions: List[str] = [] # List of account permission keys, e.g. ["digital-factory.print-job.write"] self._sync_state = SyncState.IDLE self._manual_sync_enabled = False self._update_packages_enabled = False @@ -330,9 +330,17 @@ class Account(QObject): """A dictionary which can be queried for additional account rights.""" return self._additional_rights + permissionsChanged = pyqtSignal() + @pyqtProperty("QVariantList", notify = permissionsChanged) + def permissions(self) -> List[str]: + """ + The permission keys that the user has in his account. + """ + return self._permissions + def _updatePermissions(self) -> None: """ - Update the set of permissions that the user has. + Update the list of permissions that the user has. """ def callback(reply: "QNetworkReply"): status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) @@ -353,7 +361,10 @@ class Account(QObject): return if "data" in reply_data and "permissions" in reply_data["data"]: - self._permissions = set(reply_data["data"]["permissions"]) + permissions = sorted(reply_data["data"]["permissions"]) + if permissions != self._permissions: + self._permissions = permissions + self.permissionsChanged.emit() def error_callback(reply: "QNetworkReply", error: "QNetworkReply.NetworkError"): Logger.error(f"Request for user permissions list failed. Network error: {error}") From 483911dc4a5e2734d9b2fca466a0ef32335238c3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 16:49:48 +0200 Subject: [PATCH 03/21] Fix error of undefined property when using LAN printers This property was defined for Cloud printers, but not for Network printers. In QML this then resulted in an error saying that null cannot be assigned to a bool (for the 'visible' property). The visible property then defaulted to its default, true. And so it was always visible, but still had an error. The error is now solved so that I can also check for other properties in this visible check. Contributes to issue CURA-9220. --- .../src/Network/LocalClusterOutputDevice.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index 2d27b7c3be..68eebf1000 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -96,6 +96,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def forceSendJob(self, print_job_uuid: str) -> None: self._getApiClient().forcePrintJob(print_job_uuid) + @pyqtProperty(bool, constant = True) + def supportsPrintJobQueue(self) -> bool: + """ + Whether this printer knows about queueing print jobs. + """ + return True # This API always supports print job queueing. + def setJobState(self, print_job_uuid: str, action: str) -> None: """Set the remote print job state. From b4c4371929e0bdf202a46c36f8db6467dc30b3d1 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 16:51:13 +0200 Subject: [PATCH 04/21] Add properties of whether the queue can be manipulated Not all printers can do that. Contributes to issue CURA-9220. --- .../src/Cloud/CloudOutputDevice.py | 28 ++++++++++++++++++- .../src/Network/LocalClusterOutputDevice.py | 24 +++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 8c45ce537f..a4b022d1e7 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from time import time @@ -96,6 +96,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._cloudClusterPrintersChanged) + # Trigger the permissionsChanged signal when the account's permissions change. + self._account.permissionsChanged.connect(self.permissionsChanged) # Keep server string of the last generated time to avoid updating models more than once for the same response self._received_printers = None # type: Optional[List[ClusterPrinterStatus]] @@ -340,6 +342,30 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def openPrinterControlPanel(self) -> None: QDesktopServices.openUrl(QUrl(self.clusterCloudUrl + "?utm_source=cura&utm_medium=software&utm_campaign=monitor-manage-printer")) + permissionsChanged = pyqtSignal() + + @pyqtProperty(bool, notify = permissionsChanged) + def canReadPrintJobs(self) -> bool: + """ + Whether this user can read the list of print jobs and their properties. + """ + return "digital-factory.print-job.read" in self._account.permissions + + @pyqtProperty(bool, notify = permissionsChanged) + def canWriteOthersPrintJobs(self) -> bool: + """ + Whether this user can change things about print jobs made by other + people. + """ + return "digital-factory.print-job.write" in self._account.permissions + + @pyqtProperty(bool, notify = permissionsChanged) + def canWriteOwnPrintJobs(self) -> bool: + """ + Whether this user can change things about print jobs made by themself. + """ + return "digital-factory.print-job.write.own" in self._account.permissions + @property def clusterData(self) -> CloudClusterResponse: """Gets the cluster response from which this device was created.""" diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index 68eebf1000..d7d4263b3c 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os from typing import Optional, Dict, List, Callable, Any @@ -103,6 +103,28 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): """ return True # This API always supports print job queueing. + @pyqtProperty(bool, constant = True) + def canReadPrintJobs(self) -> bool: + """ + Whether this user can read the list of print jobs and their properties. + """ + return False # On LAN, the user can always read it. + + @pyqtProperty(bool, constant = True) + def canWriteOthersPrintJobs(self) -> bool: + """ + Whether this user can change things about print jobs made by other + people. + """ + return True # On LAN, the user can always change this. + + @pyqtProperty(bool, constant = True) + def canWriteOwnPrintJobs(self) -> bool: + """ + Whether this user can change things about print jobs made by themself. + """ + return True # On LAN, the user can always change this. + def setJobState(self, print_job_uuid: str, action: str) -> None: """Set the remote print job state. From 9e1940dd4bbe20103a0341087a632ae1fea8b854 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 16:55:05 +0200 Subject: [PATCH 05/21] Move default behaviour up in inheritance hierarchy This way, any new printer types we define will automatically get these properties that are used by the monitor stage anyway. Contributes to issue CURA-9220. --- .../src/Network/LocalClusterOutputDevice.py | 29 ----------------- .../UltimakerNetworkedPrinterOutputDevice.py | 31 ++++++++++++++++++- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index d7d4263b3c..eaa3a7c4f5 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -96,35 +96,6 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def forceSendJob(self, print_job_uuid: str) -> None: self._getApiClient().forcePrintJob(print_job_uuid) - @pyqtProperty(bool, constant = True) - def supportsPrintJobQueue(self) -> bool: - """ - Whether this printer knows about queueing print jobs. - """ - return True # This API always supports print job queueing. - - @pyqtProperty(bool, constant = True) - def canReadPrintJobs(self) -> bool: - """ - Whether this user can read the list of print jobs and their properties. - """ - return False # On LAN, the user can always read it. - - @pyqtProperty(bool, constant = True) - def canWriteOthersPrintJobs(self) -> bool: - """ - Whether this user can change things about print jobs made by other - people. - """ - return True # On LAN, the user can always change this. - - @pyqtProperty(bool, constant = True) - def canWriteOwnPrintJobs(self) -> bool: - """ - Whether this user can change things about print jobs made by themself. - """ - return True # On LAN, the user can always change this. - def setJobState(self, print_job_uuid: str, action: str) -> None: """Set the remote print job state. diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 769e92610a..5fd642eaee 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os from time import time @@ -184,6 +184,35 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): def forceSendJob(self, print_job_uuid: str) -> None: raise NotImplementedError("forceSendJob must be implemented") + @pyqtProperty(bool, constant = True) + def supportsPrintJobQueue(self) -> bool: + """ + Whether this printer knows about queueing print jobs. + """ + return True # This API always supports print job queueing. + + @pyqtProperty(bool, constant = True) + def canReadPrintJobs(self) -> bool: + """ + Whether this user can read the list of print jobs and their properties. + """ + return True # On LAN, the user can always read it. + + @pyqtProperty(bool, constant = True) + def canWriteOthersPrintJobs(self) -> bool: + """ + Whether this user can change things about print jobs made by other + people. + """ + return True # On LAN, the user can always change this. + + @pyqtProperty(bool, constant = True) + def canWriteOwnPrintJobs(self) -> bool: + """ + Whether this user can change things about print jobs made by themself. + """ + return True # On LAN, the user can always change this. + @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: raise NotImplementedError("openPrintJobControlPanel must be implemented") From bef10b8b35ff6a1b0ea8a1a29ac941613ddc45dd Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 16:57:47 +0200 Subject: [PATCH 06/21] Hide print queue if the user can't read the print queue If there are no permissions, this would result in an error and a supposedly empty queue. That's incorrect. It should make it more clear that the user has no permissions to view the queue, while appearing that it is the normal workflow (it is for that user). So hide the queue entirely, just like with the UM2+C. Contributes to issue CURA-9220. --- plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml index 9e73662e1d..6e8f6b4ebd 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml @@ -69,7 +69,7 @@ Component top: printers.bottom topMargin: 48 * screenScaleFactor // TODO: Theme! } - visible: OutputDevice.supportsPrintJobQueue + visible: OutputDevice.supportsPrintJobQueue && OutputDevice.canReadPrintJobs } PrinterVideoStream From 99d93fa58acc654de51b397550e91a19710fd473 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 17:07:52 +0200 Subject: [PATCH 07/21] Check if the user can change the queue before showing option If the user can't change the queue of other prints, then it can't move prints to the top. Contributes to issue CURA-9220. --- .../UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml | 2 +- .../src/UltimakerNetworkedPrinterOutputDevice.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml index 03e9477d08..ac4c38c1cb 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml @@ -54,7 +54,7 @@ Item text: catalog.i18nc("@label", "Move to top"); visible: { if (printJob && (printJob.state == "queued" || printJob.state == "error") && !isAssigned(printJob)) { - if (OutputDevice && OutputDevice.queuedPrintJobs[0]) { + if (OutputDevice && OutputDevice.queuedPrintJobs[0] && OutputDevice.canWriteOthersPrintJobs) { return OutputDevice.queuedPrintJobs[0].key != printJob.key; } } diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 5fd642eaee..90ff129a2c 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -196,7 +196,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): """ Whether this user can read the list of print jobs and their properties. """ - return True # On LAN, the user can always read it. + return True @pyqtProperty(bool, constant = True) def canWriteOthersPrintJobs(self) -> bool: @@ -204,14 +204,14 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): Whether this user can change things about print jobs made by other people. """ - return True # On LAN, the user can always change this. + return False @pyqtProperty(bool, constant = True) def canWriteOwnPrintJobs(self) -> bool: """ Whether this user can change things about print jobs made by themself. """ - return True # On LAN, the user can always change this. + return False @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: From 8871fd6224a6e98be080ad2b6eed6c7498523fd9 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 17:28:56 +0200 Subject: [PATCH 08/21] Add property of the print job being made by the current user According to the Account API, is this print job made by the same user? This property is a bit inaccurate, in that this matches by the user name. The user name might be duplicate in some systems. Contributes to issue CURA-9220. --- cura/PrinterOutput/Models/PrintJobOutputModel.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutput/Models/PrintJobOutputModel.py b/cura/PrinterOutput/Models/PrintJobOutputModel.py index 164dc5cb67..9e2f7420e2 100644 --- a/cura/PrinterOutput/Models/PrintJobOutputModel.py +++ b/cura/PrinterOutput/Models/PrintJobOutputModel.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2022 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional, TYPE_CHECKING, List @@ -6,6 +6,8 @@ from typing import Optional, TYPE_CHECKING, List from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl from PyQt6.QtGui import QImage +from cura.CuraApplication import CuraApplication + if TYPE_CHECKING: from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel @@ -86,6 +88,13 @@ class PrintJobOutputModel(QObject): self._owner = owner self.ownerChanged.emit() + @pyqtProperty(bool, notify = ownerChanged) + def isMine(self) -> bool: + """ + Returns whether this print job was sent by the currently logged in user. + """ + return self._owner == CuraApplication.getInstance().getCuraAPI().account.userName + @pyqtProperty(QObject, notify=assignedPrinterChanged) def assignedPrinter(self): return self._assigned_printer From 5eb340e3f06221ed3285fdaf3a138316b94ffbf8 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Jul 2022 17:34:34 +0200 Subject: [PATCH 09/21] Check permissions before showing abort/pause/delete context buttons The user needs permissions to do these things, so don't show them if the user can't use them. Contributes to issue CURA-9220. --- .../resources/qml/MonitorContextMenu.qml | 93 +++++++++++++++---- .../UltimakerNetworkedPrinterOutputDevice.py | 4 +- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml index ac4c38c1cb..11cb55d42d 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml @@ -62,40 +62,65 @@ Item } } - PrintJobContextMenuItem { - onClicked: { + PrintJobContextMenuItem + { + onClicked: + { deleteConfirmationDialog.visible = true; popUp.close(); } text: catalog.i18nc("@label", "Delete"); - visible: { - if (!printJob) { + visible: + { + if(!printJob) + { return false; } + if(printJob.isMine) + { + if(!OutputDevice.canWriteOwnPrintJobs) + { + return false; + } + } + else + { + if(!OutputDevice.canWriteOthersPrintJobs) + { + return false; + } + } var states = ["queued", "error", "sent_to_printer"]; return states.indexOf(printJob.state) !== -1; } } - PrintJobContextMenuItem { + PrintJobContextMenuItem + { enabled: visible && !(printJob.state == "pausing" || printJob.state == "resuming"); - onClicked: { - if (printJob.state == "paused") { + onClicked: + { + if (printJob.state == "paused") + { printJob.setState("resume"); popUp.close(); return; } - if (printJob.state == "printing") { + if (printJob.state == "printing") + { printJob.setState("pause"); popUp.close(); return; } } - text: { - if (!printJob) { + text: + { + if(!printJob) + { return ""; } - switch(printJob.state) { + switch(printJob.state) + { case "paused": return catalog.i18nc("@label", "Resume"); case "pausing": @@ -106,26 +131,60 @@ Item catalog.i18nc("@label", "Pause"); } } - visible: { - if (!printJob) { + visible: + { + if(!printJob) + { return false; } + if(printJob.isMine) + { + if(!OutputDevice.canWriteOwnPrintJobs) + { + return false; + } + } + else + { + if(!OutputDevice.canWriteOthersPrintJobs) + { + return false; + } + } var states = ["printing", "pausing", "paused", "resuming"]; return states.indexOf(printJob.state) !== -1; } } - PrintJobContextMenuItem { + PrintJobContextMenuItem + { enabled: visible && printJob.state !== "aborting"; - onClicked: { + onClicked: + { abortConfirmationDialog.visible = true; popUp.close(); } text: printJob && printJob.state == "aborting" ? catalog.i18nc("@label", "Aborting...") : catalog.i18nc("@label", "Abort"); - visible: { - if (!printJob) { + visible: + { + if (!printJob) + { return false; } + if(printJob.isMine) + { + if(!OutputDevice.canWriteOwnPrintJobs) + { + return false; + } + } + else + { + if(!OutputDevice.canWriteOthersPrintJobs) + { + return false; + } + } var states = ["pre_print", "printing", "pausing", "paused", "resuming"]; return states.indexOf(printJob.state) !== -1; } diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 90ff129a2c..e928e4f2ef 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -204,14 +204,14 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): Whether this user can change things about print jobs made by other people. """ - return False + return True @pyqtProperty(bool, constant = True) def canWriteOwnPrintJobs(self) -> bool: """ Whether this user can change things about print jobs made by themself. """ - return False + return True @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: From e9f9730364cb81296503494e4f0307293cde9246 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 26 Jul 2022 16:36:38 +0200 Subject: [PATCH 10/21] Don't show context menu button if it is empty It will likely be empty for guest accounts on networks where the administrator is the only one with rights to access other people's prints. Contributes to issue CURA-9220. --- .../resources/qml/MonitorContextMenu.qml | 171 ++++++++++-------- .../resources/qml/MonitorPrintJobCard.qml | 6 +- .../resources/qml/MonitorPrinterCard.qml | 9 +- 3 files changed, 104 insertions(+), 82 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml index 11cb55d42d..cf7d5fe932 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml @@ -13,10 +13,94 @@ import Cura 1.6 as Cura */ Item { + id: monitorContextMenu property alias target: popUp.target property var printJob: null + //Everything in the pop-up only gets evaluated when showing the pop-up. + //However we want to show the button for showing the pop-up only if there is anything visible inside it. + //So compute here the visibility of the menu items, so that we can use it for the visibility of the button. + property bool sendToTopVisible: + { + if (printJob && (printJob.state == "queued" || printJob.state == "error") && !isAssigned(printJob)) { + if (OutputDevice && OutputDevice.queuedPrintJobs[0] && OutputDevice.canWriteOthersPrintJobs) { + return OutputDevice.queuedPrintJobs[0].key != printJob.key; + } + } + return false; + } + property bool deleteVisible: + { + if(!printJob) + { + return false; + } + if(printJob.isMine) + { + if(!OutputDevice.canWriteOwnPrintJobs) + { + return false; + } + } + else + { + if(!OutputDevice.canWriteOthersPrintJobs) + { + return false; + } + } + var states = ["queued", "error", "sent_to_printer"]; + return states.indexOf(printJob.state) !== -1; + } + property bool pauseVisible: + { + if(!printJob) + { + return false; + } + if(printJob.isMine) + { + if(!OutputDevice.canWriteOwnPrintJobs) + { + return false; + } + } + else + { + if(!OutputDevice.canWriteOthersPrintJobs) + { + return false; + } + } + var states = ["printing", "pausing", "paused", "resuming"]; + return states.indexOf(printJob.state) !== -1; + } + property bool abortVisible: + { + if(!printJob) + { + return false; + } + if(printJob.isMine) + { + if(!OutputDevice.canWriteOwnPrintJobs) + { + return false; + } + } + else + { + if(!OutputDevice.canWriteOthersPrintJobs) + { + return false; + } + } + var states = ["pre_print", "printing", "pausing", "paused", "resuming"]; + return states.indexOf(printJob.state) !== -1; + } + property bool hasItems: sendToTopVisible || deleteVisible || pauseVisible || abortVisible + GenericPopUp { id: popUp @@ -46,20 +130,15 @@ Item spacing: Math.floor(UM.Theme.getSize("default_margin").height / 2) - PrintJobContextMenuItem { - onClicked: { + PrintJobContextMenuItem + { + onClicked: + { sendToTopConfirmationDialog.visible = true; popUp.close(); } text: catalog.i18nc("@label", "Move to top"); - visible: { - if (printJob && (printJob.state == "queued" || printJob.state == "error") && !isAssigned(printJob)) { - if (OutputDevice && OutputDevice.queuedPrintJobs[0] && OutputDevice.canWriteOthersPrintJobs) { - return OutputDevice.queuedPrintJobs[0].key != printJob.key; - } - } - return false; - } + visible: monitorContextMenu.sendToTopVisible } PrintJobContextMenuItem @@ -70,29 +149,7 @@ Item popUp.close(); } text: catalog.i18nc("@label", "Delete"); - visible: - { - if(!printJob) - { - return false; - } - if(printJob.isMine) - { - if(!OutputDevice.canWriteOwnPrintJobs) - { - return false; - } - } - else - { - if(!OutputDevice.canWriteOthersPrintJobs) - { - return false; - } - } - var states = ["queued", "error", "sent_to_printer"]; - return states.indexOf(printJob.state) !== -1; - } + visible: monitorContextMenu.deleteVisible } PrintJobContextMenuItem @@ -131,29 +188,7 @@ Item catalog.i18nc("@label", "Pause"); } } - visible: - { - if(!printJob) - { - return false; - } - if(printJob.isMine) - { - if(!OutputDevice.canWriteOwnPrintJobs) - { - return false; - } - } - else - { - if(!OutputDevice.canWriteOthersPrintJobs) - { - return false; - } - } - var states = ["printing", "pausing", "paused", "resuming"]; - return states.indexOf(printJob.state) !== -1; - } + visible: monitorContextMenu.pauseVisible } PrintJobContextMenuItem @@ -165,29 +200,7 @@ Item popUp.close(); } text: printJob && printJob.state == "aborting" ? catalog.i18nc("@label", "Aborting...") : catalog.i18nc("@label", "Abort"); - visible: - { - if (!printJob) - { - return false; - } - if(printJob.isMine) - { - if(!OutputDevice.canWriteOwnPrintJobs) - { - return false; - } - } - else - { - if(!OutputDevice.canWriteOthersPrintJobs) - { - return false; - } - } - var states = ["pre_print", "printing", "pausing", "paused", "resuming"]; - return states.indexOf(printJob.state) !== -1; - } + visible: monitorContextMenu.abortVisible } } } diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml index 2974e5ce6b..64aa4e7a9c 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml @@ -206,7 +206,11 @@ Item onClicked: enabled ? contextMenu.switchPopupState() : {} visible: { - if (!printJob) + if(!printJob) + { + return false; + } + if(!contextMenu.hasItems) { return false; } diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml index 0069d017f6..67f308a64e 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml @@ -209,8 +209,13 @@ Item onClicked: enabled ? contextMenu.switchPopupState() : {} visible: { - if (!printer || !printer.activePrintJob) { - return false + if(!printer || !printer.activePrintJob) + { + return false; + } + if(!contextMenu.hasItems) + { + return false; } var states = ["queued", "error", "sent_to_printer", "pre_print", "printing", "pausing", "paused", "resuming"] return states.indexOf(printer.activePrintJob.state) !== -1 From 19ba092340e4b38c1a32302fd1c76da3762c32df Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 26 Jul 2022 16:56:19 +0200 Subject: [PATCH 11/21] Add property of whether the printer details can be read We'd assume so, but there is a permissions node for it. If it can't be read by the user, they can't navigate to the printer's overview page. So we should hide the button then. Contributes to issue CURA-9220. --- plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 7 +++++++ .../src/UltimakerNetworkedPrinterOutputDevice.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index a4b022d1e7..6431d09b7b 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -366,6 +366,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): """ return "digital-factory.print-job.write.own" in self._account.permissions + @pyqtProperty(bool, constant = True) + def canReadPrinterDetails(self) -> bool: + """ + Whether this user can read the status of the printer. + """ + return "digital-factory.printer.read" in self._account.permissions + @property def clusterData(self) -> CloudClusterResponse: """Gets the cluster response from which this device was created.""" diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index e928e4f2ef..8f25df37db 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -213,6 +213,13 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): """ return True + @pyqtProperty(bool, constant = True) + def canReadPrinterDetails(self) -> bool: + """ + Whether this user can read the status of the printer. + """ + return True + @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: raise NotImplementedError("openPrintJobControlPanel must be implemented") From 615f16bda0e0278559edaa4d48ddbd89d188a631 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 26 Jul 2022 16:57:07 +0200 Subject: [PATCH 12/21] Hide 'show in browser' when the user can't visit the printer overview No use then. They can't visit that page. Contributes to issue CURA-9220. --- plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml index 3fd500cfca..d559e99cc1 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml @@ -39,6 +39,7 @@ Item } height: 18 * screenScaleFactor // TODO: Theme! width: childrenRect.width + visible: OutputDevice.canReadPrinterDetails UM.ColorImage { From 9d820b8d029bb9e9984af0d61ba60b3cd4d13ece Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 26 Jul 2022 17:21:08 +0200 Subject: [PATCH 13/21] Redirect to USB workflow if user account doesn't have permissions If the user is not allowed to write profiles to the printers, then they'd get errors when trying to sync. Instead we'll redirect them to the USB workflow. This also works for users that have accounts but don't have the printers in the cloud. The original requirements suggest that the entire sync button must be hidden for this case. But to allow those people to still sync via USB I'm opting for this solution instead. Contributes to issue CURA-9220. --- .../Materials/MaterialsSyncDialog.qml | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/resources/qml/Preferences/Materials/MaterialsSyncDialog.qml b/resources/qml/Preferences/Materials/MaterialsSyncDialog.qml index 4fd82b3e02..ce3f9c4303 100644 --- a/resources/qml/Preferences/Materials/MaterialsSyncDialog.qml +++ b/resources/qml/Preferences/Materials/MaterialsSyncDialog.qml @@ -88,7 +88,15 @@ UM.Window { if(Cura.API.account.isLoggedIn) { - swipeView.currentIndex += 2; //Skip sign in page. + if(Cura.API.account.permissions.includes("digital-factory.printer.write")) + { + swipeView.currentIndex += 2; //Skip sign in page. Continue to sync via cloud. + } + else + { + //Logged in, but no permissions to start syncing. Direct them to USB. + swipeView.currentIndex = removableDriveSyncPage.SwipeView.index; + } } else { @@ -112,7 +120,15 @@ UM.Window { if(is_logged_in && signinPage.SwipeView.isCurrentItem) { - swipeView.currentIndex += 1; + if(Cura.API.account.permissions.includes("digital-factory.printer.write")) + { + swipeView.currentIndex += 1; + } + else + { + //Logged in, but no permissions to start syncing. Direct them to USB. + swipeView.currentIndex = removableDriveSyncPage.SwipeView.index; + } } } } From 7287644324530c57ccd10499b29fd9c6733890bb Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 26 Jul 2022 17:46:26 +0200 Subject: [PATCH 14/21] Add specific permission requirements to application switcher These functions require special permissions in the account now. Just checking for digital factory access is no longer enough. Contributes to issue CURA-9220. --- .../ApplicationSwitcherPopup.qml | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml b/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml index ede42fcd5f..d79bf29079 100644 --- a/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml +++ b/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml @@ -33,63 +33,72 @@ Popup thumbnail: UM.Theme.getIcon("PrinterTriple", "high"), description: catalog.i18nc("@tooltip:button", "Monitor printers in Ultimaker Digital Factory."), link: "https://digitalfactory.ultimaker.com/app/printers?utm_source=cura&utm_medium=software&utm_campaign=switcher-digital-factory-printers", - DFAccessRequired: true + DFAccessRequired: true, + permissionsRequired: ["digital-factory.printer.read"] }, { displayName: "Digital Library", //Not translated, since it's a brand name. thumbnail: UM.Theme.getIcon("Library", "high"), description: catalog.i18nc("@tooltip:button", "Create print projects in Digital Library."), link: "https://digitalfactory.ultimaker.com/app/library?utm_source=cura&utm_medium=software&utm_campaign=switcher-library", - DFAccessRequired: true + DFAccessRequired: true, + permissionsRequired: ["digital-factory.project.read.shared"] }, { displayName: catalog.i18nc("@label:button", "Print jobs"), thumbnail: UM.Theme.getIcon("FoodBeverages"), description: catalog.i18nc("@tooltip:button", "Monitor print jobs and reprint from your print history."), link: "https://digitalfactory.ultimaker.com/app/print-jobs?utm_source=cura&utm_medium=software&utm_campaign=switcher-digital-factory-printjobs", - DFAccessRequired: true + DFAccessRequired: true, + permissionsRequired: ["digital-factory.print-job.read"] }, { displayName: "Ultimaker Marketplace", //Not translated, since it's a brand name. thumbnail: UM.Theme.getIcon("Shop", "high"), description: catalog.i18nc("@tooltip:button", "Extend Ultimaker Cura with plugins and material profiles."), link: "https://marketplace.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-marketplace-materials", - DFAccessRequired: false + DFAccessRequired: false, + permissionsRequired: [] }, { displayName: "Ultimaker Academy", //Not translated, since it's a brand name. thumbnail: UM.Theme.getIcon("Knowledge"), description: catalog.i18nc("@tooltip:button", "Become a 3D printing expert with Ultimaker e-learning."), link: "https://academy.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-academy", - DFAccessRequired: false + DFAccessRequired: false, + permissionsRequired: [] }, { displayName: catalog.i18nc("@label:button", "Ultimaker support"), thumbnail: UM.Theme.getIcon("Help", "high"), description: catalog.i18nc("@tooltip:button", "Learn how to get started with Ultimaker Cura."), link: "https://support.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-support", - DFAccessRequired: false + DFAccessRequired: false, + permissionsRequired: [] }, { displayName: catalog.i18nc("@label:button", "Ask a question"), thumbnail: UM.Theme.getIcon("Speak", "high"), description: catalog.i18nc("@tooltip:button", "Consult the Ultimaker Community."), link: "https://community.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-community", - DFAccessRequired: false + DFAccessRequired: false, + permissionsRequired: [] }, { displayName: catalog.i18nc("@label:button", "Report a bug"), thumbnail: UM.Theme.getIcon("Bug", "high"), description: catalog.i18nc("@tooltip:button", "Let developers know that something is going wrong."), link: "https://github.com/Ultimaker/Cura/issues/new/choose", - DFAccessRequired: false + DFAccessRequired: false, + permissionsRequired: [] }, { displayName: "Ultimaker.com", //Not translated, since it's a URL. thumbnail: UM.Theme.getIcon("Browser"), description: catalog.i18nc("@tooltip:button", "Visit the Ultimaker website."), link: "https://ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-umwebsite", - DFAccessRequired: false + DFAccessRequired: false, + permissionsRequired: [] } ] @@ -99,7 +108,28 @@ Popup iconSource: modelData.thumbnail tooltipText: modelData.description isExternalLink: true - visible: modelData.DFAccessRequired ? Cura.API.account.isLoggedIn & Cura.API.account.additionalRights["df_access"] : true + visible: + { + if(modelData.DFAccessRequired && (!Cura.API.account.isLoggedIn || !Cura.API.account.additionalRights["df_access"])) + { + return false; + } + try + { + modelData.permissionsRequired.forEach(function(permission) + { + if(!Cura.API.account.isLoggedIn || !Cura.API.account.permissions.includes(permission)) //This required permission is not in the account. + { + throw "No permission to use this application."; //Can't return from within this lambda. Throw instead. + } + }); + } + catch(e) + { + return false; + } + return true; + } onClicked: Qt.openUrlExternally(modelData.link) } From f849df6ba3819149bc35c5523867e78cbe754846 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 26 Jul 2022 17:49:55 +0200 Subject: [PATCH 15/21] Remove old df_access additional right It is replaced by the new permissions system. The rights are more specific than 'digital factory access, yes or no'. It's now about whether you can read/write printers/projects/print jobs/etc and can differ whether it is your own project/job/etc or someone else's. Contributes to issue CURA-9220. --- .../DigitalLibrary/src/DigitalFactoryApiClient.py | 2 -- .../ApplicationSwitcherPopup.qml | 13 ------------- 2 files changed, 15 deletions(-) diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index de09ea2a09..13c65f79c4 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -71,8 +71,6 @@ class DigitalFactoryApiClient: has_access = response.library_max_private_projects == -1 or response.library_max_private_projects > 0 callback(has_access) self._library_max_private_projects = response.library_max_private_projects - # update the account with the additional user rights - self._account.updateAdditionalRight(df_access = has_access) else: Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}") callback(False) diff --git a/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml b/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml index d79bf29079..453f2ed9e0 100644 --- a/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml +++ b/resources/qml/ApplicationSwitcher/ApplicationSwitcherPopup.qml @@ -33,7 +33,6 @@ Popup thumbnail: UM.Theme.getIcon("PrinterTriple", "high"), description: catalog.i18nc("@tooltip:button", "Monitor printers in Ultimaker Digital Factory."), link: "https://digitalfactory.ultimaker.com/app/printers?utm_source=cura&utm_medium=software&utm_campaign=switcher-digital-factory-printers", - DFAccessRequired: true, permissionsRequired: ["digital-factory.printer.read"] }, { @@ -41,7 +40,6 @@ Popup thumbnail: UM.Theme.getIcon("Library", "high"), description: catalog.i18nc("@tooltip:button", "Create print projects in Digital Library."), link: "https://digitalfactory.ultimaker.com/app/library?utm_source=cura&utm_medium=software&utm_campaign=switcher-library", - DFAccessRequired: true, permissionsRequired: ["digital-factory.project.read.shared"] }, { @@ -49,7 +47,6 @@ Popup thumbnail: UM.Theme.getIcon("FoodBeverages"), description: catalog.i18nc("@tooltip:button", "Monitor print jobs and reprint from your print history."), link: "https://digitalfactory.ultimaker.com/app/print-jobs?utm_source=cura&utm_medium=software&utm_campaign=switcher-digital-factory-printjobs", - DFAccessRequired: true, permissionsRequired: ["digital-factory.print-job.read"] }, { @@ -57,7 +54,6 @@ Popup thumbnail: UM.Theme.getIcon("Shop", "high"), description: catalog.i18nc("@tooltip:button", "Extend Ultimaker Cura with plugins and material profiles."), link: "https://marketplace.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-marketplace-materials", - DFAccessRequired: false, permissionsRequired: [] }, { @@ -65,7 +61,6 @@ Popup thumbnail: UM.Theme.getIcon("Knowledge"), description: catalog.i18nc("@tooltip:button", "Become a 3D printing expert with Ultimaker e-learning."), link: "https://academy.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-academy", - DFAccessRequired: false, permissionsRequired: [] }, { @@ -73,7 +68,6 @@ Popup thumbnail: UM.Theme.getIcon("Help", "high"), description: catalog.i18nc("@tooltip:button", "Learn how to get started with Ultimaker Cura."), link: "https://support.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-support", - DFAccessRequired: false, permissionsRequired: [] }, { @@ -81,7 +75,6 @@ Popup thumbnail: UM.Theme.getIcon("Speak", "high"), description: catalog.i18nc("@tooltip:button", "Consult the Ultimaker Community."), link: "https://community.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-community", - DFAccessRequired: false, permissionsRequired: [] }, { @@ -89,7 +82,6 @@ Popup thumbnail: UM.Theme.getIcon("Bug", "high"), description: catalog.i18nc("@tooltip:button", "Let developers know that something is going wrong."), link: "https://github.com/Ultimaker/Cura/issues/new/choose", - DFAccessRequired: false, permissionsRequired: [] }, { @@ -97,7 +89,6 @@ Popup thumbnail: UM.Theme.getIcon("Browser"), description: catalog.i18nc("@tooltip:button", "Visit the Ultimaker website."), link: "https://ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-umwebsite", - DFAccessRequired: false, permissionsRequired: [] } ] @@ -110,10 +101,6 @@ Popup isExternalLink: true visible: { - if(modelData.DFAccessRequired && (!Cura.API.account.isLoggedIn || !Cura.API.account.additionalRights["df_access"])) - { - return false; - } try { modelData.permissionsRequired.forEach(function(permission) From d52be42e01480710920f2fc53ea2025e0798aa94 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 27 Jul 2022 10:49:30 +0200 Subject: [PATCH 16/21] Mock HttpRequestManager while changing log-in state Changing the log-in state causes additional requests to be made to get information from the account. Previously this wasn't a problem because the information was only obtained from other classes such as the DigitalLibrary to get information on how many library projects the user can make. But now that there are triggers in the Account class itself, those triggers get triggered. It'd make additional requests to the account server. We don't want the tests to make such requests. Contributes to issue CURA-9220. --- tests/API/TestAccount.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/API/TestAccount.py b/tests/API/TestAccount.py index 1ad73462c2..820e7dfbf5 100644 --- a/tests/API/TestAccount.py +++ b/tests/API/TestAccount.py @@ -1,3 +1,6 @@ +# Copyright (c) 2022 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from unittest.mock import MagicMock, patch import pytest @@ -26,7 +29,8 @@ def test_login(): mocked_auth_service.startAuthorizationFlow.assert_called_once_with(False) # Fake a successful login - account._onLoginStateChanged(True) + with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): # Don't want triggers for account information to actually make HTTP requests. + account._onLoginStateChanged(True) # Attempting to log in again shouldn't change anything. account.login() @@ -59,7 +63,8 @@ def test_logout(): assert not account.isLoggedIn # Pretend the stage changed - account._onLoginStateChanged(True) + with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): # Don't want triggers for account information to actually make HTTP requests. + account._onLoginStateChanged(True) assert account.isLoggedIn account.logout() @@ -72,12 +77,14 @@ def test_errorLoginState(application): account._authorization_service = mocked_auth_service account.loginStateChanged = MagicMock() - account._onLoginStateChanged(True, "BLARG!") + with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): # Don't want triggers for account information to actually make HTTP requests. + account._onLoginStateChanged(True, "BLARG!") # Even though we said that the login worked, it had an error message, so the login failed. account.loginStateChanged.emit.called_with(False) - account._onLoginStateChanged(True) - account._onLoginStateChanged(False, "OMGZOMG!") + with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): + account._onLoginStateChanged(True) + account._onLoginStateChanged(False, "OMGZOMG!") account.loginStateChanged.emit.called_with(False) def test_sync_success(): From 37a98cbb6f0075098dd8644b640966d7a8e36dd5 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 27 Jul 2022 11:43:31 +0200 Subject: [PATCH 17/21] Mock HttpRequestManager while changing sync state This change triggers a cascade of updates and in some cases triggers a sync. The sync trigger also triggers an update of the account permissions which crashes because the HttpRequestManager can't be started on a thread. We shouldn't make HTTP requests from our tests anyway so mock this away. Contributes to issue CURA-9220. --- tests/API/TestAccount.py | 46 +++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/tests/API/TestAccount.py b/tests/API/TestAccount.py index 820e7dfbf5..9d62646eff 100644 --- a/tests/API/TestAccount.py +++ b/tests/API/TestAccount.py @@ -93,18 +93,19 @@ def test_sync_success(): service1 = "test_service1" service2 = "test_service2" - account.setSyncState(service1, SyncState.SYNCING) - assert account.syncState == SyncState.SYNCING + with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): # Don't want triggers for account information to actually make HTTP requests. + account.setSyncState(service1, SyncState.SYNCING) + assert account.syncState == SyncState.SYNCING - account.setSyncState(service2, SyncState.SYNCING) - assert account.syncState == SyncState.SYNCING + account.setSyncState(service2, SyncState.SYNCING) + assert account.syncState == SyncState.SYNCING - account.setSyncState(service1, SyncState.SUCCESS) - # service2 still syncing - assert account.syncState == SyncState.SYNCING + account.setSyncState(service1, SyncState.SUCCESS) + # service2 still syncing + assert account.syncState == SyncState.SYNCING - account.setSyncState(service2, SyncState.SUCCESS) - assert account.syncState == SyncState.SUCCESS + account.setSyncState(service2, SyncState.SUCCESS) + assert account.syncState == SyncState.SUCCESS def test_sync_update_action(): @@ -114,23 +115,24 @@ def test_sync_update_action(): mockUpdateCallback = MagicMock() - account.setSyncState(service1, SyncState.SYNCING) - assert account.syncState == SyncState.SYNCING + with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): # Don't want triggers for account information to actually make HTTP requests. + account.setSyncState(service1, SyncState.SYNCING) + assert account.syncState == SyncState.SYNCING - account.setUpdatePackagesAction(mockUpdateCallback) - account.onUpdatePackagesClicked() - mockUpdateCallback.assert_called_once_with() - account.setSyncState(service1, SyncState.SUCCESS) + account.setUpdatePackagesAction(mockUpdateCallback) + account.onUpdatePackagesClicked() + mockUpdateCallback.assert_called_once_with() + account.setSyncState(service1, SyncState.SUCCESS) - account.sync() # starting a new sync resets the update action to None + account.sync() # starting a new sync resets the update action to None - account.setSyncState(service1, SyncState.SYNCING) - assert account.syncState == SyncState.SYNCING + account.setSyncState(service1, SyncState.SYNCING) + assert account.syncState == SyncState.SYNCING - account.onUpdatePackagesClicked() # Should not be connected to an action anymore - mockUpdateCallback.assert_called_once_with() # No additional calls - assert account.updatePackagesEnabled is False - account.setSyncState(service1, SyncState.SUCCESS) + account.onUpdatePackagesClicked() # Should not be connected to an action anymore + mockUpdateCallback.assert_called_once_with() # No additional calls + assert account.updatePackagesEnabled is False + account.setSyncState(service1, SyncState.SUCCESS) From 7c1a254812b39d3c9fa636526d51439e16cebab3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 1 Aug 2022 10:47:57 +0200 Subject: [PATCH 18/21] Add newlines before properties As suggested in the code review. Contributes to issue CURA-9220. Co-authored-by: Jelle Spijker --- .../UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml index cf7d5fe932..5b9517eaf8 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml @@ -23,13 +23,14 @@ Item //So compute here the visibility of the menu items, so that we can use it for the visibility of the button. property bool sendToTopVisible: { - if (printJob && (printJob.state == "queued" || printJob.state == "error") && !isAssigned(printJob)) { + if (printJob && printJob.state in ("queued", "error") && !isAssigned(printJob)) { if (OutputDevice && OutputDevice.queuedPrintJobs[0] && OutputDevice.canWriteOthersPrintJobs) { return OutputDevice.queuedPrintJobs[0].key != printJob.key; } } return false; } + property bool deleteVisible: { if(!printJob) @@ -53,6 +54,7 @@ Item var states = ["queued", "error", "sent_to_printer"]; return states.indexOf(printJob.state) !== -1; } + property bool pauseVisible: { if(!printJob) @@ -99,6 +101,7 @@ Item var states = ["pre_print", "printing", "pausing", "paused", "resuming"]; return states.indexOf(printJob.state) !== -1; } + property bool hasItems: sendToTopVisible || deleteVisible || pauseVisible || abortVisible GenericPopUp From da289b51d0c05ad1aa0042518e273c922bcf3bed Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 1 Aug 2022 10:33:56 +0200 Subject: [PATCH 19/21] Deprecate additionalRights property Consumers should now use the permissions system which gets the permissions from the account via a separate API call. Contributes to issue CURA-9220. --- cura/API/Account.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/API/Account.py b/cura/API/Account.py index e4bf8102a9..2651037e9e 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -7,6 +7,7 @@ from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, py from PyQt6.QtNetwork import QNetworkRequest from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING +from UM.Decorators import deprecated from UM.Logger import Logger from UM.Message import Message from UM.i18n import i18nCatalog @@ -319,12 +320,14 @@ class Account(QObject): self._authorization_service.deleteAuthData() + @deprecated("Get permissions from the 'permissions' property", since = "5.2.0") def updateAdditionalRight(self, **kwargs) -> None: """Update the additional rights of the account. The argument(s) are the rights that need to be set""" self._additional_rights.update(kwargs) self.additionalRightsChanged.emit(self._additional_rights) + @deprecated("Get permissions from the 'permissions' property", since = "5.2.0") @pyqtProperty("QVariantMap", notify = additionalRightsChanged) def additionalRights(self) -> Dict[str, Any]: """A dictionary which can be queried for additional account rights.""" From 84cf72d58f5b21dd9ae2621ed4b8f1bd32506163 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 1 Aug 2022 10:37:33 +0200 Subject: [PATCH 20/21] Document shortcoming of isMine check Contributes to issue CURA-9220. --- cura/PrinterOutput/Models/PrintJobOutputModel.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cura/PrinterOutput/Models/PrintJobOutputModel.py b/cura/PrinterOutput/Models/PrintJobOutputModel.py index 9e2f7420e2..6b8e07b29e 100644 --- a/cura/PrinterOutput/Models/PrintJobOutputModel.py +++ b/cura/PrinterOutput/Models/PrintJobOutputModel.py @@ -92,6 +92,11 @@ class PrintJobOutputModel(QObject): def isMine(self) -> bool: """ Returns whether this print job was sent by the currently logged in user. + + This checks the owner of the print job with the owner of the currently + logged in account. Both of these are human-readable account names which + may be duplicate. In practice the harm here is limited, but it's the + best we can do with the information available to the API. """ return self._owner == CuraApplication.getInstance().getCuraAPI().account.userName From 8a55a2aff628b3d144425e3a8a59f4fa19804837 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 1 Aug 2022 10:49:12 +0200 Subject: [PATCH 21/21] Further review suggestions Contributes to issue CURA-9220. --- plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml | 1 + .../UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml index 5b9517eaf8..33fdb0eb38 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml @@ -78,6 +78,7 @@ Item var states = ["printing", "pausing", "paused", "resuming"]; return states.indexOf(printJob.state) !== -1; } + property bool abortVisible: { if(!printJob) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index eaa3a7c4f5..f5afb0b14e 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os from typing import Optional, Dict, List, Callable, Any