2026-09-07 19:43:48 +03:00

849 lines
33 KiB
Python
Raw Permalink 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.

"""
Management команда для проверки и очистки чатов с некорректными типами.
Использование:
# Найти все чаты с некорректными типами
python manage.py check_chats --find
# Проверить конкретный чат
python manage.py check_chats --check 6
# Удалить все чаты с некорректными типами
python manage.py check_chats --cleanup
# Удалить конкретный чат
python manage.py check_chats --delete 6
"""
from django.core.management.base import BaseCommand, CommandError
from communications.models import Chat
VALID_TYPES = [
"private",
"group",
"channel",
"announcement",
"global",
"comments",
]
class Command(BaseCommand):
help = "Проверка и очистка чатов с некорректными типами"
def add_arguments(self, parser):
parser.add_argument(
"--find",
action="store_true",
help="Найти все чаты с некорректными типами",
)
parser.add_argument(
"--check",
type=int,
metavar="CHAT_ID",
help="Проверить конкретный чат по ID",
)
parser.add_argument(
"--fix",
action="store_true",
help=("Исправить все чаты с некорректными типами (заменить на group)"),
)
parser.add_argument(
"--change-type",
nargs=2,
metavar=(
"CHAT_ID",
"NEW_TYPE",
),
help=("Изменить тип конкретного чата (например: --change-type 6 group)"),
)
parser.add_argument(
"--cleanup",
action="store_true",
help="Удалить все чаты с некорректными типами",
)
parser.add_argument(
"--delete",
type=int,
metavar="CHAT_ID",
help="Удалить конкретный чат по ID",
)
parser.add_argument(
"--stats",
action="store_true",
help="Показать статистику чатов по типам",
)
parser.add_argument(
"--list-type",
type=str,
metavar="TYPE",
help="Показать все чаты указанного типа (например: private)",
)
parser.add_argument(
"--check-visibility",
type=int,
metavar="USER_ID",
help=("Проверить видимость чатов для конкретного пользователя (как в API)"),
)
parser.add_argument(
"--migrate-memberships",
action="store_true",
help=("Создать ChatMembership для всех участников из participants"),
)
parser.add_argument(
"--export",
type=str,
metavar="TYPE",
help="Экспортировать чаты указанного типа в JSON файл",
)
parser.add_argument(
"--output",
type=str,
default="chats_export.json",
help="Путь к файлу для экспорта (по умолчанию: chats_export.json)",
)
parser.add_argument(
"--target-type",
type=str,
default="group",
choices=VALID_TYPES,
help="Целевой тип для исправления (по умолчанию: group)",
)
parser.add_argument(
"--no-confirm",
action="store_true",
help="Не запрашивать подтверждение при удалении/изменении",
)
def handle(self, *args, **options):
if options["stats"]:
self.show_stats()
elif options["check_visibility"]:
self.check_visibility(options["check_visibility"])
elif options["migrate_memberships"]:
self.migrate_memberships(no_confirm=options["no_confirm"])
elif options["export"]:
self.export_chats(options["export"], options["output"])
elif options["list_type"]:
self.list_chats_by_type(options["list_type"])
elif options["find"]:
self.find_invalid_chats()
elif options["check"]:
self.check_chat(options["check"])
elif options["fix"]:
self.fix_invalid_chats(
target_type=options["target_type"],
no_confirm=options["no_confirm"],
)
elif options["change_type"]:
chat_id = int(options["change_type"][0])
new_type = options["change_type"][1]
self.change_chat_type(chat_id, new_type, no_confirm=options["no_confirm"])
elif options["cleanup"]:
self.cleanup_invalid_chats(no_confirm=options["no_confirm"])
elif options["delete"]:
self.delete_chat(options["delete"], no_confirm=options["no_confirm"])
else:
self.stdout.write(
self.style.ERROR(
"Укажите действие: --stats, --check-visibility, "
"--migrate-memberships, --export, --list-type, --find, "
"--check, --fix, --change-type, --cleanup или --delete"
)
)
self.stdout.write("Используйте --help для справки")
def show_stats(self):
"""Показывает статистику чатов по типам"""
from django.db.models import Count
total_chats = Chat.objects.count()
self.stdout.write(
self.style.SUCCESS(f"📊 Статистика чатов (всего: {total_chats})\n")
)
# Статистика по типам
stats = (
Chat.objects.values("type").annotate(count=Count("id")).order_by("-count")
)
type_labels = {
"private": "Личные диалоги",
"group": "Групповые чаты",
"channel": "Каналы",
"announcement": "Объявления",
"global": "Глобальные",
"comments": "Комментарии",
}
valid_count = 0
invalid_count = 0
for stat in stats:
chat_type = stat["type"]
count = stat["count"]
label = type_labels.get(chat_type, f"❌ {chat_type} (некорректный)")
if chat_type in VALID_TYPES:
valid_count += count
self.stdout.write(f" ✅ {label}: {count}")
else:
invalid_count += count
self.stdout.write(self.style.ERROR(f" ❌ {label}: {count}"))
if invalid_count > 0:
self.stdout.write(
self.style.WARNING(
f"\n⚠️ Найдено {invalid_count} чатов с некорректными типами"
)
)
self.stdout.write(
self.style.NOTICE(
"Для исправления используйте: python manage.py check_chats --fix"
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f"\n✅ Все {valid_count} чатов имеют корректные типы"
)
)
def check_visibility(self, user_id):
"""Проверяет видимость чатов для пользователя (эмулирует API логику)"""
from django.contrib.auth import get_user_model
from django.db.models import Q
from communications.models import ChatMembership
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise CommandError(f"❌ Пользователь с ID {user_id} не найден")
user_name = f"{getattr(user, 'last_name', '')} {
getattr(user, 'first_name', '')
}".strip()
self.stdout.write(
self.style.SUCCESS("🔍 Проверка видимости чатов для пользователя:")
)
self.stdout.write(f" ID: {user.id}")
self.stdout.write(f" Имя: {user_name or user.email}")
self.stdout.write(f" Email: {user.email}\n")
# Точная логика из ChatViewSet.get_queryset()
all_chats = (
Chat.objects.filter(
Q(memberships__user=user, memberships__is_active=True)
| Q(include_all_users=True)
| Q(created_by=user)
)
.distinct()
.order_by("-created_at")
)
# Отдельно чаты для list action (без comments)
list_chats = all_chats.exclude(type="comments")
total_visible = all_chats.count()
list_visible = list_chats.count()
comments_hidden = total_visible - list_visible
self.stdout.write(self.style.SUCCESS("📊 Итого:"))
self.stdout.write(f" Всего доступных чатов: {total_visible}")
self.stdout.write(f" Видимых в списке API: {list_visible}")
if comments_hidden > 0:
self.stdout.write(
self.style.WARNING(f" Скрыто (type=comments): {comments_hidden}")
)
# Группировка по типам
self.stdout.write("\n📋 Детали по типам:")
for chat_type in [
"private",
"group",
"channel",
"global",
"announcement",
"comments",
]:
type_chats = all_chats.filter(type=chat_type)
count = type_chats.count()
if count == 0:
continue
visible_in_list = "да" if chat_type != "comments" else "нет (скрыт в list)"
type_label = {
"private": "Личные",
"group": "Групповые",
"channel": "Каналы",
"announcement": "Объявления",
"global": "Глобальные",
"comments": "Комментарии",
}.get(chat_type, chat_type)
self.stdout.write(
f"\n {type_label}: {count} шт. (в списке: {visible_in_list})"
)
# Проверяем причину видимости для каждого чата
for chat in type_chats[:5]: # Показываем первые 5
reasons = []
# Проверка membership
membership = ChatMembership.objects.filter(
chat=chat, user=user, is_active=True
).first()
if membership:
role_emoji = {
"owner": "👑",
"admin": "🔴",
"moderator": "🟠",
"member": "🟢",
"guest": "⚪",
}.get(membership.role, "❓")
reasons.append(f"membership:{role_emoji}{membership.role}")
if chat.include_all_users:
reasons.append("include_all_users")
if chat.created_by_id == user.id:
reasons.append("created_by")
reason_str = (
" | ".join(reasons) if reasons else "❌ ПРИЧИНА НЕ НАЙДЕНА!"
)
self.stdout.write(
f" • ID:{chat.id} {chat.name or '[без названия]'} → {reason_str}"
)
if count > 5:
self.stdout.write(f" ... и еще {count - 5} чатов")
# Проверяем чаты, где user является участником, но is_active=False
inactive_memberships = ChatMembership.objects.filter(
user=user, is_active=False
).select_related("chat")
if inactive_memberships.exists():
self.stdout.write(
self.style.ERROR(
f"\n⚠️ НЕАКТИВНЫЕ УЧАСТИЯ (is_active=False): {
inactive_memberships.count()
}"
)
)
for membership in inactive_memberships[:10]:
chat = membership.chat
self.stdout.write(
f" • ID:{chat.id} {chat.name or '[без названия]'} (тип: {
chat.type
}, роль: {membership.role})"
)
def migrate_memberships(self, no_confirm=False):
"""Создает ChatMembership для всех участников из participants"""
from communications.models import ChatMembership
self.stdout.write(
self.style.SUCCESS("🔄 Миграция participants → ChatMembership\n")
)
# Находим все чаты с участниками
chats_with_participants = Chat.objects.prefetch_related("participants").all()
total_chats = 0
total_created = 0
total_existing = 0
total_participants = 0
for chat in chats_with_participants:
participants = chat.participants.all()
if not participants.exists():
continue
total_chats += 1
chat_created = 0
for participant in participants:
total_participants += 1
# Проверяем, есть ли уже membership
membership, created = ChatMembership.objects.get_or_create(
chat=chat,
user=participant,
defaults={
"role": "admin"
if chat.created_by_id == participant.id
else "member",
"is_active": True,
"invited_by": chat.created_by,
},
)
if created:
total_created += 1
chat_created += 1
else:
total_existing += 1
# Если membership существует, но неактивный - активируем
if not membership.is_active:
membership.is_active = True
membership.save(update_fields=["is_active"])
self.stdout.write(
f" ✅ Активирован ID:{chat.id} для пользователя {
participant.email
}"
)
if chat_created > 0:
self.stdout.write(
f' 📁 Чат ID:{chat.id} "{
chat.name or "[без названия]"
}" → создано {chat_created} memberships'
)
self.stdout.write(self.style.SUCCESS("\n✅ Миграция завершена:"))
self.stdout.write(f" Обработано чатов: {total_chats}")
self.stdout.write(f" Обработано участников: {total_participants}")
self.stdout.write(f" Создано memberships: {total_created}")
self.stdout.write(f" Уже существовали: {total_existing}")
if total_created > 0:
self.stdout.write(
self.style.NOTICE("\n💡 Теперь эти чаты будут видны в API")
)
def export_chats(self, chat_type, output_file):
"""Экспортирует чаты указанного типа в JSON файл"""
import json
from django.core.serializers.json import DjangoJSONEncoder
chats = Chat.objects.filter(type=chat_type).order_by("-created_at")
if not chats.exists():
self.stdout.write(
self.style.WARNING(f'Chats of type "{chat_type}" not found')
)
return
export_data = []
for chat in chats:
participants_list = []
for p in chat.participants.all():
participants_list.append(
{
"id": p.id,
"email": p.email,
"first_name": getattr(p, "first_name", ""),
"last_name": getattr(p, "last_name", ""),
}
)
chat_data = {
"id": chat.id,
"name": chat.name,
"type": chat.type,
"created_at": chat.created_at.isoformat() if chat.created_at else None,
"participants_count": chat.participants.count(),
"participants": participants_list,
"messages_count": chat.messages.count()
if hasattr(chat, "messages")
else 0,
}
if hasattr(chat, "last_message") and chat.last_message:
last_msg_date = getattr(chat.last_message, "created_at", None)
if last_msg_date:
chat_data["last_message_at"] = last_msg_date.isoformat()
export_data.append(chat_data)
with open(output_file, "w", encoding="utf-8") as f:
json.dump(
{
"total": len(export_data),
"type": chat_type,
"exported_at": DjangoJSONEncoder().default(None),
"chats": export_data,
},
f,
ensure_ascii=False,
indent=2,
cls=DjangoJSONEncoder,
)
self.stdout.write(
self.style.SUCCESS(f"Exported {len(export_data)} chats to {output_file}")
)
self.stdout.write(f"Type: {chat_type}")
self.stdout.write(f"File size: {len(json.dumps(export_data))} bytes")
def list_chats_by_type(self, chat_type):
"""Показывает все чаты указанного типа"""
chats = Chat.objects.filter(type=chat_type).order_by("-created_at")
if not chats.exists():
self.stdout.write(
self.style.WARNING(f'⚠️ Чатов типа "{chat_type}" не найдено')
)
return
count = chats.count()
type_label = {
"private": "Личных диалогов",
"group": "Групповых чатов",
"channel": "Каналов",
"announcement": "Объявлений",
"global": "Глобальных чатов",
"comments": "Чатов комментариев",
}.get(chat_type, f'Чатов типа "{chat_type}"')
self.stdout.write(self.style.SUCCESS(f"📋 {type_label}: {count}\n"))
for chat in chats:
participants_count = chat.participants.count()
messages_count = (
chat.messages.count() if hasattr(chat, "messages") else "N/A"
)
# Заголовок чата
self.stdout.write(
self.style.SUCCESS(
f"═══ ID {chat.id}: {chat.name or '[без названия]'} ═══"
)
)
# Базовые поля
self.stdout.write(f" 📌 Тип: {chat.type}")
self.stdout.write(
f" 📅 Создан: {chat.created_at.strftime('%Y-%m-%d %H:%M')}"
)
# Создатель
if chat.created_by:
creator_name = f"{getattr(chat.created_by, 'last_name', '')} {
getattr(chat.created_by, 'first_name', '')
}".strip()
self.stdout.write(
f" 👤 Создатель: {creator_name or chat.created_by.email}"
)
else:
self.stdout.write(" 👤 Создатель: [не указан]")
# Критичные флаги
flags = []
if getattr(chat, "is_blocked", False):
flags.append("🚫 ЗАБЛОКИРОВАН")
if getattr(chat, "is_main", False):
flags.append("⭐ ОСНОВНОЙ")
if getattr(chat, "include_all_users", False):
flags.append("👥 ВСЕ ПОЛЬЗОВАТЕЛИ")
if flags:
self.stdout.write(f" 🚩 Флаги: {' | '.join(flags)}")
# Контекстный объект
if hasattr(chat, "context_content_type") and chat.context_content_type:
ctx = f"{chat.context_content_type.model}"
if hasattr(chat, "context_object_id") and chat.context_object_id:
ctx += f" (ID: {chat.context_object_id})"
self.stdout.write(f" 🔗 Контекст: {ctx}")
# Аватар
if chat.avatar:
self.stdout.write(
f" 🖼️ Аватар: {
chat.avatar.url if hasattr(chat.avatar, 'url') else 'есть'
}"
)
# Описание
if chat.description:
desc = (
chat.description[:60] + "..."
if len(chat.description) > 60
else chat.description
)
self.stdout.write(f" 📝 Описание: {desc}")
# Участники
self.stdout.write(f" 👥 Участников: {participants_count}")
if participants_count > 0:
participants = chat.participants.all()
for p in participants:
full_name = f"{getattr(p, 'last_name', '')} {
getattr(p, 'first_name', '')
}".strip()
email = getattr(p, "email", "N/A")
user_id = p.id
# Проверяем membership для детальной инфы
membership_info = ""
try:
from communications.models import ChatMembership
membership = ChatMembership.objects.filter(
chat=chat, user=p
).first()
if membership:
role_emoji = {
"owner": "👑",
"admin": "🔴",
"moderator": "🟠",
"member": "🟢",
"guest": "⚪",
}.get(membership.role, "❓")
membership_info = f" {role_emoji}{membership.role}"
if not membership.is_active:
membership_info += " [НЕАКТИВЕН]"
except Exception as exc:
self.stderr.write(
f"Не удалось прочитать membership пользователя {user_id}: {exc}"
)
if full_name:
self.stdout.write(
f" • ID:{user_id} {full_name} "
f"({email}){membership_info}"
)
else:
self.stdout.write(
f" • ID:{user_id} {email}{membership_info}"
)
# Сообщения
self.stdout.write(f" 💬 Сообщений: {messages_count}")
# Последнее сообщение
if hasattr(chat, "last_message") and chat.last_message:
last_msg = chat.last_message
last_msg_date = getattr(last_msg, "created_at", None)
if last_msg_date:
last_msg_preview = getattr(last_msg, "content", "")[:50]
self.stdout.write(
f" 💭 Последнее сообщение: {
last_msg_date.strftime('%Y-%m-%d %H:%M')
}"
)
if last_msg_preview:
self.stdout.write(f' "{last_msg_preview}..."')
# JSON поля
if hasattr(chat, "flags") and chat.flags:
self.stdout.write(f" 🏴 Flags: {chat.flags}")
if hasattr(chat, "extra_data") and chat.extra_data:
self.stdout.write(f" 📦 Extra data: {chat.extra_data}")
self.stdout.write("")
self.stdout.write(self.style.SUCCESS(f"Всего найдено: {count} чатов"))
def find_invalid_chats(self):
"""Находит все чаты с некорректными типами"""
invalid_chats = Chat.objects.exclude(type__in=VALID_TYPES)
if not invalid_chats.exists():
self.stdout.write(self.style.SUCCESS("✅ Все чаты имеют корректные типы"))
return
count = invalid_chats.count()
self.stdout.write(
self.style.WARNING(f"⚠️ Найдено {count} чатов с некорректными типами:\n")
)
for chat in invalid_chats:
self.stdout.write(f"ID {chat.id}:")
self.stdout.write(f" Название: {chat.name}")
self.stdout.write(f" Тип: {chat.type}")
self.stdout.write(f" Участников: {chat.participants.count()}")
self.stdout.write(f" Создан: {chat.created_at}")
self.stdout.write("")
self.stdout.write(self.style.WARNING(f"Всего найдено: {count} чатов"))
self.stdout.write(
self.style.NOTICE(
"Для удаления используйте: python manage.py check_chats --cleanup"
)
)
def check_chat(self, chat_id):
"""Проверяет конкретный чат"""
try:
chat = Chat.objects.get(id=chat_id)
except Chat.DoesNotExist:
raise CommandError(f"❌ Чат с ID {chat_id} не найден")
self.stdout.write(f"Чат ID {chat_id}:")
self.stdout.write(f" Название: {chat.name}")
self.stdout.write(f" Тип: {chat.type}")
self.stdout.write(f" Участников: {chat.participants.count()}")
self.stdout.write(f" Создан: {chat.created_at}")
if chat.type not in VALID_TYPES:
self.stdout.write(
self.style.ERROR(f"\n⚠️ Чат имеет некорректный тип: {chat.type}")
)
self.stdout.write(
self.style.NOTICE(f"Допустимые типы: {', '.join(VALID_TYPES)}")
)
self.stdout.write(
self.style.NOTICE(
"Для изменения типа используйте: "
f"python manage.py check_chats --change-type {chat_id} "
"group"
)
)
self.stdout.write(
self.style.NOTICE(
"Для удаления используйте: "
f"python manage.py check_chats --delete {chat_id}"
)
)
else:
self.stdout.write(self.style.SUCCESS("\n✅ Чат в порядке, тип корректен"))
def fix_invalid_chats(self, target_type="group", no_confirm=False):
"""Исправляет все чаты с некорректными типами"""
invalid_chats = Chat.objects.exclude(type__in=VALID_TYPES)
if not invalid_chats.exists():
self.stdout.write(self.style.SUCCESS("✅ Все чаты имеют корректные типы"))
return
count = invalid_chats.count()
self.stdout.write(
self.style.WARNING(f"⚠️ Найдено {count} чатов с некорректными типами\n")
)
self.stdout.write(
f"Чаты для исправления (будет установлен тип: {target_type}):"
)
for chat in invalid_chats:
self.stdout.write(
f" • ID {chat.id}: {chat.name} (текущий тип: {chat.type} → новый тип: {
target_type
})"
)
if not no_confirm:
confirm = input(
f'\n⚠️ Изменить тип у {count} чатов на "{target_type}"? (yes/no): '
)
if confirm.lower() not in ["yes", "y", "да"]:
self.stdout.write(self.style.ERROR("❌ Отменено"))
return
updated = invalid_chats.update(type=target_type)
self.stdout.write(self.style.SUCCESS(f"\n✅ Успешно обновлено {updated} чатов"))
self.stdout.write(f"Всем чатам установлен тип: {target_type}")
def change_chat_type(self, chat_id, new_type, no_confirm=False):
"""Изменяет тип конкретного чата"""
try:
chat = Chat.objects.get(id=chat_id)
except Chat.DoesNotExist:
raise CommandError(f"❌ Чат с ID {chat_id} не найден")
if new_type not in VALID_TYPES:
raise CommandError(
f"❌ Некорректный тип: {new_type}\n"
f"Допустимые типы: {', '.join(VALID_TYPES)}"
)
self.stdout.write(f"Изменение типа чата ID {chat_id}:")
self.stdout.write(f" Название: {chat.name}")
self.stdout.write(f" Текущий тип: {chat.type}")
self.stdout.write(f" Новый тип: {new_type}")
self.stdout.write(f" Участников: {chat.participants.count()}")
if chat.type == new_type:
self.stdout.write(
self.style.WARNING(f'\n⚠️ Чат уже имеет тип "{new_type}"')
)
return
if not no_confirm:
confirm = input(
f'\n⚠️ Изменить тип с "{chat.type}" на "{new_type}"? (yes/no): '
)
if confirm.lower() not in ["yes", "y", "да"]:
self.stdout.write(self.style.ERROR("❌ Отменено"))
return
old_type = chat.type
chat.type = new_type
chat.save(update_fields=["type"])
self.stdout.write(
self.style.SUCCESS(
f'\n✅ Тип чата "{chat.name}" изменен с "{old_type}" на "{new_type}"'
)
)
def cleanup_invalid_chats(self, no_confirm=False):
"""Удаляет все чаты с некорректными типами"""
invalid_chats = Chat.objects.exclude(type__in=VALID_TYPES)
if not invalid_chats.exists():
self.stdout.write(self.style.SUCCESS("✅ Все чаты имеют корректные типы"))
return
count = invalid_chats.count()
self.stdout.write(
self.style.WARNING(f"⚠️ Найдено {count} чатов с некорректными типами\n")
)
self.stdout.write("Чаты для удаления:")
for chat in invalid_chats:
self.stdout.write(f" • ID {chat.id}: {chat.name} (тип: {chat.type})")
if not no_confirm:
confirm = input(f"\n⚠️ Удалить {count} чатов? (yes/no): ")
if confirm.lower() not in ["yes", "y", "да"]:
self.stdout.write(self.style.ERROR("❌ Отменено"))
return
deleted, details = invalid_chats.delete()
self.stdout.write(
self.style.SUCCESS(f"\n✅ Успешно удалено {deleted} объектов")
)
# Показываем детали удаления
if details:
self.stdout.write("\nУдалено объектов по типам:")
for model, count in details.items():
self.stdout.write(f" • {model}: {count}")
def delete_chat(self, chat_id, no_confirm=False):
"""Удаляет конкретный чат"""
try:
chat = Chat.objects.get(id=chat_id)
except Chat.DoesNotExist:
raise CommandError(f"❌ Чат с ID {chat_id} не найден")
self.stdout.write(f"Удаление чата ID {chat_id}:")
self.stdout.write(f" Название: {chat.name}")
self.stdout.write(f" Тип: {chat.type}")
self.stdout.write(f" Участников: {chat.participants.count()}")
if not no_confirm:
confirm = input("\n⚠️ Подтвердите удаление (yes/no): ")
if confirm.lower() not in ["yes", "y", "да"]:
self.stdout.write(self.style.ERROR("❌ Отменено"))
return
chat_name = chat.name
chat.delete()
self.stdout.write(
self.style.SUCCESS(f'\n✅ Чат "{chat_name}" (ID {chat_id}) успешно удален')
)