Добавлено: - Команда clear_tenant_data для полной очистки данных тенанта без удаления схемы * Очищает все таблицы через TRUNCATE CASCADE * Сбрасывает ID-последовательности * Сохраняет схему БД и запись Client * Поддержка флага --noinput для автоматизации - Команда init_tenant_data для инициализации системных данных тенанта * Создаёт системного клиента (АНОНИМНЫЙ ПОКУПАТЕЛЬ для POS) * Создаёт 8 системных статусов заказов * Создаёт 5 системных способов оплаты * Поддержка флага --reset для пересоздания данных Исправлено: - Заменены устаревшие фильтры is_active на status='active' для Product и ProductKit * products/views/category_views.py: исправлены фильтры в build_category_tree и get_context_data * products/services/kit_pricing.py: исправлены фильтры при получении товаров из variant_group * products/models/kits.py: исправлен фильтр в get_available_products * Устранена ошибка FieldError при работе со списком категорий Улучшено: - Команда clear_tenant_data теперь предлагает пользователю инициализировать системные данные после очистки - Добавлена детальная информация о процессе очистки и инициализации данных
201 lines
9.7 KiB
Python
201 lines
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Management команда для полной очистки данных тенанта без удаления схемы и самого тенанта.
|
||
|
||
ВАЖНО: Это необратимая операция! Все данные тенанта будут удалены, но схема и запись в Client останутся.
|
||
|
||
Использование:
|
||
# Очистка всех данных тенанта
|
||
python manage.py clear_tenant_data --schema=anatol --noinput
|
||
|
||
# С подтверждением
|
||
python manage.py clear_tenant_data --schema=anatol
|
||
"""
|
||
from django.core.management.base import BaseCommand
|
||
from django.db import connection
|
||
from django.apps import apps
|
||
from django_tenants.utils import schema_context
|
||
from tenants.models import Client
|
||
|
||
|
||
class Command(BaseCommand):
|
||
help = 'Полная очистка всех данных в схеме тенанта без удаления схемы и Client'
|
||
|
||
def add_arguments(self, parser):
|
||
parser.add_argument(
|
||
'--schema',
|
||
type=str,
|
||
required=True,
|
||
help='Имя схемы БД тенанта для очистки (пример: anatol)'
|
||
)
|
||
parser.add_argument(
|
||
'--noinput',
|
||
action='store_true',
|
||
help='Не запрашивать подтверждение (для автоматизации)'
|
||
)
|
||
|
||
def handle(self, *args, **options):
|
||
self.stdout.write(self.style.SUCCESS('\n=== Очистка данных тенанта ===\n'))
|
||
|
||
# Получаем параметры
|
||
schema_name = options.get('schema')
|
||
noinput = options.get('noinput', False)
|
||
|
||
# Проверяем что тенант существует
|
||
try:
|
||
tenant = Client.objects.get(schema_name=schema_name)
|
||
except Client.DoesNotExist:
|
||
self.stdout.write(self.style.ERROR(f'\nОШИБКА: Тенант со схемой "{schema_name}" не найден\n'))
|
||
return
|
||
|
||
# Получаем список всех таблиц в схеме (кроме служебных django)
|
||
tables = self._get_tenant_tables(schema_name)
|
||
|
||
# Показываем информацию о том что будет удалено
|
||
self.stdout.write('='*70)
|
||
self.stdout.write(self.style.WARNING('ВНИМАНИЕ! Следующие данные будут удалены:'))
|
||
self.stdout.write('='*70)
|
||
self.stdout.write(f'\n[Тенант]')
|
||
self.stdout.write(f' Название: {tenant.name}')
|
||
self.stdout.write(f' Схема: {tenant.schema_name}')
|
||
self.stdout.write(f' Владелец: {tenant.owner_email}')
|
||
|
||
self.stdout.write(f'\n[База данных]')
|
||
self.stdout.write(f' Схема: "{schema_name}" (НЕ будет удалена)')
|
||
self.stdout.write(f' Будет очищено таблиц: {len(tables)}')
|
||
|
||
if tables:
|
||
self.stdout.write(f'\n[Таблицы для очистки]')
|
||
for table in tables[:20]: # Показываем первые 20
|
||
self.stdout.write(f' - {table}')
|
||
if len(tables) > 20:
|
||
self.stdout.write(f' ... и ещё {len(tables) - 20} таблиц')
|
||
|
||
self.stdout.write(f'\n[Что НЕ будет удалено]')
|
||
self.stdout.write(f' ✓ Запись Client (тенант)')
|
||
self.stdout.write(f' ✓ Схема БД "{schema_name}"')
|
||
self.stdout.write(f' ✓ Домены тенанта')
|
||
|
||
self.stdout.write('\n' + '='*70)
|
||
|
||
# Запрашиваем подтверждение
|
||
if not noinput:
|
||
confirm = input(self.style.WARNING('\nВы уверены? Введите "yes" для подтверждения: '))
|
||
if confirm.lower() not in ['yes', 'да', 'y']:
|
||
self.stdout.write(self.style.ERROR('\nОтменено\n'))
|
||
return
|
||
|
||
# Начинаем очистку
|
||
self.stdout.write('\n' + '='*70)
|
||
self.stdout.write(self.style.SUCCESS('Начинаю очистку данных...'))
|
||
self.stdout.write('='*70 + '\n')
|
||
|
||
try:
|
||
deleted_count = self._clear_tenant_data(schema_name, tables)
|
||
|
||
# Итоговое сообщение
|
||
self.stdout.write('\n' + '='*70)
|
||
self.stdout.write(self.style.SUCCESS('УСПЕХ: Данные тенанта очищены!'))
|
||
self.stdout.write('='*70)
|
||
self.stdout.write(self.style.WARNING('\nИтоги очистки:'))
|
||
self.stdout.write(f' - Очищено таблиц: {deleted_count}')
|
||
self.stdout.write(f' - Схема "{schema_name}" сохранена')
|
||
self.stdout.write(f' - Тенант "{tenant.name}" сохранён')
|
||
|
||
# Предлагаем инициализировать системные данные
|
||
self._suggest_init_data(schema_name)
|
||
|
||
except Exception as e:
|
||
self.stdout.write(self.style.ERROR(f'\nОШИБКА при очистке данных: {e}'))
|
||
self.stdout.write(self.style.ERROR(' Данные могут быть частично удалены. Проверьте состояние БД.\n'))
|
||
raise
|
||
|
||
def _get_tenant_tables(self, schema_name):
|
||
"""
|
||
Получает список всех таблиц в схеме тенанта (кроме служебных).
|
||
"""
|
||
with connection.cursor() as cursor:
|
||
cursor.execute("""
|
||
SELECT table_name
|
||
FROM information_schema.tables
|
||
WHERE table_schema = %s
|
||
AND table_type = 'BASE TABLE'
|
||
AND table_name NOT IN (
|
||
'django_migrations',
|
||
'django_content_type',
|
||
'django_admin_log',
|
||
'django_session'
|
||
)
|
||
ORDER BY table_name
|
||
""", [schema_name])
|
||
|
||
return [row[0] for row in cursor.fetchall()]
|
||
|
||
def _clear_tenant_data(self, schema_name, tables):
|
||
"""
|
||
Очищает все данные в таблицах тенанта.
|
||
Использует TRUNCATE CASCADE для удаления связанных данных.
|
||
"""
|
||
deleted_count = 0
|
||
|
||
with connection.cursor() as cursor:
|
||
# Устанавливаем search_path на схему тенанта
|
||
cursor.execute(f'SET search_path TO {schema_name}')
|
||
|
||
# Отключаем триггеры и ограничения для быстрой очистки
|
||
self.stdout.write('[1] Отключение триггеров и ограничений...')
|
||
cursor.execute('SET session_replication_role = replica;')
|
||
|
||
# Очищаем каждую таблицу
|
||
self.stdout.write(f'[2] Очистка {len(tables)} таблиц...')
|
||
for i, table in enumerate(tables, 1):
|
||
try:
|
||
# Используем TRUNCATE CASCADE для удаления связанных данных
|
||
cursor.execute(f'TRUNCATE TABLE {table} CASCADE;')
|
||
deleted_count += 1
|
||
|
||
if i % 10 == 0:
|
||
self.stdout.write(f' Очищено таблиц: {i}/{len(tables)}')
|
||
|
||
except Exception as e:
|
||
self.stdout.write(
|
||
self.style.WARNING(f' Предупреждение: не удалось очистить таблицу {table}: {e}')
|
||
)
|
||
|
||
# Включаем триггеры и ограничения обратно
|
||
self.stdout.write('[3] Включение триггеров и ограничений...')
|
||
cursor.execute('SET session_replication_role = DEFAULT;')
|
||
|
||
# Сбрасываем последовательности (sequences) для ID
|
||
self.stdout.write('[4] Сброс последовательностей (ID sequences)...')
|
||
cursor.execute("""
|
||
SELECT 'SELECT SETVAL(' ||
|
||
quote_literal(quote_ident(sequence_schema) || '.' || quote_ident(sequence_name)) ||
|
||
', 1, false);'
|
||
FROM information_schema.sequences
|
||
WHERE sequence_schema = %s;
|
||
""", [schema_name])
|
||
|
||
for row in cursor.fetchall():
|
||
try:
|
||
cursor.execute(row[0])
|
||
except Exception as e:
|
||
# Игнорируем ошибки при сбросе последовательностей
|
||
pass
|
||
|
||
self.stdout.write(self.style.SUCCESS(f' OK: Все данные очищены'))
|
||
|
||
return deleted_count
|
||
|
||
def _suggest_init_data(self, schema_name):
|
||
"""
|
||
Предлагает пользователю инициализировать системные данные.
|
||
"""
|
||
self.stdout.write('\n' + '-'*70)
|
||
self.stdout.write(self.style.WARNING('Рекомендация:'))
|
||
self.stdout.write('-'*70)
|
||
self.stdout.write('Для работы системы необходимы системные данные.')
|
||
self.stdout.write('Выполните команду для их создания:\n')
|
||
self.stdout.write(self.style.SUCCESS(f' python manage.py init_tenant_data --schema={schema_name}'))
|
||
self.stdout.write('')
|