# -*- 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=\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