From c5ca53b4be1ffc6693234b7ed9a65d5808585353 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 31 Jul 2026 22:58:29 +0530 Subject: [PATCH 1/3] feat: add command to reactivate workspace members with error handling --- .../commands/reactivate_workspace_member.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 apps/api/plane/db/management/commands/reactivate_workspace_member.py diff --git a/apps/api/plane/db/management/commands/reactivate_workspace_member.py b/apps/api/plane/db/management/commands/reactivate_workspace_member.py new file mode 100644 index 00000000000..7176fb1b199 --- /dev/null +++ b/apps/api/plane/db/management/commands/reactivate_workspace_member.py @@ -0,0 +1,67 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +# Django imports +from django.core.management import BaseCommand, CommandError + +# Module imports +from plane.db.models import User, Workspace, WorkspaceMember + + +class Command(BaseCommand): + help = "Reactivate a workspace member given a workspace slug and user email" + + def add_arguments(self, parser): + # Positional arguments + parser.add_argument("slug", type=str, help="workspace slug") + parser.add_argument("email", type=str, help="user email") + + def handle(self, *args, **options): + # get the workspace slug and user email from console + slug = options.get("slug", False) + email = options.get("email", False) + + # raise error if slug is not present + if not slug: + raise CommandError("Error: Workspace slug is required") + + # raise error if email is not present + if not email: + raise CommandError("Error: Email is required") + + # filter the user + user = User.objects.filter(email=email).first() + + # Raise error if the user is not present + if not user: + raise CommandError(f"Error: User with {email} does not exists") + + # filter the workspace + workspace = Workspace.objects.filter(slug=slug).first() + + # Raise error if the workspace is not present + if not workspace: + raise CommandError(f"Error: Workspace with slug {slug} does not exists") + + # Find the workspace membership (includes inactive members; soft-deleted are excluded by default manager) + workspace_member = WorkspaceMember.objects.filter(workspace=workspace, member=user).first() + + # Raise error if the membership is not present + if not workspace_member: + raise CommandError(f"Error: User {email} is not a member of workspace {slug}") + + # If already active, report without erroring + if workspace_member.is_active: + self.stdout.write( + self.style.SUCCESS(f"User {email} is already an active member of workspace {slug}") + ) + return + + # Reactivate the membership + workspace_member.is_active = True + workspace_member.save() + + self.stdout.write( + self.style.SUCCESS(f"User {email} reactivated successfully in workspace {slug}") + ) From 4b4bc138b6b0e43fdbb2c898423ac8b8fdf4883d Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 2 Aug 2026 02:54:29 +0530 Subject: [PATCH 2/3] fix: address review comments on reactivate command - normalize email input to match User.save lowercasing - fix grammar in error messages - limit save to is_active so audit fields are not clobbered Claude-Session: https://claude.ai/code/session_01NGjXVUi4D8JGWy7b7KDNaN --- .../commands/reactivate_workspace_member.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/api/plane/db/management/commands/reactivate_workspace_member.py b/apps/api/plane/db/management/commands/reactivate_workspace_member.py index 7176fb1b199..ab0fe720652 100644 --- a/apps/api/plane/db/management/commands/reactivate_workspace_member.py +++ b/apps/api/plane/db/management/commands/reactivate_workspace_member.py @@ -30,19 +30,22 @@ def handle(self, *args, **options): if not email: raise CommandError("Error: Email is required") + # emails are stored lowercased and stripped (User.save) + email = email.strip().lower() + # filter the user user = User.objects.filter(email=email).first() # Raise error if the user is not present if not user: - raise CommandError(f"Error: User with {email} does not exists") + raise CommandError(f"Error: User with {email} does not exist") # filter the workspace workspace = Workspace.objects.filter(slug=slug).first() # Raise error if the workspace is not present if not workspace: - raise CommandError(f"Error: Workspace with slug {slug} does not exists") + raise CommandError(f"Error: Workspace with slug {slug} does not exist") # Find the workspace membership (includes inactive members; soft-deleted are excluded by default manager) workspace_member = WorkspaceMember.objects.filter(workspace=workspace, member=user).first() @@ -58,9 +61,10 @@ def handle(self, *args, **options): ) return - # Reactivate the membership + # Reactivate the membership; limit the write so audit fields set by + # BaseModel.save (no request user here) are not persisted workspace_member.is_active = True - workspace_member.save() + workspace_member.save(update_fields=["is_active"]) self.stdout.write( self.style.SUCCESS(f"User {email} reactivated successfully in workspace {slug}") From c78ea5c72e78ad51a24f0d0330a6171ff10cd875 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 2 Aug 2026 03:13:04 +0530 Subject: [PATCH 3/3] fix: normalize inputs before validation and report partial reactivation - strip slug/email before the required checks so whitespace-only args are rejected - bump updated_at and pass disable_auto_set_user so the audit fields survive - report the restored role, inactive project memberships, and inactive accounts Claude-Session: https://claude.ai/code/session_01NGjXVUi4D8JGWy7b7KDNaN --- .../commands/reactivate_workspace_member.py | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/apps/api/plane/db/management/commands/reactivate_workspace_member.py b/apps/api/plane/db/management/commands/reactivate_workspace_member.py index ab0fe720652..4ff6b4777e8 100644 --- a/apps/api/plane/db/management/commands/reactivate_workspace_member.py +++ b/apps/api/plane/db/management/commands/reactivate_workspace_member.py @@ -6,7 +6,7 @@ from django.core.management import BaseCommand, CommandError # Module imports -from plane.db.models import User, Workspace, WorkspaceMember +from plane.db.models import ProjectMember, User, Workspace, WorkspaceMember class Command(BaseCommand): @@ -19,8 +19,12 @@ def add_arguments(self, parser): def handle(self, *args, **options): # get the workspace slug and user email from console - slug = options.get("slug", False) - email = options.get("email", False) + slug = options.get("slug") or "" + email = options.get("email") or "" + + # normalize before validating; emails are stored lowercased and stripped (User.save) + slug = slug.strip() + email = email.strip().lower() # raise error if slug is not present if not slug: @@ -30,9 +34,6 @@ def handle(self, *args, **options): if not email: raise CommandError("Error: Email is required") - # emails are stored lowercased and stripped (User.save) - email = email.strip().lower() - # filter the user user = User.objects.filter(email=email).first() @@ -56,16 +57,36 @@ def handle(self, *args, **options): # If already active, report without erroring if workspace_member.is_active: - self.stdout.write( - self.style.SUCCESS(f"User {email} is already an active member of workspace {slug}") - ) + self.stdout.write(self.style.SUCCESS(f"User {email} is already an active member of workspace {slug}")) return - # Reactivate the membership; limit the write so audit fields set by - # BaseModel.save (no request user here) are not persisted + # Reactivate the membership. update_fields keeps the write to the columns that change, and + # disable_auto_set_user stops BaseModel.save from nulling created_by/updated_by when there + # is no request user, as is the case in a management command. workspace_member.is_active = True - workspace_member.save(update_fields=["is_active"]) + workspace_member.save(update_fields=["is_active", "updated_at"], disable_auto_set_user=True) self.stdout.write( - self.style.SUCCESS(f"User {email} reactivated successfully in workspace {slug}") + self.style.SUCCESS( + f"User {email} reactivated successfully in workspace {slug} as {workspace_member.get_role_display()}" + ) ) + + # Removing a member also deactivates their project memberships, which this command leaves alone + inactive_projects = ProjectMember.objects.filter(workspace=workspace, member=user, is_active=False).count() + if inactive_projects: + self.stdout.write( + self.style.WARNING( + f"Note: {inactive_projects} project membership(s) remain inactive; " + "removing a member also deactivates their project memberships" + ) + ) + + # A member removed by deactivating their account also has an inactive user record + if not user.is_active: + self.stdout.write( + self.style.WARNING( + f"Note: the account for {email} is deactivated and cannot sign in. " + f"Run 'python manage.py activate_user {email}' to activate it." + ) + )