""" 通過審査管理画面用API 参加者全体の得点とクラス別ランキング表示機能 """ import logging from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status from django.db.models import Sum, Q, Count from django.db import models from rog.models import NewEvent2, Entry, GpsCheckin, NewCategory logger = logging.getLogger(__name__) @api_view(['GET']) @permission_classes([IsAuthenticated]) def get_event_participants_ranking(request): """ イベント参加者全体のクラス別得点ランキング取得 GET /api/event-participants-ranking/?event_code=FC岐阜 """ try: event_code = request.GET.get('event_code') if not event_code: return Response({ 'error': 'event_code parameter is required' }, status=status.HTTP_400_BAD_REQUEST) # イベントの検索(完全一致を優先) event = None if event_code: # まず完全一致でイベント名検索 event = NewEvent2.objects.filter(event_name=event_code).first() if not event: # 次にイベントコードで検索 event = NewEvent2.objects.filter(event_code=event_code).first() if not event: return Response({ 'error': 'Event not found' }, status=status.HTTP_404_NOT_FOUND) # イベント参加者の取得と得点計算 entries = Entry.objects.filter( event=event, is_active=True ).select_related('category', 'team').prefetch_related('team__members') ranking_data = [] for entry in entries: # このエントリーのチェックイン記録を取得 checkins = GpsCheckin.objects.filter( zekken_number=str(entry.zekken_number), event_code=event_code ) # 得点計算 total_points = 0 cp_points = 0 buy_points = 0 confirmed_points = 0 # 確定済み得点 unconfirmed_points = 0 # 未確定得点 late_penalty = 0 for checkin in checkins: if checkin.points: if checkin.validate_location: # 確定済み confirmed_points += checkin.points if checkin.buy_flag: buy_points += checkin.points else: cp_points += checkin.points else: # 未確定 unconfirmed_points += checkin.points if checkin.late_point: late_penalty += checkin.late_point total_points = confirmed_points - late_penalty # チェックイン確定状況 total_checkins = checkins.count() confirmed_checkins = checkins.filter(validate_location=True).count() confirmation_rate = (confirmed_checkins / total_checkins * 100) if total_checkins > 0 else 0 # チームメンバー情報 team_members = [] if entry.team and entry.team.members.exists(): team_members = [ { 'name': f"{member.user.firstname} {member.user.lastname}" if member.user else member.name, 'age': member.age if hasattr(member, 'age') else None } for member in entry.team.members.all() ] ranking_data.append({ 'rank': 0, # 後で設定 'zekken_number': entry.zekken_number, 'zekken_label': entry.zekken_label or f"{entry.zekken_number}", 'team_name': entry.team.team_name if entry.team else "チーム名不明", 'category': { 'name': entry.category.category_name, 'class_name': entry.category.category_name # class_nameプロパティがない場合はcategory_nameを使用 }, 'members': team_members, 'points': { 'total': total_points, 'cp_points': cp_points, 'buy_points': buy_points, 'confirmed_points': confirmed_points, 'unconfirmed_points': unconfirmed_points, 'late_penalty': late_penalty }, 'checkin_status': { 'total_checkins': total_checkins, 'confirmed_checkins': confirmed_checkins, 'unconfirmed_checkins': total_checkins - confirmed_checkins, 'confirmation_rate': round(confirmation_rate, 1) }, 'entry_status': { 'has_participated': entry.hasParticipated, 'has_goaled': entry.hasGoaled } }) # クラス別にソートしてランキング設定 ranking_data.sort(key=lambda x: (x['category']['class_name'], -x['points']['total'])) # クラス別ランキングの設定 current_class = None current_rank = 0 for i, item in enumerate(ranking_data): if item['category']['class_name'] != current_class: current_class = item['category']['class_name'] current_rank = 1 else: current_rank += 1 item['rank'] = current_rank item['class_rank'] = current_rank # クラス別にグループ化 classes_ranking = {} for item in ranking_data: class_name = item['category']['class_name'] if class_name not in classes_ranking: classes_ranking[class_name] = [] classes_ranking[class_name].append(item) return Response({ 'status': 'success', 'event': { 'event_code': event_code, 'event_name': event.event_name, 'total_participants': len(ranking_data) }, 'classes_ranking': classes_ranking, 'all_participants': ranking_data, 'participants': ranking_data # JavaScript互換性のため }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Error in get_event_participants_ranking: {str(e)}") return Response({ 'error': str(e) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['GET']) @permission_classes([IsAuthenticated]) def get_participant_validation_details(request): """ 参加者の通過情報詳細と確定状況の取得 GET /api/participant-validation-details/?event_code=FC岐阜&zekken_number=123 """ try: event_code = request.GET.get('event_code') zekken_number = request.GET.get('zekken_number') if not all([event_code, zekken_number]): return Response({ 'error': 'event_code and zekken_number parameters are required' }, status=status.HTTP_400_BAD_REQUEST) # イベントの確認 try: event = NewEvent2.objects.get(event_code=event_code) except NewEvent2.DoesNotExist: return Response({ 'error': 'Event not found' }, status=status.HTTP_404_NOT_FOUND) # エントリーの確認 try: entry = Entry.objects.get( event=event, zekken_number=zekken_number ) except Entry.DoesNotExist: return Response({ 'error': 'Participant not found' }, status=status.HTTP_404_NOT_FOUND) # チェックイン記録の取得 checkins = GpsCheckin.objects.filter( zekken_number=str(zekken_number), event_code=event_code ).order_by('path_order') checkin_details = [] for checkin in checkins: checkin_details.append({ 'id': checkin.id, 'path_order': checkin.path_order, 'cp_number': checkin.cp_number, 'checkin_time': checkin.create_at.isoformat() if checkin.create_at else None, 'image_url': checkin.image_address, 'gps_location': { 'latitude': checkin.lattitude, 'longitude': checkin.longitude } if checkin.lattitude and checkin.longitude else None, 'validation': { 'is_confirmed': checkin.validate_location, 'buy_flag': checkin.buy_flag, 'points': checkin.points or 0 }, 'metadata': { 'create_user': checkin.create_user, 'update_user': checkin.update_user, 'update_time': checkin.update_at.isoformat() if checkin.update_at else None } }) # 統計情報 stats = { 'total_checkins': len(checkin_details), 'confirmed_checkins': sum(1 for c in checkin_details if c['validation']['is_confirmed']), 'unconfirmed_checkins': sum(1 for c in checkin_details if not c['validation']['is_confirmed']), 'total_points': sum(c['validation']['points'] for c in checkin_details if c['validation']['is_confirmed']), 'potential_points': sum(c['validation']['points'] for c in checkin_details) } return Response({ 'status': 'success', 'participant': { 'zekken_number': entry.zekken_number, 'team_name': entry.team_name, 'category': entry.category.name, 'class_name': entry.class_name }, 'statistics': stats, 'checkins': checkin_details }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Error in get_participant_validation_details: {str(e)}") return Response({ 'error': str(e) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['POST']) @permission_classes([IsAuthenticated]) def get_event_zekken_list(request): """ イベントのゼッケン番号リスト取得(ALLオプション付き) POST /api/event-zekken-list/ { "event_code": "FC岐阜" } """ try: import json data = json.loads(request.body) event_code = data.get('event_code') if event_code is None: return Response({ 'error': 'event_code parameter is required' }, status=status.HTTP_400_BAD_REQUEST) # イベントの確認 - event_code=Noneの場合の処理を追加 try: if event_code == '' or event_code is None: # event_code=Noneまたは空文字列の場合 event = NewEvent2.objects.filter(event_code=None).first() else: # まずevent_nameで正確な検索を試す try: event = NewEvent2.objects.get(event_name=event_code) except NewEvent2.DoesNotExist: # event_nameで見つからない場合はevent_codeで検索 event = NewEvent2.objects.get(event_code=event_code) if not event: return Response({ 'error': 'Event not found' }, status=status.HTTP_404_NOT_FOUND) except NewEvent2.DoesNotExist: return Response({ 'error': 'Event not found' }, status=status.HTTP_404_NOT_FOUND) # 参加エントリーの取得 entries = Entry.objects.filter( event=event, is_active=True ).order_by('zekken_number') zekken_list = [] # ALLオプションを最初に追加 zekken_list.append({ 'value': 'ALL', 'label': 'ALL(全参加者)', 'team_name': '全参加者表示', 'category': '全クラス' }) # 各参加者のゼッケン番号を追加 for entry in entries: team_name = entry.team.team_name if entry.team else 'チーム名未設定' category_name = entry.category.category_name if entry.category else 'クラス未設定' zekken_list.append({ 'value': str(entry.zekken_number), 'label': f"{entry.zekken_number} - {team_name}", 'team_name': team_name, 'category': category_name }) return Response({ 'status': 'success', 'event_code': event_code, 'zekken_options': zekken_list }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Error in get_event_zekken_list: {str(e)}") return Response({ 'error': str(e) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)