mirror of
https://github.com/makeplane/plane.git
synced 2026-08-29 01:58:36 +02:00
[WEB-8512] feat: add workspace member reactivation command (#9520)
* feat: add command to reactivate workspace members with error handling * 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 * 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 --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# 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 ProjectMember, 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") 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:
|
||||
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 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 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()
|
||||
|
||||
# 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. 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", "updated_at"], disable_auto_set_user=True)
|
||||
|
||||
self.stdout.write(
|
||||
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."
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user