Files
rogaining_srv/rog/views_apis/api_admin_validation.py
2025-08-29 18:09:32 +09:00

353 lines
13 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.

"""
通過審査管理画面用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=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=str(zekken_number),
event_code=event_code
).order_by('serial_number')
checkin_details = []
for checkin in checkins:
checkin_details.append({
'id': checkin.id,
'path_order': checkin.serial_number,
'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)