Skip to content

Commit 4c41e59

Browse files
author
Mario G.
committed
fix(projects): add project coordinator
1 parent 0b1e856 commit 4c41e59

File tree

18 files changed

+392
-24
lines changed

18 files changed

+392
-24
lines changed

src/liceo/infra/adapters/migrations/20250915_initial_migration.sql

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,39 @@ CREATE TABLE IF NOT EXISTS liceo_projects (
196196
last_updated_by TEXT,
197197
CONSTRAINT fk_projects_created_by FOREIGN KEY (created_by) REFERENCES liceo_users(id),
198198
CONSTRAINT fk_projects_last_updated_by FOREIGN KEY (last_updated_by) REFERENCES liceo_users(id)
199-
)
199+
);
200+
201+
CREATE TABLE IF NOT EXISTS liceo_projects_members (
202+
id TEXT PRIMARY KEY,
203+
version INTEGER NOT NULL,
204+
project_id TEXT NOT NULL,
205+
person_id TEXT NOT NULL,
206+
created_at TIMESTAMP NOT NULL,
207+
created_by TEXT NOT NULL,
208+
last_updated_at TIMESTAMP,
209+
last_updated_by TEXT,
210+
UNIQUE("project_id", "person_id"),
211+
CONSTRAINT fk_projects_members_project_id FOREIGN KEY (project_id) REFERENCES liceo_projects(id),
212+
CONSTRAINT fk_projects_members_person_id FOREIGN KEY (person_id) REFERENCES liceo_people(id),
213+
CONSTRAINT fk_projects_members_created_by FOREIGN KEY (created_by) REFERENCES liceo_users(id),
214+
CONSTRAINT fk_projects_members_last_updated_by FOREIGN KEY (last_updated_by) REFERENCES liceo_users(id)
215+
);
216+
217+
CREATE TABLE IF NOT EXISTS liceo_projects_coordinators (
218+
id TEXT PRIMARY KEY,
219+
version INTEGER NOT NULL,
220+
project_id TEXT NOT NULL,
221+
user_id TEXT NOT NULL,
222+
is_owner BOOLEAN,
223+
created_at TIMESTAMP NOT NULL,
224+
created_by TEXT NOT NULL,
225+
last_updated_at TIMESTAMP,
226+
last_updated_by TEXT,
227+
UNIQUE("project_id", "user_id"),
228+
CONSTRAINT fk_projects_coordinators_project_id FOREIGN KEY (project_id) REFERENCES liceo_projects(id),
229+
CONSTRAINT fk_projects_coordinators_user_id FOREIGN KEY (user_id) REFERENCES liceo_users(id),
230+
CONSTRAINT fk_projects_coordinators_created_by FOREIGN KEY (created_by) REFERENCES liceo_users(id),
231+
CONSTRAINT fk_projects_coordinators_last_updated_by FOREIGN KEY (last_updated_by) REFERENCES liceo_users(id)
232+
);
233+
234+
CREATE UNIQUE INDEX liceo_projects_coordinators_only_one_owner ON liceo_projects_coordinators (is_owner, project_id) WHERE is_owner = TRUE;

src/liceo/infra/adapters/migrations/20260103_initial_data.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ INSERT INTO liceo_permissions (id, name, description) VALUES ('33Eccx9gpXqbE9mCB
9797
INSERT INTO liceo_permissions (id, name, description) VALUES ('mgewHChKbk5rAbbc5fAnQU', 'PROJECTS_CREATE', 'allows to create projects');
9898
INSERT INTO liceo_permissions (id, name, description) VALUES ('LGoiyMGdNGPFXhzVP7MXnf', 'PROJECTS_LIST', 'allows to list or filter projects');
9999
INSERT INTO liceo_permissions (id, name, description) VALUES ('KrMBkC2Qg5aBFo2GoTMe7z', 'PROJECTS_ADD_MEMBERSHIP', 'allows to add a person to the project');
100+
INSERT INTO liceo_permissions (id, name, description) VALUES ('8fvxbEm9GZ96gAmF67EExy', 'PROJECTS_ADD_COORDINATOR', 'allows to add a coordinator to the project');
101+
INSERT INTO liceo_permissions (id, name, description) VALUES ('JEaWqpg6guams3awBJ2QTb', 'PROJECTS_QUERY_COORDINATORS_BY_PROJECTS', 'allows to find coordinators in the projects passed');
100102

101103

102104
-- #######################
@@ -188,6 +190,10 @@ INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeG
188190
INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeGLfmboVS7PkXC', 'LGoiyMGdNGPFXhzVP7MXnf');
189191
-- ADMIN/PROJECTS_ADD_MEMBERSHIP
190192
INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeGLfmboVS7PkXC', 'KrMBkC2Qg5aBFo2GoTMe7z');
193+
-- ADMIN/PROJECTS_ADD_COORDINATOR
194+
INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeGLfmboVS7PkXC', '8fvxbEm9GZ96gAmF67EExy');
195+
-- ADMIN/PROJECTS_QUERY_COORDINATORS_BY_PROJECTS
196+
INSERT INTO liceo_roles_permissions (role_id, permission_id) VALUES ('rDCJKyPVeGLfmboVS7PkXC', 'JEaWqpg6guams3awBJ2QTb');
191197

192198

193199
-- #######################

src/liceo/labs/db/sql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def wrapper(self: SQLRepository, *args: Any, **kwargs: Any) -> A:
144144
return mapper(all)
145145

146146
if func.__name__.startswith("find_") or func.__name__.startswith("insert_"):
147-
logger.debug("executing find___ query")
147+
logger.debug(f"executing find___ query: {func.__name__}")
148148
one = self._get_connection().one(sql=sql_content, params=kwargs)
149149
return mapper(one)
150150

src/liceo/people/projects/adapters/di.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from typing import Annotated
22
from fastapi import Depends, Body, Query, Path
3+
from liceo.security.users.adapters.di import UsersServiceDependency
34
from liceo.infra.adapters.di import ConnectionFactoryDependency, ConnectionManagerDependency, TransactionManagerDependency, EventStoreDependency
45
from liceo.security.common.adapters.di import UserInfo
5-
from ..application.repository import ProjectRepository, ProjectMemberRepository
6+
from ..application.repository import ProjectRepository, ProjectMemberRepository, ProjectCoordinatorRepository
67
from ..application.service import ProjectService
7-
from .repository import SQLProjectRepository, SQLProjectMemberRepository
8+
from .repository import SQLProjectRepository, SQLProjectMemberRepository, SQLProjectCoordinatorRepository
89
from .service import DatabaseAwareProjectService
9-
from .requests import CreateProjectRequest, CreateProjectDetails, ListProjectsRequest, AddMemberToProjectRequest, AddMemberToProjectPath
10+
from .requests import CreateProjectRequest, CreateProjectDetails, ListProjectsRequest, AddMemberToProjectRequest, AddMemberToProjectPath, AddCoordinatorRequest, AddCoordinatorPayload, FindAllCoordinatorsByProjectsRequest
1011

1112
# ------ REPOSITORIES
1213

@@ -27,19 +28,30 @@ def create_project_members_repository(factory: ConnectionFactoryDependency) -> P
2728
create_project_members_repository)]
2829

2930

31+
def create_project_coordinators_repository(factory: ConnectionFactoryDependency) -> ProjectCoordinatorRepository:
32+
return SQLProjectCoordinatorRepository(factory=factory)
33+
34+
35+
ProjectCoordinatorRepositoryDependency = Annotated[ProjectCoordinatorRepository, Depends(
36+
create_project_coordinators_repository)]
37+
3038
# ------ SERVICES
3139

3240

3341
def create_project_service(
3442
projects: ProjectRepositoryDependency,
43+
users: UsersServiceDependency,
3544
project_members: ProjectMemberRepositoryDependency,
45+
project_coordinators: ProjectCoordinatorRepositoryDependency,
3646
transaction_manager_factory: TransactionManagerDependency,
3747
connection_manager_factory: ConnectionManagerDependency,
3848
event_store: EventStoreDependency
3949
) -> ProjectService:
4050
return DatabaseAwareProjectService(
4151
projects=projects,
52+
users=users,
4253
project_members=project_members,
54+
project_coordinators=project_coordinators,
4355
transaction_manager_factory=transaction_manager_factory,
4456
connection_manager_factory=connection_manager_factory,
4557
event_store=event_store
@@ -74,3 +86,26 @@ def created_add_project_member_request(
7486

7587
AddMemberToProjectRequestDependency = Annotated[AddMemberToProjectRequest, Depends(
7688
created_add_project_member_request)]
89+
90+
91+
def create_add_cordinator_payload(
92+
id: str = Path(),
93+
user_id: str = Path()
94+
):
95+
return AddCoordinatorPayload(id=id, user_id=user_id)
96+
97+
98+
def create_add_coordinator_request(
99+
user_info: UserInfo,
100+
is_owner: bool = Body(),
101+
payload: AddCoordinatorPayload = Depends(create_add_cordinator_payload)
102+
):
103+
return AddCoordinatorRequest(added_by=user_info, details=payload, is_owner=is_owner)
104+
105+
106+
AddCoordinatorRequestDependency = Annotated[AddCoordinatorRequest, Depends(
107+
create_add_coordinator_request)]
108+
109+
110+
FindAllCoordinatorsByProjectsRequestDependency = Annotated[FindAllCoordinatorsByProjectsRequest, Body(
111+
)]

src/liceo/people/projects/adapters/endpoints.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def create(
3939

4040

4141
@router.post(
42-
path="/{id}/membership/{person_id}",
42+
path="/{id}/memberships/{person_id}",
4343
summary="Adds a person membership to the project",
4444
dependencies=[has_permission(permissions.PROJECTS_ADD_MEMBERSHIP)],
4545
openapi_extra={**open_api_permissions([permissions.PROJECTS_ADD_MEMBERSHIP])}
@@ -49,3 +49,32 @@ def add_member(
4949
service: di.ProjectServiceDependency
5050
) -> responses.ProjectMembershipResponse | None:
5151
return responses.ProjectMembershipResponse.from_project_membership(service.add_new_member(request.to_dto()))
52+
53+
54+
@router.post(
55+
path="/{id}/coordinators/{user_id}",
56+
summary="Adds a coordinator to the project",
57+
dependencies=[has_permission(permissions.PROJECTS_ADD_COORDINATOR)],
58+
openapi_extra={**open_api_permissions([permissions.PROJECTS_ADD_COORDINATOR])}
59+
)
60+
def add_coordinator(
61+
request: di.AddCoordinatorRequestDependency,
62+
service: di.ProjectServiceDependency
63+
) -> responses.ProjectCoordinatorResponse | None:
64+
return responses.ProjectCoordinatorResponse.from_coordinator(service.add_coordinator(request.to_dto()))
65+
66+
67+
@router.post(
68+
path="/query/coordinators-by-projects",
69+
summary="Query to get a distinct limited number of coordinators in the projects passed",
70+
dependencies=[has_permission(permissions.PROJECTS_QUERY_COORDINATORS_BY_PROJECTS)],
71+
openapi_extra={
72+
**open_api_permissions([permissions.PROJECTS_QUERY_COORDINATORS_BY_PROJECTS])}
73+
)
74+
def find_all_coordinators_by_project_ids(
75+
request: di.FindAllCoordinatorsByProjectsRequestDependency,
76+
service: di.ProjectServiceDependency
77+
) -> responses.ListProjectsCoordinatorsResponse:
78+
return responses.ListProjectsCoordinatorsResponse.from_paged(
79+
service.find_all_coordinators_by_project_ids(request.to_dto())
80+
)

src/liceo/people/projects/adapters/mappers.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,25 @@
22
from ..domain import entities, vo
33

44

5-
def from_row_to_project(row: dict) -> entities.Project:
5+
def from_row_to_project(row: dict) -> entities.Project | None:
6+
print(row)
7+
if not row:
8+
return
69
project = entities.Project(vo.ProjectId(row["id"]))
710
project._version = row["version"]
811
project.name = row["name"]
912
project.description = row["description"]
1013
project.audit = AuditInfo(created_by=vo.UserId(row["created_by"]))
1114
project.audit.created_at = row["created_at"]
1215
return project
16+
17+
18+
def from_row_to_project_coordinator(row: dict) -> entities.ProjectCoordinator:
19+
coordinator = entities.ProjectCoordinator(vo.ProjectCoordinatorId(row["id"]))
20+
coordinator._version = row["version"]
21+
coordinator.project = vo.ProjectId(row["project_id"])
22+
coordinator.user = vo.UserId(row["user_id"])
23+
coordinator.is_owner = row["is_owner"]
24+
coordinator.audit = AuditInfo(created_by=vo.UserId(row["created_by"]))
25+
coordinator.audit.created_at = row["created_at"]
26+
return coordinator
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
PROJECTS_CREATE: str = "PROJECTS_CREATE"
22
PROJECTS_LIST: str = "PROJECTS_LIST"
33
PROJECTS_ADD_MEMBERSHIP: str = "PROJECTS_ADD_MEMBERSHIP"
4+
PROJECTS_ADD_COORDINATOR: str = "PROJECTS_ADD_COORDINATOR"
5+
PROJECTS_QUERY_COORDINATORS_BY_PROJECTS: str = "PROJECTS_QUERY_COORDINATORS_BY_PROJECTS"
6+
# PROJECTS_LIST_COORDINATORS: str = "PROJECTS_LIST_COORDINATORS"
7+
# PROJECTS_LIST_MEMBERSHIPS: str = "PROJECTS_LIST_MEMBERSHIPS"

src/liceo/people/projects/adapters/repository.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from shortuuid import uuid
22
from typing import override
33
from liceo.infra.domain.vo import Paged, Pagination
4-
from liceo.people.projects.domain.entities import Project, ProjectMembership
4+
from liceo.people.projects.domain.entities import Project, ProjectCoordinator, ProjectMembership
55
from liceo.labs.db.sql import SQLRepository, sql
6-
from liceo.people.projects.domain.vo import ProjectId, ProjectMembershipId
7-
from ..application.repository import ProjectRepository, ProjectMemberRepository
8-
from .mappers import from_row_to_project
6+
from liceo.people.projects.domain.vo import ProjectCoordinatorId, ProjectId, ProjectMembershipId
7+
from ..application.repository import ProjectRepository, ProjectMemberRepository, ProjectCoordinatorRepository
8+
from . import mappers
99

1010

1111
class SQLProjectRepository(ProjectRepository, SQLRepository):
1212
@override
1313
def generate_id(self) -> ProjectId:
1414
return ProjectId(uuid())
1515

16-
@sql(from_row_to_project)
16+
@sql(mappers.from_row_to_project)
1717
def find_project_by_id(self, id: str) -> Project | None:
1818
return None
1919

@@ -47,19 +47,20 @@ def filter_projects_by_name(self, name: str | None, pagination: Pagination) -> P
4747
self.sql_optimize_params(sql, params),
4848
params
4949
)
50+
transformed = list(map(mappers.from_row_to_project, result))
5051
return Paged(
5152
total=result[0]["total_count"] if result else 0,
52-
data=list(map(from_row_to_project, result))
53+
data=[t for t in transformed if t]
5354
)
5455

5556

5657
class SQLProjectMemberRepository(ProjectMemberRepository, SQLRepository):
5758
def generate_id(self) -> ProjectMembershipId:
5859
return ProjectMembershipId(uuid())
5960

60-
def save_membership(self, membership: ProjectMembership) -> ProjectMembership:
61+
def save_project_membership(self, membership: ProjectMembership) -> ProjectMembership:
6162
self._get_connection().execute(
62-
sql=self.resolve_sql(self.save_membership),
63+
sql=self.resolve_sql(self.save_project_membership),
6364
params={
6465
"id": membership.id,
6566
"version": membership._version,
@@ -70,3 +71,45 @@ def save_membership(self, membership: ProjectMembership) -> ProjectMembership:
7071
}
7172
)
7273
return membership
74+
75+
76+
class SQLProjectCoordinatorRepository(ProjectCoordinatorRepository, SQLRepository):
77+
def generate_id(self) -> ProjectCoordinatorId:
78+
return ProjectCoordinatorId(uuid())
79+
80+
def save_project_coordinator(self, project_coordinator: ProjectCoordinator) -> ProjectCoordinator | None:
81+
self._get_connection().execute(
82+
sql=self.resolve_sql(self.save_project_coordinator),
83+
params={
84+
"id": project_coordinator.id.id,
85+
"version": project_coordinator._version,
86+
"project_id": project_coordinator.project.id,
87+
"user_id": project_coordinator.user.id,
88+
"is_owner": project_coordinator.is_owner,
89+
"created_by": project_coordinator.created_by.id,
90+
"created_at": project_coordinator.created_at
91+
}
92+
)
93+
return project_coordinator
94+
95+
def find_all_coordinators_by_project_ids(self, project_ids: list[str]) -> Paged[ProjectCoordinator]:
96+
sql = self.resolve_sql(self.find_all_coordinators_by_project_ids)
97+
rows = self._get_connection().all(
98+
sql=sql,
99+
params={
100+
"ids": project_ids
101+
}
102+
)
103+
104+
if not rows or len(rows) <= 0:
105+
return Paged.empty()
106+
107+
return Paged(
108+
total=rows[0]["total_count"],
109+
data=list(
110+
map(
111+
mappers.from_row_to_project_coordinator,
112+
rows
113+
)
114+
)
115+
)

src/liceo/people/projects/adapters/requests.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,29 @@ def to_dto(self) -> dtos.AddMemberToProjectDTO:
4949
person_id=self.details.person_id,
5050
created_by=self.created_by.id
5151
)
52+
53+
54+
class AddCoordinatorPayload(BaseModel):
55+
id: str
56+
user_id: str
57+
58+
59+
class AddCoordinatorRequest(BaseModel):
60+
added_by: UserContextModel
61+
is_owner: bool = Field(default=False)
62+
details: AddCoordinatorPayload
63+
64+
def to_dto(self) -> dtos.AddCoordinatorToProjectDTO:
65+
return dtos.AddCoordinatorToProjectDTO(
66+
user_id=self.details.user_id,
67+
project_id=self.details.id,
68+
added_by=self.added_by.id,
69+
is_owner=self.is_owner
70+
)
71+
72+
73+
class FindAllCoordinatorsByProjectsRequest(BaseModel):
74+
projects: list[str]
75+
76+
def to_dto(self) -> dtos.FindAllCoordinatorsByProjectIdsDTO:
77+
return dtos.FindAllCoordinatorsByProjectIdsDTO(projects=self.projects)

src/liceo/people/projects/adapters/responses.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pydantic import BaseModel
22
from liceo.infra.domain.vo import Paged
3-
from ..domain.entities import Project, ProjectMembership
3+
from ..domain.entities import Project, ProjectMembership, ProjectCoordinator
44

55

66
class MinimalProjectResponse(BaseModel):
@@ -56,3 +56,35 @@ def from_project_membership(membership: ProjectMembership | None):
5656
person_id=membership.person.id,
5757
project_id=membership.project.id
5858
)
59+
60+
61+
class ProjectCoordinatorResponse(BaseModel):
62+
id: str
63+
version: int
64+
user: str
65+
project: str
66+
is_owner: bool
67+
68+
@staticmethod
69+
def from_coordinator(coordinator: ProjectCoordinator | None):
70+
if coordinator:
71+
return ProjectCoordinatorResponse(
72+
id=coordinator.id.id,
73+
version=coordinator._version,
74+
user=coordinator.user.id,
75+
project=coordinator.project.id,
76+
is_owner=coordinator.is_owner
77+
)
78+
79+
80+
class ListProjectsCoordinatorsResponse(BaseModel):
81+
data: list[ProjectCoordinatorResponse]
82+
total: int
83+
84+
@staticmethod
85+
def from_paged(paged: Paged[ProjectCoordinator]):
86+
coordinators = map(ProjectCoordinatorResponse.from_coordinator, paged.data)
87+
return ListProjectsCoordinatorsResponse(
88+
data=[c for c in coordinators if c],
89+
total=paged.total
90+
)

0 commit comments

Comments
 (0)