mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-18 00:55:59 +08:00
Merge pull request #12866 from Ultimaker/CURA-9220_hide_if_no_permission
Hide cloud interaction buttons if the user has no permissions to them
This commit is contained in:
commit
7b768ca810
@ -1,19 +1,26 @@
|
|||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import enum
|
import enum
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import json
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, pyqtEnum
|
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, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.i18n import i18nCatalog
|
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.AuthorizationService import AuthorizationService
|
||||||
from cura.OAuth2.Models import OAuth2Settings, UserProfile
|
from cura.OAuth2.Models import OAuth2Settings, UserProfile
|
||||||
from cura.UltimakerCloud import UltimakerCloudConstants
|
from cura.UltimakerCloud import UltimakerCloudConstants
|
||||||
|
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from PyQt6.QtNetwork import QNetworkReply
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
@ -78,6 +85,7 @@ class Account(QObject):
|
|||||||
self._logged_in = False
|
self._logged_in = False
|
||||||
self._user_profile: Optional[UserProfile] = None
|
self._user_profile: Optional[UserProfile] = None
|
||||||
self._additional_rights: Dict[str, Any] = {}
|
self._additional_rights: Dict[str, Any] = {}
|
||||||
|
self._permissions: List[str] = [] # List of account permission keys, e.g. ["digital-factory.print-job.write"]
|
||||||
self._sync_state = SyncState.IDLE
|
self._sync_state = SyncState.IDLE
|
||||||
self._manual_sync_enabled = False
|
self._manual_sync_enabled = False
|
||||||
self._update_packages_enabled = False
|
self._update_packages_enabled = False
|
||||||
@ -109,6 +117,7 @@ class Account(QObject):
|
|||||||
|
|
||||||
self._sync_services: Dict[str, int] = {}
|
self._sync_services: Dict[str, int] = {}
|
||||||
"""contains entries "service_name" : SyncState"""
|
"""contains entries "service_name" : SyncState"""
|
||||||
|
self.syncRequested.connect(self._updatePermissions)
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._authorization_service.initialize(self._application.getPreferences())
|
self._authorization_service.initialize(self._application.getPreferences())
|
||||||
@ -311,13 +320,63 @@ class Account(QObject):
|
|||||||
|
|
||||||
self._authorization_service.deleteAuthData()
|
self._authorization_service.deleteAuthData()
|
||||||
|
|
||||||
|
@deprecated("Get permissions from the 'permissions' property", since = "5.2.0")
|
||||||
def updateAdditionalRight(self, **kwargs) -> None:
|
def updateAdditionalRight(self, **kwargs) -> None:
|
||||||
"""Update the additional rights of the account.
|
"""Update the additional rights of the account.
|
||||||
The argument(s) are the rights that need to be set"""
|
The argument(s) are the rights that need to be set"""
|
||||||
self._additional_rights.update(kwargs)
|
self._additional_rights.update(kwargs)
|
||||||
self.additionalRightsChanged.emit(self._additional_rights)
|
self.additionalRightsChanged.emit(self._additional_rights)
|
||||||
|
|
||||||
|
@deprecated("Get permissions from the 'permissions' property", since = "5.2.0")
|
||||||
@pyqtProperty("QVariantMap", notify = additionalRightsChanged)
|
@pyqtProperty("QVariantMap", notify = additionalRightsChanged)
|
||||||
def additionalRights(self) -> Dict[str, Any]:
|
def additionalRights(self) -> Dict[str, Any]:
|
||||||
"""A dictionary which can be queried for additional account rights."""
|
"""A dictionary which can be queried for additional account rights."""
|
||||||
return self._additional_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 list 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"]:
|
||||||
|
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}")
|
||||||
|
|
||||||
|
HttpRequestManager.getInstance().get(
|
||||||
|
url = f"{self._oauth_root}/users/permissions",
|
||||||
|
scope = JsonDecoratorScope(UltimakerCloudScope(self._application)),
|
||||||
|
callback = callback,
|
||||||
|
error_callback = error_callback,
|
||||||
|
timeout = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING, List
|
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.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
|
||||||
from PyQt6.QtGui import QImage
|
from PyQt6.QtGui import QImage
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||||
@ -86,6 +88,18 @@ class PrintJobOutputModel(QObject):
|
|||||||
self._owner = owner
|
self._owner = owner
|
||||||
self.ownerChanged.emit()
|
self.ownerChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = ownerChanged)
|
||||||
|
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
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
||||||
def assignedPrinter(self):
|
def assignedPrinter(self):
|
||||||
return self._assigned_printer
|
return self._assigned_printer
|
||||||
|
@ -71,8 +71,6 @@ class DigitalFactoryApiClient:
|
|||||||
has_access = response.library_max_private_projects == -1 or response.library_max_private_projects > 0
|
has_access = response.library_max_private_projects == -1 or response.library_max_private_projects > 0
|
||||||
callback(has_access)
|
callback(has_access)
|
||||||
self._library_max_private_projects = response.library_max_private_projects
|
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:
|
else:
|
||||||
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
|
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
|
||||||
callback(False)
|
callback(False)
|
||||||
|
@ -13,10 +13,98 @@ import Cura 1.6 as Cura
|
|||||||
*/
|
*/
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
|
id: monitorContextMenu
|
||||||
property alias target: popUp.target
|
property alias target: popUp.target
|
||||||
|
|
||||||
property var printJob: null
|
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 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)
|
||||||
|
{
|
||||||
|
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
|
GenericPopUp
|
||||||
{
|
{
|
||||||
id: popUp
|
id: popUp
|
||||||
@ -46,56 +134,54 @@ Item
|
|||||||
|
|
||||||
spacing: Math.floor(UM.Theme.getSize("default_margin").height / 2)
|
spacing: Math.floor(UM.Theme.getSize("default_margin").height / 2)
|
||||||
|
|
||||||
PrintJobContextMenuItem {
|
PrintJobContextMenuItem
|
||||||
onClicked: {
|
{
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
sendToTopConfirmationDialog.visible = true;
|
sendToTopConfirmationDialog.visible = true;
|
||||||
popUp.close();
|
popUp.close();
|
||||||
}
|
}
|
||||||
text: catalog.i18nc("@label", "Move to top");
|
text: catalog.i18nc("@label", "Move to top");
|
||||||
visible: {
|
visible: monitorContextMenu.sendToTopVisible
|
||||||
if (printJob && (printJob.state == "queued" || printJob.state == "error") && !isAssigned(printJob)) {
|
|
||||||
if (OutputDevice && OutputDevice.queuedPrintJobs[0]) {
|
|
||||||
return OutputDevice.queuedPrintJobs[0].key != printJob.key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintJobContextMenuItem {
|
PrintJobContextMenuItem
|
||||||
onClicked: {
|
{
|
||||||
|
onClicked:
|
||||||
|
{
|
||||||
deleteConfirmationDialog.visible = true;
|
deleteConfirmationDialog.visible = true;
|
||||||
popUp.close();
|
popUp.close();
|
||||||
}
|
}
|
||||||
text: catalog.i18nc("@label", "Delete");
|
text: catalog.i18nc("@label", "Delete");
|
||||||
visible: {
|
visible: monitorContextMenu.deleteVisible
|
||||||
if (!printJob) {
|
|
||||||
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");
|
enabled: visible && !(printJob.state == "pausing" || printJob.state == "resuming");
|
||||||
onClicked: {
|
onClicked:
|
||||||
if (printJob.state == "paused") {
|
{
|
||||||
|
if (printJob.state == "paused")
|
||||||
|
{
|
||||||
printJob.setState("resume");
|
printJob.setState("resume");
|
||||||
popUp.close();
|
popUp.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (printJob.state == "printing") {
|
if (printJob.state == "printing")
|
||||||
|
{
|
||||||
printJob.setState("pause");
|
printJob.setState("pause");
|
||||||
popUp.close();
|
popUp.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text: {
|
text:
|
||||||
if (!printJob) {
|
{
|
||||||
|
if(!printJob)
|
||||||
|
{
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
switch(printJob.state) {
|
switch(printJob.state)
|
||||||
|
{
|
||||||
case "paused":
|
case "paused":
|
||||||
return catalog.i18nc("@label", "Resume");
|
return catalog.i18nc("@label", "Resume");
|
||||||
case "pausing":
|
case "pausing":
|
||||||
@ -106,29 +192,19 @@ Item
|
|||||||
catalog.i18nc("@label", "Pause");
|
catalog.i18nc("@label", "Pause");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visible: {
|
visible: monitorContextMenu.pauseVisible
|
||||||
if (!printJob) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var states = ["printing", "pausing", "paused", "resuming"];
|
|
||||||
return states.indexOf(printJob.state) !== -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintJobContextMenuItem {
|
PrintJobContextMenuItem
|
||||||
|
{
|
||||||
enabled: visible && printJob.state !== "aborting";
|
enabled: visible && printJob.state !== "aborting";
|
||||||
onClicked: {
|
onClicked:
|
||||||
|
{
|
||||||
abortConfirmationDialog.visible = true;
|
abortConfirmationDialog.visible = true;
|
||||||
popUp.close();
|
popUp.close();
|
||||||
}
|
}
|
||||||
text: printJob && printJob.state == "aborting" ? catalog.i18nc("@label", "Aborting...") : catalog.i18nc("@label", "Abort");
|
text: printJob && printJob.state == "aborting" ? catalog.i18nc("@label", "Aborting...") : catalog.i18nc("@label", "Abort");
|
||||||
visible: {
|
visible: monitorContextMenu.abortVisible
|
||||||
if (!printJob) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var states = ["pre_print", "printing", "pausing", "paused", "resuming"];
|
|
||||||
return states.indexOf(printJob.state) !== -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,11 @@ Item
|
|||||||
onClicked: enabled ? contextMenu.switchPopupState() : {}
|
onClicked: enabled ? contextMenu.switchPopupState() : {}
|
||||||
visible:
|
visible:
|
||||||
{
|
{
|
||||||
if (!printJob)
|
if(!printJob)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(!contextMenu.hasItems)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -209,8 +209,13 @@ Item
|
|||||||
onClicked: enabled ? contextMenu.switchPopupState() : {}
|
onClicked: enabled ? contextMenu.switchPopupState() : {}
|
||||||
visible:
|
visible:
|
||||||
{
|
{
|
||||||
if (!printer || !printer.activePrintJob) {
|
if(!printer || !printer.activePrintJob)
|
||||||
return false
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(!contextMenu.hasItems)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
var states = ["queued", "error", "sent_to_printer", "pre_print", "printing", "pausing", "paused", "resuming"]
|
var states = ["queued", "error", "sent_to_printer", "pre_print", "printing", "pausing", "paused", "resuming"]
|
||||||
return states.indexOf(printer.activePrintJob.state) !== -1
|
return states.indexOf(printer.activePrintJob.state) !== -1
|
||||||
|
@ -39,6 +39,7 @@ Item
|
|||||||
}
|
}
|
||||||
height: 18 * screenScaleFactor // TODO: Theme!
|
height: 18 * screenScaleFactor // TODO: Theme!
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
|
visible: OutputDevice.canReadPrinterDetails
|
||||||
|
|
||||||
UM.ColorImage
|
UM.ColorImage
|
||||||
{
|
{
|
||||||
|
@ -69,7 +69,7 @@ Component
|
|||||||
top: printers.bottom
|
top: printers.bottom
|
||||||
topMargin: 48 * screenScaleFactor // TODO: Theme!
|
topMargin: 48 * screenScaleFactor // TODO: Theme!
|
||||||
}
|
}
|
||||||
visible: OutputDevice.supportsPrintJobQueue
|
visible: OutputDevice.supportsPrintJobQueue && OutputDevice.canReadPrintJobs
|
||||||
}
|
}
|
||||||
|
|
||||||
PrinterVideoStream
|
PrinterVideoStream
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from time import time
|
from time import time
|
||||||
@ -96,6 +96,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||||||
|
|
||||||
# Trigger the printersChanged signal when the private signal is triggered.
|
# Trigger the printersChanged signal when the private signal is triggered.
|
||||||
self.printersChanged.connect(self._cloudClusterPrintersChanged)
|
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
|
# 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]]
|
self._received_printers = None # type: Optional[List[ClusterPrinterStatus]]
|
||||||
@ -340,6 +342,37 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||||||
def openPrinterControlPanel(self) -> None:
|
def openPrinterControlPanel(self) -> None:
|
||||||
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl + "?utm_source=cura&utm_medium=software&utm_campaign=monitor-manage-printer"))
|
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
|
||||||
|
|
||||||
|
@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
|
@property
|
||||||
def clusterData(self) -> CloudClusterResponse:
|
def clusterData(self) -> CloudClusterResponse:
|
||||||
"""Gets the cluster response from which this device was created."""
|
"""Gets the cluster response from which this device was created."""
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import os
|
import os
|
||||||
from typing import Optional, Dict, List, Callable, Any
|
from typing import Optional, Dict, List, Callable, Any
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import os
|
import os
|
||||||
from time import time
|
from time import time
|
||||||
@ -184,6 +184,42 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||||||
def forceSendJob(self, print_job_uuid: str) -> None:
|
def forceSendJob(self, print_job_uuid: str) -> None:
|
||||||
raise NotImplementedError("forceSendJob must be implemented")
|
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
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canWriteOthersPrintJobs(self) -> bool:
|
||||||
|
"""
|
||||||
|
Whether this user can change things about print jobs made by other
|
||||||
|
people.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canWriteOwnPrintJobs(self) -> bool:
|
||||||
|
"""
|
||||||
|
Whether this user can change things about print jobs made by themself.
|
||||||
|
"""
|
||||||
|
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")
|
@pyqtSlot(name="openPrintJobControlPanel")
|
||||||
def openPrintJobControlPanel(self) -> None:
|
def openPrintJobControlPanel(self) -> None:
|
||||||
raise NotImplementedError("openPrintJobControlPanel must be implemented")
|
raise NotImplementedError("openPrintJobControlPanel must be implemented")
|
||||||
|
@ -33,63 +33,63 @@ Popup
|
|||||||
thumbnail: UM.Theme.getIcon("PrinterTriple", "high"),
|
thumbnail: UM.Theme.getIcon("PrinterTriple", "high"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Monitor printers in Ultimaker Digital Factory."),
|
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",
|
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"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: "Digital Library", //Not translated, since it's a brand name.
|
displayName: "Digital Library", //Not translated, since it's a brand name.
|
||||||
thumbnail: UM.Theme.getIcon("Library", "high"),
|
thumbnail: UM.Theme.getIcon("Library", "high"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Create print projects in Digital Library."),
|
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",
|
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"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: catalog.i18nc("@label:button", "Print jobs"),
|
displayName: catalog.i18nc("@label:button", "Print jobs"),
|
||||||
thumbnail: UM.Theme.getIcon("FoodBeverages"),
|
thumbnail: UM.Theme.getIcon("FoodBeverages"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Monitor print jobs and reprint from your print history."),
|
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",
|
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"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: "Ultimaker Marketplace", //Not translated, since it's a brand name.
|
displayName: "Ultimaker Marketplace", //Not translated, since it's a brand name.
|
||||||
thumbnail: UM.Theme.getIcon("Shop", "high"),
|
thumbnail: UM.Theme.getIcon("Shop", "high"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Extend Ultimaker Cura with plugins and material profiles."),
|
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",
|
link: "https://marketplace.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-marketplace-materials",
|
||||||
DFAccessRequired: false
|
permissionsRequired: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: "Ultimaker Academy", //Not translated, since it's a brand name.
|
displayName: "Ultimaker Academy", //Not translated, since it's a brand name.
|
||||||
thumbnail: UM.Theme.getIcon("Knowledge"),
|
thumbnail: UM.Theme.getIcon("Knowledge"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Become a 3D printing expert with Ultimaker e-learning."),
|
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",
|
link: "https://academy.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-academy",
|
||||||
DFAccessRequired: false
|
permissionsRequired: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: catalog.i18nc("@label:button", "Ultimaker support"),
|
displayName: catalog.i18nc("@label:button", "Ultimaker support"),
|
||||||
thumbnail: UM.Theme.getIcon("Help", "high"),
|
thumbnail: UM.Theme.getIcon("Help", "high"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Learn how to get started with Ultimaker Cura."),
|
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",
|
link: "https://support.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-support",
|
||||||
DFAccessRequired: false
|
permissionsRequired: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: catalog.i18nc("@label:button", "Ask a question"),
|
displayName: catalog.i18nc("@label:button", "Ask a question"),
|
||||||
thumbnail: UM.Theme.getIcon("Speak", "high"),
|
thumbnail: UM.Theme.getIcon("Speak", "high"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Consult the Ultimaker Community."),
|
description: catalog.i18nc("@tooltip:button", "Consult the Ultimaker Community."),
|
||||||
link: "https://community.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-community",
|
link: "https://community.ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-community",
|
||||||
DFAccessRequired: false
|
permissionsRequired: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: catalog.i18nc("@label:button", "Report a bug"),
|
displayName: catalog.i18nc("@label:button", "Report a bug"),
|
||||||
thumbnail: UM.Theme.getIcon("Bug", "high"),
|
thumbnail: UM.Theme.getIcon("Bug", "high"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Let developers know that something is going wrong."),
|
description: catalog.i18nc("@tooltip:button", "Let developers know that something is going wrong."),
|
||||||
link: "https://github.com/Ultimaker/Cura/issues/new/choose",
|
link: "https://github.com/Ultimaker/Cura/issues/new/choose",
|
||||||
DFAccessRequired: false
|
permissionsRequired: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: "Ultimaker.com", //Not translated, since it's a URL.
|
displayName: "Ultimaker.com", //Not translated, since it's a URL.
|
||||||
thumbnail: UM.Theme.getIcon("Browser"),
|
thumbnail: UM.Theme.getIcon("Browser"),
|
||||||
description: catalog.i18nc("@tooltip:button", "Visit the Ultimaker website."),
|
description: catalog.i18nc("@tooltip:button", "Visit the Ultimaker website."),
|
||||||
link: "https://ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-umwebsite",
|
link: "https://ultimaker.com/?utm_source=cura&utm_medium=software&utm_campaign=switcher-umwebsite",
|
||||||
DFAccessRequired: false
|
permissionsRequired: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -99,7 +99,24 @@ Popup
|
|||||||
iconSource: modelData.thumbnail
|
iconSource: modelData.thumbnail
|
||||||
tooltipText: modelData.description
|
tooltipText: modelData.description
|
||||||
isExternalLink: true
|
isExternalLink: true
|
||||||
visible: modelData.DFAccessRequired ? Cura.API.account.isLoggedIn & Cura.API.account.additionalRights["df_access"] : true
|
visible:
|
||||||
|
{
|
||||||
|
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)
|
onClicked: Qt.openUrlExternally(modelData.link)
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,15 @@ UM.Window
|
|||||||
{
|
{
|
||||||
if(Cura.API.account.isLoggedIn)
|
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
|
else
|
||||||
{
|
{
|
||||||
@ -112,7 +120,15 @@ UM.Window
|
|||||||
{
|
{
|
||||||
if(is_logged_in && signinPage.SwipeView.isCurrentItem)
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -26,7 +29,8 @@ def test_login():
|
|||||||
mocked_auth_service.startAuthorizationFlow.assert_called_once_with(False)
|
mocked_auth_service.startAuthorizationFlow.assert_called_once_with(False)
|
||||||
|
|
||||||
# Fake a successful login
|
# 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.
|
# Attempting to log in again shouldn't change anything.
|
||||||
account.login()
|
account.login()
|
||||||
@ -59,7 +63,8 @@ def test_logout():
|
|||||||
assert not account.isLoggedIn
|
assert not account.isLoggedIn
|
||||||
|
|
||||||
# Pretend the stage changed
|
# 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
|
assert account.isLoggedIn
|
||||||
|
|
||||||
account.logout()
|
account.logout()
|
||||||
@ -72,12 +77,14 @@ def test_errorLoginState(application):
|
|||||||
account._authorization_service = mocked_auth_service
|
account._authorization_service = mocked_auth_service
|
||||||
account.loginStateChanged = MagicMock()
|
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.
|
# Even though we said that the login worked, it had an error message, so the login failed.
|
||||||
account.loginStateChanged.emit.called_with(False)
|
account.loginStateChanged.emit.called_with(False)
|
||||||
|
|
||||||
account._onLoginStateChanged(True)
|
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"):
|
||||||
account._onLoginStateChanged(False, "OMGZOMG!")
|
account._onLoginStateChanged(True)
|
||||||
|
account._onLoginStateChanged(False, "OMGZOMG!")
|
||||||
account.loginStateChanged.emit.called_with(False)
|
account.loginStateChanged.emit.called_with(False)
|
||||||
|
|
||||||
def test_sync_success():
|
def test_sync_success():
|
||||||
@ -86,18 +93,19 @@ def test_sync_success():
|
|||||||
service1 = "test_service1"
|
service1 = "test_service1"
|
||||||
service2 = "test_service2"
|
service2 = "test_service2"
|
||||||
|
|
||||||
account.setSyncState(service1, SyncState.SYNCING)
|
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): # Don't want triggers for account information to actually make HTTP requests.
|
||||||
assert account.syncState == SyncState.SYNCING
|
account.setSyncState(service1, SyncState.SYNCING)
|
||||||
|
assert account.syncState == SyncState.SYNCING
|
||||||
|
|
||||||
account.setSyncState(service2, SyncState.SYNCING)
|
account.setSyncState(service2, SyncState.SYNCING)
|
||||||
assert account.syncState == SyncState.SYNCING
|
assert account.syncState == SyncState.SYNCING
|
||||||
|
|
||||||
account.setSyncState(service1, SyncState.SUCCESS)
|
account.setSyncState(service1, SyncState.SUCCESS)
|
||||||
# service2 still syncing
|
# service2 still syncing
|
||||||
assert account.syncState == SyncState.SYNCING
|
assert account.syncState == SyncState.SYNCING
|
||||||
|
|
||||||
account.setSyncState(service2, SyncState.SUCCESS)
|
account.setSyncState(service2, SyncState.SUCCESS)
|
||||||
assert account.syncState == SyncState.SUCCESS
|
assert account.syncState == SyncState.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
def test_sync_update_action():
|
def test_sync_update_action():
|
||||||
@ -107,23 +115,24 @@ def test_sync_update_action():
|
|||||||
|
|
||||||
mockUpdateCallback = MagicMock()
|
mockUpdateCallback = MagicMock()
|
||||||
|
|
||||||
account.setSyncState(service1, SyncState.SYNCING)
|
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance"): # Don't want triggers for account information to actually make HTTP requests.
|
||||||
assert account.syncState == SyncState.SYNCING
|
account.setSyncState(service1, SyncState.SYNCING)
|
||||||
|
assert account.syncState == SyncState.SYNCING
|
||||||
|
|
||||||
account.setUpdatePackagesAction(mockUpdateCallback)
|
account.setUpdatePackagesAction(mockUpdateCallback)
|
||||||
account.onUpdatePackagesClicked()
|
account.onUpdatePackagesClicked()
|
||||||
mockUpdateCallback.assert_called_once_with()
|
mockUpdateCallback.assert_called_once_with()
|
||||||
account.setSyncState(service1, SyncState.SUCCESS)
|
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)
|
account.setSyncState(service1, SyncState.SYNCING)
|
||||||
assert account.syncState == SyncState.SYNCING
|
assert account.syncState == SyncState.SYNCING
|
||||||
|
|
||||||
account.onUpdatePackagesClicked() # Should not be connected to an action anymore
|
account.onUpdatePackagesClicked() # Should not be connected to an action anymore
|
||||||
mockUpdateCallback.assert_called_once_with() # No additional calls
|
mockUpdateCallback.assert_called_once_with() # No additional calls
|
||||||
assert account.updatePackagesEnabled is False
|
assert account.updatePackagesEnabled is False
|
||||||
account.setSyncState(service1, SyncState.SUCCESS)
|
account.setSyncState(service1, SyncState.SUCCESS)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user