From 9d6bd4b29adea730ad4ae9a25a481ed4cc00a962 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 8 Feb 2019 21:03:59 +0100 Subject: [PATCH 01/22] Check access token before using it --- cura/OAuth2/AuthorizationService.py | 18 ++++++++---------- .../src/Cloud/CloudApiClient.py | 5 +++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index fc004e5e0d..8ec5232158 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -51,13 +51,11 @@ class AuthorizationService: # \return UserProfile if a user is logged in, None otherwise. # \sa _parseJWT def getUserProfile(self) -> Optional["UserProfile"]: - if not self._user_profile: - # If no user profile was stored locally, we try to get it from JWT. - try: - self._user_profile = self._parseJWT() - except requests.exceptions.ConnectionError: - # Unable to get connection, can't login. - return None + try: + self._user_profile = self._parseJWT() + except requests.exceptions.ConnectionError: + # Unable to get connection, can't login. + return None if not self._user_profile and self._auth_data: # If there is still no user profile from the JWT, we have to log in again. @@ -87,13 +85,13 @@ class AuthorizationService: return self._auth_helpers.parseJWT(self._auth_data.access_token) - # Get the access token as provided by the repsonse data. + # Get the access token as provided by the response data. def getAccessToken(self) -> Optional[str]: if not self.getUserProfile(): # We check if we can get the user profile. # If we can't get it, that means the access token (JWT) was invalid or expired. - Logger.log("w", "Unable to get the user profile.") - return None + # In that case we try to refresh the access token. + self.refreshAccessToken() if self._auth_data is None: Logger.log("d", "No auth data to retrieve the access_token from") diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 9c1e8e1cdb..adff94bbbc 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -103,8 +103,9 @@ class CloudApiClient: request = QNetworkRequest(QUrl(path)) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) - if self._account.isLoggedIn: - request.setRawHeader(b"Authorization", "Bearer {}".format(self._account.accessToken).encode()) + access_token = self._account.accessToken + if access_token: + request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) return request ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. From 05c4b6012eac68e64294edaa213606c366e27b73 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 8 Feb 2019 21:39:45 +0100 Subject: [PATCH 02/22] Calculate expiry date to determine if token refresh is needed --- cura/API/Account.py | 1 - cura/OAuth2/AuthorizationHelpers.py | 9 ++++++--- cura/OAuth2/AuthorizationRequestHandler.py | 1 - cura/OAuth2/AuthorizationService.py | 20 +++++++++++--------- cura/OAuth2/Models.py | 3 ++- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index a04f97ef1c..8a8b708cfa 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -57,7 +57,6 @@ class Account(QObject): def initialize(self) -> None: self._authorization_service.initialize(self._application.getPreferences()) - self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged) self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged) self._authorization_service.loadAuthDataFromPreferences() diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index 87b8c45d6d..d1ba60fcd6 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -1,10 +1,11 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from datetime import datetime import json import random from hashlib import sha512 from base64 import b64encode -from typing import Dict, Optional +from typing import Optional import requests @@ -75,7 +76,8 @@ class AuthorizationHelpers: access_token=token_data["access_token"], refresh_token=token_data["refresh_token"], expires_in=token_data["expires_in"], - scope=token_data["scope"]) + scope=token_data["scope"], + received_at=datetime.now()) # Calls the authentication API endpoint to get the token data. # \param access_token: The encoded JWT token. @@ -99,7 +101,8 @@ class AuthorizationHelpers: return UserProfile( user_id = user_data["user_id"], username = user_data["username"], - profile_image_url = user_data.get("profile_image_url", "") + profile_image_url = user_data.get("profile_image_url", ""), + generated_at = datetime.now() ) @staticmethod diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py index 7e0a659a56..193a41305a 100644 --- a/cura/OAuth2/AuthorizationRequestHandler.py +++ b/cura/OAuth2/AuthorizationRequestHandler.py @@ -9,7 +9,6 @@ from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS if TYPE_CHECKING: from cura.OAuth2.Models import ResponseStatus - from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers # This handler handles all HTTP requests on the local web server. diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 8ec5232158..011db0011e 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import json import webbrowser +from datetime import timedelta, datetime from typing import Optional, TYPE_CHECKING from urllib.parse import urlencode import requests.exceptions @@ -51,11 +52,12 @@ class AuthorizationService: # \return UserProfile if a user is logged in, None otherwise. # \sa _parseJWT def getUserProfile(self) -> Optional["UserProfile"]: - try: - self._user_profile = self._parseJWT() - except requests.exceptions.ConnectionError: - # Unable to get connection, can't login. - return None + if not self._user_profile: + try: + self._user_profile = self._parseJWT() + except requests.exceptions.ConnectionError: + # Unable to get connection, can't login. + return None if not self._user_profile and self._auth_data: # If there is still no user profile from the JWT, we have to log in again. @@ -87,10 +89,10 @@ class AuthorizationService: # Get the access token as provided by the response data. def getAccessToken(self) -> Optional[str]: - if not self.getUserProfile(): - # We check if we can get the user profile. - # If we can't get it, that means the access token (JWT) was invalid or expired. - # In that case we try to refresh the access token. + # Check if the current access token is expired and refresh it if that is the case. + creation_date = self._auth_data.received_at or datetime(2000, 1, 1) + expiry_date = creation_date + timedelta(seconds = float(self._auth_data.expires_in)) + if datetime.now() > expiry_date: self.refreshAccessToken() if self._auth_data is None: diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py index 0515e789e6..818aed64e2 100644 --- a/cura/OAuth2/Models.py +++ b/cura/OAuth2/Models.py @@ -1,6 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - +from datetime import datetime from typing import Optional @@ -38,6 +38,7 @@ class AuthenticationResponse(BaseModel): expires_in = None # type: Optional[str] scope = None # type: Optional[str] err_message = None # type: Optional[str] + received_at = None # type: Optional[datetime] # Response status template. From 48c756b01dbdff7754a9b97e1fe4a85536ffeb35 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 8 Feb 2019 22:24:14 +0100 Subject: [PATCH 03/22] Fixes for storing timestamp --- cura/OAuth2/AuthorizationHelpers.py | 5 ++++- cura/OAuth2/AuthorizationService.py | 19 ++++++++++--------- cura/OAuth2/Models.py | 5 ++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index d1ba60fcd6..0fa82cc7ef 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -14,6 +14,9 @@ from UM.Logger import Logger from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings +TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" + + # Class containing several helpers to deal with the authorization flow. class AuthorizationHelpers: def __init__(self, settings: "OAuth2Settings") -> None: @@ -77,7 +80,7 @@ class AuthorizationHelpers: refresh_token=token_data["refresh_token"], expires_in=token_data["expires_in"], scope=token_data["scope"], - received_at=datetime.now()) + received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)) # Calls the authentication API endpoint to get the token data. # \param access_token: The encoded JWT token. diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 011db0011e..ff5afd8e5b 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -2,7 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import json import webbrowser -from datetime import timedelta, datetime +from datetime import datetime, timedelta from typing import Optional, TYPE_CHECKING from urllib.parse import urlencode import requests.exceptions @@ -12,7 +12,7 @@ from UM.Logger import Logger from UM.Signal import Signal from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer -from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers +from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT from cura.OAuth2.Models import AuthenticationResponse if TYPE_CHECKING: @@ -89,17 +89,18 @@ class AuthorizationService: # Get the access token as provided by the response data. def getAccessToken(self) -> Optional[str]: - # Check if the current access token is expired and refresh it if that is the case. - creation_date = self._auth_data.received_at or datetime(2000, 1, 1) - expiry_date = creation_date + timedelta(seconds = float(self._auth_data.expires_in)) - if datetime.now() > expiry_date: - self.refreshAccessToken() - if self._auth_data is None: Logger.log("d", "No auth data to retrieve the access_token from") return None - return self._auth_data.access_token + # Check if the current access token is expired and refresh it if that is the case. + received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \ + if self._auth_data.received_at else datetime(2000, 1, 1) + expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0)) + if datetime.now() > expiry_date: + self.refreshAccessToken() + + return self._auth_data.access_token if self._auth_data else None # Try to refresh the access token. This should be used when it has expired. def refreshAccessToken(self) -> None: diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py index 818aed64e2..001eb4b8bc 100644 --- a/cura/OAuth2/Models.py +++ b/cura/OAuth2/Models.py @@ -1,6 +1,5 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from datetime import datetime from typing import Optional @@ -38,13 +37,13 @@ class AuthenticationResponse(BaseModel): expires_in = None # type: Optional[str] scope = None # type: Optional[str] err_message = None # type: Optional[str] - received_at = None # type: Optional[datetime] + received_at = None # type: Optional[str] # Response status template. class ResponseStatus(BaseModel): code = 200 # type: int - message = "" # type str + message = "" # type: str # Response data template. From 4192c9e7644c3e358d119d98f7f1bcafe378d932 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 8 Feb 2019 22:30:31 +0100 Subject: [PATCH 04/22] Fix typing issue --- cura/OAuth2/AuthorizationRequestHandler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py index 193a41305a..28fe6eb285 100644 --- a/cura/OAuth2/AuthorizationRequestHandler.py +++ b/cura/OAuth2/AuthorizationRequestHandler.py @@ -9,6 +9,7 @@ from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS if TYPE_CHECKING: from cura.OAuth2.Models import ResponseStatus + from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers # This handler handles all HTTP requests on the local web server. @@ -18,7 +19,7 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): super().__init__(request, client_address, server) # These values will be injected by the HTTPServer that this handler belongs to. - self.authorization_helpers = None # type: Optional["AuthorizationHelpers"] + self.authorization_helpers = None # type: Optional[AuthorizationHelpers] self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]] self.verification_code = None # type: Optional[str] From 62c7ba56599dfc45dfa9d6d165aa9055ef626ea8 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 8 Feb 2019 22:31:36 +0100 Subject: [PATCH 05/22] remove unused argument --- cura/OAuth2/AuthorizationHelpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index 0fa82cc7ef..f125876879 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -104,8 +104,7 @@ class AuthorizationHelpers: return UserProfile( user_id = user_data["user_id"], username = user_data["username"], - profile_image_url = user_data.get("profile_image_url", ""), - generated_at = datetime.now() + profile_image_url = user_data.get("profile_image_url", "") ) @staticmethod From a7071e2d3da26f0741615ad91d0e8fcf480232e9 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 8 Feb 2019 22:53:12 +0100 Subject: [PATCH 06/22] Fix unit tests --- tests/TestOAuth2.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/TestOAuth2.py b/tests/TestOAuth2.py index 608d529e9f..26d6b8e332 100644 --- a/tests/TestOAuth2.py +++ b/tests/TestOAuth2.py @@ -1,8 +1,9 @@ import webbrowser +from datetime import datetime from unittest.mock import MagicMock, patch from UM.Preferences import Preferences -from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers +from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer from cura.OAuth2.Models import OAuth2Settings, AuthenticationResponse, UserProfile @@ -12,19 +13,27 @@ OAUTH_ROOT = "https://account.ultimaker.com" CLOUD_API_ROOT = "https://api.ultimaker.com" OAUTH_SETTINGS = OAuth2Settings( - OAUTH_SERVER_URL= OAUTH_ROOT, - CALLBACK_PORT=CALLBACK_PORT, - CALLBACK_URL="http://localhost:{}/callback".format(CALLBACK_PORT), - CLIENT_ID="", - CLIENT_SCOPES="", - AUTH_DATA_PREFERENCE_KEY="test/auth_data", - AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(OAUTH_ROOT), - AUTH_FAILED_REDIRECT="{}/app/auth-error".format(OAUTH_ROOT) - ) + OAUTH_SERVER_URL= OAUTH_ROOT, + CALLBACK_PORT=CALLBACK_PORT, + CALLBACK_URL="http://localhost:{}/callback".format(CALLBACK_PORT), + CLIENT_ID="", + CLIENT_SCOPES="", + AUTH_DATA_PREFERENCE_KEY="test/auth_data", + AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(OAUTH_ROOT), + AUTH_FAILED_REDIRECT="{}/app/auth-error".format(OAUTH_ROOT) +) -FAILED_AUTH_RESPONSE = AuthenticationResponse(success = False, err_message = "FAILURE!") +FAILED_AUTH_RESPONSE = AuthenticationResponse( + success = False, + err_message = "FAILURE!" +) -SUCCESFULL_AUTH_RESPONSE = AuthenticationResponse(access_token = "beep", refresh_token = "beep?") +SUCCESSFUL_AUTH_RESPONSE = AuthenticationResponse( + access_token = "beep", + refresh_token = "beep?", + received_at = datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT), + expires_in = 300 # 5 minutes should be more than enough for testing +) MALFORMED_AUTH_RESPONSE = AuthenticationResponse() @@ -64,7 +73,7 @@ def test_storeAuthData(get_user_profile) -> None: authorization_service.initialize() # Write stuff to the preferences. - authorization_service._storeAuthData(SUCCESFULL_AUTH_RESPONSE) + authorization_service._storeAuthData(SUCCESSFUL_AUTH_RESPONSE) preference_value = preferences.getValue(OAUTH_SETTINGS.AUTH_DATA_PREFERENCE_KEY) # Check that something was actually put in the preferences assert preference_value is not None and preference_value != {} @@ -73,7 +82,7 @@ def test_storeAuthData(get_user_profile) -> None: second_auth_service = AuthorizationService(OAUTH_SETTINGS, preferences) second_auth_service.initialize() second_auth_service.loadAuthDataFromPreferences() - assert second_auth_service.getAccessToken() == SUCCESFULL_AUTH_RESPONSE.access_token + assert second_auth_service.getAccessToken() == SUCCESSFUL_AUTH_RESPONSE.access_token @patch.object(LocalAuthorizationServer, "stop") @@ -101,9 +110,9 @@ def test_loginAndLogout() -> None: authorization_service.onAuthStateChanged.emit = MagicMock() authorization_service.initialize() - # Let the service think there was a succesfull response + # Let the service think there was a successful response with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()): - authorization_service._onAuthStateChanged(SUCCESFULL_AUTH_RESPONSE) + authorization_service._onAuthStateChanged(SUCCESSFUL_AUTH_RESPONSE) # Ensure that the error signal was not triggered assert authorization_service.onAuthenticationError.emit.call_count == 0 From 14c361a29741bfcb354ef04d3d68433b6bd8b590 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 11 Feb 2019 09:45:50 +0100 Subject: [PATCH 07/22] Add comment to clarify usage of fallback date --- cura/OAuth2/AuthorizationService.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index ff5afd8e5b..a76e8cf304 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -94,6 +94,7 @@ class AuthorizationService: return None # Check if the current access token is expired and refresh it if that is the case. + # We have a fallback on a date far in the past for currently stored auth data in cura.cfg. received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \ if self._auth_data.received_at else datetime(2000, 1, 1) expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0)) From 0e913044dee301f64ed9bcfab687506c764091cd Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 11 Feb 2019 10:21:54 +0100 Subject: [PATCH 08/22] Ensure pop-up is not shown if cloud connection is already configured Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index a4d5bedb1f..93bf72eac4 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -412,10 +412,15 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"] if active_machine: - # Check 1: Printer isn't already configured for cloud + # Check 1A: Printer isn't already configured for cloud if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes: Logger.log("d", "Active machine was already configured for cloud.") return + + # Check 1B: Printer isn't already configured for cloud + if active_machine.getMetaDataEntry("cloud_flow_complete", "value") is True: + Logger.log("d", "Active machine was already configured for cloud.") + return # Check 2: User did not already say "Don't ask me again" if active_machine.getMetaDataEntry("show_cloud_message", "value") is False: From c50fcf42c5631966176313188aab148ef2be7e38 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 12 Feb 2019 11:39:03 +0100 Subject: [PATCH 09/22] Fix crash on no output devices in list. --- cura/Settings/MachineManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index b250b27cd3..ee285bda84 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -519,13 +519,13 @@ class MachineManager(QObject): @pyqtProperty(str, notify = globalContainerChanged) def activeMachineFirmwareVersion(self) -> str: - if not self._printer_output_devices[0]: + if not self._printer_output_devices: return "" return self._printer_output_devices[0].firmwareVersion @pyqtProperty(str, notify = globalContainerChanged) def activeMachineAddress(self) -> str: - if not self._printer_output_devices[0]: + if not self._printer_output_devices: return "" return self._printer_output_devices[0].address From c6c0ed5f0022e675adbe9c3c008ad992570ca354 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 12 Feb 2019 11:57:01 +0100 Subject: [PATCH 10/22] Fix sign-in size for cloud icon (and add for dark theme). Co-authored-by: Yi-An Lai --- .../cura-dark/icons/sign_in_to_cloud.svg | 16 ++++++++++ .../cura-light/icons/sign_in_to_cloud.svg | 29 ++++++------------- 2 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 resources/themes/cura-dark/icons/sign_in_to_cloud.svg diff --git a/resources/themes/cura-dark/icons/sign_in_to_cloud.svg b/resources/themes/cura-dark/icons/sign_in_to_cloud.svg new file mode 100644 index 0000000000..09ba300b6a --- /dev/null +++ b/resources/themes/cura-dark/icons/sign_in_to_cloud.svg @@ -0,0 +1,16 @@ + + + + Group-cloud Copy + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/sign_in_to_cloud.svg b/resources/themes/cura-light/icons/sign_in_to_cloud.svg index 5c8f39a85b..27471fddce 100644 --- a/resources/themes/cura-light/icons/sign_in_to_cloud.svg +++ b/resources/themes/cura-light/icons/sign_in_to_cloud.svg @@ -1,25 +1,14 @@ - - - Group 2 + + + Group-cloud Created with Sketch. - - - - - - - - - - - - - - - - - + + + + + + From 3c2791fefeee638e7398f2ea7fba7dd41717b41f Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 12 Feb 2019 12:12:59 +0100 Subject: [PATCH 11/22] Improve cloud connection or not checking Contributes to CL-1165 --- cura/Settings/MachineManager.py | 8 ++++++-- .../resources/qml/MonitorPrintJobCard.qml | 2 +- .../resources/qml/MonitorPrinterCard.qml | 2 +- resources/qml/PrinterSelector/MachineSelector.qml | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index ae74d76734..aacdfb0952 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -541,14 +541,18 @@ class MachineManager(QObject): return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1 @pyqtProperty(bool, notify = printerConnectedStatusChanged) - def activeMachineHasActiveNetworkConnection(self) -> bool: + def activeMachineHasNetworkConnection(self) -> bool: # A network connection is only available if any output device is actually a network connected device. return any(d.connectionType == ConnectionType.NetworkConnection for d in self._printer_output_devices) @pyqtProperty(bool, notify = printerConnectedStatusChanged) - def activeMachineHasActiveCloudConnection(self) -> bool: + def activeMachineHasCloudConnection(self) -> bool: # A cloud connection is only available if any output device actually is a cloud connected device. return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices) + + @pyqtProperty(bool, notify = printerConnectedStatusChanged) + def activeMachineIsUsingCloudConnection(self) -> bool: + return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection def activeMachineNetworkKey(self) -> str: if self._global_container_stack: diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml index c7588b83bc..701be69d67 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml @@ -24,7 +24,7 @@ Item // If the printer is a cloud printer or not. Other items base their enabled state off of this boolean. In the future // they might not need to though. - property bool cloudConnection: Cura.MachineManager.activeMachineHasActiveCloudConnection + property bool cloudConnection: Cura.MachineManager.activeMachineIsUsingCloudConnection width: parent.width height: childrenRect.height diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml index 0e541c484d..8c63e1ef1a 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml @@ -30,7 +30,7 @@ Item // If the printer is a cloud printer or not. Other items base their enabled state off of this boolean. In the future // they might not need to though. - property bool cloudConnection: Cura.MachineManager.activeMachineHasActiveCloudConnection + property bool cloudConnection: Cura.MachineManager.activeMachineIsUsingCloudConnection width: 834 * screenScaleFactor // TODO: Theme! height: childrenRect.height diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index cd5e041606..e9452f4d35 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -11,8 +11,8 @@ Cura.ExpandablePopup { id: machineSelector - property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasActiveNetworkConnection - property bool isCloudPrinter: Cura.MachineManager.activeMachineHasActiveCloudConnection + property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasNetworkConnection + property bool isCloudPrinter: Cura.MachineManager.activeMachineHasCloudConnection property bool isGroup: Cura.MachineManager.activeMachineIsGroup contentPadding: UM.Theme.getSize("default_lining").width From aa454ea357b8a64405601b37e1a0ec5f230a34c5 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 12 Feb 2019 12:18:29 +0100 Subject: [PATCH 12/22] Use parseBool instead of "is True" and "is False" Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 93bf72eac4..947f2fc402 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -19,6 +19,7 @@ from UM.Signal import Signal, signalemitter from UM.Version import Version from UM.Message import Message from UM.i18n import i18nCatalog +from UM.Util import parseBool from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager @@ -418,12 +419,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): return # Check 1B: Printer isn't already configured for cloud - if active_machine.getMetaDataEntry("cloud_flow_complete", "value") is True: + if parseBool(active_machine.getMetaDataEntry("cloud_flow_complete", "value")): Logger.log("d", "Active machine was already configured for cloud.") return # Check 2: User did not already say "Don't ask me again" - if active_machine.getMetaDataEntry("show_cloud_message", "value") is False: + if not parseBool(active_machine.getMetaDataEntry("show_cloud_message", "value")): Logger.log("d", "Active machine shouldn't ask about cloud anymore.") return From 70fc363048116fe2af92a4f91fef6664553321db Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 10:49:53 +0100 Subject: [PATCH 13/22] Fix cloud pop up paths Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 947f2fc402..8418e01028 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -4,6 +4,7 @@ import json from queue import Queue from threading import Event, Thread from time import time +import os from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager @@ -454,7 +455,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): if not self._start_cloud_flow_message: self._start_cloud_flow_message = Message( text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), - image_source = "../../../../../Cura/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-start.svg", + image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/svg/cloud-flow-start.svg", image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"), option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), option_state = False @@ -475,7 +476,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._cloud_flow_complete_message = Message( text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."), lifetime = 30, - image_source = "../../../../../Cura/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-completed.svg", + image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/svg/cloud-flow-completed.svg"), image_caption = i18n_catalog.i18nc("@info:status", "Connected!") ) self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon From aa2acac2447d4ba378d3b8a2438f03f1a78c35ed Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 10:52:37 +0100 Subject: [PATCH 14/22] Fix typo Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 8418e01028..c3d1f3794a 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -455,7 +455,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): if not self._start_cloud_flow_message: self._start_cloud_flow_message = Message( text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), - image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/svg/cloud-flow-start.svg", + image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/svg/cloud-flow-start.svg"), image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"), option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), option_state = False From c15e49876a17efe275af09d918d9b9c85e6bb262 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 11:00:11 +0100 Subject: [PATCH 15/22] Revert "Use parseBool instead of "is True" and "is False"" This reverts commit aa454ea357b8a64405601b37e1a0ec5f230a34c5. --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index c3d1f3794a..aea8de8155 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -20,7 +20,6 @@ from UM.Signal import Signal, signalemitter from UM.Version import Version from UM.Message import Message from UM.i18n import i18nCatalog -from UM.Util import parseBool from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager @@ -420,12 +419,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): return # Check 1B: Printer isn't already configured for cloud - if parseBool(active_machine.getMetaDataEntry("cloud_flow_complete", "value")): + if active_machine.getMetaDataEntry("cloud_flow_complete", "value") is True: Logger.log("d", "Active machine was already configured for cloud.") return # Check 2: User did not already say "Don't ask me again" - if not parseBool(active_machine.getMetaDataEntry("show_cloud_message", "value")): + if active_machine.getMetaDataEntry("show_cloud_message", "value") is False: Logger.log("d", "Active machine shouldn't ask about cloud anymore.") return From 8fecf7fb394ea9b3152fd2f5c63801bd667149e4 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 11:04:51 +0100 Subject: [PATCH 16/22] Don't crash when clicking don't show again Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index aea8de8155..c5121ecb5e 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -488,7 +488,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): active_machine.setMetaDataEntry("cloud_flow_complete", True) return - def _onDontAskMeAgain(self, messageId: str, checked: bool) -> None: + def _onDontAskMeAgain(self, messageId: str) -> None: active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"] if active_machine: active_machine.setMetaDataEntry("show_cloud_message", False) From d74f4f36bd42bd2134c671d24f82991bf61a51c2 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 11:30:31 +0100 Subject: [PATCH 17/22] Improve cloud flow checks Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index c5121ecb5e..1c9d1e2938 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -419,12 +419,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): return # Check 1B: Printer isn't already configured for cloud - if active_machine.getMetaDataEntry("cloud_flow_complete", "value") is True: + if active_machine.getMetaDataEntry("cloud_flow_complete", False): Logger.log("d", "Active machine was already configured for cloud.") return # Check 2: User did not already say "Don't ask me again" - if active_machine.getMetaDataEntry("show_cloud_message", "value") is False: + if not active_machine.getMetaDataEntry("show_cloud_message", True): Logger.log("d", "Active machine shouldn't ask about cloud anymore.") return From cf4bcf81e7d69ab4de861e7e6bafa2f221ea4171 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 11:49:53 +0100 Subject: [PATCH 18/22] Make pop up permanent Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 1c9d1e2938..26ad49ce57 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -454,6 +454,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): if not self._start_cloud_flow_message: self._start_cloud_flow_message = Message( text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), + lifetime = 0, image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/svg/cloud-flow-start.svg"), image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"), option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), From d08b8d813db760d0156cb9a4e99671a0f2494320 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 13 Feb 2019 12:23:41 +0100 Subject: [PATCH 19/22] Generate the correct path for the svg on Windows. CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 26ad49ce57..782663061e 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -455,7 +455,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._start_cloud_flow_message = Message( text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), lifetime = 0, - image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/svg/cloud-flow-start.svg"), + image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "resources", "svg", + "cloud-flow-start.svg"), image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"), option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), option_state = False @@ -476,7 +477,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._cloud_flow_complete_message = Message( text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."), lifetime = 30, - image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/svg/cloud-flow-completed.svg"), + image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "resources", "svg", + "cloud-flow-completed.svg"), image_caption = i18n_catalog.i18nc("@info:status", "Connected!") ) self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon From 25cfd4f496bca16385faf3e443a0a82ac9b9ff55 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 14:21:23 +0100 Subject: [PATCH 20/22] Avoid double negatives in cloud flow metadata Contributes to CL-1222 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 782663061e..c454bf7846 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -424,7 +424,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): return # Check 2: User did not already say "Don't ask me again" - if not active_machine.getMetaDataEntry("show_cloud_message", True): + if active_machine.getMetaDataEntry("do_not_show_cloud_message", False): Logger.log("d", "Active machine shouldn't ask about cloud anymore.") return @@ -488,13 +488,13 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers active_machine = self._application.getMachineManager().activeMachine if active_machine: - active_machine.setMetaDataEntry("cloud_flow_complete", True) + active_machine.setMetaDataEntry("do_not_show_cloud_message", True) return def _onDontAskMeAgain(self, messageId: str) -> None: active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"] if active_machine: - active_machine.setMetaDataEntry("show_cloud_message", False) + active_machine.setMetaDataEntry("do_not_show_cloud_message", True) Logger.log("d", "Will not ask the user again to cloud connect for current printer.") return From e39e09a69244e3021b44a383ccb08c8e6995f8ef Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 13 Feb 2019 15:21:09 +0100 Subject: [PATCH 21/22] Fix incorrect method name Contributes to CL-1165 --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 2470a9a188..e57cd15960 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -434,7 +434,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): return # Check 4: Machine is configured for network connectivity - if not self._application.getMachineManager().activeMachineHasActiveNetworkConnection: + if not self._application.getMachineManager().activeMachineHasNetworkConnection: Logger.log("d", "Cloud Flow not possible: Machine is not connected!") return From c1b53f0a90a747aa2fbc77f2a16756e0ac167410 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 14 Feb 2019 10:42:25 +0100 Subject: [PATCH 22/22] Change header color in about window This makes it work for both light & dark theme --- resources/qml/Dialogs/AboutDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/Dialogs/AboutDialog.qml b/resources/qml/Dialogs/AboutDialog.qml index 9a92bcbe1f..604cbc16ba 100644 --- a/resources/qml/Dialogs/AboutDialog.qml +++ b/resources/qml/Dialogs/AboutDialog.qml @@ -28,7 +28,7 @@ UM.Dialog anchors.topMargin: - margin anchors.horizontalCenter: parent.horizontalCenter - color: UM.Theme.getColor("viewport_background") + color: UM.Theme.getColor("main_window_header_background") } Image @@ -52,7 +52,7 @@ UM.Dialog text: catalog.i18nc("@label","version: %1").arg(UM.Application.version) font: UM.Theme.getFont("large_bold") - color: UM.Theme.getColor("text") + color: UM.Theme.getColor("button_text") anchors.right : logo.right anchors.top: logo.bottom anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0