Skip to content

Commit 3d9198a

Browse files
authored
Merge pull request #2022 from kili-technology/feature/lab-4173-aau-i-use-specific-methods-to-manage-reviewers-in-the
fix(LAB-4173): create methods to add or remove reviewers on a step an…
2 parents 3973c3a + 3e233d3 commit 3d9198a

File tree

5 files changed

+224
-28
lines changed

5 files changed

+224
-28
lines changed

src/kili/adapters/kili_api_gateway/project_workflow/operations.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,12 @@ def get_update_project_workflow_mutation(fragment: str) -> str:
1212
"""
1313

1414

15-
GQL_GET_STEPS = """
16-
query getSteps($where: ProjectWhere!, $first: PageSize!, $skip: Int!) {
17-
data: projects(where: $where, first: $first, skip: $skip) {
18-
id
19-
steps {
20-
id
21-
name
22-
type
23-
}
24-
}
25-
}
26-
"""
15+
def get_steps_query(fragment: str) -> str:
16+
"""Return the GraphQL getSteps query."""
17+
return f"""
18+
query getSteps($where: ProjectWhere!, $first: PageSize!, $skip: Int!) {{
19+
data: projects(where: $where, first: $first, skip: $skip) {{
20+
{fragment}
21+
}}
22+
}}
23+
"""

src/kili/adapters/kili_api_gateway/project_workflow/operations_mixin.py

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
"""Mixin extending Kili API Gateway class with Projects related operations."""
2-
2+
import warnings
33

44
from kili.adapters.kili_api_gateway.base import BaseOperationMixin
55
from 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
810
from kili.domain.project import ProjectId
11+
from kili.domain.types import ListOrTuple
912
from kili.exceptions import NotFound
1013

1114
from .mappers import project_input_mapper
1215
from .operations import (
13-
GQL_GET_STEPS,
16+
get_steps_query,
1417
get_update_project_workflow_mutation,
1518
)
1619
from .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

src/kili/domain_api/projects.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@
77

88
from collections.abc import Generator, Iterable, Sequence
99
from functools import cached_property
10-
from typing import (
11-
TYPE_CHECKING,
12-
Any,
13-
Literal,
14-
Optional,
15-
TypedDict,
16-
)
10+
from typing import TYPE_CHECKING, Any, List, Literal, Optional, TypedDict
1711

1812
from typeguard import typechecked
1913
from typing_extensions import deprecated
@@ -285,6 +279,48 @@ def list(self, project_id: str) -> list[dict[str, Any]]:
285279
"""
286280
return self._client.get_steps(project_id=project_id)
287281

282+
@typechecked
283+
def add_reviewers(
284+
self,
285+
project_id: str,
286+
step_name: str,
287+
emails: List[str],
288+
) -> List[str]:
289+
"""Add reviewers to a specific step.
290+
291+
Args:
292+
project_id: Id of the project.
293+
step_name: Name of the step.
294+
emails: List of emails to add.
295+
296+
Returns:
297+
A list with emails added to the step.
298+
"""
299+
return self._client.add_reviewers_to_step(
300+
project_id=project_id, step_name=step_name, emails=emails
301+
)
302+
303+
@typechecked
304+
def remove_reviewers(
305+
self,
306+
project_id: str,
307+
step_name: str,
308+
emails: List[str],
309+
) -> List[str]:
310+
"""Remove reviewers from a specific step.
311+
312+
Args:
313+
project_id: Id of the project.
314+
step_name: Name of the step.
315+
emails: List of emails to remove.
316+
317+
Returns:
318+
A list with emails removed from the step.
319+
"""
320+
return self._client.remove_reviewers_from_step(
321+
project_id=project_id, step_name=step_name, emails=emails
322+
)
323+
288324

289325
class ProjectsNamespace(DomainNamespace):
290326
"""Projects domain namespace providing project-related operations.

src/kili/presentation/client/project_workflow.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from kili.domain.project import ProjectId, WorkflowStepCreate, WorkflowStepUpdate
88
from kili.use_cases.project_workflow import ProjectWorkflowUseCases
99

10+
from ...domain.types import ListOrTuple
1011
from .base import BaseClientMethods
1112

1213

@@ -49,15 +50,60 @@ def update_project_workflow(
4950
def get_steps(
5051
self,
5152
project_id: str,
53+
fields: ListOrTuple[str] = (
54+
"steps.type",
55+
"steps.name",
56+
"steps.id",
57+
"steps.assignees.email",
58+
"steps.assignees.id",
59+
),
5260
) -> list[dict[str, Any]]:
5361
"""Get steps in a project workflow.
5462
5563
Args:
5664
project_id: Id of the project.
65+
fields: All the fields to request among the possible fields for the project.
66+
See the documentation for all possible fields.
5767
5868
Returns:
5969
A dict with the steps of the project workflow.
6070
"""
6171
return ProjectWorkflowUseCases(self.kili_api_gateway).get_steps(
62-
project_id=ProjectId(project_id),
72+
project_id=ProjectId(project_id), fields=fields
73+
)
74+
75+
@typechecked
76+
def add_reviewers_to_step(
77+
self, project_id: str, step_name: str, emails: list[str]
78+
) -> list[str]:
79+
"""Add reviewers to a specific step.
80+
81+
Args:
82+
project_id: Id of the project.
83+
step_name: Name of the step.
84+
emails: List of emails to add.
85+
86+
Returns:
87+
A list with the added emails.
88+
"""
89+
return ProjectWorkflowUseCases(self.kili_api_gateway).add_reviewers_to_step(
90+
project_id=project_id, step_name=step_name, emails=emails
91+
)
92+
93+
@typechecked
94+
def remove_reviewers_from_step(
95+
self, project_id: str, step_name: str, emails: list[str]
96+
) -> list[str]:
97+
"""Remove reviewers from a specific step.
98+
99+
Args:
100+
project_id: Id of the project.
101+
step_name: Name of the step.
102+
emails: List of emails to remove.
103+
104+
Returns:
105+
A list with the removed emails.
106+
"""
107+
return ProjectWorkflowUseCases(self.kili_api_gateway).remove_reviewers_from_step(
108+
project_id=project_id, step_name=step_name, emails=emails
63109
)

src/kili/use_cases/project_workflow/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
ProjectWorkflowDataKiliAPIGatewayInput,
77
)
88
from kili.domain.project import ProjectId, WorkflowStepCreate, WorkflowStepUpdate
9+
from kili.domain.types import ListOrTuple
910
from kili.use_cases.base import BaseUseCases
1011

1112

@@ -33,6 +34,19 @@ def update_project_workflow(
3334
def get_steps(
3435
self,
3536
project_id: ProjectId,
37+
fields: ListOrTuple[str],
3638
) -> list[dict[str, object]]:
3739
"""Get steps in a project workflow."""
38-
return self._kili_api_gateway.get_steps(project_id)
40+
return self._kili_api_gateway.get_steps(project_id, fields)
41+
42+
def add_reviewers_to_step(
43+
self, project_id: str, step_name: str, emails: list[str]
44+
) -> list[str]:
45+
"""Add reviewers to a specific step."""
46+
return self._kili_api_gateway.add_reviewers_to_step(project_id, step_name, emails)
47+
48+
def remove_reviewers_from_step(
49+
self, project_id: str, step_name: str, emails: list[str]
50+
) -> list[str]:
51+
"""Remove reviewers from a specific step."""
52+
return self._kili_api_gateway.remove_reviewers_from_step(project_id, step_name, emails)

0 commit comments

Comments
 (0)