2025-08-24 19:44:36 +09:00
|
|
|
|
"""
|
|
|
|
|
|
通過審査管理画面用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(
|
2025-08-29 16:38:01 +09:00
|
|
|
|
zekken=str(entry.zekken_number),
|
2025-08-24 19:44:36 +09:00
|
|
|
|
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(
|
2025-08-29 16:38:01 +09:00
|
|
|
|
zekken=str(zekken_number),
|
2025-08-24 19:44:36 +09:00
|
|
|
|
event_code=event_code
|
2025-08-29 18:09:32 +09:00
|
|
|
|
).order_by('serial_number')
|
2025-08-24 19:44:36 +09:00
|
|
|
|
|
|
|
|
|
|
checkin_details = []
|
|
|
|
|
|
for checkin in checkins:
|
|
|
|
|
|
checkin_details.append({
|
|
|
|
|
|
'id': checkin.id,
|
2025-08-29 18:09:32 +09:00
|
|
|
|
'path_order': checkin.serial_number,
|
2025-08-24 19:44:36 +09:00
|
|
|
|
'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)
|