From 2d45b8c2cd7ca7e72b44b16c7543299e1688b4b5 Mon Sep 17 00:00:00 2001 From: Konstantinos Karmas Date: Tue, 13 Jul 2021 17:28:38 +0200 Subject: [PATCH] Show an "Upgrade plan" button to users that have reached maximum projects Instead of letting users go through the project creation process only to get rejected with a "subscription limits reached" message, now the "New Library project" button is being replaced with an "Upgrade plan" button when the maximum allowed projects have been reached for the specific amount. The button is accompanied by a tooltip that explains the situation to the user. Once clicked, the user is redirected to the subscriptions page. CURA-8112 --- .../resources/qml/SelectProjectPage.qml | 17 +++++++++++++- .../src/DigitalFactoryApiClient.py | 23 +++++++++++++++++++ .../src/DigitalFactoryController.py | 18 +++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index 2de0e78cc7..0f8544285f 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -18,7 +18,7 @@ Item width: parent.width height: parent.height - property alias createNewProjectButtonVisible: createNewProjectButton.visible + property bool createNewProjectButtonVisible: true anchors { @@ -48,6 +48,7 @@ Item anchors.verticalCenter: selectProjectLabel.verticalCenter anchors.right: parent.right text: "New Library project" + visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) onClicked: { @@ -56,6 +57,20 @@ Item busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress } + + Cura.SecondaryButton + { + id: upgradePlanButton + + anchors.verticalCenter: selectProjectLabel.verticalCenter + anchors.right: parent.right + text: "Upgrade plan" + visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) + tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects." + + onClicked: Qt.openUrlExternally("https://ultimaker.com/software/enterprise-software") + } + Item { id: noLibraryProjectsContainer diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index e1a62fdd5c..acb7b9a5a5 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -55,6 +55,7 @@ class DigitalFactoryApiClient: self._http = HttpRequestManager.getInstance() self._on_error = on_error self._file_uploader = None # type: Optional[DFFileUploader] + self._library_max_private_projects: Optional[int] = None self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager] @@ -69,6 +70,7 @@ class DigitalFactoryApiClient: callback( response.library_max_private_projects == -1 or # Note: -1 is unlimited response.library_max_private_projects > 0) + self._library_max_private_projects = response.library_max_private_projects else: Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}") callback(False) @@ -79,6 +81,27 @@ class DigitalFactoryApiClient: error_callback = callbackWrap, timeout = self.DEFAULT_REQUEST_TIMEOUT) + def checkUserCanCreateNewLibraryProject(self, callback: Callable) -> None: + """ + Checks if the user is allowed to create new library projects. + A user is allowed to create new library projects if the haven't reached their maximum allowed private projects. + """ + + def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None: + if response is not None: + if self._library_max_private_projects == -1 or isinstance(response, DigitalFactoryProjectResponse): + callback(True) + elif isinstance(response, list) and all(isinstance(r, DigitalFactoryProjectResponse) for r in response): + callback(len(response) < self._library_max_private_projects) + else: + Logger.warning(f"Digital Factory: Incorrect response type received when requesting private projects: {str(response)}") + callback(False) + else: + Logger.warning(f"Digital Factory: Response is empty, likely an error: {str(response)}") + callback(False) + + self.getProjectsFirstPage(on_finished = callbackWrap, failed = callbackWrap) + 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 368b29219a..cf4174d7c9 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryController.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryController.py @@ -92,6 +92,9 @@ class DigitalFactoryController(QObject): """Signal to inform about the state of user access.""" userAccessStateChanged = pyqtSignal(bool) + """Signal to inform whether the user is allowed to create more Library projects.""" + userCanCreateNewLibraryProjectChanged = pyqtSignal(bool) + def __init__(self, application: CuraApplication) -> None: super().__init__(parent = None) @@ -136,6 +139,7 @@ class DigitalFactoryController(QObject): self._application.initializationFinished.connect(self._applicationInitializationFinished) self._user_has_access = False + self._user_account_can_create_new_project = False def clear(self) -> None: self._project_model.clearProjects() @@ -166,6 +170,11 @@ class DigitalFactoryController(QObject): subscriptions = self._account.userProfile.get("subscriptions", []) if len(subscriptions) > 0: return True + if self._user_has_access: + # The user has access even though they have no subscriptions. This means they are an Essential user and they + # have limited personal private projects available. In this case, we need to check whether they have already + # reached their limit. + self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject) return self._user_has_access def initialize(self, preselected_project_id: Optional[str] = None) -> None: @@ -517,6 +526,7 @@ class DigitalFactoryController(QObject): self._project_model.clearProjects() self.setSelectedProjectIndex(-1) self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) + self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject) self.setRetrievingProjectsStatus(RetrievalStatus.InProgress) self._has_preselected_project = new_has_preselected_project self.preselectedProjectChanged.emit() @@ -525,6 +535,14 @@ class DigitalFactoryController(QObject): def hasPreselectedProject(self) -> bool: return self._has_preselected_project + def setCanCreateNewLibraryProject(self, can_create_new_library_project: bool) -> None: + self._user_account_can_create_new_project = can_create_new_library_project + self.userCanCreateNewLibraryProjectChanged.emit(self._user_account_can_create_new_project) + + @pyqtProperty(bool, fset = setCanCreateNewLibraryProject, notify = userCanCreateNewLibraryProjectChanged) + def userAccountCanCreateNewLibraryProject(self) -> bool: + return self._user_account_can_create_new_project + @pyqtSlot(str, "QStringList") def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None: """