- 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>
178 lines
8.5 KiB
Python
178 lines
8.5 KiB
Python
# -*- 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
|