Conversation
✅ Deploy Preview for antenna-preview canceled.
|
6cbef28 to
562f734
Compare
|
Note Other AI code review bot(s) detectedCodeRabbit 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. 📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
…sions to members viewset
…Lab/antenna into feat/role-management-api
Role Management APIThis API provides endpoints for managing project roles and member memberships. 1. List Available RolesEndpoint Name: List Available Roles Request: 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 MembersEndpoint Name: List Project Members Request: 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 DetailsEndpoint Name: Get Member Details Request: 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 MemberEndpoint Name: Add Member Request: Request Body: {
"email": "user@example.com",
"role_id": "Researcher"
}Request Fields:
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 RoleEndpoint Name: Update Member Role Request: Note: Use Request Body: {
"role_id": "Identifier"
}Request Fields:
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 MemberEndpoint Name: Remove Member Request: Note: Use 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) |
ami/users/api/serializers.py
Outdated
| class Meta: | ||
| model = User | ||
| fields = ["id", "name", "details", "image"] | ||
| fields = ["id", "name", "details", "image", "email"] |
There was a problem hiding this comment.
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
|
Thank you so much @mohamedelabbas1996, this looks great! Here are some notes from when hooking this up with FE. Notes
|
…izer and update the members viewset to use the UserProjectMembership model
…Lab/antenna into feat/role-management-api
…eck can access it
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>
|
@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.
|
There was a problem hiding this comment.
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 Noneami/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_clswithout the defensivegetattrpattern used inperform_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.""" |
There was a problem hiding this comment.
@annavik @mohamedelabbas1996 new serializer that includes email. the other one doesn't now.
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>
7074063 to
c7be19f
Compare
…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.
There was a problem hiding this comment.
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.
|
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! |
|
Yes I believe these existing users have a Role already, but I am not seeing
it in the UI until I change it there.
…On Tue, Jan 20, 2026 at 1:30 AM Anna Viklund ***@***.***> wrote:
*annavik* left a comment (RolnickLab/antenna#801)
<#801 (comment)>
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? That would be more of a problem!
—
Reply to this email directly, view it on GitHub
<#801 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABGTX2TJ32LQ73AITSIGSD4HXYS3AVCNFSM6AAAAAB2WTFF5GVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTONZRHA4DIOBYGA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
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 |
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! |
There was a problem hiding this comment.
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 usingbulk_createfor better performance on large datasets.The current implementation uses
get_or_createin 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,
executemanycould 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_sqldrops a table that doesn't exist at reversal time (since thebackwards()function recreates it afterward). While functionally correct due toIF EXISTS, this is semantically confusing. Consider adding a comment explaining this is intentionally a no-op sincebackwards()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 EXISTSbut add a comment explaining why it's intentionally mirrored.
|
@mohamedelabbas1996 thanks for your response. It turns out my local database did not have roles! I had never run the |
That’s great to hear! |
There was a problem hiding this comment.
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. """
| 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. |
There was a problem hiding this comment.
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.
| 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).





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 rolesGET /projects/<project-id>/members/to list project membersPOST /projects/<project-id>/members/to add a member and assign a rolePATCH /projects/<project-id>/members/<membership-id>/to update a member’s roleDELETE /projects/<project-id>/members/<membership-id>/to remove a member from the projectIntroduced
UserProjectMembershipas an explicit through modelAdded project-scoped permissions for membership management
Added nested routing for roles and members under projects
Added
is_memberfield to the project details responsetrueif the user is a project member or a superuserAdded UI support for managing project members and roles
Removed member management from the Project details admin page
Test coverage for membership API
Related Issues
#727
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.