Skip to content

PLT-2655 - Fix UserGroup -project membership- and remove "users" #1989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 3 additions & 66 deletions libs/labelbox/src/labelbox/schema/user_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,14 @@ class UserGroup(BaseModel):
"""Represents a user group in Labelbox.

UserGroups allow organizing users and projects together for access control
and collaboration. This implementation provides enhanced validation and
member management capabilities.
and collaboration. Each user is added with an explicit role via UserGroupMember.

Attributes:
id: Unique identifier for the user group.
name: Display name of the user group.
color: Visual color identifier for the group.
description: Optional description of the group's purpose.
notify_members: Whether to notify members of group changes.
default_role: Default role assigned to users added via the legacy users field.
users: Legacy set of users (maintained for backward compatibility).
members: Set of UserGroupMember objects with explicit roles.
projects: Set of projects associated with this group.
client: Labelbox client instance for API communication.
Expand All @@ -136,8 +133,6 @@ class UserGroup(BaseModel):
color: UserGroupColor
description: str = ""
notify_members: bool = False
default_role: Optional[Role] = None
users: Set[User] = Field(default_factory=set)
members: Set[UserGroupMember] = Field(default_factory=set)
projects: Set[Project] = Field(default_factory=set)
client: Client
Expand All @@ -151,8 +146,6 @@ def __init__(
color: UserGroupColor = UserGroupColor.BLUE,
description: str = "",
notify_members: bool = False,
default_role: Optional[Role] = None,
users: Optional[Set[User]] = None,
members: Optional[Set[UserGroupMember]] = None,
projects: Optional[Set[Project]] = None,
) -> None:
Expand All @@ -165,8 +158,6 @@ def __init__(
color: Visual color identifier.
description: Optional description.
notify_members: Whether to notify members of changes.
default_role: Default role for users added via legacy users field.
users: Legacy set of users for backward compatibility.
members: Set of members with explicit roles.
projects: Set of associated projects.
"""
Expand All @@ -177,38 +168,10 @@ def __init__(
color=color,
description=description,
notify_members=notify_members,
default_role=default_role,
users=users or set(),
members=members or set(),
projects=projects or set(),
)

def model_post_init(self, __context: Any) -> None:
"""Validate that default_role is set when users field is used.

Args:
__context: Pydantic context (unused).

Raises:
ValueError: If users is set but default_role is not provided, or if default_role is invalid.
"""
# Validate that default_role is set when legacy users field is used
if self.users and self.default_role is None:
raise ValueError(
"default_role must be set when using the 'users' field."
)

# Validate that default_role is not an invalid role for UserGroups
if self.default_role and hasattr(self.default_role, "name"):
role_name = (
self.default_role.name.upper() if self.default_role.name else ""
)
if role_name in INVALID_USERGROUP_ROLES:
raise ValueError(
f"default_role cannot be '{role_name}'. "
f"UserGroup members cannot have '{role_name}' roles."
)

def get(self) -> UserGroup:
"""Reload the user group information from the server.

Expand Down Expand Up @@ -283,12 +246,6 @@ def update(self) -> UserGroup:
f"Project {project.uid} not found or inaccessible"
)

# Validate default_role is set when legacy users field is used
if self.users and self.default_role is None:
raise ValueError(
"default_role must be set when using the 'users' field."
)

# Filter eligible users and build user roles
eligible_users = self._filter_project_based_users()
user_roles = self._build_user_roles(eligible_users)
Expand Down Expand Up @@ -372,19 +329,12 @@ def create(self) -> UserGroup:
f"Project {project.uid} not found or inaccessible"
)

# Validate default_role is set when legacy users field is used
if self.users and self.default_role is None:
raise ValueError(
"default_role must be explicitly set when using the 'users' field. "
"This ensures you are aware of what role will be assigned to legacy users."
)

# Filter eligible users and build user roles
eligible_users = self._filter_project_based_users()
user_roles = self._build_user_roles(eligible_users)

query = """
mutation CreateUserGroupPyApi($name: String!, $description: String, $color: String!, $projectIds: [ID!], $userRoles: [UserRoleInput!], $notifyMembers: Boolean, $roleId: String, $searchQuery: AlignerrSearchServiceQuery) {
mutation CreateUserGroupPyApi($name: String!, $description: String, $color: String!, $projectIds: [ID!]!, $userRoles: [UserRoleInput!]!, $notifyMembers: Boolean, $roleId: String, $searchQuery: AlignerrSearchServiceQuery) {
createUserGroupV3(
data: {
name: $name
Expand Down Expand Up @@ -415,8 +365,6 @@ def create(self) -> UserGroup:
"projectIds": [project.uid for project in self.projects],
"userRoles": user_roles,
"notifyMembers": self.notify_members,
"roleId": None,
"searchQuery": None,
}

try:
Expand Down Expand Up @@ -554,7 +502,7 @@ def _filter_project_based_users(self) -> Set[User]:
Returns:
Set of users that are eligible to be added to the group.
"""
all_users = set(self.users)
all_users = set()
for member in self.members:
all_users.add(member.user)

Expand Down Expand Up @@ -632,13 +580,6 @@ def _build_user_roles(
"""
user_roles: List[Dict[str, str]] = []

# Add legacy users with default role
for user in self.users:
if user in eligible_users and self.default_role is not None:
user_roles.append(
{"userId": user.uid, "roleId": self.default_role.uid}
)

# Add members with their explicit roles
for member in self.members:
if member.user in eligible_users:
Expand All @@ -662,7 +603,6 @@ def _update_from_response(self, group_data: Dict[str, Any]) -> None:
# notifyMembers field is not available in GraphQL response, so we keep the current value
self.projects = self._get_projects_set(group_data["projects"]["nodes"])
self.members = self._get_members_set(group_data["members"])
self.users = set() # Clear legacy users

def _handle_user_validation_error(
self, error: Exception, operation: str
Expand Down Expand Up @@ -764,8 +704,5 @@ def _get_members_set(
role = Role(self.client, role_values)

members.add(UserGroupMember(user=user, role=role))
elif self.default_role:
# Fallback to default role if no role mapping found
members.add(UserGroupMember(user=user, role=self.default_role))

return members
Loading
Loading