@@ -230,7 +230,7 @@ def update(self) -> UserGroup:
230230 Raises:
231231 ValueError: If group ID or name is not set, or if projects don't exist.
232232 ResourceNotFoundError: If the group or projects are not found.
233- UnprocessableEntityError: If user validation fails.
233+ UnprocessableEntityError: If user validation fails or users have workspace-level org roles .
234234 """
235235 if not self .id :
236236 raise ValueError ("Group id is required" )
@@ -247,8 +247,11 @@ def update(self) -> UserGroup:
247247 )
248248
249249 # Filter eligible users and build user roles
250- eligible_users = self ._filter_project_based_users ()
251- user_roles = self ._build_user_roles (eligible_users )
250+ try :
251+ eligible_users = self ._filter_project_based_users ()
252+ user_roles = self ._build_user_roles (eligible_users )
253+ except ValueError as e :
254+ raise UnprocessableEntityError (str (e )) from e
252255
253256 query = """
254257 mutation UpdateUserGroupPyApi($id: ID!, $name: String!, $description: String, $color: String!, $projectIds: [ID!]!, $userRoles: [UserRoleInput!], $notifyMembers: Boolean) {
@@ -312,7 +315,7 @@ def create(self) -> UserGroup:
312315
313316 Raises:
314317 ValueError: If group already has ID, name is invalid, or projects don't exist.
315- ResourceCreationError: If creation fails or user validation fails.
318+ ResourceCreationError: If creation fails, user validation fails, or users have workspace-level org roles .
316319 ResourceConflict: If a group with the same name already exists.
317320 """
318321 if self .id :
@@ -330,8 +333,11 @@ def create(self) -> UserGroup:
330333 )
331334
332335 # Filter eligible users and build user roles
333- eligible_users = self ._filter_project_based_users ()
334- user_roles = self ._build_user_roles (eligible_users )
336+ try :
337+ eligible_users = self ._filter_project_based_users ()
338+ user_roles = self ._build_user_roles (eligible_users )
339+ except ValueError as e :
340+ raise ResourceCreationError (str (e )) from e
335341
336342 query = """
337343 mutation CreateUserGroupPyApi($name: String!, $description: String, $color: String!, $projectIds: [ID!]!, $userRoles: [UserRoleInput!]!, $notifyMembers: Boolean, $roleId: String, $searchQuery: AlignerrSearchServiceQuery) {
@@ -496,11 +502,14 @@ def get_user_groups(
496502 def _filter_project_based_users (self ) -> Set [User ]:
497503 """Filter users to only include users eligible for UserGroups.
498504
499- Filters out users with specific admin organization roles that cannot be
500- added to UserGroups. Most users should be eligible .
505+ Only project-based users (org role "NONE") can be added to UserGroups.
506+ Users with any workspace-level organization role cannot be added .
501507
502508 Returns:
503509 Set of users that are eligible to be added to the group.
510+
511+ Raises:
512+ ValueError: If any user has a workspace-level organization role.
504513 """
505514 all_users = set ()
506515 for member in self .members :
@@ -509,63 +518,52 @@ def _filter_project_based_users(self) -> Set[User]:
509518 if not all_users :
510519 return set ()
511520
512- user_ids = [user .uid for user in all_users ]
513- query = """
514- query CheckUserOrgRolesPyApi($userIds: [ID!]!) {
515- users(where: {id_in: $userIds}) {
516- id
517- orgRole { id name }
518- }
519- }
520- """
521+ # Check each user's org role directly
522+ invalid_users = []
523+ eligible_users = set ()
521524
522- try :
523- result = self .client .execute (query , {"userIds" : user_ids })
524- if not result or "users" not in result :
525- return all_users # Fallback: let server handle validation
526-
527- # Check for users with org roles that cannot be used in UserGroups
528- # Only users with no org role (project-based users) can be assigned to UserGroups
529- eligible_user_ids = set ()
530- invalid_users = []
531-
532- for user_data in result ["users" ]:
533- org_role = user_data .get ("orgRole" )
534- user_id = user_data ["id" ]
535- user_email = user_data .get ("email" , "unknown" )
536-
537- if org_role is None :
538- # Users with no org role (project-based users) are eligible
539- eligible_user_ids .add (user_id )
525+ for user in all_users :
526+ try :
527+ # Get the user's organization role directly
528+ org_role = user .org_role ()
529+ if org_role is None or org_role .name .upper () == "NONE" :
530+ # Users with no org role or "NONE" role are project-based and eligible
531+ eligible_users .add (user )
540532 else :
541- # Users with ANY workspace org role cannot be assigned to UserGroups
533+ # Users with any workspace org role cannot be assigned to UserGroups
542534 invalid_users .append (
543535 {
544- "id" : user_id ,
545- "email" : user_email ,
546- "org_role" : org_role .get ( " name" ) ,
536+ "id" : user . uid ,
537+ "email" : getattr ( user , "email" , "unknown" ) ,
538+ "org_role" : org_role .name ,
547539 }
548540 )
541+ except Exception as e :
542+ # If we can't determine the user's role, treat as invalid for safety
543+ invalid_users .append (
544+ {
545+ "id" : user .uid ,
546+ "email" : getattr (user , "email" , "unknown" ),
547+ "org_role" : f"unknown (error: { str (e )} )" ,
548+ }
549+ )
549550
550- # Raise error if any invalid users found
551- if invalid_users :
552- error_details = []
553- for user in invalid_users :
554- error_details .append (
555- f"User { user ['id' ]} ({ user ['email' ]} ) has org role '{ user ['org_role' ]} '"
556- )
557-
558- raise ValueError (
559- f"Cannot create UserGroup with users who have organization roles. "
560- f"Only project-based users (no org role) can be assigned to UserGroups.\n "
561- f"Invalid users:\n "
562- + "\n " .join (f" • { detail } " for detail in error_details )
551+ # Raise error if any invalid users found
552+ if invalid_users :
553+ error_details = []
554+ for user_info in invalid_users :
555+ error_details .append (
556+ f"User { user_info ['id' ]} ({ user_info ['email' ]} ) has org role '{ user_info ['org_role' ]} '"
563557 )
564558
565- return {user for user in all_users if user .uid in eligible_user_ids }
559+ raise ValueError (
560+ f"Cannot create UserGroup with users who have organization roles. "
561+ f"Only project-based users (no org role or role 'NONE') can be assigned to UserGroups.\n "
562+ f"Invalid users:\n "
563+ + "\n " .join (f" • { detail } " for detail in error_details )
564+ )
566565
567- except Exception :
568- return all_users # Fallback: let server handle validation
566+ return eligible_users
569567
570568 def _build_user_roles (
571569 self , eligible_users : Set [User ]
@@ -679,6 +677,12 @@ def _get_members_set(
679677 member_nodes = members_data .get ("nodes" , [])
680678 user_group_roles = members_data .get ("userGroupRoles" , [])
681679
680+ # Get all roles to map IDs to names
681+ from labelbox .schema .role import get_roles
682+
683+ all_roles = get_roles (self .client )
684+ role_id_to_role = {role .uid : role for role in all_roles .values ()}
685+
682686 # Create a mapping from userId to roleId
683687 user_role_mapping = {
684688 role_data ["userId" ]: role_data ["roleId" ]
@@ -694,15 +698,9 @@ def _get_members_set(
694698
695699 # Get the role for this user from the mapping
696700 role_id = user_role_mapping .get (node ["id" ])
697- if role_id :
698- # We need to fetch the role details since we only have the roleId
699- # For now, create a minimal Role object with just the ID
700- role_values : defaultdict [str , Any ] = defaultdict (lambda : None )
701- role_values ["id" ] = role_id
702- # We don't have the role name from this response, so we'll leave it as None
703- # The Role object will fetch the name when needed
704- role = Role (self .client , role_values )
705-
701+ if role_id and role_id in role_id_to_role :
702+ # Use the actual Role object with proper name resolution
703+ role = role_id_to_role [role_id ]
706704 members .add (UserGroupMember (user = user , role = role ))
707705
708706 return members
0 commit comments