Skip to content

Commit

Permalink
fix: add status_in filter to projectUsers query (#1397)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonas1312 authored Aug 30, 2023
1 parent 1660d55 commit 52e7ce2
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 53 deletions.
2 changes: 1 addition & 1 deletion src/kili/core/graphql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def execute_query_from_paginated_call(
options: QueryOptions,
post_call_function: Optional[Callable],
) -> Generator[Dict, None, None]:
"""Builds a row generator from paginated calls.
"""Build a row generator from paginated calls.
Args:
query: The object query to execute and to send to graphQL, in string format
Expand Down
13 changes: 10 additions & 3 deletions src/kili/core/graphql/operations/project_user/queries.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
"""GraphQL Queries of Project Users."""


from typing import Optional
from typing import Literal, Optional

from kili.core.graphql import BaseQueryWhere, GraphQLQuery


class ProjectUserWhere(BaseQueryWhere):
"""Tuple to be passed to the ProjectUserQuery to restrict query."""

# pylint: disable=too-many-arguments
def __init__(
self,
project_id: str,
email: Optional[str] = None,
_id: Optional[str] = None,
organization_id: Optional[str] = None,
):
status: Optional[Literal["ACTIVATED", "ORG_ADMIN", "ORG_SUSPENDED"]] = None,
active_in_project: Optional[bool] = None,
) -> None:
self.project_id = project_id
self.email = email
self._id = _id
self.organization_id = organization_id
self.status = status
self.active_in_project = active_in_project # user not deleted and nbr of labeled assets > 0
super().__init__()

def graphql_where_builder(self):
"""Build the GraphQL Where payload sent in the resolver from the SDK ProjectUserWhere."""
return {
"id": self._id,
"status": self.status,
"activeInProject": self.active_in_project,
"project": {
"id": self.project_id,
},
Expand All @@ -42,7 +49,7 @@ class ProjectUserQuery(GraphQLQuery):
"""ProjectUser query."""

@staticmethod
def query(fragment):
def query(fragment) -> str:
"""Return the GraphQL projectUsers query."""
return f"""
query projectUsers($where: ProjectUserWhere!, $first: PageSize!, $skip: Int!) {{
Expand Down
2 changes: 1 addition & 1 deletion src/kili/entrypoints/mutations/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def append_to_roles(

project_data = self.format_result("data", result)
for project_user in project_data["roles"]:
if project_user["user"]["email"] == user_email and project_user["role"] == role:
if project_user["user"]["email"] == user_email.lower() and project_user["role"] == role:
return project_user

raise MutationError(
Expand Down
103 changes: 86 additions & 17 deletions src/kili/entrypoints/queries/project_user/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
"""Project user queries."""

from typing import Dict, Generator, Iterable, List, Literal, Optional, overload
from typing import (
Dict,
Generator,
Iterable,
List,
Literal,
Optional,
Sequence,
overload,
)

from typeguard import typechecked

Expand All @@ -27,13 +36,18 @@ def project_users(
email: Optional[str] = None,
id: Optional[str] = None,
organization_id: Optional[str] = None,
status_in: Optional[Sequence[Literal["ACTIVATED", "ORG_ADMIN", "ORG_SUSPENDED"]]] = (
"ACTIVATED",
"ORG_ADMIN",
),
fields: List[str] = [
"activated",
"id",
"role",
"starred",
"user.email",
"user.id",
"status",
],
first: Optional[int] = None,
skip: int = 0,
Expand All @@ -50,13 +64,18 @@ def project_users(
email: Optional[str] = None,
id: Optional[str] = None,
organization_id: Optional[str] = None,
status_in: Optional[Sequence[Literal["ACTIVATED", "ORG_ADMIN", "ORG_SUSPENDED"]]] = (
"ACTIVATED",
"ORG_ADMIN",
),
fields: List[str] = [
"activated",
"id",
"role",
"starred",
"user.email",
"user.id",
"status",
],
first: Optional[int] = None,
skip: int = 0,
Expand All @@ -73,13 +92,18 @@ def project_users(
email: Optional[str] = None,
id: Optional[str] = None,
organization_id: Optional[str] = None,
status_in: Optional[Sequence[Literal["ACTIVATED", "ORG_ADMIN", "ORG_SUSPENDED"]]] = (
"ACTIVATED",
"ORG_ADMIN",
),
fields: List[str] = [
"activated",
"id",
"role",
"starred",
"user.email",
"user.id",
"status",
],
first: Optional[int] = None,
skip: int = 0,
Expand All @@ -91,35 +115,54 @@ def project_users(
"""Return project users (possibly with their KPIs) that match a set of criteria.
Args:
project_id: Identifier of the project
email: Email of the user
id: Identifier of the user
organization_id: Identifier of the user's organization
fields: All the fields to request among the possible fields for the projectUsers
project_id: Identifier of the project.
email: Email of the user.
id: Identifier of the user.
organization_id: Identifier of the user's organization.
status_in: If `None`, all users are returned.
- `ORG_ADMIN`: Is an Organization Admin. Is automatically added to projects.
- `ACTIVATED`: Has been invited to the project. Is not an Organization Admin
- `ORG_SUSPENDED`: Has been suspended at the organization level. Can no longer access any projects.
fields: All the fields to request among the possible fields for the projectUsers.
See [the documentation](https://docs.kili-technology.com/reference/graphql-api#projectuser) for all possible fields.
first: Maximum number of users to return
skip: Number of project users to skip
disable_tqdm: If `True`, the progress bar will be disabled
first: Maximum number of users to return.
skip: Number of project users to skip.
disable_tqdm: If `True`, the progress bar will be disabled.
as_generator: If `True`, a generator on the project users is returned.
Returns:
An iterable with the project users that match the criteria.
Examples:
```
```python
# Retrieve consensus marks of all users in project
>>> kili.project_users(project_id=project_id, fields=['consensusMark', 'user.email'])
```
"""
if status_in is not None and "status" not in fields:
fields = [*fields, "status"]

where = ProjectUserWhere(
project_id=project_id, email=email, _id=id, organization_id=organization_id
project_id=project_id,
email=email,
_id=id,
organization_id=organization_id,
)
disable_tqdm = disable_tqdm_if_as_generator(as_generator, disable_tqdm)
options = QueryOptions(disable_tqdm, first, skip)
project_users_gen = ProjectUserQuery(self.graphql_client, self.http_client)(
where, fields, options
)

if status_in is not None:
status_in_set = set(status_in)
project_users_gen = (
project_user
for project_user in project_users_gen
if project_user["status"] in status_in_set
)

if as_generator:
return project_users_gen
return list(project_users_gen)
Expand All @@ -131,19 +174,45 @@ def count_project_users(
email: Optional[str] = None,
id: Optional[str] = None,
organization_id: Optional[str] = None,
status_in: Optional[Sequence[Literal["ACTIVATED", "ORG_ADMIN", "ORG_SUSPENDED"]]] = (
"ACTIVATED",
"ORG_ADMIN",
),
) -> int:
"""Counts the number of projects and their users that match a set of criteria.
# pylint: disable=line-too-long
"""Count the number of projects and their users that match a set of criteria.
Args:
project_id: Identifier of the project
email: Email of the user
id: Identifier of the user
organization_id: Identifier of the user's organization
project_id: Identifier of the project
status_in: If `None`, all users are returned.
- `ORG_ADMIN`: Is an Organization Admin. Is automatically added to projects.
- `ACTIVATED`: Has been invited to the project. Is not an Organization Admin
- `ORG_SUSPENDED`: Has been suspended at the organization level. Can no longer access any projects.
Returns:
The number of project users with the parameters provided
"""
where = ProjectUserWhere(
project_id=project_id, email=email, _id=id, organization_id=organization_id
)
return ProjectUserQuery(self.graphql_client, self.http_client).count(where)
if status_in is None:
where = ProjectUserWhere(
project_id=project_id,
email=email,
_id=id,
organization_id=organization_id,
)
return ProjectUserQuery(self.graphql_client, self.http_client).count(where)

count = 0
for status in set(status_in):
where = ProjectUserWhere(
project_id=project_id,
email=email,
_id=id,
organization_id=organization_id,
status=status,
)
count += ProjectUserQuery(self.graphql_client, self.http_client).count(where)
return count
5 changes: 2 additions & 3 deletions src/kili/entrypoints/queries/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,11 @@ def users(
Examples:
```
# List all users in my organization
>>> organization = kili.organizations()
>>> organization_id = organizations[0]['id]
>>> organization = kili.organizations()[0]
>>> organization_id = organization['id']
>>> kili.users(organization_id=organization_id)
```
"""

where = UserWhere(api_key=api_key, email=email, organization_id=organization_id)
disable_tqdm = disable_tqdm_if_as_generator(as_generator, disable_tqdm)
options = QueryOptions(disable_tqdm, first, skip)
Expand Down
73 changes: 73 additions & 0 deletions tests/e2e/test_query_project_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import uuid

import pytest

from kili.client import Kili


@pytest.fixture()
def kili() -> Kili:
return Kili()


@pytest.fixture()
def project_id_suspended_user_email(kili: Kili):
project = kili.create_project(
input_type="TEXT", title="test_query_project_users.py sdk", json_interface={"jobs": {}}
)

# add a user that we desactivate
suspended_user_email = f"john.doe{uuid.uuid4()}[email protected]"
kili.append_to_roles(
project_id=project["id"],
user_email=suspended_user_email,
role="LABELER",
)
kili.update_properties_in_user(email=suspended_user_email, activated=False)

yield project["id"], suspended_user_email

kili.delete_project(project_id=project["id"])


def test_given_project_when_querying_project_users_it_works(
kili: Kili, project_id_suspended_user_email
):
# Given
project_id, suspended_user_email = project_id_suspended_user_email
api_user = kili.get_user()
fields = ["activated", "deletedAt", "id", "role", "user.email", "user.id", "status"]

# When
all_users = kili.project_users(project_id=project_id, fields=fields, status_in=None)

# Then
assert len(all_users) > 0

# When
activated_users = kili.project_users(
project_id=project_id, fields=fields, status_in=["ACTIVATED"]
)

# Then, only one activated user: the api user
assert len(activated_users) == 1, activated_users
assert activated_users[0]["user"]["email"] == api_user["email"], activated_users

# When
admin_users = kili.project_users(project_id=project_id, fields=fields, status_in=["ORG_ADMIN"])

# Then, admin users are not api user or disabled user
for proj_user in admin_users:
assert proj_user["user"]["email"] not in {
api_user["email"],
suspended_user_email,
}, admin_users

# When
disabled_users = kili.project_users(
project_id=project_id, fields=fields, status_in=["ORG_SUSPENDED"]
)

# Then, only one disabled user
assert len(disabled_users) == 1, disabled_users
assert disabled_users[0]["user"]["email"] == suspended_user_email, disabled_users
4 changes: 0 additions & 4 deletions tests/e2e/test_text_encoding_consistency.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# pylint: disable=missing-function-docstring,redefined-outer-name
"""Tests that the external id check is strict."""
import json
from pathlib import Path
from tempfile import NamedTemporaryFile

import pytest
import requests

Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,11 @@
"""Module for testing the user-facing-queries method."""


from typing import Dict, Generator, List
from unittest.mock import patch

import pytest
from typeguard import check_type

from kili.core.graphql.operations.asset.queries import AssetQuery
from kili.core.graphql.operations.user.queries import UserQuery
from kili.entrypoints.queries.asset import QueriesAsset
from kili.entrypoints.queries.user import QueriesUser


@pytest.mark.parametrize(
"args, kwargs, expected_return_type",
[
((), {}, List[Dict]),
((), {"as_generator": True}, Generator[Dict, None, None]),
((), {"as_generator": False}, List[Dict]),
((), {"email": "[email protected]", "as_generator": False}, List[Dict]),
],
)
@patch.object(UserQuery, "__call__")
def test_users_query_return_type(mocker, args, kwargs, expected_return_type):
kili = QueriesUser()
kili.graphql_client = mocker.MagicMock()
kili.http_client = mocker.MagicMock()

result = kili.users(*args, **kwargs)
assert check_type("result", result, expected_return_type) is None


@pytest.mark.parametrize(
Expand Down
Loading

0 comments on commit 52e7ce2

Please sign in to comment.