11"""Mixin extending Kili API Gateway class with Projects related operations."""
2-
2+ import warnings
33
44from kili .adapters .kili_api_gateway .base import BaseOperationMixin
55from kili .adapters .kili_api_gateway .helpers .queries import (
6+ QueryOptions ,
67 fragment_builder ,
78)
9+ from kili .core .graphql .operations .project_user .queries import ProjectUserQuery , ProjectUserWhere
810from kili .domain .project import ProjectId
11+ from kili .domain .types import ListOrTuple
912from kili .exceptions import NotFound
1013
1114from .mappers import project_input_mapper
1215from .operations import (
13- GQL_GET_STEPS ,
16+ get_steps_query ,
1417 get_update_project_workflow_mutation ,
1518)
1619from .types import ProjectWorkflowDataKiliAPIGatewayInput
@@ -37,13 +40,12 @@ def update_project_workflow(
3740 result = self .graphql_client .execute (mutation , variables )
3841 return result ["data" ]
3942
40- def get_steps (
41- self ,
42- project_id : str ,
43- ) -> list [dict ]:
43+ def get_steps (self , project_id : str , fields : ListOrTuple [str ]) -> list [dict ]:
4444 """Get steps in a project workflow."""
45+ fragment = fragment_builder (fields )
46+ query = get_steps_query (fragment )
4547 variables = {"where" : {"id" : project_id }, "first" : 1 , "skip" : 0 }
46- result = self .graphql_client .execute (GQL_GET_STEPS , variables )
48+ result = self .graphql_client .execute (query , variables )
4749 project = result ["data" ]
4850
4951 if len (project ) == 0 :
@@ -57,3 +59,104 @@ def get_steps(
5759 )
5860
5961 return steps
62+
63+ def add_reviewers_to_step (
64+ self , project_id : str , step_name : str , emails : list [str ]
65+ ) -> list [str ]:
66+ """Add reviewers to a specific step."""
67+ existing_members = ProjectUserQuery (self .graphql_client , self .http_client )(
68+ where = ProjectUserWhere (project_id = project_id , status = "ACTIVATED" ),
69+ fields = ["role" , "user.email" , "user.id" , "activated" ],
70+ options = QueryOptions (None ),
71+ )
72+ members_by_email = {m ["user" ]["email" ]: m for m in (existing_members or [])}
73+ assignees_to_add = []
74+ assignees_added = []
75+ assignees_not_added = []
76+ for email in emails :
77+ member = members_by_email .get (email )
78+
79+ if member and member .get ("role" ) != "LABELER" :
80+ user_id = member ["user" ]["id" ]
81+ assignees_to_add .append (user_id )
82+ assignees_added .append (email )
83+ else :
84+ assignees_not_added .append (email )
85+ if assignees_not_added :
86+ warnings .warn (
87+ "These emails were not added (not found or can not review): "
88+ + ", " .join (assignees_not_added )
89+ )
90+ steps = self .get_steps (
91+ project_id , fields = ["steps.id" , "steps.name" , "steps.type" , "steps.assignees.id" ]
92+ )
93+ target_step = next ((s for s in steps if s .get ("name" ) == step_name ), None )
94+ if not target_step :
95+ raise ValueError (f"Step '{ step_name } ' not found in project workflow" )
96+ if target_step .get ("type" ) == "DEFAULT" :
97+ raise ValueError ("The step must be a review step, can't add reviewers to a label step" )
98+ current_ids = [a ["id" ] for a in target_step .get ("assignees" , [])]
99+
100+ merged_ids = list (dict .fromkeys (current_ids + assignees_to_add ))
101+
102+ self .update_project_workflow (
103+ project_id = ProjectId (project_id ),
104+ project_workflow_data = ProjectWorkflowDataKiliAPIGatewayInput (
105+ None , None , [{"id" : target_step ["id" ], "assignees" : merged_ids }], None
106+ ),
107+ )
108+ return assignees_added
109+
110+ def remove_reviewers_from_step (
111+ self , project_id : str , step_name : str , emails : list [str ]
112+ ) -> list [str ]:
113+ """Remove reviewers from a specific step."""
114+ steps = self .get_steps (
115+ project_id ,
116+ fields = [
117+ "steps.id" ,
118+ "steps.name" ,
119+ "steps.type" ,
120+ "steps.assignees.id" ,
121+ "steps.assignees.email" ,
122+ ],
123+ )
124+ target_step = next ((s for s in steps if s .get ("name" ) == step_name ), None )
125+ if not target_step :
126+ raise ValueError (f"Step '{ step_name } ' not found in project workflow" )
127+ if target_step .get ("type" ) == "DEFAULT" :
128+ raise ValueError (
129+ "The step must be a review step, can't remove reviewers from a label step"
130+ )
131+ assignees = target_step .get ("assignees" , [])
132+ email_to_id = {a ["email" ]: a ["id" ] for a in assignees }
133+ removed_emails = []
134+ not_removed_emails = []
135+ ids_to_remove = []
136+ for email in emails :
137+ user_id = email_to_id .get (email )
138+ if not user_id :
139+ not_removed_emails .append (email )
140+ continue
141+ removed_emails .append (email )
142+ ids_to_remove .append (user_id )
143+
144+ if ids_to_remove :
145+ new_assignees_ids = [
146+ a ["id" ] for a in assignees if a .get ("id" ) and a ["id" ] not in ids_to_remove
147+ ]
148+
149+ self .update_project_workflow (
150+ project_id = ProjectId (project_id ),
151+ project_workflow_data = ProjectWorkflowDataKiliAPIGatewayInput (
152+ None , None , [{"id" : target_step ["id" ], "assignees" : new_assignees_ids }], None
153+ ),
154+ )
155+
156+ if not_removed_emails :
157+ warnings .warn (
158+ "These emails were not removed because they are not assigned to this step: "
159+ + ", " .join (not_removed_emails ),
160+ )
161+
162+ return removed_emails
0 commit comments