diff --git a/apiserver/app.py b/apiserver/app.py index 5c9ae4454..e310eee9f 100644 --- a/apiserver/app.py +++ b/apiserver/app.py @@ -11,6 +11,7 @@ from dora.api.incidents import app as incidents_api from dora.api.integrations import app as integrations_api from dora.api.deployment_analytics import app as deployment_analytics_api +from dora.api.teams import app as teams_api from dora.api.sync import app as sync_api from dora.store.initialise_db import initialize_database @@ -23,6 +24,7 @@ app.register_blueprint(incidents_api) app.register_blueprint(deployment_analytics_api) app.register_blueprint(integrations_api) +app.register_blueprint(teams_api) app.register_blueprint(sync_api) configure_db_with_app(app) diff --git a/apiserver/dora/api/resources/core_resources.py b/apiserver/dora/api/resources/core_resources.py index 1a14e7d15..63330887d 100644 --- a/apiserver/dora/api/resources/core_resources.py +++ b/apiserver/dora/api/resources/core_resources.py @@ -1,4 +1,5 @@ from typing import Dict +from dora.store.models.core.teams import Team from dora.store.models import Users @@ -19,3 +20,14 @@ def adapt_user_info( "avatar_url": username_user_map[author].avatar_url, }, } + + +def adapt_team(team: Team): + return { + "id": str(team.id), + "org_id": str(team.org_id), + "name": team.name, + "member_ids": [str(member_id) for member_id in team.member_ids], + "created_at": team.created_at.isoformat(), + "updated_at": team.updated_at.isoformat(), + } diff --git a/apiserver/dora/api/teams.py b/apiserver/dora/api/teams.py new file mode 100644 index 000000000..b2b484dc5 --- /dev/null +++ b/apiserver/dora/api/teams.py @@ -0,0 +1,79 @@ +from flask import Blueprint +from typing import List +from voluptuous import Required, Schema, Optional +from dora.api.resources.core_resources import adapt_team +from dora.store.models.core.teams import Team +from dora.service.core.teams import get_team_service + +from dora.api.request_utils import dataschema +from dora.service.query_validator import get_query_validator + +app = Blueprint("teams", __name__) + + +@app.route("/team/", methods={"GET"}) +def fetch_team(team_id): + + query_validator = get_query_validator() + team: Team = query_validator.team_validator(team_id) + + return adapt_team(team) + + +@app.route("/team/", methods={"PATCH"}) +@dataschema( + Schema( + { + Optional("name"): str, + Optional("member_ids"): list, + } + ), +) +def update_team_patch(team_id: str, name: str = None, member_ids: List[str] = None): + + query_validator = get_query_validator() + team: Team = query_validator.team_validator(team_id) + + if member_ids: + query_validator.users_validator(member_ids) + + team_service = get_team_service() + + team: Team = team_service.update_team(team_id, name, member_ids) + + return adapt_team(team) + + +@app.route("/org//team", methods={"POST"}) +@dataschema( + Schema( + { + Required("name"): str, + Required("member_ids"): list, + } + ), +) +def create_team(org_id: str, name: str, member_ids: List[str]): + + query_validator = get_query_validator() + query_validator.org_validator(org_id) + query_validator.users_validator(member_ids) + + team_service = get_team_service() + + team: Team = team_service.create_team(org_id, name, member_ids) + + return adapt_team(team) + + +@app.route("/team/", methods={"DELETE"}) +def delete_team(team_id: str): + + query_validator = get_query_validator() + team: Team = query_validator.team_validator(team_id) + + team_service = get_team_service() + + team = team_service.delete_team(team_id) + + return adapt_team(team) diff --git a/apiserver/dora/service/core/teams.py b/apiserver/dora/service/core/teams.py new file mode 100644 index 000000000..c9b233298 --- /dev/null +++ b/apiserver/dora/service/core/teams.py @@ -0,0 +1,35 @@ +from typing import List, Optional +from dora.store.models.core.teams import Team +from dora.store.repos.core import CoreRepoService + + +class TeamService: + def __init__(self, core_repo_service: CoreRepoService): + self._core_repo_service = core_repo_service + + def get_team(self, team_id: str) -> Optional[Team]: + return self._core_repo_service.get_team(team_id) + + def delete_team(self, team_id: str) -> Optional[Team]: + return self._core_repo_service.delete_team(team_id) + + def create_team(self, org_id: str, name: str, member_ids: List[str] = None) -> Team: + return self._core_repo_service.create_team(org_id, name, member_ids or []) + + def update_team( + self, team_id: str, name: str = None, member_ids: List[str] = None + ) -> Team: + + team = self._core_repo_service.get_team(team_id) + + if name is not None: + team.name = name + + if member_ids is not None: + team.member_ids = member_ids + + return self._core_repo_service.update_team(team) + + +def get_team_service(): + return TeamService(CoreRepoService()) diff --git a/apiserver/dora/service/query_validator.py b/apiserver/dora/service/query_validator.py index b49733d7b..1f6834bc6 100644 --- a/apiserver/dora/service/query_validator.py +++ b/apiserver/dora/service/query_validator.py @@ -61,6 +61,17 @@ def user_validator(self, user_id: str) -> Users: raise NotFound(f"User {user_id} not found") return user + def users_validator(self, user_ids: List[str]) -> List[Users]: + users: List[Users] = self.repo_service.get_users(user_ids) + + if len(users) != len(user_ids): + query_user_ids = set(user_ids) + found_user_ids = set(map(lambda x: str(x.id), users)) + missing_user_ids = query_user_ids - found_user_ids + raise NotFound(f"User(s) not found: {missing_user_ids}") + + return users + def get_query_validator(): return QueryValidator(CoreRepoService()) diff --git a/apiserver/dora/store/models/core/teams.py b/apiserver/dora/store/models/core/teams.py index 7a58b79b4..13884113b 100644 --- a/apiserver/dora/store/models/core/teams.py +++ b/apiserver/dora/store/models/core/teams.py @@ -20,7 +20,7 @@ class Team(db.Model): updated_at = db.Column( db.DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) - is_deleted = db.Column(db.Boolean) + is_deleted = db.Column(db.Boolean, default=False) def __hash__(self): return hash(self.id) diff --git a/apiserver/dora/store/repos/core.py b/apiserver/dora/store/repos/core.py index d26d06008..14d3d0fe5 100644 --- a/apiserver/dora/store/repos/core.py +++ b/apiserver/dora/store/repos/core.py @@ -51,10 +51,38 @@ def delete_team(self, team_id: str): self._db.session.commit() return self._db.session.query(Team).filter(Team.id == team_id).one_or_none() + @rollback_on_exc + def create_team(self, org_id: str, name: str, member_ids: List[str]) -> Team: + team = Team( + name=name, + org_id=org_id, + member_ids=member_ids or [], + is_deleted=False, + ) + self._db.session.add(team) + self._db.session.commit() + + return self.get_team(team.id) + + @rollback_on_exc + def update_team(self, team: Team) -> Team: + self._db.session.merge(team) + self._db.session.commit() + + return self.get_team(team.id) + @rollback_on_exc def get_user(self, user_id) -> Optional[Users]: return self._db.session.query(Users).filter(Users.id == user_id).one_or_none() + @rollback_on_exc + def get_users(self, user_ids: List[str]) -> List[Users]: + return ( + self._db.session.query(Users) + .filter(and_(Users.id.in_(user_ids), Users.is_deleted == False)) + .all() + ) + @rollback_on_exc def get_org_integrations_for_names(self, org_id: str, provider_names: List[str]): return (