mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-06-04 11:14:21 +08:00
Merge branch 'CURA-5744-move-oauth-login' of github.com:Ultimaker/Cura
This commit is contained in:
commit
dabe747ec0
117
cura/API/Account.py
Normal file
117
cura/API/Account.py
Normal file
@ -0,0 +1,117 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Dict, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.Models import OAuth2Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The account API provides a version-proof bridge to use Ultimaker Accounts
|
||||
#
|
||||
# Usage:
|
||||
# ``from cura.API import CuraAPI
|
||||
# api = CuraAPI()
|
||||
# api.account.login()
|
||||
# api.account.logout()
|
||||
# api.account.userProfile # Who is logged in``
|
||||
#
|
||||
class Account(QObject):
|
||||
# Signal emitted when user logged in or out.
|
||||
loginStateChanged = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
|
||||
self._error_message = None # type: Optional[Message]
|
||||
self._logged_in = False
|
||||
|
||||
self._callback_port = 32118
|
||||
self._oauth_root = "https://account.ultimaker.com"
|
||||
self._cloud_api_root = "https://api.ultimaker.com"
|
||||
|
||||
self._oauth_settings = OAuth2Settings(
|
||||
OAUTH_SERVER_URL= self._oauth_root,
|
||||
CALLBACK_PORT=self._callback_port,
|
||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||
CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
|
||||
CLIENT_SCOPES="user.read drive.backups.read drive.backups.write",
|
||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||
)
|
||||
|
||||
self._authorization_service = AuthorizationService(self._oauth_settings)
|
||||
|
||||
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()
|
||||
|
||||
@pyqtProperty(bool, notify=loginStateChanged)
|
||||
def isLoggedIn(self) -> bool:
|
||||
return self._logged_in
|
||||
|
||||
def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
|
||||
if error_message:
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
|
||||
self._error_message.show()
|
||||
|
||||
if self._logged_in != logged_in:
|
||||
self._logged_in = logged_in
|
||||
self.loginStateChanged.emit(logged_in)
|
||||
|
||||
@pyqtSlot()
|
||||
def login(self) -> None:
|
||||
if self._logged_in:
|
||||
# Nothing to do, user already logged in.
|
||||
return
|
||||
self._authorization_service.startAuthorizationFlow()
|
||||
|
||||
@pyqtProperty(str, notify=loginStateChanged)
|
||||
def userName(self):
|
||||
user_profile = self._authorization_service.getUserProfile()
|
||||
if not user_profile:
|
||||
return None
|
||||
return user_profile.username
|
||||
|
||||
@pyqtProperty(str, notify = loginStateChanged)
|
||||
def profileImageUrl(self):
|
||||
user_profile = self._authorization_service.getUserProfile()
|
||||
if not user_profile:
|
||||
return None
|
||||
return user_profile.profile_image_url
|
||||
|
||||
@pyqtProperty(str, notify=loginStateChanged)
|
||||
def accessToken(self) -> Optional[str]:
|
||||
return self._authorization_service.getAccessToken()
|
||||
|
||||
# Get the profile of the logged in user
|
||||
# @returns None if no user is logged in, a dict containing user_id, username and profile_image_url
|
||||
@pyqtProperty("QVariantMap", notify = loginStateChanged)
|
||||
def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
|
||||
user_profile = self._authorization_service.getUserProfile()
|
||||
if not user_profile:
|
||||
return None
|
||||
return user_profile.__dict__
|
||||
|
||||
@pyqtSlot()
|
||||
def logout(self) -> None:
|
||||
if not self._logged_in:
|
||||
return # Nothing to do, user isn't logged in.
|
||||
|
||||
self._authorization_service.deleteAuthData()
|
@ -1,9 +1,12 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, TYPE_CHECKING
|
||||
|
||||
from cura.Backups.BackupsManager import BackupsManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The back-ups API provides a version-proof bridge between Cura's
|
||||
# BackupManager and plug-ins that hook into it.
|
||||
@ -13,9 +16,10 @@ from cura.Backups.BackupsManager import BackupsManager
|
||||
# api = CuraAPI()
|
||||
# api.backups.createBackup()
|
||||
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
|
||||
|
||||
class Backups:
|
||||
manager = BackupsManager() # Re-used instance of the backups manager.
|
||||
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
self.manager = BackupsManager(application)
|
||||
|
||||
## Create a new back-up using the BackupsManager.
|
||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||
|
@ -1,7 +1,11 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The Interface.Settings API provides a version-proof bridge between Cura's
|
||||
# (currently) sidebar UI and plug-ins that hook into it.
|
||||
@ -19,8 +23,9 @@ from cura.CuraApplication import CuraApplication
|
||||
# api.interface.settings.addContextMenuItem(data)``
|
||||
|
||||
class Settings:
|
||||
# Re-used instance of Cura:
|
||||
application = CuraApplication.getInstance() # type: CuraApplication
|
||||
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
self.application = application
|
||||
|
||||
## Add items to the sidebar context menu.
|
||||
# \param menu_item dict containing the menu item to add.
|
||||
@ -30,4 +35,4 @@ class Settings:
|
||||
## Get all custom items currently added to the sidebar context menu.
|
||||
# \return List containing all custom context menu items.
|
||||
def getContextMenuItems(self) -> list:
|
||||
return self.application.getSidebarCustomMenuItems()
|
||||
return self.application.getSidebarCustomMenuItems()
|
||||
|
@ -1,9 +1,15 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Interface.Settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The Interface class serves as a common root for the specific API
|
||||
# methods for each interface element.
|
||||
#
|
||||
@ -20,5 +26,6 @@ class Interface:
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
|
||||
# API methods specific to the settings portion of the UI
|
||||
settings = Settings()
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
# API methods specific to the settings portion of the UI
|
||||
self.settings = Settings(application)
|
||||
|
@ -1,8 +1,17 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Backups import Backups
|
||||
from cura.API.Interface import Interface
|
||||
from cura.API.Account import Account
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The official Cura API that plug-ins can use to interact with Cura.
|
||||
#
|
||||
@ -10,14 +19,47 @@ from cura.API.Interface import Interface
|
||||
# this API provides a version-safe interface with proper deprecation warnings
|
||||
# etc. Usage of any other methods than the ones provided in this API can cause
|
||||
# plug-ins to be unstable.
|
||||
|
||||
class CuraAPI:
|
||||
class CuraAPI(QObject):
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
__instance = None # type: "CuraAPI"
|
||||
_application = None # type: CuraApplication
|
||||
|
||||
# Backups API
|
||||
backups = Backups()
|
||||
# This is done to ensure that the first time an instance is created, it's forced that the application is set.
|
||||
# The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
|
||||
# Since the API is intended to be used by plugins, the cura application should have already created this.
|
||||
def __new__(cls, application: Optional["CuraApplication"] = None):
|
||||
if cls.__instance is None:
|
||||
if application is None:
|
||||
raise Exception("Upon first time creation, the application must be set.")
|
||||
cls.__instance = super(CuraAPI, cls).__new__(cls)
|
||||
cls._application = application
|
||||
return cls.__instance
|
||||
|
||||
# Interface API
|
||||
interface = Interface()
|
||||
def __init__(self, application: Optional["CuraApplication"] = None) -> None:
|
||||
super().__init__(parent = CuraAPI._application)
|
||||
|
||||
# Accounts API
|
||||
self._account = Account(self._application)
|
||||
|
||||
# Backups API
|
||||
self._backups = Backups(self._application)
|
||||
|
||||
# Interface API
|
||||
self._interface = Interface(self._application)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._account.initialize()
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def account(self) -> "Account":
|
||||
return self._account
|
||||
|
||||
@property
|
||||
def backups(self) -> "Backups":
|
||||
return self._backups
|
||||
|
||||
@property
|
||||
def interface(self) -> "Interface":
|
||||
return self._interface
|
@ -4,18 +4,18 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
import shutil
|
||||
|
||||
from typing import Dict, Optional
|
||||
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.Resources import Resources
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The back-up class holds all data about a back-up.
|
||||
@ -29,24 +29,25 @@ class Backup:
|
||||
# Re-use translation catalog.
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def __init__(self, zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
|
||||
def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
|
||||
self._application = application
|
||||
self.zip_file = zip_file # type: Optional[bytes]
|
||||
self.meta_data = meta_data # type: Optional[Dict[str, str]]
|
||||
|
||||
## Create a back-up from the current user config folder.
|
||||
def makeFromCurrent(self) -> None:
|
||||
cura_release = CuraApplication.getInstance().getVersion()
|
||||
cura_release = self._application.getVersion()
|
||||
version_data_dir = Resources.getDataStoragePath()
|
||||
|
||||
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
|
||||
|
||||
# Ensure all current settings are saved.
|
||||
CuraApplication.getInstance().saveSettings()
|
||||
self._application.saveSettings()
|
||||
|
||||
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
||||
# When restoring a backup on Linux, we move it back.
|
||||
if Platform.isLinux():
|
||||
preferences_file_name = CuraApplication.getInstance().getApplicationName()
|
||||
preferences_file_name = self._application.getApplicationName()
|
||||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
||||
@ -58,7 +59,7 @@ class Backup:
|
||||
if archive is None:
|
||||
return
|
||||
files = archive.namelist()
|
||||
|
||||
|
||||
# Count the metadata items. We do this in a rather naive way at the moment.
|
||||
machine_count = len([s for s in files if "machine_instances/" in s]) - 1
|
||||
material_count = len([s for s in files if "materials/" in s]) - 1
|
||||
@ -112,7 +113,7 @@ class Backup:
|
||||
"Tried to restore a Cura backup without having proper data or meta data."))
|
||||
return False
|
||||
|
||||
current_version = CuraApplication.getInstance().getVersion()
|
||||
current_version = self._application.getVersion()
|
||||
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||
if current_version != version_to_restore:
|
||||
# Cannot restore version older or newer than current because settings might have changed.
|
||||
@ -128,7 +129,7 @@ class Backup:
|
||||
|
||||
# Under Linux, preferences are stored elsewhere, so we copy the file to there.
|
||||
if Platform.isLinux():
|
||||
preferences_file_name = CuraApplication.getInstance().getApplicationName()
|
||||
preferences_file_name = self._application.getApplicationName()
|
||||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
|
||||
|
@ -1,11 +1,13 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
from cura.Backups.Backup import Backup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The BackupsManager is responsible for managing the creating and restoring of
|
||||
@ -13,15 +15,15 @@ from cura.CuraApplication import CuraApplication
|
||||
#
|
||||
# Back-ups themselves are represented in a different class.
|
||||
class BackupsManager:
|
||||
def __init__(self):
|
||||
self._application = CuraApplication.getInstance()
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
self._application = application
|
||||
|
||||
## Get a back-up of the current configuration.
|
||||
# \return A tuple containing a ZipFile (the actual back-up) and a dict
|
||||
# containing some metadata (like version).
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
|
||||
self._disableAutoSave()
|
||||
backup = Backup()
|
||||
backup = Backup(self._application)
|
||||
backup.makeFromCurrent()
|
||||
self._enableAutoSave()
|
||||
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
|
||||
@ -39,7 +41,7 @@ class BackupsManager:
|
||||
|
||||
self._disableAutoSave()
|
||||
|
||||
backup = Backup(zip_file = zip_file, meta_data = meta_data)
|
||||
backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data)
|
||||
restored = backup.restore()
|
||||
if restored:
|
||||
# At this point, Cura will need to restart for the changes to take effect.
|
||||
|
@ -44,6 +44,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||
|
||||
from cura.API import CuraAPI
|
||||
from cura.Arranging.Arrange import Arrange
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
@ -61,6 +62,7 @@ from cura.Scene.CuraSceneController import CuraSceneController
|
||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
|
||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||
@ -203,6 +205,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self._quality_profile_drop_down_menu_model = None
|
||||
self._custom_quality_profile_drop_down_menu_model = None
|
||||
self._cura_API = CuraAPI(self)
|
||||
|
||||
self._physics = None
|
||||
self._volume = None
|
||||
@ -241,6 +244,8 @@ class CuraApplication(QtApplication):
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
self._container_registry_class = CuraContainerRegistry
|
||||
# Redefined here in order to please the typing.
|
||||
self._container_registry = None # type: CuraContainerRegistry
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
self._package_manager_class = CuraPackageManager
|
||||
|
||||
@ -265,6 +270,9 @@ class CuraApplication(QtApplication):
|
||||
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.")
|
||||
self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.")
|
||||
|
||||
def getContainerRegistry(self) -> "CuraContainerRegistry":
|
||||
return self._container_registry
|
||||
|
||||
def parseCliOptions(self):
|
||||
super().parseCliOptions()
|
||||
|
||||
@ -674,7 +682,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
Logger.log("i", "Initializing quality manager")
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
self._quality_manager = QualityManager(container_registry, parent = self)
|
||||
self._quality_manager = QualityManager(self, parent = self)
|
||||
self._quality_manager.initialize()
|
||||
|
||||
Logger.log("i", "Initializing machine manager")
|
||||
@ -706,6 +714,9 @@ class CuraApplication(QtApplication):
|
||||
default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
|
||||
self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
|
||||
|
||||
# Initialize Cura API
|
||||
self._cura_API.initialize()
|
||||
|
||||
# Detect in which mode to run and execute that mode
|
||||
if self._is_headless:
|
||||
self.runWithoutGUI()
|
||||
@ -893,6 +904,9 @@ class CuraApplication(QtApplication):
|
||||
self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
|
||||
return self._custom_quality_profile_drop_down_menu_model
|
||||
|
||||
def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
|
||||
return self._cura_API
|
||||
|
||||
## Registers objects for the QML engine to use.
|
||||
#
|
||||
# \param engine The QML engine.
|
||||
@ -941,6 +955,9 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
||||
|
||||
from cura.API import CuraAPI
|
||||
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
||||
|
||||
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
|
||||
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
|
||||
qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Dict, Union
|
||||
import os
|
||||
import urllib.parse
|
||||
from configparser import ConfigParser
|
||||
@ -60,7 +60,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
||||
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = []
|
||||
items = [] # type: List[Dict[str, Union[str, int, List[str]]]]
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
try:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
||||
@ -79,7 +79,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
||||
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
|
||||
continue
|
||||
|
||||
settings = []
|
||||
settings = [] # type: List[str]
|
||||
for section in parser.sections():
|
||||
if section == 'general':
|
||||
continue
|
||||
@ -98,7 +98,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
||||
except Exception:
|
||||
Logger.logException("e", "Failed to load setting preset %s", file_path)
|
||||
|
||||
items.sort(key = lambda k: (int(k["weight"]), k["id"]))
|
||||
items.sort(key = lambda k: (int(k["weight"]), k["id"])) # type: ignore
|
||||
# Put "custom" at the top
|
||||
items.insert(0, {"id": "custom",
|
||||
"name": "Custom selection",
|
||||
|
@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Optional, cast, Dict, List
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
@ -21,7 +20,6 @@ if TYPE_CHECKING:
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
||||
#
|
||||
@ -38,11 +36,11 @@ class QualityManager(QObject):
|
||||
|
||||
qualitiesUpdated = pyqtSignal()
|
||||
|
||||
def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = Application.getInstance() # type: CuraApplication
|
||||
self._application = application
|
||||
self._material_manager = self._application.getMaterialManager()
|
||||
self._container_registry = container_registry
|
||||
self._container_registry = self._application.getContainerRegistry()
|
||||
|
||||
self._empty_quality_container = self._application.empty_quality_container
|
||||
self._empty_quality_changes_container = self._application.empty_quality_changes_container
|
||||
@ -458,7 +456,7 @@ class QualityManager(QObject):
|
||||
# stack and clear the user settings.
|
||||
@pyqtSlot(str)
|
||||
def createQualityChanges(self, base_name: str) -> None:
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
machine_manager = self._application.getMachineManager()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
if not global_stack:
|
||||
|
112
cura/OAuth2/AuthorizationHelpers.py
Normal file
112
cura/OAuth2/AuthorizationHelpers.py
Normal file
@ -0,0 +1,112 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import random
|
||||
from hashlib import sha512
|
||||
from base64 import b64encode
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
||||
|
||||
|
||||
# Class containing several helpers to deal with the authorization flow.
|
||||
class AuthorizationHelpers:
|
||||
def __init__(self, settings: "OAuth2Settings") -> None:
|
||||
self._settings = settings
|
||||
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
||||
|
||||
@property
|
||||
# The OAuth2 settings object.
|
||||
def settings(self) -> "OAuth2Settings":
|
||||
return self._settings
|
||||
|
||||
# Request the access token from the authorization server.
|
||||
# \param authorization_code: The authorization code from the 1st step.
|
||||
# \param verification_code: The verification code needed for the PKCE extension.
|
||||
# \return: An AuthenticationResponse object.
|
||||
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
|
||||
data = {
|
||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
||||
"grant_type": "authorization_code",
|
||||
"code": authorization_code,
|
||||
"code_verifier": verification_code,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
|
||||
# Request the access token from the authorization server using a refresh token.
|
||||
# \param refresh_token:
|
||||
# \return: An AuthenticationResponse object.
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
||||
data = {
|
||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
|
||||
@staticmethod
|
||||
# Parse the token response from the authorization server into an AuthenticationResponse object.
|
||||
# \param token_response: The JSON string data response from the authorization server.
|
||||
# \return: An AuthenticationResponse object.
|
||||
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
|
||||
token_data = None
|
||||
|
||||
try:
|
||||
token_data = json.loads(token_response.text)
|
||||
except ValueError:
|
||||
Logger.log("w", "Could not parse token response data: %s", token_response.text)
|
||||
|
||||
if not token_data:
|
||||
return AuthenticationResponse(success=False, err_message="Could not read response.")
|
||||
|
||||
if token_response.status_code not in (200, 201):
|
||||
return AuthenticationResponse(success=False, err_message=token_data["error_description"])
|
||||
|
||||
return AuthenticationResponse(success=True,
|
||||
token_type=token_data["token_type"],
|
||||
access_token=token_data["access_token"],
|
||||
refresh_token=token_data["refresh_token"],
|
||||
expires_in=token_data["expires_in"],
|
||||
scope=token_data["scope"])
|
||||
|
||||
# Calls the authentication API endpoint to get the token data.
|
||||
# \param access_token: The encoded JWT token.
|
||||
# \return: Dict containing some profile data.
|
||||
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
if token_request.status_code not in (200, 201):
|
||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||
return None
|
||||
user_data = token_request.json().get("data")
|
||||
if not user_data or not isinstance(user_data, dict):
|
||||
Logger.log("w", "Could not parse user data from token: %s", user_data)
|
||||
return None
|
||||
return UserProfile(
|
||||
user_id = user_data["user_id"],
|
||||
username = user_data["username"],
|
||||
profile_image_url = user_data.get("profile_image_url", "")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
# Generate a 16-character verification code.
|
||||
# \param code_length: How long should the code be?
|
||||
def generateVerificationCode(code_length: int = 16) -> str:
|
||||
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
|
||||
|
||||
@staticmethod
|
||||
# Generates a base64 encoded sha512 encrypted version of a given string.
|
||||
# \param verification_code:
|
||||
# \return: The encrypted code in base64 format.
|
||||
def generateVerificationCodeChallenge(verification_code: str) -> str:
|
||||
encoded = sha512(verification_code.encode()).digest()
|
||||
return b64encode(encoded, altchars = b"_-").decode()
|
101
cura/OAuth2/AuthorizationRequestHandler.py
Normal file
101
cura/OAuth2/AuthorizationRequestHandler.py
Normal file
@ -0,0 +1,101 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
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.
|
||||
# It also requests the access token for the 2nd stage of the OAuth flow.
|
||||
class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
def __init__(self, request, client_address, server) -> None:
|
||||
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_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
||||
self.verification_code = None # type: Optional[str]
|
||||
|
||||
def do_GET(self) -> None:
|
||||
# Extract values from the query string.
|
||||
parsed_url = urlparse(self.path)
|
||||
query = parse_qs(parsed_url.query)
|
||||
|
||||
# Handle the possible requests
|
||||
if parsed_url.path == "/callback":
|
||||
server_response, token_response = self._handleCallback(query)
|
||||
else:
|
||||
server_response = self._handleNotFound()
|
||||
token_response = None
|
||||
|
||||
# Send the data to the browser.
|
||||
self._sendHeaders(server_response.status, server_response.content_type, server_response.redirect_uri)
|
||||
|
||||
if server_response.data_stream:
|
||||
# If there is data in the response, we send it.
|
||||
self._sendData(server_response.data_stream)
|
||||
|
||||
if token_response and self.authorization_callback is not None:
|
||||
# Trigger the callback if we got a response.
|
||||
# This will cause the server to shut down, so we do it at the very end of the request handling.
|
||||
self.authorization_callback(token_response)
|
||||
|
||||
# Handler for the callback URL redirect.
|
||||
# \param query: Dict containing the HTTP query parameters.
|
||||
# \return: HTTP ResponseData containing a success page to show to the user.
|
||||
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
|
||||
code = self._queryGet(query, "code")
|
||||
if code and self.authorization_helpers is not None and self.verification_code is not None:
|
||||
# If the code was returned we get the access token.
|
||||
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
|
||||
code, self.verification_code)
|
||||
|
||||
elif self._queryGet(query, "error_code") == "user_denied":
|
||||
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
err_message="Please give the required permissions when authorizing this application."
|
||||
)
|
||||
|
||||
else:
|
||||
# We don't know what went wrong here, so instruct the user to check the logs.
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
error_message="Something unexpected happened when trying to log in, please try again."
|
||||
)
|
||||
if self.authorization_helpers is None:
|
||||
return ResponseData(), token_response
|
||||
|
||||
return ResponseData(
|
||||
status=HTTP_STATUS["REDIRECT"],
|
||||
data_stream=b"Redirecting...",
|
||||
redirect_uri=self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
||||
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
|
||||
), token_response
|
||||
|
||||
@staticmethod
|
||||
# Handle all other non-existing server calls.
|
||||
def _handleNotFound() -> ResponseData:
|
||||
return ResponseData(status=HTTP_STATUS["NOT_FOUND"], content_type="text/html", data_stream=b"Not found.")
|
||||
|
||||
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
|
||||
self.send_response(status.code, status.message)
|
||||
self.send_header("Content-type", content_type)
|
||||
if redirect_uri:
|
||||
self.send_header("Location", redirect_uri)
|
||||
self.end_headers()
|
||||
|
||||
def _sendData(self, data: bytes) -> None:
|
||||
self.wfile.write(data)
|
||||
|
||||
@staticmethod
|
||||
# Convenience Helper for getting values from a pre-parsed query string
|
||||
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str]=None) -> Optional[str]:
|
||||
return query_data.get(key, [default])[0]
|
26
cura/OAuth2/AuthorizationRequestServer.py
Normal file
26
cura/OAuth2/AuthorizationRequestServer.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from http.server import HTTPServer
|
||||
from typing import Callable, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import AuthenticationResponse
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
|
||||
|
||||
# The authorization request callback handler server.
|
||||
# This subclass is needed to be able to pass some data to the request handler.
|
||||
# This cannot be done on the request handler directly as the HTTPServer creates an instance of the handler after
|
||||
# init.
|
||||
class AuthorizationRequestServer(HTTPServer):
|
||||
# Set the authorization helpers instance on the request handler.
|
||||
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
|
||||
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
|
||||
|
||||
# Set the authorization callback on the request handler.
|
||||
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
|
||||
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
|
||||
|
||||
# Set the verification code on the request handler.
|
||||
def setVerificationCode(self, verification_code: str) -> None:
|
||||
self.RequestHandlerClass.verification_code = verification_code # type: ignore
|
168
cura/OAuth2/AuthorizationService.py
Normal file
168
cura/OAuth2/AuthorizationService.py
Normal file
@ -0,0 +1,168 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import webbrowser
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
|
||||
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.Models import AuthenticationResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
|
||||
class AuthorizationService:
|
||||
"""
|
||||
The authorization service is responsible for handling the login flow,
|
||||
storing user credentials and providing account information.
|
||||
"""
|
||||
|
||||
# Emit signal when authentication is completed.
|
||||
onAuthStateChanged = Signal()
|
||||
|
||||
# Emit signal when authentication failed.
|
||||
onAuthenticationError = Signal()
|
||||
|
||||
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
|
||||
self._settings = settings
|
||||
self._auth_helpers = AuthorizationHelpers(settings)
|
||||
self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
|
||||
self._auth_data = None # type: Optional[AuthenticationResponse]
|
||||
self._user_profile = None # type: Optional["UserProfile"]
|
||||
self._preferences = preferences
|
||||
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
|
||||
|
||||
def initialize(self, preferences: Optional["Preferences"] = None) -> None:
|
||||
if preferences is not None:
|
||||
self._preferences = preferences
|
||||
if self._preferences:
|
||||
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
|
||||
|
||||
# Get the user profile as obtained from the JWT (JSON Web Token).
|
||||
# If the JWT is not yet parsed, calling this will take care of that.
|
||||
# \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.
|
||||
self._user_profile = self._parseJWT()
|
||||
if not self._user_profile:
|
||||
# If there is still no user profile from the JWT, we have to log in again.
|
||||
return None
|
||||
|
||||
return self._user_profile
|
||||
|
||||
# Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
|
||||
# \return UserProfile if it was able to parse, None otherwise.
|
||||
def _parseJWT(self) -> Optional["UserProfile"]:
|
||||
if not self._auth_data or self._auth_data.access_token is None:
|
||||
# If no auth data exists, we should always log in again.
|
||||
return None
|
||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
if user_data:
|
||||
# If the profile was found, we return it immediately.
|
||||
return user_data
|
||||
# The JWT was expired or invalid and we should request a new one.
|
||||
if self._auth_data.refresh_token is None:
|
||||
return None
|
||||
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||
if not self._auth_data or self._auth_data.access_token is None:
|
||||
# The token could not be refreshed using the refresh token. We should login again.
|
||||
return None
|
||||
|
||||
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
|
||||
# Get the access token as provided by the repsonse 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.
|
||||
return None
|
||||
|
||||
if self._auth_data is None:
|
||||
return None
|
||||
|
||||
return self._auth_data.access_token
|
||||
|
||||
# Try to refresh the access token. This should be used when it has expired.
|
||||
def refreshAccessToken(self) -> None:
|
||||
if self._auth_data is None or self._auth_data.refresh_token is None:
|
||||
Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
|
||||
return
|
||||
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token))
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
|
||||
# Delete the authentication data that we have stored locally (eg; logout)
|
||||
def deleteAuthData(self) -> None:
|
||||
if self._auth_data is not None:
|
||||
self._storeAuthData()
|
||||
self.onAuthStateChanged.emit(logged_in=False)
|
||||
|
||||
# Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
|
||||
def startAuthorizationFlow(self) -> None:
|
||||
Logger.log("d", "Starting new OAuth2 flow...")
|
||||
|
||||
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
|
||||
# This is needed because the CuraDrivePlugin is a untrusted (open source) client.
|
||||
# More details can be found at https://tools.ietf.org/html/rfc7636.
|
||||
verification_code = self._auth_helpers.generateVerificationCode()
|
||||
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
|
||||
|
||||
# Create the query string needed for the OAuth2 flow.
|
||||
query_string = urlencode({
|
||||
"client_id": self._settings.CLIENT_ID,
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"scope": self._settings.CLIENT_SCOPES,
|
||||
"response_type": "code",
|
||||
"state": "CuraDriveIsAwesome",
|
||||
"code_challenge": challenge_code,
|
||||
"code_challenge_method": "S512"
|
||||
})
|
||||
|
||||
# Open the authorization page in a new browser window.
|
||||
webbrowser.open_new("{}?{}".format(self._auth_url, query_string))
|
||||
|
||||
# Start a local web server to receive the callback URL on.
|
||||
self._server.start(verification_code)
|
||||
|
||||
# Callback method for the authentication flow.
|
||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
||||
if auth_response.success:
|
||||
self._storeAuthData(auth_response)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
else:
|
||||
self.onAuthenticationError.emit(logged_in=False, error_message=auth_response.err_message)
|
||||
self._server.stop() # Stop the web server at all times.
|
||||
|
||||
# Load authentication data from preferences.
|
||||
def loadAuthDataFromPreferences(self) -> None:
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
|
||||
return
|
||||
try:
|
||||
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
|
||||
if preferences_data:
|
||||
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
except ValueError:
|
||||
Logger.logException("w", "Could not load auth data from preferences")
|
||||
|
||||
# Store authentication data in preferences.
|
||||
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
||||
return
|
||||
|
||||
self._auth_data = auth_data
|
||||
if auth_data:
|
||||
self._user_profile = self.getUserProfile()
|
||||
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data)))
|
||||
else:
|
||||
self._user_profile = None
|
||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
64
cura/OAuth2/LocalAuthorizationServer.py
Normal file
64
cura/OAuth2/LocalAuthorizationServer.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import threading
|
||||
from typing import Optional, Callable, Any, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer
|
||||
from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import AuthenticationResponse
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
|
||||
|
||||
class LocalAuthorizationServer:
|
||||
# The local LocalAuthorizationServer takes care of the oauth2 callbacks.
|
||||
# Once the flow is completed, this server should be closed down again by calling stop()
|
||||
# \param auth_helpers: An instance of the authorization helpers class.
|
||||
# \param auth_state_changed_callback: A callback function to be called when the authorization state changes.
|
||||
# \param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped
|
||||
# at shutdown. Their resources (e.g. open files) may never be released.
|
||||
def __init__(self, auth_helpers: "AuthorizationHelpers",
|
||||
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
|
||||
daemon: bool) -> None:
|
||||
self._web_server = None # type: Optional[AuthorizationRequestServer]
|
||||
self._web_server_thread = None # type: Optional[threading.Thread]
|
||||
self._web_server_port = auth_helpers.settings.CALLBACK_PORT
|
||||
self._auth_helpers = auth_helpers
|
||||
self._auth_state_changed_callback = auth_state_changed_callback
|
||||
self._daemon = daemon
|
||||
|
||||
# Starts the local web server to handle the authorization callback.
|
||||
# \param verification_code: The verification code part of the OAuth2 client identification.
|
||||
def start(self, verification_code: str) -> None:
|
||||
if self._web_server:
|
||||
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
|
||||
# We still inject the new verification code though.
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
return
|
||||
|
||||
if self._web_server_port is None:
|
||||
raise Exception("Unable to start server without specifying the port.")
|
||||
|
||||
Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port)
|
||||
|
||||
# Create the server and inject the callback and code.
|
||||
self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler)
|
||||
self._web_server.setAuthorizationHelpers(self._auth_helpers)
|
||||
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
|
||||
# Start the server on a new thread.
|
||||
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
|
||||
self._web_server_thread.start()
|
||||
|
||||
# Stops the web server if it was running. It also does some cleanup.
|
||||
def stop(self) -> None:
|
||||
Logger.log("d", "Stopping local oauth2 web server...")
|
||||
|
||||
if self._web_server:
|
||||
self._web_server.server_close()
|
||||
self._web_server = None
|
||||
self._web_server_thread = None
|
60
cura/OAuth2/Models.py
Normal file
60
cura/OAuth2/Models.py
Normal file
@ -0,0 +1,60 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BaseModel:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
# OAuth OAuth2Settings data template.
|
||||
class OAuth2Settings(BaseModel):
|
||||
CALLBACK_PORT = None # type: Optional[int]
|
||||
OAUTH_SERVER_URL = None # type: Optional[str]
|
||||
CLIENT_ID = None # type: Optional[str]
|
||||
CLIENT_SCOPES = None # type: Optional[str]
|
||||
CALLBACK_URL = None # type: Optional[str]
|
||||
AUTH_DATA_PREFERENCE_KEY = "" # type: str
|
||||
AUTH_SUCCESS_REDIRECT = "https://ultimaker.com" # type: str
|
||||
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
|
||||
|
||||
|
||||
# User profile data template.
|
||||
class UserProfile(BaseModel):
|
||||
user_id = None # type: Optional[str]
|
||||
username = None # type: Optional[str]
|
||||
profile_image_url = None # type: Optional[str]
|
||||
|
||||
|
||||
# Authentication data template.
|
||||
class AuthenticationResponse(BaseModel):
|
||||
"""Data comes from the token response with success flag and error message added."""
|
||||
success = True # type: bool
|
||||
token_type = None # type: Optional[str]
|
||||
access_token = None # type: Optional[str]
|
||||
refresh_token = None # type: Optional[str]
|
||||
expires_in = None # type: Optional[str]
|
||||
scope = None # type: Optional[str]
|
||||
err_message = None # type: Optional[str]
|
||||
|
||||
|
||||
# Response status template.
|
||||
class ResponseStatus(BaseModel):
|
||||
code = 200 # type: int
|
||||
message = "" # type str
|
||||
|
||||
|
||||
# Response data template.
|
||||
class ResponseData(BaseModel):
|
||||
status = None # type: ResponseStatus
|
||||
data_stream = None # type: Optional[bytes]
|
||||
redirect_uri = None # type: Optional[str]
|
||||
content_type = "text/html" # type: str
|
||||
|
||||
|
||||
# Possible HTTP responses.
|
||||
HTTP_STATUS = {
|
||||
"OK": ResponseStatus(code=200, message="OK"),
|
||||
"NOT_FOUND": ResponseStatus(code=404, message="NOT FOUND"),
|
||||
"REDIRECT": ResponseStatus(code=302, message="REDIRECT")
|
||||
}
|
2
cura/OAuth2/__init__.py
Normal file
2
cura/OAuth2/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
@ -5,6 +5,7 @@ from PyQt5.QtCore import QTimer
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
@ -18,6 +19,8 @@ from typing import TYPE_CHECKING, Any, Optional
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
from UM.Math.Matrix import Matrix
|
||||
|
||||
|
||||
## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
|
||||
@ -33,17 +36,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
# Make sure the timer is created on the main thread
|
||||
self._recompute_convex_hull_timer = None # type: Optional[QTimer]
|
||||
|
||||
if Application.getInstance() is not None:
|
||||
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
from cura.CuraApplication import CuraApplication
|
||||
if CuraApplication.getInstance() is not None:
|
||||
CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
|
||||
self._raft_thickness = 0.0
|
||||
self._build_volume = Application.getInstance().getBuildVolume()
|
||||
self._build_volume = CuraApplication.getInstance().getBuildVolume()
|
||||
self._build_volume.raftThicknessChanged.connect(self._onChanged)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
Application.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
||||
Application.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
||||
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
CuraApplication.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
||||
CuraApplication.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
||||
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
@ -61,9 +64,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
previous_node.parentChanged.disconnect(self._onChanged)
|
||||
|
||||
super().setNode(node)
|
||||
|
||||
self._node.transformationChanged.connect(self._onChanged)
|
||||
self._node.parentChanged.connect(self._onChanged)
|
||||
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
|
||||
node.transformationChanged.connect(self._onChanged)
|
||||
node.parentChanged.connect(self._onChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
@ -78,9 +81,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
hull = self._compute2DConvexHull()
|
||||
|
||||
if self._global_stack and self._node and hull is not None:
|
||||
if self._global_stack and self._node is not None and hull is not None:
|
||||
# Parent can be None if node is just loaded.
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
|
||||
hull = self._add2DAdhesionMargin(hull)
|
||||
return hull
|
||||
@ -92,6 +95,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
return self._compute2DConvexHeadFull()
|
||||
|
||||
@staticmethod
|
||||
def hasGroupAsParent(node: "SceneNode") -> bool:
|
||||
parent = node.getParent()
|
||||
if parent is None:
|
||||
return False
|
||||
return bool(parent.callDecoration("isGroup"))
|
||||
|
||||
## Get convex hull of the object + head size
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# For one at the time this is area with intersection of mirrored head
|
||||
@ -100,8 +110,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
head_with_fans = self._compute2DConvexHeadMin()
|
||||
if head_with_fans is None:
|
||||
return None
|
||||
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
||||
return head_with_fans_with_adhesion_margin
|
||||
return None
|
||||
@ -114,7 +126,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
# Printing one at a time and it's not an object in a group
|
||||
return self._compute2DConvexHull()
|
||||
return None
|
||||
@ -153,15 +165,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
def _init2DConvexHullCache(self) -> None:
|
||||
# Cache for the group code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_group_child_polygon = None
|
||||
self._2d_convex_hull_group_result = None
|
||||
self._2d_convex_hull_group_child_polygon = None # type: Optional[Polygon]
|
||||
self._2d_convex_hull_group_result = None # type: Optional[Polygon]
|
||||
|
||||
# Cache for the mesh code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_mesh = None
|
||||
self._2d_convex_hull_mesh_world_transform = None
|
||||
self._2d_convex_hull_mesh_result = None
|
||||
self._2d_convex_hull_mesh = None # type: Optional[MeshData]
|
||||
self._2d_convex_hull_mesh_world_transform = None # type: Optional[Matrix]
|
||||
self._2d_convex_hull_mesh_result = None # type: Optional[Polygon]
|
||||
|
||||
def _compute2DConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isGroup"):
|
||||
points = numpy.zeros((0, 2), dtype=numpy.int32)
|
||||
for child in self._node.getChildren():
|
||||
@ -187,47 +201,47 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
return offset_hull
|
||||
|
||||
else:
|
||||
offset_hull = None
|
||||
if self._node.getMeshData():
|
||||
mesh = self._node.getMeshData()
|
||||
world_transform = self._node.getWorldTransformation()
|
||||
|
||||
# Check the cache
|
||||
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
|
||||
return self._2d_convex_hull_mesh_result
|
||||
|
||||
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
|
||||
# Don't use data below 0.
|
||||
# TODO; We need a better check for this as this gives poor results for meshes with long edges.
|
||||
# Do not throw away vertices: the convex hull may be too small and objects can collide.
|
||||
# vertex_data = vertex_data[vertex_data[:,1] >= -0.01]
|
||||
|
||||
if len(vertex_data) >= 4:
|
||||
# Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
|
||||
# This is done to greatly speed up further convex hull calculations as the convex hull
|
||||
# becomes much less complex when dealing with highly detailed models.
|
||||
vertex_data = numpy.round(vertex_data, 1)
|
||||
|
||||
vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
|
||||
|
||||
# Grab the set of unique points.
|
||||
#
|
||||
# This basically finds the unique rows in the array by treating them as opaque groups of bytes
|
||||
# which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
|
||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||
|
||||
hull = Polygon(vertex_data)
|
||||
|
||||
if len(vertex_data) >= 3:
|
||||
convex_hull = hull.getConvexHull()
|
||||
offset_hull = self._offsetHull(convex_hull)
|
||||
else:
|
||||
offset_hull = Polygon([])
|
||||
mesh = self._node.getMeshData()
|
||||
if mesh is None:
|
||||
return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
|
||||
|
||||
world_transform = self._node.getWorldTransformation()
|
||||
|
||||
# Check the cache
|
||||
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
|
||||
return self._2d_convex_hull_mesh_result
|
||||
|
||||
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
|
||||
# Don't use data below 0.
|
||||
# TODO; We need a better check for this as this gives poor results for meshes with long edges.
|
||||
# Do not throw away vertices: the convex hull may be too small and objects can collide.
|
||||
# vertex_data = vertex_data[vertex_data[:,1] >= -0.01]
|
||||
|
||||
if len(vertex_data) >= 4: # type: ignore # mypy and numpy don't play along well just yet.
|
||||
# Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
|
||||
# This is done to greatly speed up further convex hull calculations as the convex hull
|
||||
# becomes much less complex when dealing with highly detailed models.
|
||||
vertex_data = numpy.round(vertex_data, 1)
|
||||
|
||||
vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
|
||||
|
||||
# Grab the set of unique points.
|
||||
#
|
||||
# This basically finds the unique rows in the array by treating them as opaque groups of bytes
|
||||
# which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
|
||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||
|
||||
hull = Polygon(vertex_data)
|
||||
|
||||
if len(vertex_data) >= 3:
|
||||
convex_hull = hull.getConvexHull()
|
||||
offset_hull = self._offsetHull(convex_hull)
|
||||
|
||||
# Store the result in the cache
|
||||
self._2d_convex_hull_mesh = mesh
|
||||
self._2d_convex_hull_mesh_world_transform = world_transform
|
||||
@ -338,7 +352,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
|
||||
def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any:
|
||||
if not self._global_stack:
|
||||
if self._global_stack is None or self._node is None:
|
||||
return None
|
||||
per_mesh_stack = self._node.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
@ -358,7 +372,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
return self._global_stack.getProperty(setting_key, prop)
|
||||
|
||||
## Returns True if node is a descendant or the same as the root node.
|
||||
def __isDescendant(self, root: "SceneNode", node: "SceneNode") -> bool:
|
||||
def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool:
|
||||
if node is None:
|
||||
return False
|
||||
if root is node:
|
||||
|
@ -28,10 +28,10 @@ if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from cura.Settings.MachineManager import MachineManager
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
@ -52,7 +52,7 @@ class ContainerManager(QObject):
|
||||
|
||||
self._application = application # type: CuraApplication
|
||||
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
||||
self._container_registry = self._application.getContainerRegistry() # type: ContainerRegistry
|
||||
self._container_registry = self._application.getContainerRegistry() # type: CuraContainerRegistry
|
||||
self._machine_manager = self._application.getMachineManager() # type: MachineManager
|
||||
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||
self._quality_manager = self._application.getQualityManager() # type: QualityManager
|
||||
@ -391,7 +391,8 @@ class ContainerManager(QObject):
|
||||
continue
|
||||
|
||||
mime_type = self._container_registry.getMimeTypeForContainer(container_type)
|
||||
|
||||
if mime_type is None:
|
||||
continue
|
||||
entry = {
|
||||
"type": serialize_type,
|
||||
"mime": mime_type,
|
||||
|
@ -1148,7 +1148,7 @@ class MachineManager(QObject):
|
||||
self._fixQualityChangesGroupToNotSupported(quality_changes_group)
|
||||
|
||||
quality_changes_container = self._empty_quality_changes_container
|
||||
quality_container = self._empty_quality_container
|
||||
quality_container = self._empty_quality_container # type: Optional[InstanceContainer]
|
||||
if quality_changes_group.node_for_global and quality_changes_group.node_for_global.getContainer():
|
||||
quality_changes_container = cast(InstanceContainer, quality_changes_group.node_for_global.getContainer())
|
||||
if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.getContainer():
|
||||
|
@ -21,6 +21,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Signal import Signal
|
||||
from UM.View.CompositePass import CompositePass
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.View.GL.OpenGLContext import OpenGLContext
|
||||
|
||||
@ -36,7 +37,7 @@ from .SimulationViewProxy import SimulationViewProxy
|
||||
import numpy
|
||||
import os.path
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, List
|
||||
from typing import Optional, TYPE_CHECKING, List, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
@ -64,7 +65,7 @@ class SimulationView(View):
|
||||
self._minimum_layer_num = 0
|
||||
self._current_layer_mesh = None
|
||||
self._current_layer_jumps = None
|
||||
self._top_layers_job = None
|
||||
self._top_layers_job = None # type: Optional["_CreateTopLayersJob"]
|
||||
self._activity = False
|
||||
self._old_max_layers = 0
|
||||
|
||||
@ -78,10 +79,10 @@ class SimulationView(View):
|
||||
|
||||
self._ghost_shader = None # type: Optional["ShaderProgram"]
|
||||
self._layer_pass = None # type: Optional[SimulationPass]
|
||||
self._composite_pass = None # type: Optional[RenderPass]
|
||||
self._old_layer_bindings = None
|
||||
self._composite_pass = None # type: Optional[CompositePass]
|
||||
self._old_layer_bindings = None # type: Optional[List[str]]
|
||||
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
|
||||
self._old_composite_shader = None
|
||||
self._old_composite_shader = None # type: Optional["ShaderProgram"]
|
||||
|
||||
self._global_container_stack = None # type: Optional[ContainerStack]
|
||||
self._proxy = SimulationViewProxy()
|
||||
@ -204,9 +205,11 @@ class SimulationView(View):
|
||||
|
||||
if not self._ghost_shader:
|
||||
self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
|
||||
self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb()))
|
||||
theme = CuraApplication.getInstance().getTheme()
|
||||
if theme is not None:
|
||||
self._ghost_shader.setUniformValue("u_color", Color(*theme.getColor("layerview_ghost").getRgb()))
|
||||
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
# We do not want to render ConvexHullNode as it conflicts with the bottom layers.
|
||||
# However, it is somewhat relevant when the node is selected, so do render it then.
|
||||
if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
|
||||
@ -347,7 +350,7 @@ class SimulationView(View):
|
||||
self._old_max_layers = self._max_layers
|
||||
## Recalculate num max layers
|
||||
new_max_layers = -1
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
@ -398,7 +401,7 @@ class SimulationView(View):
|
||||
def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
|
||||
# Update the currentPath
|
||||
scene = self.getController().getScene()
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
@ -474,15 +477,17 @@ class SimulationView(View):
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
if not self._simulationview_composite_shader:
|
||||
self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader"))
|
||||
theme = Application.getInstance().getTheme()
|
||||
self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
|
||||
self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
|
||||
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("SimulationView"))
|
||||
self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(plugin_path, "simulationview_composite.shader"))
|
||||
theme = CuraApplication.getInstance().getTheme()
|
||||
if theme is not None:
|
||||
self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
|
||||
self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
|
||||
|
||||
if not self._composite_pass:
|
||||
self._composite_pass = self.getRenderer().getRenderPass("composite")
|
||||
self._composite_pass = cast(CompositePass, self.getRenderer().getRenderPass("composite"))
|
||||
|
||||
self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
|
||||
self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
|
||||
self._composite_pass.getLayerBindings().append("simulationview")
|
||||
self._old_composite_shader = self._composite_pass.getCompositeShader()
|
||||
self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
|
||||
@ -496,8 +501,8 @@ class SimulationView(View):
|
||||
self._nozzle_node.setParent(None)
|
||||
self.getRenderer().removeRenderPass(self._layer_pass)
|
||||
if self._composite_pass:
|
||||
self._composite_pass.setLayerBindings(self._old_layer_bindings)
|
||||
self._composite_pass.setCompositeShader(self._old_composite_shader)
|
||||
self._composite_pass.setLayerBindings(cast(List[str], self._old_layer_bindings))
|
||||
self._composite_pass.setCompositeShader(cast(ShaderProgram, self._old_composite_shader))
|
||||
|
||||
return False
|
||||
|
||||
@ -606,7 +611,7 @@ class _CreateTopLayersJob(Job):
|
||||
|
||||
def run(self) -> None:
|
||||
layer_data = None
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
for node in DepthFirstIterator(self._scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if layer_data:
|
||||
break
|
||||
|
@ -8,7 +8,7 @@ import QtQuick.Layouts 1.1
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "Menus"
|
||||
|
||||
@ -21,7 +21,6 @@ UM.MainWindow
|
||||
property bool showPrintMonitor: false
|
||||
|
||||
backgroundColor: UM.Theme.getColor("viewport_background")
|
||||
|
||||
// This connection is here to support legacy printer output devices that use the showPrintMonitor signal on Application to switch to the monitor stage
|
||||
// It should be phased out in newer plugin versions.
|
||||
Connections
|
||||
|
133
tests/TestOAuth2.py
Normal file
133
tests/TestOAuth2.py
Normal file
@ -0,0 +1,133 @@
|
||||
import webbrowser
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from UM.Preferences import Preferences
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||
from cura.OAuth2.Models import OAuth2Settings, AuthenticationResponse, UserProfile
|
||||
|
||||
CALLBACK_PORT = 32118
|
||||
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)
|
||||
)
|
||||
|
||||
FAILED_AUTH_RESPONSE = AuthenticationResponse(success = False, err_message = "FAILURE!")
|
||||
|
||||
SUCCESFULL_AUTH_RESPONSE = AuthenticationResponse(access_token = "beep", refresh_token = "beep?")
|
||||
|
||||
MALFORMED_AUTH_RESPONSE = AuthenticationResponse()
|
||||
|
||||
|
||||
def test_cleanAuthService() -> None:
|
||||
# Ensure that when setting up an AuthorizationService, no data is set.
|
||||
authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences())
|
||||
authorization_service.initialize()
|
||||
assert authorization_service.getUserProfile() is None
|
||||
assert authorization_service.getAccessToken() is None
|
||||
|
||||
|
||||
def test_failedLogin() -> None:
|
||||
authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences())
|
||||
authorization_service.onAuthenticationError.emit = MagicMock()
|
||||
authorization_service.onAuthStateChanged.emit = MagicMock()
|
||||
authorization_service.initialize()
|
||||
|
||||
# Let the service think there was a failed response
|
||||
authorization_service._onAuthStateChanged(FAILED_AUTH_RESPONSE)
|
||||
|
||||
# Check that the error signal was triggered
|
||||
assert authorization_service.onAuthenticationError.emit.call_count == 1
|
||||
|
||||
# Since nothing changed, this should still be 0.
|
||||
assert authorization_service.onAuthStateChanged.emit.call_count == 0
|
||||
|
||||
# Validate that there is no user profile or token
|
||||
assert authorization_service.getUserProfile() is None
|
||||
assert authorization_service.getAccessToken() is None
|
||||
|
||||
|
||||
@patch.object(AuthorizationService, "getUserProfile", return_value=UserProfile())
|
||||
def test_storeAuthData(get_user_profile) -> None:
|
||||
preferences = Preferences()
|
||||
authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
|
||||
authorization_service.initialize()
|
||||
|
||||
# Write stuff to the preferences.
|
||||
authorization_service._storeAuthData(SUCCESFULL_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 != {}
|
||||
|
||||
# Create a second auth service, so we can load the data.
|
||||
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
|
||||
|
||||
|
||||
@patch.object(LocalAuthorizationServer, "stop")
|
||||
@patch.object(LocalAuthorizationServer, "start")
|
||||
@patch.object(webbrowser, "open_new")
|
||||
def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None:
|
||||
preferences = Preferences()
|
||||
authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
|
||||
authorization_service.startAuthorizationFlow()
|
||||
assert webbrowser_open.call_count == 1
|
||||
|
||||
# Ensure that the Authorization service tried to start the server.
|
||||
assert start_auth_server.call_count == 1
|
||||
assert stop_auth_server.call_count == 0
|
||||
authorization_service._onAuthStateChanged(FAILED_AUTH_RESPONSE)
|
||||
|
||||
# Ensure that it stopped the server.
|
||||
assert stop_auth_server.call_count == 1
|
||||
|
||||
|
||||
def test_loginAndLogout() -> None:
|
||||
preferences = Preferences()
|
||||
authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences)
|
||||
authorization_service.onAuthenticationError.emit = MagicMock()
|
||||
authorization_service.onAuthStateChanged.emit = MagicMock()
|
||||
authorization_service.initialize()
|
||||
|
||||
# Let the service think there was a succesfull response
|
||||
with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()):
|
||||
authorization_service._onAuthStateChanged(SUCCESFULL_AUTH_RESPONSE)
|
||||
|
||||
# Ensure that the error signal was not triggered
|
||||
assert authorization_service.onAuthenticationError.emit.call_count == 0
|
||||
|
||||
# Since we said that it went right this time, validate that we got a signal.
|
||||
assert authorization_service.onAuthStateChanged.emit.call_count == 1
|
||||
assert authorization_service.getUserProfile() is not None
|
||||
assert authorization_service.getAccessToken() == "beep"
|
||||
|
||||
# Check that we stored the authentication data, so next time the user won't have to log in again.
|
||||
assert preferences.getValue("test/auth_data") is not None
|
||||
|
||||
# We're logged in now, also check if logging out works
|
||||
authorization_service.deleteAuthData()
|
||||
assert authorization_service.onAuthStateChanged.emit.call_count == 2
|
||||
assert authorization_service.getUserProfile() is None
|
||||
|
||||
# Ensure the data is gone after we logged out.
|
||||
assert preferences.getValue("test/auth_data") == "{}"
|
||||
|
||||
|
||||
def test_wrongServerResponses() -> None:
|
||||
authorization_service = AuthorizationService(OAUTH_SETTINGS, Preferences())
|
||||
authorization_service.initialize()
|
||||
with patch.object(AuthorizationHelpers, "parseJWT", return_value=UserProfile()):
|
||||
authorization_service._onAuthStateChanged(MALFORMED_AUTH_RESPONSE)
|
||||
assert authorization_service.getUserProfile() is None
|
Loading…
x
Reference in New Issue
Block a user