Files
octopus/myproject/tenants/management/commands/cleanup_tenant.py
Andrey Smakotin 8079abe939 Feat: Add cleanup_tenant management command for improved tenant deletion
- Create new cleanup_tenant command with better tenant deletion workflow
- Handle TenantRegistration automatic processing (keep history or delete)
- Add --purge-registration flag to remove TenantRegistration records
- Add --delete-files flag to remove physical tenant files from /media/tenants/
- Provide detailed deletion summary with confirmation step
- Update УДАЛЕНИЕ_ТЕНАНТОВ.md documentation with:
  * Complete guide for new cleanup_tenant command
  * Explanation of TenantRegistration problem and solutions
  * Recommendations for different use cases (test vs production)
  * Examples of all command variants

Tested:
- Successfully deleted grach tenant (no registration)
- Successfully deleted bingo tenant with --purge-registration flag
- Verified registration records are properly managed
- All database and file operations work correctly

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:32:32 +03:00

178 lines
8.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Management команда для полного удаления тенанта и его данных.
ВАЖНО: Это необратимая операция! Все данные тенанта будут удалены.
Использование:
# Базовое удаление (только Client и БД, заявка остается с tenant=NULL)
python manage.py cleanup_tenant --schema=papa --noinput
# Удалить Client, БД и TenantRegistration
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration
# Полная очистка: Client, БД, файлы и заявка
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files
"""
from django.core.management.base import BaseCommand
from django.db import transaction, connection
from tenants.models import Client, TenantRegistration
import shutil
import os
class Command(BaseCommand):
help = 'Полное удаление тенанта (магазина) с его схемой БД и опционально файлами'
def add_arguments(self, parser):
parser.add_argument(
'--schema',
type=str,
help='Имя схемы БД тенанта для удаления (пример: papa)'
)
parser.add_argument(
'--noinput',
action='store_true',
help='Не запрашивать подтверждение (для автоматизации)'
)
parser.add_argument(
'--purge-registration',
action='store_true',
help='Также удалить связанную заявку на регистрацию (TenantRegistration)'
)
parser.add_argument(
'--delete-files',
action='store_true',
help='Также удалить физические файлы тенанта из /media/tenants/{schema_name}/'
)
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('\n=== Tenant (Shop) Deletion ===\n'))
# Получаем параметры
schema_name = options.get('schema')
noinput = options.get('noinput', False)
purge_registration = options.get('purge_registration', False)
delete_files = options.get('delete_files', False)
# Валидация параметров
if not schema_name:
self.stdout.write(self.style.ERROR('\nERROR: specify --schema=<schema_name>\n'))
return
# Проверяем что тенант существует
try:
tenant = Client.objects.get(schema_name=schema_name)
except Client.DoesNotExist:
self.stdout.write(self.style.ERROR(f'\nERROR: Tenant with schema="{schema_name}" not found\n'))
return
# Показываем информацию о том что будет удалено
self.stdout.write('='*70)
self.stdout.write(self.style.WARNING('WARNING! The following data will be deleted:'))
self.stdout.write('='*70)
self.stdout.write(f'\n[Tenant]')
self.stdout.write(f' Name: {tenant.name}')
self.stdout.write(f' Schema: {tenant.schema_name}')
self.stdout.write(f' Owner: {tenant.owner_email}')
self.stdout.write(f'\n[Database]')
self.stdout.write(f' Schema "{schema_name}" will be completely deleted')
self.stdout.write(f' All tables and data will be deleted')
# Проверяем TenantRegistration
registration = TenantRegistration.objects.filter(tenant=tenant).first()
if registration:
self.stdout.write(f'\n[TenantRegistration]')
self.stdout.write(f' From: {registration.owner_name} ({registration.owner_email})')
self.stdout.write(f' Status: {registration.get_status_display()}')
if purge_registration:
self.stdout.write(f' Action: DELETE (--purge-registration)')
else:
self.stdout.write(f' Action: keep with tenant=NULL (keep history)')
# Проверяем файлы
if delete_files:
tenant_files_path = f'media/tenants/{schema_name}'
if os.path.exists(tenant_files_path):
file_count = sum([len(files) for r, d, files in os.walk(tenant_files_path)])
self.stdout.write(f'\n[Files]')
self.stdout.write(f' Path: {tenant_files_path}')
self.stdout.write(f' Count: {file_count}')
self.stdout.write(f' Action: DELETE')
else:
self.stdout.write(f'\n[Files]')
self.stdout.write(f' Path {tenant_files_path} not found (skipping)')
self.stdout.write('\n' + '='*70)
# Запрашиваем подтверждение
if not noinput:
confirm = input(self.style.WARNING('\nAre you sure? Type "yes" to confirm: '))
if confirm.lower() not in ['yes', 'y']:
self.stdout.write(self.style.ERROR('\nCancelled\n'))
return
# Начинаем удаление
self.stdout.write('\n' + '='*70)
self.stdout.write(self.style.SUCCESS('Starting deletion...'))
self.stdout.write('='*70 + '\n')
try:
with transaction.atomic():
# 1. Обновляем TenantRegistration (если нужно)
if registration:
if purge_registration:
self.stdout.write('[1] Deleting TenantRegistration...')
reg_id = registration.id
registration.delete()
self.stdout.write(self.style.SUCCESS(f' OK: TenantRegistration (ID={reg_id}) deleted'))
else:
self.stdout.write('[1] Updating TenantRegistration (keeping history)...')
registration.tenant = None
registration.save()
self.stdout.write(self.style.SUCCESS(' OK: TenantRegistration updated (tenant=NULL)'))
# 2. Удаляем Client (это вызовет удаление схемы БД через django-tenants)
self.stdout.write('[2] Deleting Client and DB schema...')
tenant_name = tenant.name
tenant_schema = tenant.schema_name
tenant.delete()
self.stdout.write(self.style.SUCCESS(f' OK: Client "{tenant_name}" deleted'))
self.stdout.write(self.style.SUCCESS(f' OK: DB schema "{tenant_schema}" deleted'))
# 3. Удаляем файлы (вне transaction, потому что это файловая система, а не БД)
if delete_files:
self.stdout.write('[3] Deleting tenant files...')
tenant_files_path = f'media/tenants/{schema_name}'
if os.path.exists(tenant_files_path):
try:
shutil.rmtree(tenant_files_path)
self.stdout.write(self.style.SUCCESS(f' OK: Folder {tenant_files_path} deleted'))
except Exception as e:
self.stdout.write(self.style.ERROR(f' ERROR: Failed to delete folder: {e}'))
raise
else:
self.stdout.write(self.style.WARNING(f' WARN: Folder {tenant_files_path} not found'))
# Итоговое сообщение
self.stdout.write('\n' + '='*70)
self.stdout.write(self.style.SUCCESS('SUCCESS: Tenant has been deleted!'))
self.stdout.write('='*70)
self.stdout.write(self.style.WARNING('\nDeletion summary:'))
self.stdout.write(f' - Client deleted')
self.stdout.write(f' - DB schema "{schema_name}" deleted')
if registration:
if purge_registration:
self.stdout.write(f' - TenantRegistration deleted')
else:
self.stdout.write(f' - TenantRegistration kept (in history)')
if delete_files:
self.stdout.write(f' - Files deleted from /media/tenants/{schema_name}/')
self.stdout.write('')
except Exception as e:
self.stdout.write(self.style.ERROR(f'\nERROR during tenant deletion: {e}'))
self.stdout.write(self.style.ERROR(' Data may be partially deleted. Check DB state.\n'))
raise