Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class ProjectPatch(InputSchema):
),
] = None
quality: dict[str, Any] | None = None
template_type: ProjectTemplateType | None = None

def to_domain_model(self) -> dict[str, Any]:
return self.model_dump(exclude_unset=True, by_alias=False)
Expand Down
8 changes: 4 additions & 4 deletions packages/models-library/src/models_library/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from datetime import datetime
from enum import Enum, auto
from enum import Enum
from typing import Annotated, Any, Final, TypeAlias
from uuid import UUID

Expand Down Expand Up @@ -54,9 +54,9 @@ class ProjectType(str, Enum):


class ProjectTemplateType(StrAutoEnum):
TEMPLATE = auto()
TUTORIAL = auto()
HYPERTOOL = auto()
TEMPLATE = "TEMPLATE"
TUTORIAL = "TUTORIAL"
HYPERTOOL = "HYPERTOOL"


class BaseProjectModel(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
""" Facade for webserver service
"""Facade for webserver service

Facade to direct access to models in the database by
the webserver service
Facade to direct access to models in the database by
the webserver service

"""

from .models.api_keys import api_keys
from .models.classifiers import group_classifiers
from .models.comp_pipeline import StateType, comp_pipeline
from .models.comp_tasks import DB_CHANNEL_NAME, NodeClass, comp_tasks
from .models.confirmations import ConfirmationAction, confirmations
from .models.groups import GroupType, groups, user_to_groups
from .models.products import products
from .models.projects import ProjectType, projects
from .models.projects import ProjectTemplateType, ProjectType, projects
from .models.projects_nodes import projects_nodes
from .models.projects_tags import projects_tags
from .models.projects_to_wallet import projects_to_wallet
Expand All @@ -35,6 +36,7 @@
"projects",
"projects_nodes",
"ProjectType",
"ProjectTemplateType",
"scicrunch_resources",
"StateType",
"projects_tags",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3962,6 +3962,14 @@ paths:
schema:
$ref: '#/components/schemas/ProjectTypeAPI'
default: all
- name: template_type
in: query
required: false
schema:
anyOf:
- $ref: '#/components/schemas/ProjectTemplateType'
- type: 'null'
title: Template Type
- name: show_hidden
in: query
required: false
Expand Down Expand Up @@ -14868,6 +14876,10 @@ components:
- type: object
- type: 'null'
title: Quality
templateType:
anyOf:
- $ref: '#/components/schemas/ProjectTemplateType'
- type: 'null'
type: object
title: ProjectPatch
ProjectPermalink:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from ..exceptions import (
ClustersKeeperNotAvailableError,
DefaultPricingUnitNotFoundError,
InsufficientRoleForProjectTemplateTypeUpdateError,
NodeNotFoundError,
ParentNodeNotFoundError,
ProjectDeleteError,
Expand All @@ -40,6 +41,7 @@
ProjectOwnerNotFoundInTheProjectAccessRightsError,
ProjectStartsTooManyDynamicNodesError,
ProjectTooManyProjectOpenedError,
ProjectTypeAndTemplateIncompatibilityError,
ProjectWalletPendingTransactionError,
WrongTagIdsInQueryError,
)
Expand Down Expand Up @@ -88,6 +90,10 @@
status.HTTP_403_FORBIDDEN,
"Do not have sufficient access rights on project {project_uuid} for this action",
),
InsufficientRoleForProjectTemplateTypeUpdateError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"Do not have sufficient access rights on updating project template type",
),
ProjectInvalidUsageError: HttpErrorInfo(
status.HTTP_422_UNPROCESSABLE_ENTITY,
"Invalid usage for project",
Expand Down Expand Up @@ -124,6 +130,10 @@
status.HTTP_400_BAD_REQUEST,
"Wrong tag IDs in query",
),
ProjectTypeAndTemplateIncompatibilityError: HttpErrorInfo(
status.HTTP_400_BAD_REQUEST,
"Wrong project type and template type combination: {reason}",
),
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ async def list_projects(request: web.Request):
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
project_type=query_params.project_type,
template_type=query_params.template_type,
show_hidden=query_params.show_hidden,
trashed=query_params.filters.trashed,
folder_id=query_params.folder_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from models_library.basic_types import IDStr
from models_library.folders import FolderID
from models_library.projects import ProjectID
from models_library.projects import ProjectID, ProjectTemplateType
from models_library.projects_nodes_io import NodeID
from models_library.rest_base import RequestParameters
from models_library.rest_filters import Filters, FiltersQueryParameters
Expand Down Expand Up @@ -136,6 +136,7 @@ class ProjectFilters(Filters):

class ProjectsListExtraQueryParams(RequestParameters):
project_type: Annotated[ProjectTypeAPI, Field(alias="type")] = ProjectTypeAPI.all
template_type: ProjectTemplateType | None = None
show_hidden: Annotated[
bool, Field(description="includes projects marked as hidden in the listing")
] = False
Expand Down Expand Up @@ -167,6 +168,20 @@ def _search_check_empty_string(cls, v):
return None
return v

_template_type_null_or_none_str_to_none_validator = field_validator(
"template_type", mode="before"
)(null_or_none_str_to_none_validator)

@model_validator(mode="after")
def _check_template_type_compatibility(self):
if (
self.project_type in [ProjectTypeAPI.all, ProjectTypeAPI.user]
and self.template_type is not None
):
msg = f"When {self.project_type=} is `all` or `user` the {self.template_type=} should be None"
raise ValueError(msg)
return self

_null_or_none_str_to_none_validator = field_validator("folder_id", mode="before")(
null_or_none_str_to_none_validator
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@

from aiohttp import web
from models_library.folders import FolderID, FolderQuery, FolderScope
from models_library.projects import ProjectID
from models_library.projects import ProjectID, ProjectTemplateType
from models_library.rest_ordering import OrderBy
from models_library.users import UserID
from models_library.workspaces import WorkspaceID, WorkspaceQuery, WorkspaceScope
from pydantic import NonNegativeInt
from servicelib.utils import logged_gather
from simcore_postgres_database.models.projects import ProjectType
from simcore_postgres_database.webserver_models import (
ProjectTemplateType as ProjectTemplateTypeDB,
)
from simcore_postgres_database.webserver_models import ProjectType as ProjectTypeDB

from ..catalog import catalog_service
Expand Down Expand Up @@ -91,6 +94,7 @@ async def list_projects( # pylint: disable=too-many-arguments
folder_id: FolderID | None,
# attrs filter
project_type: ProjectTypeAPI,
template_type: ProjectTemplateType | None,
show_hidden: bool, # NOTE: Be careful, this filters only hidden projects
trashed: bool | None,
# search
Expand Down Expand Up @@ -148,6 +152,9 @@ async def list_projects( # pylint: disable=too-many-arguments
),
# attrs
filter_by_project_type=ProjectTypeAPI.to_project_type_db(project_type),
filter_by_template_type=(
ProjectTemplateTypeDB(template_type) if template_type else None
),
filter_by_services=user_available_services,
filter_trashed=trashed,
filter_hidden=show_hidden,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
from simcore_postgres_database.aiopg_errors import UniqueViolation
from simcore_postgres_database.models.groups import user_to_groups
from simcore_postgres_database.models.project_to_groups import project_to_groups
from simcore_postgres_database.models.projects import ProjectTemplateType
from simcore_postgres_database.models.projects_nodes import projects_nodes
from simcore_postgres_database.models.projects_tags import projects_tags
from simcore_postgres_database.models.projects_to_folders import projects_to_folders
Expand All @@ -58,7 +57,12 @@
ProjectNodeCreate,
ProjectNodesRepo,
)
from simcore_postgres_database.webserver_models import ProjectType, projects, users
from simcore_postgres_database.webserver_models import (
ProjectTemplateType,
ProjectType,
projects,
users,
)
from sqlalchemy import func, literal_column, sql
from sqlalchemy.dialects.postgresql import BOOLEAN, INTEGER
from sqlalchemy.dialects.postgresql import insert as pg_insert
Expand Down Expand Up @@ -583,6 +587,7 @@ def _create_shared_workspace_query(
def _create_attributes_filters(
*,
filter_by_project_type: ProjectType | None,
filter_by_template_type: ProjectTemplateType | None,
filter_hidden: bool | None,
filter_published: bool | None,
filter_trashed: bool | None,
Expand All @@ -595,6 +600,11 @@ def _create_attributes_filters(
if filter_by_project_type is not None:
attributes_filters.append(projects.c.type == filter_by_project_type.value)

if filter_by_template_type is not None:
attributes_filters.append(
projects.c.template_type == filter_by_template_type.value
)

if filter_hidden is not None:
attributes_filters.append(projects.c.hidden.is_(filter_hidden))

Expand Down Expand Up @@ -647,6 +657,7 @@ async def list_projects_dicts( # pylint: disable=too-many-arguments,too-many-st
folder_query: FolderQuery,
# attribute filters
filter_by_project_type: ProjectType | None = None,
filter_by_template_type: ProjectTemplateType | None = None,
filter_by_services: list[dict] | None = None,
filter_published: bool | None = None,
filter_hidden: bool | None = False,
Expand Down Expand Up @@ -694,6 +705,7 @@ async def list_projects_dicts( # pylint: disable=too-many-arguments,too-many-st

attributes_filters = self._create_attributes_filters(
filter_by_project_type=filter_by_project_type,
filter_by_template_type=filter_by_template_type,
filter_hidden=filter_hidden,
filter_published=filter_published,
filter_trashed=filter_trashed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
from .exceptions import (
ClustersKeeperNotAvailableError,
DefaultPricingUnitNotFoundError,
InsufficientRoleForProjectTemplateTypeUpdateError,
InvalidEC2TypeInResourcesSpecsError,
InvalidKeysInResourcesSpecsError,
NodeNotFoundError,
Expand All @@ -150,6 +151,7 @@
ProjectOwnerNotFoundInTheProjectAccessRightsError,
ProjectStartsTooManyDynamicNodesError,
ProjectTooManyProjectOpenedError,
ProjectTypeAndTemplateIncompatibilityError,
)
from .models import ProjectDict, ProjectPatchInternalExtended
from .settings import ProjectsSettings, get_plugin_settings
Expand Down Expand Up @@ -327,7 +329,27 @@ async def patch_project(
if new_prj_access_rights[_prj_owner_primary_group] != _prj_required_permissions:
raise ProjectOwnerNotFoundInTheProjectAccessRightsError

# 4. Patch the project
# 4. If patching template type
if new_template_type := patch_project_data.get("template_type"):
# 4.1 Check if user is a tester
current_user: dict = await get_user(app, user_id)
if UserRole(current_user["role"]) < UserRole.TESTER:
raise InsufficientRoleForProjectTemplateTypeUpdateError
# 4.2 Check the compatibility of the template type with the project
if project_db.type == ProjectType.STANDARD and new_template_type is not None:
raise ProjectTypeAndTemplateIncompatibilityError(
project_uuid=project_uuid,
project_type=project_db.type,
project_template=new_template_type,
)
if project_db.type == ProjectType.TEMPLATE and new_template_type is None:
raise ProjectTypeAndTemplateIncompatibilityError(
project_uuid=project_uuid,
project_type=project_db.type,
project_template=new_template_type,
)

# 5. Patch the project
await _projects_repository.patch_project(
app=app,
project_uuid=project_uuid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ def debug_message(self):
return f"{self.code}: {self}"


class ProjectInvalidUsageError(BaseProjectError):
...
class ProjectInvalidUsageError(BaseProjectError): ...


class ProjectOwnerNotFoundInTheProjectAccessRightsError(BaseProjectError):
Expand Down Expand Up @@ -83,8 +82,17 @@ class ProjectsBatchDeleteError(BaseProjectError):
msg_template = "One or more projects could not be deleted in the batch: {errors}"


class ProjectTrashError(BaseProjectError):
...
class ProjectsPatchError(BaseProjectError): ...


class ProjectTypeAndTemplateIncompatibilityError(ProjectsPatchError):
msg_template = "Patching project '{project_uuid}' type {project_type} and template {project_template} is not allowed"


class InsufficientRoleForProjectTemplateTypeUpdateError(ProjectsPatchError): ...


class ProjectTrashError(BaseProjectError): ...


class ProjectStoppingError(ProjectTrashError):
Expand Down Expand Up @@ -146,12 +154,10 @@ def __init__(self, *, max_num_projects: int, **ctx):
self.max_num_projects = max_num_projects


class PermalinkNotAllowedError(BaseProjectError):
...
class PermalinkNotAllowedError(BaseProjectError): ...


class PermalinkFactoryError(BaseProjectError):
...
class PermalinkFactoryError(BaseProjectError): ...


class ProjectNodeResourcesInvalidError(BaseProjectError):
Expand All @@ -178,12 +184,10 @@ class InvalidEC2TypeInResourcesSpecsError(ProjectNodeResourcesInvalidError):
)


class ProjectNodeResourcesInsufficientRightsError(BaseProjectError):
...
class ProjectNodeResourcesInsufficientRightsError(BaseProjectError): ...


class ProjectNodeRequiredInputsNotSetError(BaseProjectError):
...
class ProjectNodeRequiredInputsNotSetError(BaseProjectError): ...


class ProjectNodeConnectionsMissingError(ProjectNodeRequiredInputsNotSetError):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ async def _list_root_child_projects(
show_hidden=False,
workspace_id=workspace_id,
project_type=ProjectTypeAPI.all,
template_type=None,
folder_id=None,
trashed=None,
offset=page_params.offset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ async def delete_workspace_with_all_content(
show_hidden=False,
workspace_id=workspace_id,
project_type=ProjectTypeAPI.all,
template_type=None,
folder_id=None,
trashed=None,
offset=page_params.offset,
Expand Down
Loading
Loading