mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-16 17:35:55 +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.
|
||||
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, List, Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Decorators import deprecated
|
||||
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 +85,7 @@ class Account(QObject):
|
||||
self._logged_in = False
|
||||
self._user_profile: Optional[UserProfile] = None
|
||||
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._manual_sync_enabled = False
|
||||
self._update_packages_enabled = False
|
||||
@ -109,6 +117,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())
|
||||
@ -311,13 +320,63 @@ 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."""
|
||||
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.
|
||||
|
||||
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,18 @@ 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.
|
||||
|
||||
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)
|
||||
def assignedPrinter(self):
|
||||
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
|
||||
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)
|
||||
|
@ -13,10 +13,98 @@ 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 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
|
||||
{
|
||||
id: popUp
|
||||
@ -46,56 +134,54 @@ 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]) {
|
||||
return OutputDevice.queuedPrintJobs[0].key != printJob.key;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
visible: monitorContextMenu.sendToTopVisible
|
||||
}
|
||||
|
||||
PrintJobContextMenuItem {
|
||||
onClicked: {
|
||||
PrintJobContextMenuItem
|
||||
{
|
||||
onClicked:
|
||||
{
|
||||
deleteConfirmationDialog.visible = true;
|
||||
popUp.close();
|
||||
}
|
||||
text: catalog.i18nc("@label", "Delete");
|
||||
visible: {
|
||||
if (!printJob) {
|
||||
return false;
|
||||
}
|
||||
var states = ["queued", "error", "sent_to_printer"];
|
||||
return states.indexOf(printJob.state) !== -1;
|
||||
}
|
||||
visible: monitorContextMenu.deleteVisible
|
||||
}
|
||||
|
||||
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,29 +192,19 @@ Item
|
||||
catalog.i18nc("@label", "Pause");
|
||||
}
|
||||
}
|
||||
visible: {
|
||||
if (!printJob) {
|
||||
return false;
|
||||
}
|
||||
var states = ["printing", "pausing", "paused", "resuming"];
|
||||
return states.indexOf(printJob.state) !== -1;
|
||||
}
|
||||
visible: monitorContextMenu.pauseVisible
|
||||
}
|
||||
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
var states = ["pre_print", "printing", "pausing", "paused", "resuming"];
|
||||
return states.indexOf(printJob.state) !== -1;
|
||||
}
|
||||
visible: monitorContextMenu.abortVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -206,7 +206,11 @@ Item
|
||||
onClicked: enabled ? contextMenu.switchPopupState() : {}
|
||||
visible:
|
||||
{
|
||||
if (!printJob)
|
||||
if(!printJob)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if(!contextMenu.hasItems)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -39,6 +39,7 @@ Item
|
||||
}
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
width: childrenRect.width
|
||||
visible: OutputDevice.canReadPrinterDetails
|
||||
|
||||
UM.ColorImage
|
||||
{
|
||||
|
@ -69,7 +69,7 @@ Component
|
||||
top: printers.bottom
|
||||
topMargin: 48 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
visible: OutputDevice.supportsPrintJobQueue
|
||||
visible: OutputDevice.supportsPrintJobQueue && OutputDevice.canReadPrintJobs
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
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,37 @@ 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
|
||||
|
||||
@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."""
|
||||
|
@ -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.
|
||||
import os
|
||||
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.
|
||||
import os
|
||||
from time import time
|
||||
@ -184,6 +184,42 @@ 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
|
||||
|
||||
@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")
|
||||
def openPrintJobControlPanel(self) -> None:
|
||||
raise NotImplementedError("openPrintJobControlPanel must be implemented")
|
||||
|
@ -33,63 +33,63 @@ 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"]
|
||||
},
|
||||
{
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
permissionsRequired: []
|
||||
}
|
||||
]
|
||||
|
||||
@ -99,7 +99,24 @@ Popup
|
||||
iconSource: modelData.thumbnail
|
||||
tooltipText: modelData.description
|
||||
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)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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():
|
||||
@ -86,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():
|
||||
@ -107,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)
|
||||
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user