diff --git a/backend/api/v1/communications/poll_views.py b/backend/api/v1/communications/poll_views.py deleted file mode 100644 index 76e9b130..00000000 --- a/backend/api/v1/communications/poll_views.py +++ /dev/null @@ -1,397 +0,0 @@ -# backend/api/v1/communications/poll_views.py -"""API views для голосований""" - -import copy - -from django.contrib.auth.decorators import login_required -from django.db import transaction -from django.db.models import F -from django.http import JsonResponse -from django.shortcuts import get_object_or_404 -from django.utils import timezone -from django.views.decorators.csrf import csrf_protect -from django.views.decorators.http import require_GET, require_POST - -from asgiref.sync import async_to_sync -from channels.layers import get_channel_layer - -from communications.models import ( - Chat, - Message, - Poll, - PollOption, - PollVote, -) - - -@csrf_protect -@login_required -@require_POST -def create_poll(request): - """ - Создать голосование в чате. - - POST параметры: - - chat_id: ID чата - - question: Вопрос голосования - - options: Список вариантов ответа (массив строк) - - is_anonymous: Анонимное голосование (опционально) - - is_multiple_choice: Множественный выбор (опционально) - - is_quiz: Викторина (опционально) - - correct_option_index: Индекс правильного ответа для викторины - - closes_in_minutes: Автозакрытие через N минут (опционально) - """ - import json - - try: - data = json.loads(request.body) - except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON'}, status=400) - - chat_id = data.get('chat_id') - question = data.get('question', '').strip() - options = data.get('options', []) - - # Валидация - if not chat_id: - return JsonResponse({'error': 'chat_id is required'}, status=400) - - if not question: - return JsonResponse( - {'error': 'question is required'}, - status=400 - ) - - if not options or len(options) < 2: - return JsonResponse( - {'error': 'At least 2 options are required'}, - status=400 - ) - - if len(options) > 10: - return JsonResponse( - {'error': 'Maximum 10 options allowed'}, - status=400 - ) - - # Проверка доступа к чату - chat = get_object_or_404(Chat, id=chat_id) - - # TODO: проверить права пользователя на отправку в чат - # Можно использовать существующую функцию user_can_access_chat - - # Настройки голосования - is_anonymous = data.get('is_anonymous', False) - is_multiple_choice = data.get('is_multiple_choice', False) - is_quiz = data.get('is_quiz', False) - correct_option_index = data.get('correct_option_index') - closes_in_minutes = data.get('closes_in_minutes') - - # Викторина требует правильный ответ - if is_quiz and correct_option_index is None: - return JsonResponse( - {'error': 'Quiz requires correct_option_index'}, - status=400 - ) - - if is_quiz and not (0 <= correct_option_index < len(options)): - return JsonResponse( - {'error': 'Invalid correct_option_index'}, - status=400 - ) - - # Создание голосования - with transaction.atomic(): - # Создаём сообщение - message = Message.objects.create( - chat=chat, - author=request.user, - content=f"📊 {question}", - is_system=False - ) - - # Создаём голосование - poll = Poll.objects.create( - message=message, - author=request.user, - question=question, - is_anonymous=is_anonymous, - is_multiple_choice=is_multiple_choice, - is_quiz=is_quiz - ) - - # Автозакрытие - if closes_in_minutes: - poll.closes_at = timezone.now() + timezone.timedelta( - minutes=closes_in_minutes - ) - poll.save(update_fields=['closes_at']) - - # Создаём варианты ответов - poll_options = [] - for idx, option_text in enumerate(options): - if not option_text.strip(): - continue - - is_correct = (is_quiz and idx == correct_option_index) - - poll_option = PollOption.objects.create( - poll=poll, - text=option_text.strip(), - position=idx, - is_correct=is_correct - ) - poll_options.append(poll_option) - - # Отправка через WebSocket - channel_layer = get_channel_layer() - if channel_layer: - from communications.consumers import serialize_message - - # Перезагружаем сообщение со всеми связанными объектами - message = Message.objects.select_related( - 'author', - 'reply_to', - 'reply_to__author', - 'forwarded_from_author', - 'poll' - ).prefetch_related( - 'attachments', - 'reactions', - 'reactions__user', - 'poll__options' - ).get(pk=message.id) - - payload = serialize_message(message) - group_name = f'chat_{chat_id}' - - async_to_sync(channel_layer.group_send)( - group_name, - { - 'type': 'chat.message', - 'chat_id': chat.id, - 'payload': payload - } - ) - - return JsonResponse({ - 'success': True, - 'poll_id': poll.id, - 'message_id': message.id - }) - - -@csrf_protect -@login_required -@require_POST -def vote_poll(request, poll_id): - """ - Проголосовать в голосовании. - - URL параметры: - - poll_id: ID голосования - - POST параметры: - - option_ids: Массив ID выбранных вариантов - """ - import json - - try: - data = json.loads(request.body) - except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON'}, status=400) - - option_ids = data.get('option_ids', []) - - if not option_ids: - return JsonResponse( - {'error': 'option_ids is required'}, - status=400 - ) - - # Получаем голосование - poll = get_object_or_404( - Poll.objects.select_related('message__chat'), - id=poll_id - ) - - # Проверки - if poll.is_closed: - return JsonResponse( - {'error': 'Poll is closed'}, - status=400 - ) - - # Автозакрытие - if poll.closes_at and poll.closes_at <= timezone.now(): - poll.close() - return JsonResponse( - {'error': 'Poll has expired'}, - status=400 - ) - - # Проверка множественного выбора - if not poll.is_multiple_choice and len(option_ids) > 1: - return JsonResponse( - {'error': 'Multiple choice not allowed'}, - status=400 - ) - - # Проверка доступа к чату - chat = poll.message.chat - # TODO: проверить user_can_access_chat - - with transaction.atomic(): - # Удаляем старые голоса пользователя (если меняет выбор) - old_votes = PollVote.objects.filter( - poll=poll, - voter=request.user - ) - - had_voted = old_votes.exists() - old_option_ids = set(old_votes.values_list('option_id', flat=True)) - - # Обновляем счётчики старых вариантов - for old_vote in old_votes: - PollOption.objects.filter( - id=old_vote.option_id - ).update(vote_count=F('vote_count') - 1) - - old_votes.delete() - - # Создаём новые голоса - new_votes = [] - for option_id in option_ids: - # Проверяем что вариант принадлежит этому голосованию - option = get_object_or_404( - PollOption, - id=option_id, - poll=poll - ) - - vote = PollVote.objects.create( - poll=poll, - option=option, - voter=request.user - ) - new_votes.append(vote) - - # Увеличиваем счётчик - PollOption.objects.filter(id=option_id).update( - vote_count=F('vote_count') + 1 - ) - - # Обновляем счётчик уникальных голосующих - if not had_voted: - Poll.objects.filter(id=poll.id).update( - total_voters=F('total_voters') + 1 - ) - - # Получаем обновлённые результаты - poll.refresh_from_db() - results = poll.get_results() - user_vote_ids = list(PollVote.objects.filter( - poll=poll, - voter=request.user - ).values_list('option_id', flat=True)) - results_for_user = copy.deepcopy(results) - results_for_user['user_voted_option_ids'] = user_vote_ids - - # Отправка обновления через WebSocket - channel_layer = get_channel_layer() - if channel_layer: - group_name = f'chat_{chat.id}' - - ws_message = { - 'type': 'chat.poll_update', # Правильный формат для group_send - 'chat_id': chat.id, - 'payload': { - 'poll_id': poll.id, - 'message_id': poll.message.id, - 'results': results - } - } - - async_to_sync(channel_layer.group_send)( - group_name, - ws_message - ) - - return JsonResponse({ - 'success': True, - 'results': results_for_user - }) - - -@csrf_protect -@login_required -@require_POST -def close_poll(request, poll_id): - """ - Закрыть голосование (только автор или модератор). - - URL параметры: - - poll_id: ID голосования - """ - poll = get_object_or_404( - Poll.objects.select_related('message__chat'), - id=poll_id - ) - - # Проверка прав (только автор) - if poll.author != request.user: - # TODO: проверить модераторские права - return JsonResponse( - {'error': 'Only author can close poll'}, - status=403 - ) - - poll.close() - - # Отправка обновления через WebSocket - channel_layer = get_channel_layer() - if channel_layer: - group_name = f'chat_{poll.message.chat.id}' - - ws_message = { - 'type': 'chat.poll_update', # Правильный формат для group_send - 'chat_id': poll.message.chat.id, - 'payload': { - 'poll_id': poll.id, - 'message_id': poll.message.id, - 'results': poll.get_results() - } - } - - async_to_sync(channel_layer.group_send)( - group_name, - ws_message - ) - - return JsonResponse({'success': True}) - - -@login_required -@require_GET -def get_poll_results(request, poll_id): - """Получить результаты голосования""" - - poll = get_object_or_404( - Poll.objects.select_related('message__chat'), - id=poll_id - ) - - # Проверка доступа к чату - # TODO: user_can_access_chat - - results = poll.get_results() - - # Добавляем информацию о голосе текущего пользователя - user_votes = PollVote.objects.filter( - poll=poll, - voter=request.user - ).values_list('option_id', flat=True) - - results['user_voted_option_ids'] = list(user_votes) - - return JsonResponse(results)