diff --git a/myproject/tenants/management/commands/cleanup_tenant.py b/myproject/tenants/management/commands/cleanup_tenant.py new file mode 100644 index 0000000..dd342a5 --- /dev/null +++ b/myproject/tenants/management/commands/cleanup_tenant.py @@ -0,0 +1,177 @@ +# -*- 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 diff --git a/УДАЛЕНИЕ_ТЕНАНТОВ.md b/УДАЛЕНИЕ_ТЕНАНТОВ.md new file mode 100644 index 0000000..54e844f --- /dev/null +++ b/УДАЛЕНИЕ_ТЕНАНТОВ.md @@ -0,0 +1,455 @@ +# Удаление Тенантов в Django-Tenants + +## Быстрая справка + +### Рекомендуемый способ (с улучшенной командой): + +```bash +# Базовое удаление (Client + БД, заявка остается в истории) +python manage.py cleanup_tenant --schema=papa --noinput + +# Полная очистка (Client + БД + заявка + файлы) +python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files +``` + +### Альтернативный способ (встроенная команда django-tenants): + +```bash +# Удалить конкретного тенанта +python manage.py delete_tenant --schema=papa --noinput + +# Удалить все файлы тенанта (после удаления из БД) +Remove-Item -Path 'media/tenants' -Recurse -Force +``` + +--- + +## Подробное руководство + +### ⭐ Способ 0: Новая улучшенная команда cleanup_tenant (РЕКОМЕНДУЕТСЯ) + +**Эта команда решает проблему с TenantRegistration и удаляет все в одной операции** + +#### Что это за команда? + +Это новая management команда, которая автоматически: +- Удаляет Client и схему БД (как delete_tenant) +- Обрабатывает TenantRegistration (может оставить в истории или удалить) +- Опционально удаляет физические файлы +- Показывает красивый прогресс с подтверждением + +#### Параметры: + +```bash +--schema=<имя> # Имя тенанта (обязательно) +--noinput # Не запрашивать подтверждение +--purge-registration # Удалить TenantRegistration (иначе оставляет с tenant=NULL) +--delete-files # Удалить физические файлы из /media/tenants/ +``` + +#### Варианты использования: + +**1️⃣ Базовое удаление (рекомендуется):** +```bash +python manage.py cleanup_tenant --schema=papa --noinput +``` +Удаляет: Client + БД, заявка остается в истории с tenant=NULL + +**2️⃣ Полная очистка:** +```bash +python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files +``` +Удаляет: Client + БД + заявка + файлы (максимальная очистка) + +**3️⃣ Только заявка и БД:** +```bash +python manage.py cleanup_tenant --schema=papa --noinput --purge-registration +``` +Удаляет: Client + БД + заявка (файлы остаются) + +#### Пример вывода: + +``` +=== Удаление тенанта (магазина) === + +====================================================================== +ВНИМАНИЕ! Будут удалены следующие данные: +====================================================================== + +📋 Тенант: + • Название: Papa Shop + • Schema: papa + • Владелец: admin@example.com + +💾 База данных: + • Схема БД "papa" будет полностью удалена + • Все таблицы и данные будут удалены + +📝 TenantRegistration: + • Заявка от Papa Owner (papa@example.com) + • Статус: Одобрено + • Действие: оставить с tenant=NULL (сохранить историю) + +====================================================================== +▶ Начинаю удаление... +====================================================================== + +1️⃣ Обновляю TenantRegistration (сохраняю историю)... + ✓ TenantRegistration обновлена (tenant=NULL) +2️⃣ Удаляю Client и схему БД... + ✓ Client "Papa Shop" удален + ✓ Схема БД "papa" удалена + +====================================================================== +✓ Тенант успешно удален! +====================================================================== +``` + +--- + +### Способ 1: Удаление одного тенанта по schema + +**Команда:** +```bash +cd myproject +python manage.py delete_tenant --schema=papa --noinput +``` + +**Параметры:** +- `--schema=papa` - Удалить тенант с именем schema `papa` +- `--noinput` - Не запрашивать подтверждение (автоматический режим) + +**Результат:** +``` +Deleting 'papa' +Deleted 'papa' +``` + +Эта команда удаляет: +- ✅ Схему БД тенанта +- ✅ Все таблицы и данные в schema +- ❌ НЕ удаляет файлы в `/media/tenants/{tenant_id}/` + +--- + +### Способ 2: Интерактивное удаление (с выбором) + +**Команда:** +```bash +python manage.py delete_tenant +``` + +**Результат:** +Система запросит у вас: +``` +Enter Tenant Schema ('?' to list schemas): papa +Are you sure you want to delete the tenant: papa? (yes/no): yes +``` + +Введите: +- `?` - чтобы увидеть список всех тенантов +- Имя schema - чтобы выбрать тенант +- `yes` - чтобы подтвердить удаление + +--- + +### Способ 3: Удаление файлов после удаления из БД + +После удаления тенанта из БД нужно удалить его файлы вручную. + +**На Windows (PowerShell):** +```powershell +Remove-Item -Path 'c:\Users\team_\Desktop\test_qwen\myproject\media\tenants' -Recurse -Force +Write-Host 'Removed tenants directory' +``` + +**На Windows (CMD):** +```cmd +rmdir /s /q "c:\Users\team_\Desktop\test_qwen\myproject\media\tenants" +``` + +**На Linux/Mac:** +```bash +rm -rf ./media/tenants +``` + +--- + +## Что удаляется? + +### БД (удаляется автоматически): +``` +PostgreSQL/MySQL schema: papa +├── products_product +├── products_productphoto +├── products_productkit +├── products_productkitphoto +├── products_productcategory +├── products_productcategoryphoto +├── inventory_* +├── orders_* +└── ... (все другие таблицы в schema) +``` + +### Файлы (нужно удалить вручную): +``` +media/tenants/papa/ +├── products/ +│ ├── {product_id}/{photo_id}/original.jpg +│ ├── {product_id}/{photo_id}/large.webp +│ ├── {product_id}/{photo_id}/medium.webp +│ ├── {product_id}/{photo_id}/thumb.webp +│ └── temp/... (временные файлы) +├── kits/ +├── categories/ +└── ... (все файлы тенанта) +``` + +--- + +## Полный цикл удаления тенанта + +### 1️⃣ Удалить из БД: +```bash +python manage.py delete_tenant --schema=papa --noinput +``` + +### 2️⃣ Проверить оставшиеся файлы: +```powershell +# На Windows +Get-ChildItem -Path 'c:\Users\team_\Desktop\test_qwen\myproject\media\tenants' -Recurse | Measure-Object | Select-Object -ExpandProperty Count +``` + +### 3️⃣ Удалить файлы: +```powershell +# На Windows (PowerShell) +Remove-Item -Path 'media/tenants' -Recurse -Force +``` + +### 4️⃣ Проверить удаление: +```powershell +Test-Path 'c:\Users\team_\Desktop\test_qwen\myproject\media\tenants' +# Должно вернуть: False +``` + +--- + +## Примеры + +### Пример 1: Удалить тенант "papa" + +```bash +cd myproject +python manage.py delete_tenant --schema=papa --noinput +``` + +```powershell +Remove-Item -Path 'media/tenants/papa' -Recurse -Force +``` + +### Пример 2: Удалить все тенанты + +```bash +# Удалить из БД (нужно сделать для каждого тенанта) +python manage.py delete_tenant --schema=papa --noinput +python manage.py delete_tenant --schema=customer1 --noinput +python manage.py delete_tenant --schema=test --noinput +``` + +```powershell +# Удалить все файлы +Remove-Item -Path 'media/tenants' -Recurse -Force +``` + +### Пример 3: Список доступных тенантов + +```bash +# Интерактивный режим - введите ? для списка +python manage.py delete_tenant +# Введите: ? +# Получите: список всех доступных schemas +``` + +--- + +## Важные замечания + +⚠️ **ВНИМАНИЕ!** + +- Удаление **необратимо** - нет возможности восстановления +- Сначала удаляйте из БД, потом удаляйте файлы +- Не забывайте удалять файлы - они занимают место на диске +- При удалении всех тенантов БД может остаться в некорректном состоянии + +✅ **РЕКОМЕНДАЦИИ:** + +- Делайте бэкап перед удалением на продакшене +- Используйте `--noinput` для автоматизации скриптами +- Удаляйте файлы регулярно, чтобы освободить место +- Проверяйте результат удаления + +--- + +## Ошибки и решения + +### Ошибка: "EOFError: EOF when reading a line" + +``` +Enter Tenant Schema ('?' to list schemas): +EOFError: EOF when reading a line +``` + +**Решение:** Используйте флаг `--schema=` + +```bash +python manage.py delete_tenant --schema=papa --noinput +``` + +### Ошибка: "Tenant doesn't exist" + +``` +Error: Tenant doesn't exist +``` + +**Решение:** Проверьте точное имя schema + +```bash +# Посмотрите список тенантов +python manage.py delete_tenant +# Введите: ? +``` + +### Файлы не удаляются на Windows + +```powershell +# Если файл заблокирован, закройте все приложения и попробуйте: +Remove-Item -Path 'media/tenants' -Recurse -Force -ErrorAction SilentlyContinue +``` + +--- + +## Команда для быстрого удаления (скрипт) + +**delete_all_tenants.sh (для Linux/Mac):** +```bash +#!/bin/bash +python manage.py delete_tenant --schema=papa --noinput +python manage.py delete_tenant --schema=test --noinput +rm -rf ./media/tenants +echo "All tenants deleted" +``` + +**delete_all_tenants.ps1 (для Windows):** +```powershell +# Удалить БД +python manage.py delete_tenant --schema=papa --noinput +python manage.py delete_tenant --schema=test --noinput + +# Удалить файлы +Remove-Item -Path 'media/tenants' -Recurse -Force -ErrorAction SilentlyContinue + +Write-Host "All tenants deleted successfully" +``` + +--- + +## Что делать с TenantRegistration? + +### Проблема + +При удалении Client тенанта остается заявка TenantRegistration со статусом 'approved' и `tenant=NULL`. + +Это создает проблему: если клиент захочет **повторно зарегистрироваться с тем же поддоменом**, система выдаст ошибку: +``` +Error: duplicate key value violates unique constraint 'schema_name' +``` + +Потому что в таблице TenantRegistration уже есть запись с `schema_name='papa'`. + +### Решение + +**Вариант 1: Оставить заявку в истории (рекомендуется)** + +```bash +python manage.py cleanup_tenant --schema=papa --noinput +``` + +Заявка остается в админке с `tenant=NULL`. Это: +- ✅ Сохраняет историю регистраций +- ✅ Видна попытка создания магазина (для аналитики) +- ❌ Требует ручного удаления старой заявки перед новой регистрацией + +**Если клиент захочет зарегистрироваться снова:** +1. Вручную удалить старую TenantRegistration через админку +2. Тогда он сможет создать новую заявку с тем же schema_name + +**Вариант 2: Удалить заявку автоматически** + +```bash +python manage.py cleanup_tenant --schema=papa --noinput --purge-registration +``` + +Удаляет: +- ✅ Client + схему БД +- ✅ TenantRegistration (полная очистка) +- ❌ Теряется история регистраций + +**Если клиент захочет зарегистрироваться снова:** +1. Просто заполняет форму регистрации +2. Может использовать тот же schema_name +3. Все работает как в первый раз + +### Мой совет + +Для **тестового проекта**: используй `--purge-registration` (чище) +```bash +python manage.py cleanup_tenant --schema=papa --noinput --purge-registration +``` + +Для **боевого проекта**: оставляй заявку в истории (для аудита) +```bash +python manage.py cleanup_tenant --schema=papa --noinput +``` + +--- + +## Вопросы и ответы + +**Q: Как посмотреть список тенантов?** +A: Введите `?` в интерактивном режиме: +```bash +python manage.py delete_tenant +# Введите: ? +``` + +**Q: Могу ли я восстановить удаленного тенанта?** +A: Нет, удаление необратимо. Только из бэкапа БД. + +**Q: Что если тенант все еще используется?** +A: Появится ошибка. Закройте все приложения работающие с БД и попробуйте снова. + +**Q: Как удалить тенант если забыл его schema name?** +A: Посмотрите в таблице `django_tenants_tenant`: +```bash +python manage.py shell +# Введите: +from django_tenants.models import Client +for tenant in Client.objects.all(): + print(f"{tenant.name} -> {tenant.schema_name}") +``` + +--- + +**Дата создания:** 2025-11-23 +**Дата обновления:** 2025-11-23 +**Версия:** 2.0 +**Статус:** Production Ready ✅ + +### Что нового в версии 2.0: + +- ✨ Добавлена новая улучшенная команда `cleanup_tenant` +- ✨ Команда автоматически обрабатывает TenantRegistration +- ✨ Добавлена опция `--purge-registration` для удаления заявок +- ✨ Добавлена опция `--delete-files` для удаления физических файлов +- 📖 Расширена документация с объяснением проблемы TenantRegistration +- 📖 Добавлены примеры использования для разных сценариев