mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
Merge branch 'CURA-8112_Inform_the_user_about_their_subscription_limits'
This commit is contained in:
commit
cc82ff97a7
6 changed files with 82 additions and 8 deletions
|
@ -20,7 +20,7 @@ Item
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
property alias createNewProjectButtonVisible: createNewProjectButton.visible
|
property bool createNewProjectButtonVisible: true
|
||||||
|
|
||||||
anchors
|
anchors
|
||||||
{
|
{
|
||||||
|
@ -61,6 +61,7 @@ Item
|
||||||
id: createNewProjectButton
|
id: createNewProjectButton
|
||||||
|
|
||||||
text: "New Library project"
|
text: "New Library project"
|
||||||
|
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
|
||||||
|
|
||||||
onClicked:
|
onClicked:
|
||||||
{
|
{
|
||||||
|
@ -68,6 +69,20 @@ Item
|
||||||
}
|
}
|
||||||
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
|
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Cura.SecondaryButton
|
||||||
|
{
|
||||||
|
id: upgradePlanButton
|
||||||
|
|
||||||
|
text: "Upgrade plan"
|
||||||
|
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||||
|
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."
|
||||||
|
tooltipWidth: parent.width * 0.5
|
||||||
|
|
||||||
|
onClicked: Qt.openUrlExternally("https://ultimaker.com/software/ultimaker-essentials/sign-up-cura?utm_source=cura&utm_medium=software&utm_campaign=lib-max")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item
|
Item
|
||||||
|
|
|
@ -55,6 +55,7 @@ class DigitalFactoryApiClient:
|
||||||
self._http = HttpRequestManager.getInstance()
|
self._http = HttpRequestManager.getInstance()
|
||||||
self._on_error = on_error
|
self._on_error = on_error
|
||||||
self._file_uploader = None # type: Optional[DFFileUploader]
|
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]
|
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(
|
callback(
|
||||||
response.library_max_private_projects == -1 or # Note: -1 is unlimited
|
response.library_max_private_projects == -1 or # Note: -1 is unlimited
|
||||||
response.library_max_private_projects > 0)
|
response.library_max_private_projects > 0)
|
||||||
|
self._library_max_private_projects = response.library_max_private_projects
|
||||||
else:
|
else:
|
||||||
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
|
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
|
||||||
callback(False)
|
callback(False)
|
||||||
|
@ -79,6 +81,41 @@ class DigitalFactoryApiClient:
|
||||||
error_callback = callbackWrap,
|
error_callback = callbackWrap,
|
||||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
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 isinstance(response, DigitalFactoryProjectResponse): # The user has only one private project
|
||||||
|
callback(True)
|
||||||
|
elif isinstance(response, list) and all(isinstance(r, DigitalFactoryProjectResponse) for r in response):
|
||||||
|
callback(len(response) < cast(int, 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)
|
||||||
|
|
||||||
|
if self._library_max_private_projects is not None and self._library_max_private_projects > 0:
|
||||||
|
# The user has a limit in the number of private projects they can create. Check whether they have already
|
||||||
|
# reached that limit.
|
||||||
|
# Note: Set the pagination manager to None when doing this get request, or else the next/previous links
|
||||||
|
# of the pagination will become corrupted
|
||||||
|
url = f"{self.CURA_API_ROOT}/projects?shared=false&limit={self._library_max_private_projects}"
|
||||||
|
self._http.get(url,
|
||||||
|
scope = self._scope,
|
||||||
|
callback = self._parseCallback(callbackWrap, DigitalFactoryProjectResponse, callbackWrap, pagination_manager = None),
|
||||||
|
error_callback = callbackWrap,
|
||||||
|
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
else:
|
||||||
|
# If the limit is -1, then the user is allowed unlimited projects. If its 0 then they are not allowed to
|
||||||
|
# create any projects
|
||||||
|
callback(self._library_max_private_projects == -1)
|
||||||
|
|
||||||
def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None:
|
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.
|
Retrieves a digital factory project by its library project id.
|
||||||
|
|
|
@ -94,6 +94,9 @@ class DigitalFactoryController(QObject):
|
||||||
"""Signal to inform about the state of user access."""
|
"""Signal to inform about the state of user access."""
|
||||||
userAccessStateChanged = pyqtSignal(bool)
|
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:
|
def __init__(self, application: CuraApplication) -> None:
|
||||||
super().__init__(parent = None)
|
super().__init__(parent = None)
|
||||||
|
|
||||||
|
@ -143,6 +146,7 @@ class DigitalFactoryController(QObject):
|
||||||
self._application.initializationFinished.connect(self._applicationInitializationFinished)
|
self._application.initializationFinished.connect(self._applicationInitializationFinished)
|
||||||
|
|
||||||
self._user_has_access = False
|
self._user_has_access = False
|
||||||
|
self._user_account_can_create_new_project = False
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self._project_model.clearProjects()
|
self._project_model.clearProjects()
|
||||||
|
@ -169,10 +173,8 @@ class DigitalFactoryController(QObject):
|
||||||
|
|
||||||
:return: True if the user account has Digital Library access, else False
|
:return: True if the user account has Digital Library access, else False
|
||||||
"""
|
"""
|
||||||
if self._account.userProfile:
|
if self._user_has_access:
|
||||||
subscriptions = self._account.userProfile.get("subscriptions", [])
|
self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
|
||||||
if len(subscriptions) > 0:
|
|
||||||
return True
|
|
||||||
return self._user_has_access
|
return self._user_has_access
|
||||||
|
|
||||||
def initialize(self, preselected_project_id: Optional[str] = None) -> None:
|
def initialize(self, preselected_project_id: Optional[str] = None) -> None:
|
||||||
|
@ -556,6 +558,7 @@ class DigitalFactoryController(QObject):
|
||||||
self._project_model.clearProjects()
|
self._project_model.clearProjects()
|
||||||
self.setSelectedProjectIndex(-1)
|
self.setSelectedProjectIndex(-1)
|
||||||
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
|
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
|
||||||
|
self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
|
||||||
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
|
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
|
||||||
self._has_preselected_project = new_has_preselected_project
|
self._has_preselected_project = new_has_preselected_project
|
||||||
self.preselectedProjectChanged.emit()
|
self.preselectedProjectChanged.emit()
|
||||||
|
@ -564,6 +567,14 @@ class DigitalFactoryController(QObject):
|
||||||
def hasPreselectedProject(self) -> bool:
|
def hasPreselectedProject(self) -> bool:
|
||||||
return self._has_preselected_project
|
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")
|
@pyqtSlot(str, "QStringList")
|
||||||
def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None:
|
def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -16,9 +16,9 @@ class DigitalFactoryFeatureBudgetResponse(BaseModel):
|
||||||
library_can_use_status: Optional[bool] = False,
|
library_can_use_status: Optional[bool] = False,
|
||||||
library_can_use_tags: Optional[bool] = False,
|
library_can_use_tags: Optional[bool] = False,
|
||||||
library_can_use_technical_requirements: Optional[bool] = False,
|
library_can_use_technical_requirements: Optional[bool] = False,
|
||||||
library_max_organization_shared_projects: Optional[int] = False, # -1 means unlimited
|
library_max_organization_shared_projects: Optional[int] = None, # -1 means unlimited
|
||||||
library_max_private_projects: Optional[int] = False, # -1 means unlimited
|
library_max_private_projects: Optional[int] = None, # -1 means unlimited
|
||||||
library_max_team_shared_projects: Optional[int] = False, # -1 means unlimited
|
library_max_team_shared_projects: Optional[int] = None, # -1 means unlimited
|
||||||
**kwargs) -> None:
|
**kwargs) -> None:
|
||||||
|
|
||||||
self.library_can_use_business_value = library_can_use_business_value
|
self.library_can_use_business_value = library_can_use_business_value
|
||||||
|
|
|
@ -18,6 +18,7 @@ Button
|
||||||
property alias textFont: buttonText.font
|
property alias textFont: buttonText.font
|
||||||
property alias cornerRadius: backgroundRect.radius
|
property alias cornerRadius: backgroundRect.radius
|
||||||
property alias tooltip: tooltip.tooltipText
|
property alias tooltip: tooltip.tooltipText
|
||||||
|
property alias tooltipWidth: tooltip.width
|
||||||
|
|
||||||
property color color: UM.Theme.getColor("primary")
|
property color color: UM.Theme.getColor("primary")
|
||||||
property color hoverColor: UM.Theme.getColor("primary_hover")
|
property color hoverColor: UM.Theme.getColor("primary_hover")
|
||||||
|
|
|
@ -417,6 +417,7 @@ UM.MainWindow
|
||||||
Cura.PrimaryButton
|
Cura.PrimaryButton
|
||||||
{
|
{
|
||||||
text: model.name
|
text: model.name
|
||||||
|
iconSource: UM.Theme.getIcon(model.icon)
|
||||||
height: UM.Theme.getSize("message_action_button").height
|
height: UM.Theme.getSize("message_action_button").height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -426,6 +427,7 @@ UM.MainWindow
|
||||||
Cura.SecondaryButton
|
Cura.SecondaryButton
|
||||||
{
|
{
|
||||||
text: model.name
|
text: model.name
|
||||||
|
iconSource: UM.Theme.getIcon(model.icon)
|
||||||
height: UM.Theme.getSize("message_action_button").height
|
height: UM.Theme.getSize("message_action_button").height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,6 +436,14 @@ UM.MainWindow
|
||||||
Cura.TertiaryButton
|
Cura.TertiaryButton
|
||||||
{
|
{
|
||||||
text: model.name
|
text: model.name
|
||||||
|
iconSource:
|
||||||
|
{
|
||||||
|
if (model.icon == null || model.icon == "")
|
||||||
|
{
|
||||||
|
return UM.Theme.getIcon("LinkExternal")
|
||||||
|
}
|
||||||
|
return UM.Theme.getIcon(model.icon)
|
||||||
|
}
|
||||||
height: UM.Theme.getSize("message_action_button").height
|
height: UM.Theme.getSize("message_action_button").height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue