From c78618bc157a56ace65f6363453139a12d50fa15 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Fri, 9 Jul 2021 21:06:49 +0200 Subject: [PATCH] Digital Library: Allow 'personal users' to DL. Users with an account and an UM printer should have some basic access to the Digital Library. To this end, and to remain future proof, the online team has made an extension to its API so now feature budgets can be gauge. At the moment it's only checked wether the user has any access to personal projects at all. If so, the interface shows Digital Library functionality. Known issue: Removing the last printer from DF while still logged in leaves the DL access in the Cura interface until logged out or Cura restarted. Additionally, I think the response for a logged in user without any printer from the API is just 'data = empty list' instead of everything set to False and 0 (which should be the case as they're all listed as required fields in their docs ... maybe I'm missing something). In any case, the code as is now can handle that as well. CURA-8138 --- .../src/DigitalFactoryApiClient.py | 21 +++++++++ .../src/DigitalFactoryController.py | 18 +++++++- .../DigitalFactoryFeatureBudgetResponse.py | 43 +++++++++++++++++++ .../src/DigitalFactoryFileProvider.py | 4 +- .../src/DigitalFactoryOutputDevice.py | 4 +- 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index b0e34adaba..0140d9858d 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -22,6 +22,7 @@ from .DFFileUploader import DFFileUploader from .DFLibraryFileUploadRequest import DFLibraryFileUploadRequest from .DFLibraryFileUploadResponse import DFLibraryFileUploadResponse from .DFPrintJobUploadRequest import DFPrintJobUploadRequest +from .DigitalFactoryFeatureBudgetResponse import DigitalFactoryFeatureBudgetResponse from .DigitalFactoryFileResponse import DigitalFactoryFileResponse from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse from .PaginationLinks import PaginationLinks @@ -57,6 +58,26 @@ class DigitalFactoryApiClient: self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager] + def checkUserHasAccess(self, callback: Callable) -> bool: + """Checks if the user has any sort of access to the digital library. + A user is considered to have access if the max-# of private projects is greater then 0 (or -1 for unlimited). + """ + + def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None: + if response and isinstance(response, DigitalFactoryFeatureBudgetResponse): + callback( + response.library_max_private_projects == -1 or # Note: -1 is unlimited + response.library_max_private_projects > 0) + else: + Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}") + callback(False) + + self._http.get(f"{self.CURA_API_ROOT}/feature_budgets", + scope = self._scope, + callback = self._parseCallback(callbackWrap, DigitalFactoryFeatureBudgetResponse, callbackWrap), + error_callback = callbackWrap, + timeout = self.DEFAULT_REQUEST_TIMEOUT) + def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None: """ Retrieves a digital factory project by its library project id. diff --git a/plugins/DigitalLibrary/src/DigitalFactoryController.py b/plugins/DigitalLibrary/src/DigitalFactoryController.py index 352a8c70f2..368b29219a 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryController.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryController.py @@ -89,6 +89,9 @@ class DigitalFactoryController(QObject): uploadFileError = Signal() uploadFileFinished = Signal() + """Signal to inform about the state of user access.""" + userAccessStateChanged = pyqtSignal(bool) + def __init__(self, application: CuraApplication) -> None: super().__init__(parent = None) @@ -106,6 +109,7 @@ class DigitalFactoryController(QObject): self._has_more_projects_to_load = False self._account = self._application.getInstance().getCuraAPI().account # type: Account + self._account.loginStateChanged.connect(self._onLoginStateChanged) self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation() # Initialize the project model @@ -131,6 +135,8 @@ class DigitalFactoryController(QObject): self._application.engineCreatedSignal.connect(self._onEngineCreated) self._application.initializationFinished.connect(self._applicationInitializationFinished) + self._user_has_access = False + def clear(self) -> None: self._project_model.clearProjects() self._api.clear() @@ -143,16 +149,24 @@ class DigitalFactoryController(QObject): self.setSelectedProjectIndex(-1) + def _onLoginStateChanged(self, logged_in: bool) -> None: + def callback(has_access, **kwargs): + self._user_has_access = has_access + self.userAccessStateChanged.emit(logged_in) + + self._api.checkUserHasAccess(callback) + def userAccountHasLibraryAccess(self) -> bool: """ Checks whether the currently logged in user account has access to the Digital Library :return: True if the user account has Digital Library access, else False """ - subscriptions = [] # type: List[Dict[str, Any]] if self._account.userProfile: subscriptions = self._account.userProfile.get("subscriptions", []) - return len(subscriptions) > 0 + if len(subscriptions) > 0: + return True + return self._user_has_access def initialize(self, preselected_project_id: Optional[str] = None) -> None: self.clear() diff --git a/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py b/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py new file mode 100644 index 0000000000..016306a478 --- /dev/null +++ b/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from .BaseModel import BaseModel +from typing import Optional + + +class DigitalFactoryFeatureBudgetResponse(BaseModel): + """Class representing the capabilities of a user account for Digital Library. + NOTE: For each max_..._projects fields, '-1' means unlimited! + """ + + def __init__(self, + library_can_use_business_value: Optional[bool] = False, + library_can_use_comments: Optional[bool] = False, + library_can_use_status: Optional[bool] = False, + library_can_use_tags: Optional[bool] = False, + library_can_use_technical_requirements: Optional[bool] = False, + library_max_organization_shared_projects: Optional[int] = False, # -1 means unlimited + library_max_private_projects: Optional[int] = False, # -1 means unlimited + library_max_team_shared_projects: Optional[int] = False, # -1 means unlimited + **kwargs) -> None: + + self.library_can_use_business_value = library_can_use_business_value + self.library_can_use_comments = library_can_use_comments + self.library_can_use_status = library_can_use_status + self.library_can_use_tags = library_can_use_tags + self.library_can_use_technical_requirements = library_can_use_technical_requirements + self.library_max_organization_shared_projects = library_max_organization_shared_projects # -1 means unlimited + self.library_max_private_projects = library_max_private_projects # -1 means unlimited + self.library_max_team_shared_projects = library_max_team_shared_projects # -1 means unlimited + super().__init__(**kwargs) + + def __repr__(self) -> str: + return "max private: {}, max org: {}, max team: {}".format( + self.library_max_private_projects, + self.library_max_organization_shared_projects, + self.library_max_team_shared_projects) + + # Validates the model, raising an exception if the model is invalid. + def validate(self) -> None: + super().validate() + # No validation for now, as the response can be "data: []", which should be interpreted as all False and 0's diff --git a/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py b/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py index 7a544afaa1..65a727e21a 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py @@ -22,7 +22,7 @@ class DigitalFactoryFileProvider(FileProvider): self._dialog = None self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account - self._account.loginStateChanged.connect(self._onLoginStateChanged) + self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged) self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess() self.priority = 10 @@ -53,7 +53,7 @@ class DigitalFactoryFileProvider(FileProvider): if not self._dialog: Logger.log("e", "Unable to create the Digital Library Open dialog.") - def _onLoginStateChanged(self, logged_in: bool) -> None: + def _onUserAccessStateChanged(self, logged_in: bool) -> None: """ Sets the enabled status of the DigitalFactoryFileProvider according to the account's login status :param logged_in: The new login status diff --git a/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py b/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py index 202223f9b4..70e3ac34f2 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py @@ -45,7 +45,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice): self._writing = False self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account - self._account.loginStateChanged.connect(self._onLoginStateChanged) + self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged) self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess() self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation() @@ -97,7 +97,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice): if not self._dialog: Logger.log("e", "Unable to create the Digital Library Save dialog.") - def _onLoginStateChanged(self, logged_in: bool) -> None: + def _onUserAccessStateChanged(self, logged_in: bool) -> None: """ Sets the enabled status of the DigitalFactoryOutputDevice according to the account's login status :param logged_in: The new login status