Files
octopus/myproject/tenants/management/commands/clear_tenant_data.py
Andrey Smakotin e54d7d04d7 feat: Добавлены команды управления данными тенантов и исправлены фильтры по статусу товаров
Добавлено:
- Команда 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 теперь предлагает пользователю инициализировать системные данные после очистки
- Добавлена детальная информация о процессе очистки и инициализации данных
2025-12-12 04:58:26 +03:00

201 lines
9.7 KiB
Python
Raw 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 останутся.
Использование:
# Очистка всех данных тенанта
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('')