Merge branch 'main' into CURA-11617-set-print-sequence

This commit is contained in:
Saumya Jain 2024-02-16 11:19:11 +01:00 committed by GitHub
commit 285a36a38a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 91 additions and 19 deletions

View file

@ -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)

View file

@ -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:

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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;

View file

@ -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
}

View file

@ -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