# 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)