Skip to content

Role Management API and UI#801

Merged
mihow merged 68 commits intomainfrom
feat/role-management-api
Jan 22, 2026
Merged

Role Management API and UI#801
mihow merged 68 commits intomainfrom
feat/role-management-api

Conversation

@mohamedelabbas1996
Copy link
Contributor

@mohamedelabbas1996 mohamedelabbas1996 commented Apr 8, 2025

Summary

This update introduces a Role Management feature that includes both API and UI support in Antenna, allowing project managers to manage project members and their roles directly from the Antenna site.

Previously, project membership and role assignment were handled primarily through the Django admin interface, which limited role management to administrators and required manual intervention. With this change, role and membership management are exposed through the API and can be managed by project managers directly through the Antenna UI, without needing admin access.

List of Changes

  • Added project role management API endpoints

    • GET /projects/<project-id>/roles/ to list available project roles
    • GET /projects/<project-id>/members/ to list project members
    • POST /projects/<project-id>/members/ to add a member and assign a role
    • PATCH /projects/<project-id>/members/<membership-id>/ to update a member’s role
    • DELETE /projects/<project-id>/members/<membership-id>/ to remove a member from the project
  • Introduced UserProjectMembership as an explicit through model

    • Represents project membership independently of roles
    • Enforces uniqueness per user and project
    • Migrates existing project members from the old implicit M2M table
  • Added project-scoped permissions for membership management

    • View, create, update, and delete project members actions are protected by explicit project permissions
    • Users are allowed to remove themselves from a project
  • Added nested routing for roles and members under projects

  • Added is_member field to the project details response

    • Boolean field indicating whether the current user is a member of the project
    • Returns true if the user is a project member or a superuser
    • Used by the frontend to decide whether to render the Team page and allow listing and managing project members
  • Added UI support for managing project members and roles

    • UI components for listing members, adding members, updating roles, and removing members
    • Frontend authorization is driven by permissions returned from the API
  • Removed member management from the Project details admin page

    • User email addresses are only exposed in membership management endpoints
  • Test coverage for membership API

    • 15 tests covering CRUD operations, permissions, and validation
    • Tests for edge cases like self-removal and duplicate memberships

Related Issues

#727

Summary by CodeRabbit

  • New Features
    • Full team management UI: view/add/update/remove members, roles picker, roles info, leave project, and new project "team" route.
  • API Updates
    • New endpoints to list roles and manage project memberships with role-based permissions (list/create/update/delete).
  • Admin
    • Project admin form no longer exposes inline member editing; owner remains editable.
  • Localization
    • New translation keys for team, roles, member labels and messages.
  • Bug Fixes
    • Project routing now respects nested project context.
  • Tests
    • Comprehensive API tests for membership management.
  • Chores
    • Migration introducing explicit membership model and expanded permissions.

✏️ Tip: You can customize this high-level summary in your review settings.

@mohamedelabbas1996 mohamedelabbas1996 linked an issue Apr 8, 2025 that may be closed by this pull request
@netlify
Copy link

netlify bot commented Apr 8, 2025

Deploy Preview for antenna-preview canceled.

Name Link
🔨 Latest commit 0d3a0d9
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/6971d0c12e45020008f1f7d8

@mohamedelabbas1996 mohamedelabbas1996 changed the title Role Management API [Draft] Role Management API May 5, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a UserProjectMembership through-model and migration; expands role metadata and membership permissions; provides membership APIs, serializers, permission class, and signals; registers nested member routes; and implements frontend team UI, hooks, types, routes, and tests for membership management.

Changes

Cohort / File(s) Summary
Models & Migration
ami/main/models.py, ami/main/migrations/0080_userprojectmembership.py
Add UserProjectMembership through-model; convert Project.members to use through="UserProjectMembership"; data migration copies existing M2M rows; unique constraint and expanded Project Meta permissions.
Roles & Permissions
ami/users/roles.py, ami/main/models.py, ami/base/permissions.py
Add role metadata (display_name, description) and helpers (get_supported_roles, get_user_roles, get_primary_role); add membership-related permissions; remove legacy MANAGE_MEMBERS; add UserMembershipPermission with special handling for list.
Signals
ami/users/signals.py
Update m2m handlers to create/delete UserProjectMembership records (get_or_create/delete) and avoid direct Project.members manipulation; preserve signal disconnect/reconnect patterns.
API Serializers & Views
ami/users/api/serializers.py, ami/users/api/views.py, ami/main/api/serializers.py
New serializers for membership CRUD and roles; RolesAPIView and UserProjectMembershipViewSet added; ProjectSerializer gains is_member field.
Routing & Requirements
config/api_router.py, requirements/base.txt
Register nested route /projects/{project_id}/members/ via drf-nested-routers; add /users/roles/ endpoint; add drf-nested-routers==0.94.1.
Admin
ami/main/admin.py
Remove inline members editing/search/filtering from Project admin; Ownership & Access fieldset exposes only owner.
Tests
ami/users/tests/test_membership_management_api.py
New APITestCase covering roles listing and membership CRUD, validations, permission enforcement, and edge cases (self-deletion).
Frontend Routes & Models
ui/src/app.tsx, ui/src/utils/constants.ts, ui/src/data-services/constants.ts, ui/src/data-services/models/*, ui/src/data-services/models/project-details.ts
Add Team route and TEAM generator; add API routes MEMBERS and ROLES; add Member and Role TS types and ServerMember placeholder; ProjectDetails adds permissionsAdminUrl and isMember.
Frontend Hooks / Data
ui/src/data-services/hooks/team/*
Add useMembers, useAddMember, useUpdateMember, useRemoveMember; refactor useRoles to central roles endpoint; invalidate MEMBERS cache on mutations.
Frontend Team UI
ui/src/pages/project/team/*, ui/src/pages/project/team/team.tsx, ui/src/pages/project/team/team-columns.tsx
New Team page and components: Team, RolesPicker, AboutRoles, AddMemberDialog, ManageAccessDialog, RemoveMemberDialog, LeaveTeamDialog, and table columns.
Sidebar / Navigation
ui/src/pages/project/sidebar/useSidebarSections.tsx
Add "Team" sidebar item, gated by project.isMember, linking to Team route.
UI & i18n
ui/src/design-system/*, ui/src/components/form/*, ui/src/utils/language.ts, ui/src/pages/species-details/species-details.tsx
Minor style tweaks (spacing, dialog compact width), Dialog.Content accepts className, icon margin removals, many new STRING keys for team/roles, and replace hardcoded "Admin" with translation.
Docs
.agents/USER_PERMISSION_ROLES.md
New documentation describing roles, groups, signals, APIs, migrations, and common operations for the permission/role system.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant FE as Frontend UI
    participant API as Membership API
    participant RoleSvc as Role system
    participant DB as Database
    participant Signal as m2m_changed Signal

    User->>FE: Submit add-member (email, role_id) for project
    FE->>API: POST /projects/{project_id}/members (email, role_id)
    API->>DB: Lookup user by email
    API->>DB: Create/get UserProjectMembership record
    API->>Signal: Temporarily disconnect m2m_changed
    API->>RoleSvc: Unassign existing roles for user in project
    RoleSvc->>DB: Remove role assignments
    API->>RoleSvc: Assign new role to user (role_id)
    RoleSvc->>DB: Persist role assignment
    API->>Signal: Reconnect m2m_changed
    API-->>FE: 201 Created (member payload)
    FE->>FE: Refresh members list / show success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

backend

Suggested reviewers

  • annavik

Poem

🐇 I hopped through models, roles, and UI light,
I stitched up members, made their routes take flight.
From add to remove, with dialogs bright and keen,
Projects now have teams — a tidy members' scene.
Hop on and review — the rabbit's done the green!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Role Management API and UI' clearly summarizes the main change, focusing on the two primary additions (API and UI) for role management functionality.
Description check ✅ Passed The PR description follows the template structure with Summary, List of Changes, Related Issues, and is mostly complete, though it lacks explicit 'How to Test', 'Screenshots', 'Deployment Notes', and 'Checklist' sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mohamedelabbas1996
Copy link
Contributor Author

mohamedelabbas1996 commented Nov 21, 2025

Role Management API

This API provides endpoints for managing project roles and member memberships.


1. List Available Roles

Endpoint Name: List Available Roles

Request:

GET /api/v2/projects/{project_id}/roles/

Request Body:

None

Response:

[
    {
        "id": "BasicMember",
        "name": "Basic member",
        "description": "Basic project member with access to star source images, create jobs, and run single image processsing jobs."
    },
    {
        "id": "Researcher",
        "name": "Researcher",
        "description": "Researcher with all basic member permissions, plus the ability to trigger data exports"
    },
    {
        "id": "Identifier",
        "name": "Identifier",
        "description": "Identifier with all basic member permissions, plus the ability to create, update, and delete occurrence identifications."
    },
    {
        "id": "MLDataManager",
        "name": "ML Data manager",
        "description": "Machine Learning Data Manager with all basic member permissions, plus the ability to manage ML jobs, run collection population jobs, sync data storage, export data, and delete occurrences."
    },
    {
        "id": "ProjectManager",
        "name": "Project manager",
        "description": "Project manager with full administrative access, including all permissions from all roles plus the ability to manage project settings, members, deployments, collections, storage, and all project resources."
    }
]

Access Requirements:

None


2. List Project Members

Endpoint Name: List Project Members

Request:

GET /api/v2/projects/{project_id}/members/

Request Body:

None

Response:

{
    "count": 23,
    "next": "http://localhost:8000/api/v2/projects/11/members/?limit=20&offset=20",
    "previous": null,
    "results": [
        {
            "id": 187,
            "user": {
                "id": 9,
                "name": "",
                "email": "",
                "image": null,
                "details": "http://localhost:8000/api/v2/users/9/",
                "user_permissions": []
            },
            "role": "Researcher",
            "role_display_name": "Researcher",
            "role_description": "Researcher with all basic member permissions, plus the ability to trigger data exports",
            "created_at": "2025-12-05T01:15:43.701856",
            "updated_at": "2025-12-05T01:15:43.701898",
            "user_permissions": [
                "update",
                "delete"
            ]
        },
        {
            "id": 82,
            "user": {
                "id": 13,
                "name": "",
                "email": "",
                "image": null,
                "details": "http://localhost:8000/api/v2/users/13/",
                "user_permissions": []
            },
            "role": "BasicMember",
            "role_display_name": "Basic member",
            "role_description": "Basic project member with access to star source images, create jobs, and run single image processsing jobs.",
            "created_at": "2025-12-01T08:26:49.846599",
            "updated_at": "2025-12-01T08:26:49.846607",
            "user_permissions": [
                "update",
                "delete"
            ]
        }
    ],
    "user_permissions": ["create"]
}

Access Requirements:

User must be a superuser or at least a project basic member


3. Get Member Details

Endpoint Name: Get Member Details

Request:

GET /api/v2/projects/{project_id}/members/{membership_id}/

Request Body:

None

Response:

{
    "id": 187,
    "user": {
        "id": 9,
        "name": "",
        "email": "",
        "image": null,
        "details": "http://localhost:8000/api/v2/users/9/",
        "user_permissions": []
    },
    "project": "http://localhost:8000/api/v2/projects/11/",
    "role": "Researcher",
    "role_display_name": "Researcher",
    "role_description": "Researcher with all basic member permissions, plus the ability to trigger data exports",
    "created_at": "2025-12-05T01:15:43.701856",
    "updated_at": "2025-12-05T01:15:43.701898",
    "user_permissions": [
        "update",
        "delete"
    ]
}

Access Requirements:

User must be a superuser or at least a project basic member


4. Add Member

Endpoint Name: Add Member

Request:

POST /api/v2/projects/{project_id}/members/

Request Body:

{
    "email": "user@example.com",
    "role_id": "Researcher"
}

Request Fields:

  • email (required): Email address of the user to add (user must exist in the system)
  • role_id (required): One of: "BasicMember", "Researcher", "Identifier", "MLDataManager", "ProjectManager"

Response:

{
    "id": 188,
    "user": {
        "id": 51,
        "name": "",
        "email": "",
        "image": null,
        "details": "http://localhost:8000/api/v2/users/51/",
        "user_permissions": []
    },
    "project": "http://localhost:8000/api/v2/projects/11/",
    "role": "Identifier",
    "role_display_name": "Identifier",
    "role_description": "Identifier with all basic member permissions, plus the ability to create, update, and delete occurrence identifications.",
    "created_at": "2025-12-05T02:06:57.212676",
    "updated_at": "2025-12-05T02:06:57.212794",
    "user_permissions": [
        "update",
        "delete"
    ]
}

Error Response (User already member):

{
    "non_field_errors": [
        "User is already a member of this project."
    ]
}

Error Response (User not found):

{
    "email": [
        "User does not exist in the system."
    ]
}

Error Response (Invalid role):

{
    "role_id": [
        "Invalid role_id. Must be one of: ['BasicMember', 'Researcher', 'Identifier', 'MLDataManager', 'ProjectManager']"
    ]
}

Access Requirements:

User must be ProjectManager or superuser


5. Update Member Role

Endpoint Name: Update Member Role

Request:

PATCH /api/v2/projects/{project_id}/members/{membership_id}/

Note: Use membership_id (from the membership object) not user_id.

Request Body:

{
    "role_id": "Identifier"
}

Request Fields:

  • role_id (required): One of: "BasicMember", "Researcher", "Identifier", "MLDataManager", "ProjectManager"

Response:

{
    "id": 187,
    "user": {
        "id": 9,
        "name": "",
        "email": "",
        "image": null,
        "details": "http://localhost:8000/api/v2/users/9/",
        "user_permissions": []
    },
    "project": "http://localhost:8000/api/v2/projects/11/",
    "role": "MLDataManager",
    "role_display_name": "ML Data manager",
    "role_description": "Machine Learning Data Manager with all basic member permissions, plus the ability to manage ML jobs, run collection population jobs, sync data storage, export data, and delete occurrences.",
    "created_at": "2025-12-05T01:15:43.701856",
    "updated_at": "2025-12-05T02:10:15.123456",
    "user_permissions": [
        "update",
        "delete"
    ]
}

Access Requirements:

User must be a ProjectManager or superuser


6. Remove Member

Endpoint Name: Remove Member

Request:

DELETE /api/v2/projects/{project_id}/members/{membership_id}/

Note: Use membership_id (from the membership object) not user_id.

Request Body:

None

Response:

HTTP 204 No Content (empty response body)

Access Requirements:

User must be a project manager OR a superuser. Also, the user can delete their own membership (self-removal is allowed for all members)


class Meta:
model = User
fields = ["id", "name", "details", "image"]
fields = ["id", "name", "details", "image", "email"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I added email here because I needed it for the teams table, however I think this might also effect other API endpoints where we don't want to expose this in the response

@annavik
Copy link
Member

annavik commented Nov 24, 2025

Thank you so much @mohamedelabbas1996, this looks great! Here are some notes from when hooking this up with FE.

Notes

  • Can we add members by email instead of user id (and validate the email)?
  • Can we include role display name in the project members response?
  • Superusers cannot list, update or delete project members (I think they should, even if not a project member)
  • Include descriptions about every role returned that we can present in the role picker and as tooltips
  • How can I know if the teams page should be presented or not in UI?
  • How can I know if a user is allowed to update and delete members? Can we pass user permissions in the project members response?
  • Do you think the project members response should have support for sorting and pagination (similar to other list endpoints)? More nice to have and for consistency.

Add tests for error cases:
- Invalid role_id returns 400
- Nonexistent email returns 400
- Duplicate membership returns 400
- Missing email returns 400
- Missing role_id returns 400

Co-Authored-By: Claude <noreply@anthropic.com>
@mihow
Copy link
Collaborator

mihow commented Jan 20, 2026

@annavik @mohamedelabbas1996 I brought this branch up-to-date with main and did some final testing! It seems to be working well. Do we need a migration or method to assign existing users to a role? For example, all of these users are members of the project, but don't have a role in the UI until I set one.

image

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@ami/main/migrations/0080_userprojectmembership_migration.py`:
- Around line 33-39: The rollback SQL that recreates the join table in the
migration (main_project_members) is missing the foreign key constraints and the
uniqueness constraint; update the CREATE TABLE/DDL in
0080_userprojectmembership_migration.py to add FK constraints from project_id ->
main_project(id) and user_id -> main_user(id) and create a UNIQUE constraint or
unique index on (project_id, user_id) (name constraints clearly, e.g.,
main_project_members_project_id_fkey, main_project_members_user_id_fkey,
main_project_members_project_user_key) so that a backwards migration fully
restores the original M2M integrity.

In `@ami/users/api/views.py`:
- Around line 89-98: perform_destroy currently unassigns roles (via
Role.__subclasses__()) while membership deletion signal handlers are still
connected, so a signal can delete the membership mid-loop and make the later
instance.delete() operate on a stale object; fix this by temporarily
disconnecting the membership-related signal handler(s) (the same handler(s)
disconnected in perform_update) before the role-unassignment loop, perform the
Role.unassign_user(...) calls inside the transaction.atomic block, then
reconnect the signal handler(s) immediately after (ensure reconnect happens even
on error, e.g., using try/finally), so that instance.delete() runs against the
real DB row.
- Around line 73-87: perform_update can trigger the manage_project_membership
signal while wiping and reassigning roles, causing the membership to be deleted
and recreated (changing ID/timestamps); fix by temporarily disconnecting the
manage_project_membership signal around the role manipulation in perform_update
(after membership.save() and before the loop over Role.__subclasses__() and
role_cls.assign_user), then reconnect it afterward so unassign_user and
assign_user do not fire the signal during the update; keep the existing
transaction.atomic() block and ensure reconnection happens even on errors (i.e.,
use a try/finally around the signal disconnect/reconnect).

In `@ami/users/roles.py`:
- Around line 86-88: The user-facing description string for BasicMember contains
a typo: replace the substring "processsing" with "processing" in the description
variable (the description assigned in the BasicMember role/class) so the
sentence reads "...run single image processing jobs." and ensure the corrected
string preserves the parentheses and spacing.
♻️ Duplicate comments (1)
ami/main/migrations/0080_userprojectmembership_migration.py (1)

15-18: Prefer a plain SQL string for the fixed table name.

Line 17 uses an f-string for a constant table name; keep it literal to avoid the S608 warning.

🧹 Suggested cleanup
-        cursor.execute(f"SELECT project_id, user_id FROM {through_table};")
+        cursor.execute("SELECT project_id, user_id FROM main_project_members;")
🧹 Nitpick comments (2)
ami/users/api/serializers.py (1)

160-176: Avoid triple role lookups per membership serialization.

Lines 160–176 compute the primary role three times; in list views this can add unnecessary DB/group checks. Cache once per object in the serializer.

♻️ Suggested refactor
 class UserProjectMembershipSerializer(DefaultSerializer):
@@
-    def get_role(self, obj):
-        from ami.users.roles import Role
-
-        role_cls = Role.get_primary_role(obj.project, obj.user)
-        return role_cls.__name__ if role_cls else None
+    def _get_primary_role(self, obj):
+        from ami.users.roles import Role
+
+        cache = getattr(self, "_primary_role_cache", None)
+        if cache is None:
+            cache = self._primary_role_cache = {}
+        return cache.setdefault(obj.pk, Role.get_primary_role(obj.project, obj.user))
+
+    def get_role(self, obj):
+        role_cls = self._get_primary_role(obj)
+        return role_cls.__name__ if role_cls else None
@@
-    def get_role_display_name(self, obj):
-        from ami.users.roles import Role
-
-        role_cls = Role.get_primary_role(obj.project, obj.user)
-        return role_cls.display_name if role_cls else None
+    def get_role_display_name(self, obj):
+        role_cls = self._get_primary_role(obj)
+        return role_cls.display_name if role_cls else None
@@
-    def get_role_description(self, obj):
-        from ami.users.roles import Role
-
-        role_cls = Role.get_primary_role(obj.project, obj.user)
-        return role_cls.description if role_cls else None
+    def get_role_description(self, obj):
+        role_cls = self._get_primary_role(obj)
+        return role_cls.description if role_cls else None
ami/users/api/views.py (1)

51-57: Remove dead code and apply consistent defensive access pattern.

Line 53 contains commented-out dead code. Line 54 directly accesses _validated_role_cls without the defensive getattr pattern used in perform_update (line 77). Apply consistent error handling across both methods.

♻️ Suggested fix
     def perform_create(self, serializer):
         project = self.get_active_project()
-        # user = serializer._validated_user
-        role_cls = serializer._validated_role_cls
+        role_cls = getattr(serializer, "_validated_role_cls", None)
+        if not role_cls:
+            raise ValueError("role_cls not set during validation")
         with transaction.atomic():
             membership = serializer.save(project=project)
             user = membership.user



class MemberUserSerializer(UserListSerializer):
"""User serializer for membership context - includes email for management purposes."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@annavik @mohamedelabbas1996 new serializer that includes email. the other one doesn't now.

mihow and others added 3 commits January 19, 2026 23:51
Create explicit through model for Project.members M2M relationship to
support role-based permissions. This migration:

1. Creates UserProjectMembership model with timestamps
2. Migrates existing membership data from implicit M2M table
3. Drops old implicit table
4. Updates Project.members to use through model
5. Adds membership permissions to Project

This enables per-project role assignment via django-guardian groups
while maintaining backward compatibility with existing memberships.

Co-Authored-By: Claude <noreply@anthropic.com>
Changed 'processsing' to 'processing' in the user-facing role
description text.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Disconnect the manage_project_membership signal during role
manipulation in perform_update and perform_destroy to prevent the
signal from deleting and recreating memberships with new IDs.

This ensures:
- Membership IDs remain stable during role updates
- Timestamps (created_at) are preserved
- No stale instance operations during deletion

Uses the same pattern already proven in perform_create.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@mihow mihow force-pushed the feat/role-management-api branch from 7074063 to c7be19f Compare January 20, 2026 08:10
…flict

The 0.95.0 version conflicts with djangorestframework==3.14.0 and drf-spectacular==0.26.3, causing pip dependency resolution to fail during Docker build.

Version 0.94.1 maintains compatibility with our existing DRF ecosystem while providing the nested routing functionality needed for the role management API.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@requirements/base.txt`:
- Around line 1-6: The file ends without a trailing newline causing pre-commit
EOF fixer failures; open the requirements/base.txt and add a single newline
character at the end of the file (after the last line, e.g. after the commented
line "# drf-nested-routers==0.94.1") so the file terminates with a newline and
the pre-commit hook passes.

@annavik
Copy link
Member

annavik commented Jan 20, 2026

Thanks a lot Michael!! Sorry I have been slow with my testing here 🐌

For your comment about migration, I think in most cases members should have a role assigned, but if not should be pretty fast to set from UI now, so I'm fine skipping the migration step!

@annavik
Copy link
Member

annavik commented Jan 20, 2026

Thanks a lot Michael!! Sorry I have been slow with my testing here 🐌

For your comment about migration, I think in most cases members should have a role assigned, but if not should be pretty fast to set from UI now, so I'm fine skipping the migration step!

Or are you saying in some cases the role is set i admin, but not exposed from API? I have not seen that myself, but that would be more of a problem!

@mihow
Copy link
Collaborator

mihow commented Jan 20, 2026 via email

@annavik
Copy link
Member

annavik commented Jan 21, 2026

Hey @mihow! I double checked with Mohamed about your comments. The answer is yes we need a migration, but good news it's prepared! :) Will you try apply migrations/0080_userprojectmembership.py on your data‎?

@mihow
Copy link
Collaborator

mihow commented Jan 22, 2026

Hmm, I'm still seeing no roles populated in the UI after I run the migration
image

Also, I do prefer the pattern of nesting object attributes ({role: {id, display_name, description}}, rather than flattening them out (role_display_name). But perhaps it adds complexity in the serializer and for updates from the UI. I think we already have a mix, so not a big deal, but something to consider going forward!
image

@mohamedelabbas1996
Copy link
Contributor Author

mohamedelabbas1996 commented Jan 22, 2026

Hmm, I'm still seeing no roles populated in the UI after I run the migration image

Also, I do prefer the pattern of nesting object attributes ({role: {id, display_name, description}}, rather than flattening them out (role_display_name). But perhaps it adds complexity in the serializer and for updates from the UI. I think we already have a mix, so not a big deal, but something to consider going forward! image

Hey @mihow , thanks a lot for testing this out!

It’s working for me on the Mount Totumas project using the Panama DB snapshot. Could I try testing it on the specific database snapshot you're using? I'd like to see if I can reproduce the empty roles there!
@annavik, are you able to reproduce this on your end as well?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@ami/users/roles.py`:
- Around line 75-89: Fix the typo in the get_primary_role docstring: change
"practive" to "practice" in the triple-quoted string of the
Role.get_primary_role method so the comment reads "In practice, a user should
only have one role per project..." to keep documentation accurate.
🧹 Nitpick comments (3)
ami/main/migrations/0080_userprojectmembership.py (3)

22-30: Consider using bulk_create for better performance on large datasets.

The current implementation uses get_or_create in a loop, which issues separate database queries for each membership. For projects with many members, this could be slow.

♻️ Suggested refactor using bulk_create
     if not rows:
         return

     # Create new through model entries
-    for project_id, user_id in rows:
-        UserProjectMembership.objects.get_or_create(
-            project_id=project_id,
-            user_id=user_id,
-        )
+    memberships_to_create = [
+        UserProjectMembership(project_id=project_id, user_id=user_id)
+        for project_id, user_id in rows
+    ]
+    UserProjectMembership.objects.bulk_create(
+        memberships_to_create,
+        ignore_conflicts=True,
+    )

52-59: Consider batch insert for better performance.

Similar to the forwards function, this loop inserts records one at a time. For consistency and performance, executemany could be used instead.

♻️ Optional refactor using executemany
     # Copy back membership data
     memberships = UserProjectMembership.objects.all()
     with schema_editor.connection.cursor() as cursor:
-        for m in memberships:
-            cursor.execute(
-                f"INSERT INTO {OLD_THROUGH_TABLE} (project_id, user_id) VALUES (%s, %s)",
-                [m.project_id, m.user_id],
-            )
+        cursor.executemany(
+            f"INSERT INTO {OLD_THROUGH_TABLE} (project_id, user_id) VALUES (%s, %s)",
+            [(m.project_id, m.user_id) for m in memberships],
+        )

99-102: Clarify the reverse_sql intent with a comment.

The reverse_sql drops a table that doesn't exist at reversal time (since the backwards() function recreates it afterward). While functionally correct due to IF EXISTS, this is semantically confusing. Consider adding a comment explaining this is intentionally a no-op since backwards() handles table recreation.

📝 Suggested clarification
         # 3. Drop old M2M implicit table
         migrations.RunSQL(
             "DROP TABLE IF EXISTS main_project_members;",
-            reverse_sql="DROP TABLE IF EXISTS main_project_members;",
+            # reverse_sql is a no-op; backwards() in RunPython handles table recreation
+            reverse_sql="SELECT 1;",  # No-op placeholder
         ),

Alternatively, keep the current DROP TABLE IF EXISTS but add a comment explaining why it's intentionally mirrored.

@mihow
Copy link
Collaborator

mihow commented Jan 22, 2026

@mohamedelabbas1996 thanks for your response. It turns out my local database did not have roles! I had never run the assign_roles management command after importing this snapshot. It's working now! I have deployed to staging and run assign_roles and it works there now as well: https://app.preview.insectai.org/projects/18/team

@mohamedelabbas1996
Copy link
Contributor Author

@mohamedelabbas1996 thanks for your response. It turns out my local database did not have roles! I had never run the assign_roles management command after importing this snapshot. It's working now! I have deployed to staging and run assign_roles and it works there now as well: https://app.preview.insectai.org/projects/18/team

That’s great to hear!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@ami/users/roles.py`:
- Around line 83-84: Update the module/class docstring in ami/users/roles.py
where the sentence reads "it was later decided to that from" — remove the stray
"to" so the sentence reads "it was later decided that from" (edit the
module-level or class-level docstring containing that sentence).
🧹 Nitpick comments (2)
.agents/USER_PERMISSION_ROLES.md (1)

14-17: Consider using symbol-based references instead of line numbers.

Line number references (e.g., "Lines 12-85", "Lines 84-107") are fragile and will become stale as the code evolves. Consider referencing by class/method names or adding stable anchors in the source files.

ami/users/roles.py (1)

63-73: Minor docstring inconsistency.

The docstring states "Returns the names of roles" but the method actually returns role classes (not names). Consider updating to match the return type.

📝 Suggested fix
     `@staticmethod`
     def get_user_roles(project, user):
         """
-        Returns the names of roles assigned to a user for a specific project.
-        Or empty list if no role is found.
+        Returns the role classes assigned to a user for a specific project.
+        Returns an empty list if no role is found.
         """

Comment on lines +83 to +84
The original design allowed multiple roles per user per project, but it was later decided to
that from a UX and management perspective, a single role per user per project is preferable.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix grammar in docstring.

Line 83-84 has a grammatical error: "it was later decided to that from" should be "it was later decided that from".

📝 Suggested fix
-        The original design allowed multiple roles per user per project, but it was later decided to
-        that from a UX and management perspective, a single role per user per project is preferable.
+        The original design allowed multiple roles per user per project, but it was later decided
+        that from a UX and management perspective, a single role per user per project is preferable.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The original design allowed multiple roles per user per project, but it was later decided to
that from a UX and management perspective, a single role per user per project is preferable.
The original design allowed multiple roles per user per project, but it was later decided
that from a UX and management perspective, a single role per user per project is preferable.
🤖 Prompt for AI Agents
In `@ami/users/roles.py` around lines 83 - 84, Update the module/class docstring
in ami/users/roles.py where the sentence reads "it was later decided to that
from" — remove the stray "to" so the sentence reads "it was later decided that
from" (edit the module-level or class-level docstring containing that sentence).

@mihow mihow merged commit ae5ef7b into main Jan 22, 2026
9 checks passed
@mihow mihow deleted the feat/role-management-api branch January 22, 2026 07:48
@mihow mihow restored the feat/role-management-api branch January 22, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API For managing project membership & roles

3 participants

Comments