mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-06 21:44:01 -06:00
Merge branch 'main' into CURA-11617-set-print-sequence
This commit is contained in:
commit
285a36a38a
9 changed files with 91 additions and 19 deletions
|
@ -1082,6 +1082,10 @@ class CuraApplication(QtApplication):
|
|||
def getTextManager(self, *args) -> "TextManager":
|
||||
return self._text_manager
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def getWorkplaceDropToBuildplate(self, drop_to_build_plate: bool) ->None:
|
||||
return self._physics.setAppPerModelDropDown(drop_to_build_plate)
|
||||
|
||||
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
|
||||
if self._cura_formula_functions is None:
|
||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||
|
|
|
@ -16,6 +16,7 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To downlo
|
|||
|
||||
catalog = i18nCatalog("cura")
|
||||
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
REQUEST_TIMEOUT = 5 # Seconds
|
||||
|
||||
|
||||
class AuthorizationHelpers:
|
||||
|
@ -53,7 +54,8 @@ class AuthorizationHelpers:
|
|||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||
headers_dict = headers,
|
||||
callback = lambda response: self.parseTokenResponse(response, callback),
|
||||
error_callback = lambda response, _: self.parseTokenResponse(response, callback)
|
||||
error_callback = lambda response, _: self.parseTokenResponse(response, callback),
|
||||
timeout = REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||
|
@ -77,7 +79,9 @@ class AuthorizationHelpers:
|
|||
data = urllib.parse.urlencode(data).encode("UTF-8"),
|
||||
headers_dict = headers,
|
||||
callback = lambda response: self.parseTokenResponse(response, callback),
|
||||
error_callback = lambda response, _: self.parseTokenResponse(response, callback)
|
||||
error_callback = lambda response, _: self.parseTokenResponse(response, callback),
|
||||
urgent = True,
|
||||
timeout = REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None:
|
||||
|
@ -122,7 +126,8 @@ class AuthorizationHelpers:
|
|||
check_token_url,
|
||||
headers_dict = headers,
|
||||
callback = lambda reply: self._parseUserProfile(reply, success_callback, failed_callback),
|
||||
error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None
|
||||
error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None,
|
||||
timeout = REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
def _parseUserProfile(self, reply: QNetworkReply, success_callback: Optional[Callable[[UserProfile], None]], failed_callback: Optional[Callable[[], None]] = None) -> None:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Copyright (c) 2024 UltiMaker
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
|
@ -6,13 +6,14 @@ from datetime import datetime, timedelta
|
|||
from typing import Callable, Dict, Optional, TYPE_CHECKING, Union
|
||||
from urllib.parse import urlencode, quote_plus
|
||||
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtCore import QUrl, QTimer
|
||||
from PyQt6.QtGui import QDesktopServices
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To download log-in tokens.
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
|
||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||
from cura.OAuth2.Models import AuthenticationResponse, BaseModel
|
||||
|
@ -25,6 +26,8 @@ if TYPE_CHECKING:
|
|||
|
||||
MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers"
|
||||
|
||||
REFRESH_TOKEN_MAX_RETRIES = 15
|
||||
REFRESH_TOKEN_RETRY_INTERVAL = 1000
|
||||
|
||||
class AuthorizationService:
|
||||
"""The authorization service is responsible for handling the login flow, storing user credentials and providing
|
||||
|
@ -57,6 +60,12 @@ class AuthorizationService:
|
|||
|
||||
self.onAuthStateChanged.connect(self._authChanged)
|
||||
|
||||
self._refresh_token_retries = 0
|
||||
self._refresh_token_retry_timer = QTimer()
|
||||
self._refresh_token_retry_timer.setInterval(REFRESH_TOKEN_RETRY_INTERVAL)
|
||||
self._refresh_token_retry_timer.setSingleShot(True)
|
||||
self._refresh_token_retry_timer.timeout.connect(self.refreshAccessToken)
|
||||
|
||||
def _authChanged(self, logged_in):
|
||||
if logged_in and self._unable_to_get_data_message is not None:
|
||||
self._unable_to_get_data_message.hide()
|
||||
|
@ -167,16 +176,29 @@ class AuthorizationService:
|
|||
return
|
||||
|
||||
def process_auth_data(response: AuthenticationResponse) -> None:
|
||||
self._currently_refreshing_token = False
|
||||
|
||||
if response.success:
|
||||
self._refresh_token_retries = 0
|
||||
self._storeAuthData(response)
|
||||
HttpRequestManager.getInstance().setDelayRequests(False)
|
||||
self.onAuthStateChanged.emit(logged_in = True)
|
||||
else:
|
||||
Logger.warning("Failed to get a new access token from the server.")
|
||||
self.onAuthStateChanged.emit(logged_in = False)
|
||||
if self._refresh_token_retries >= REFRESH_TOKEN_MAX_RETRIES:
|
||||
self._refresh_token_retries = 0
|
||||
Logger.warning("Failed to get a new access token from the server, giving up.")
|
||||
HttpRequestManager.getInstance().setDelayRequests(False)
|
||||
self.onAuthStateChanged.emit(logged_in = False)
|
||||
else:
|
||||
# Retry a bit later, network may be offline right now and will hopefully be back soon
|
||||
Logger.warning("Failed to get a new access token from the server, retrying later.")
|
||||
self._refresh_token_retries += 1
|
||||
self._refresh_token_retry_timer.start()
|
||||
|
||||
if self._currently_refreshing_token:
|
||||
Logger.debug("Was already busy refreshing token. Do not start a new request.")
|
||||
return
|
||||
HttpRequestManager.getInstance().setDelayRequests(True)
|
||||
self._currently_refreshing_token = True
|
||||
self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data)
|
||||
|
||||
|
|
|
@ -38,7 +38,14 @@ class PlatformPhysics:
|
|||
self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models
|
||||
|
||||
Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
|
||||
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
||||
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", False)
|
||||
self._app_per_model_drop = Application.getInstance().getPreferences().getValue("physics/automatic_drop_down")
|
||||
|
||||
def getAppPerModelDropDown(self):
|
||||
return self._app_per_model_drop
|
||||
|
||||
def setAppPerModelDropDown(self, drop_to_buildplate):
|
||||
self._app_per_model_drop = drop_to_buildplate
|
||||
|
||||
def _onSceneChanged(self, source):
|
||||
if not source.callDecoration("isSliceable"):
|
||||
|
@ -71,6 +78,7 @@ class PlatformPhysics:
|
|||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||
random.shuffle(nodes)
|
||||
|
||||
for node in nodes:
|
||||
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
|
||||
continue
|
||||
|
@ -80,7 +88,10 @@ class PlatformPhysics:
|
|||
# Move it downwards if bottom is above platform
|
||||
move_vector = Vector()
|
||||
|
||||
if node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
|
||||
# if per model drop is different then app_automatic_drop, in case of 3mf loading when user changes this setting for that model
|
||||
if (self._app_per_model_drop != app_automatic_drop_down):
|
||||
node.setSetting(SceneNodeSettings.AutoDropDown, self._app_per_model_drop)
|
||||
if node.getSetting(SceneNodeSettings.AutoDropDown, self._app_per_model_drop) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
|
||||
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
||||
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
||||
|
||||
|
@ -168,6 +179,8 @@ class PlatformPhysics:
|
|||
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
||||
op.push()
|
||||
|
||||
# setting this drop to model same as app_automatic_drop_down
|
||||
self._app_per_model_drop = app_automatic_drop_down
|
||||
# After moving, we have to evaluate the boundary checks for nodes
|
||||
build_volume.updateNodeBoundaryCheck()
|
||||
|
||||
|
|
|
@ -299,6 +299,11 @@ class WorkspaceDialog(QObject):
|
|||
|
||||
Application.getInstance().getBackend().close()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def setDropToBuildPlateForModel(self, drop_to_buildplate: bool) -> None:
|
||||
CuraApplication.getInstance().getWorkplaceDropToBuildplate(drop_to_buildplate)
|
||||
|
||||
|
||||
def setMaterialConflict(self, material_conflict: bool) -> None:
|
||||
if self._has_material_conflict != material_conflict:
|
||||
self._has_material_conflict = material_conflict
|
||||
|
|
|
@ -300,6 +300,25 @@ UM.Dialog
|
|||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
id: dropToBuildPlate
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
UM.CheckBox
|
||||
{
|
||||
id: checkDropModels
|
||||
text: catalog.i18nc("@text:window", "Drop models to buildplate")
|
||||
checked: UM.Preferences.getValue("physics/automatic_drop_down")
|
||||
onCheckedChanged: manager.setDropToBuildPlateForModel(checked)
|
||||
}
|
||||
function reloadValue()
|
||||
{
|
||||
checkDropModels.checked = UM.Preferences.getValue("physics/automatic_drop_down")
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
id: clearBuildPlateWarning
|
||||
|
@ -422,6 +441,7 @@ UM.Dialog
|
|||
materialSection.reloadValues()
|
||||
profileSection.reloadValues()
|
||||
printerSection.reloadValues()
|
||||
dropToBuildPlate.reloadValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -509,11 +509,14 @@ UM.PreferencesPage
|
|||
id: dropDownCheckbox
|
||||
text: catalog.i18nc("@option:check", "Automatically drop models to the build plate")
|
||||
checked: boolCheck(UM.Preferences.getValue("physics/automatic_drop_down"))
|
||||
onCheckedChanged: UM.Preferences.setValue("physics/automatic_drop_down", checked)
|
||||
onCheckedChanged:
|
||||
{
|
||||
UM.Preferences.setValue("physics/automatic_drop_down", checked)
|
||||
CuraApplication.getWorkplaceDropToBuildplate(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
width: childrenRect.width;
|
||||
|
|
|
@ -222,7 +222,7 @@ Item
|
|||
UM.Label
|
||||
{
|
||||
id: toolHint
|
||||
text: UM.Controller.properties.getValue("ToolHint") != undefined ? UM.ActiveTool.properties.getValue("ToolHint") : ""
|
||||
text: UM.Controller.properties.getValue("ToolHint") != undefined ? UM.Controller.properties.getValue("ToolHint") : ""
|
||||
color: UM.Theme.getColor("tooltip_text")
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ def test__parseJWTNoRefreshToken():
|
|||
mock_reply = Mock() # The user profile that the service should respond with.
|
||||
mock_reply.error = Mock(return_value = QNetworkReply.NetworkError.NoError)
|
||||
http_mock = Mock()
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback: callback(mock_reply)
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback, timeout: callback(mock_reply)
|
||||
http_mock.readJSON = Mock(return_value = {"data": {"user_id": "id_ego_or_superego", "username": "Ghostkeeper"}})
|
||||
|
||||
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value = http_mock)):
|
||||
|
@ -119,8 +119,8 @@ def test__parseJWTFailOnRefresh():
|
|||
mock_reply = Mock() # The response that the request should give, containing an error about it failing to authenticate.
|
||||
mock_reply.error = Mock(return_value = QNetworkReply.NetworkError.AuthenticationRequiredError) # The reply is 403: Authentication required, meaning the server responded with a "Can't do that, Dave".
|
||||
http_mock = Mock()
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback: callback(mock_reply)
|
||||
http_mock.post = lambda url, data, headers_dict, callback, error_callback: callback(mock_reply)
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback, timeout: callback(mock_reply)
|
||||
http_mock.post = lambda url, data, headers_dict, callback, error_callback, urgent, timeout: callback(mock_reply)
|
||||
|
||||
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.readJSON", Mock(return_value = {"error_description": "Mock a failed request!"})):
|
||||
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value = http_mock)):
|
||||
|
@ -142,7 +142,7 @@ def test__parseJWTSucceedOnRefresh():
|
|||
mock_reply_failure = Mock()
|
||||
mock_reply_failure.error = Mock(return_value = QNetworkReply.NetworkError.AuthenticationRequiredError)
|
||||
http_mock = Mock()
|
||||
def mock_get(url, headers_dict, callback, error_callback):
|
||||
def mock_get(url, headers_dict, callback, error_callback, timeout):
|
||||
if(headers_dict == {"Authorization": "Bearer beep"}):
|
||||
callback(mock_reply_success)
|
||||
else:
|
||||
|
@ -181,8 +181,8 @@ def test_refreshAccessTokenFailed():
|
|||
mock_reply = Mock() # The response that the request should give, containing an error about it failing to authenticate.
|
||||
mock_reply.error = Mock(return_value = QNetworkReply.NetworkError.AuthenticationRequiredError) # The reply is 403: Authentication required, meaning the server responded with a "Can't do that, Dave".
|
||||
http_mock = Mock()
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback: callback(mock_reply)
|
||||
http_mock.post = lambda url, data, headers_dict, callback, error_callback: callback(mock_reply)
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback, timeout: callback(mock_reply)
|
||||
http_mock.post = lambda url, data, headers_dict, callback, error_callback, urgent, timeout: callback(mock_reply)
|
||||
|
||||
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.readJSON", Mock(return_value = {"error_description": "Mock a failed request!"})):
|
||||
with patch("UM.TaskManagement.HttpRequestManager.HttpRequestManager.getInstance", MagicMock(return_value = http_mock)):
|
||||
|
@ -263,7 +263,7 @@ def test_loginAndLogout() -> None:
|
|||
mock_reply = Mock() # The user profile that the service should respond with.
|
||||
mock_reply.error = Mock(return_value = QNetworkReply.NetworkError.NoError)
|
||||
http_mock = Mock()
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback: callback(mock_reply)
|
||||
http_mock.get = lambda url, headers_dict, callback, error_callback, timeout: callback(mock_reply)
|
||||
http_mock.readJSON = Mock(return_value = {"data": {"user_id": "di_resu", "username": "Emanresu"}})
|
||||
|
||||
# Let the service think there was a successful response
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue