308 lines
11 KiB
Python
308 lines
11 KiB
Python
|
|
"""
|
|||
|
|
競技状態管理API
|
|||
|
|
サーバーAPI変更要求書20250904.mdに基づく実装
|
|||
|
|
|
|||
|
|
Author: システム開発チーム
|
|||
|
|
Date: 2025-09-04
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from rest_framework.decorators import api_view
|
|||
|
|
from rest_framework.response import Response
|
|||
|
|
from rest_framework import status
|
|||
|
|
from django.db import transaction
|
|||
|
|
from django.utils import timezone
|
|||
|
|
from rog.models import NewEvent2, Entry, Location2025, GpsLog
|
|||
|
|
from django.contrib.gis.geos import Point
|
|||
|
|
from django.contrib.gis.measure import D
|
|||
|
|
import logging
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@api_view(['GET'])
|
|||
|
|
def competition_status(request):
|
|||
|
|
"""
|
|||
|
|
競技状態取得API
|
|||
|
|
|
|||
|
|
パラメータ:
|
|||
|
|
- event_code: イベントコード
|
|||
|
|
- zekken_number: ゼッケン番号
|
|||
|
|
|
|||
|
|
レスポンス:
|
|||
|
|
{
|
|||
|
|
"status": "OK",
|
|||
|
|
"data": {
|
|||
|
|
"is_in_rog": true,
|
|||
|
|
"rogaining_counted": false,
|
|||
|
|
"ready_for_goal": false,
|
|||
|
|
"is_at_goal": false,
|
|||
|
|
"start_time": "2025-09-04T09:00:00+09:00",
|
|||
|
|
"goal_time": null,
|
|||
|
|
"last_checkin_time": "2025-09-04T09:30:00+09:00"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
logger.info("competition_status API called")
|
|||
|
|
|
|||
|
|
event_code = request.query_params.get('event_code')
|
|||
|
|
zekken_number = request.query_params.get('zekken_number')
|
|||
|
|
|
|||
|
|
logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}")
|
|||
|
|
|
|||
|
|
# パラメータ検証
|
|||
|
|
if not event_code or not zekken_number:
|
|||
|
|
logger.warning("Missing required parameters")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "イベントコードとゼッケン番号が必要です"
|
|||
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# イベントの存在確認
|
|||
|
|
event = NewEvent2.objects.filter(event_name=event_code).first()
|
|||
|
|
if not event:
|
|||
|
|
logger.warning(f"Event not found: {event_code}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "指定されたイベントが見つかりません"
|
|||
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|||
|
|
|
|||
|
|
# エントリーの存在確認
|
|||
|
|
entry = Entry.objects.filter(
|
|||
|
|
event=event,
|
|||
|
|
zekken_number=zekken_number
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
if not entry:
|
|||
|
|
logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "指定されたゼッケン番号のエントリーが見つかりません"
|
|||
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|||
|
|
|
|||
|
|
# 競技状態データを返す
|
|||
|
|
data = {
|
|||
|
|
"is_in_rog": entry.is_in_rog,
|
|||
|
|
"rogaining_counted": entry.rogaining_counted,
|
|||
|
|
"ready_for_goal": entry.ready_for_goal,
|
|||
|
|
"is_at_goal": entry.is_at_goal,
|
|||
|
|
"start_time": entry.start_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.start_time else None,
|
|||
|
|
"goal_time": entry.goal_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.goal_time else None,
|
|||
|
|
"last_checkin_time": entry.last_checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.last_checkin_time else None
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.info(f"Competition status retrieved for zekken {zekken_number} in event {event_code}")
|
|||
|
|
|
|||
|
|
return Response({
|
|||
|
|
"status": "OK",
|
|||
|
|
"data": data
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error in competition_status: {str(e)}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "サーバーエラーが発生しました"
|
|||
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@api_view(['POST'])
|
|||
|
|
def update_competition_status(request):
|
|||
|
|
"""
|
|||
|
|
競技状態更新API
|
|||
|
|
|
|||
|
|
パラメータ:
|
|||
|
|
{
|
|||
|
|
"event_code": "EVENT2025",
|
|||
|
|
"zekken_number": "001",
|
|||
|
|
"is_in_rog": true,
|
|||
|
|
"rogaining_counted": false,
|
|||
|
|
"ready_for_goal": false,
|
|||
|
|
"is_at_goal": false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
レスポンス:
|
|||
|
|
{
|
|||
|
|
"status": "OK",
|
|||
|
|
"message": "Competition status updated successfully",
|
|||
|
|
"updated_at": "2025-09-04T10:00:00+09:00"
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
logger.info("update_competition_status API called")
|
|||
|
|
|
|||
|
|
event_code = request.data.get('event_code')
|
|||
|
|
zekken_number = request.data.get('zekken_number')
|
|||
|
|
is_in_rog = request.data.get('is_in_rog')
|
|||
|
|
rogaining_counted = request.data.get('rogaining_counted')
|
|||
|
|
ready_for_goal = request.data.get('ready_for_goal')
|
|||
|
|
is_at_goal = request.data.get('is_at_goal')
|
|||
|
|
|
|||
|
|
logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}")
|
|||
|
|
|
|||
|
|
# パラメータ検証
|
|||
|
|
if not event_code or not zekken_number:
|
|||
|
|
logger.warning("Missing required parameters")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "イベントコードとゼッケン番号が必要です"
|
|||
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# イベントの存在確認
|
|||
|
|
event = NewEvent2.objects.filter(event_name=event_code).first()
|
|||
|
|
if not event:
|
|||
|
|
logger.warning(f"Event not found: {event_code}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "指定されたイベントが見つかりません"
|
|||
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|||
|
|
|
|||
|
|
# エントリーの存在確認
|
|||
|
|
entry = Entry.objects.filter(
|
|||
|
|
event=event,
|
|||
|
|
zekken_number=zekken_number
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
if not entry:
|
|||
|
|
logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "指定されたゼッケン番号のエントリーが見つかりません"
|
|||
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|||
|
|
|
|||
|
|
# トランザクション内で状態更新
|
|||
|
|
with transaction.atomic():
|
|||
|
|
# 状態の更新(Noneでない値のみ更新)
|
|||
|
|
if is_in_rog is not None:
|
|||
|
|
entry.is_in_rog = is_in_rog
|
|||
|
|
if rogaining_counted is not None:
|
|||
|
|
entry.rogaining_counted = rogaining_counted
|
|||
|
|
if ready_for_goal is not None:
|
|||
|
|
entry.ready_for_goal = ready_for_goal
|
|||
|
|
if is_at_goal is not None:
|
|||
|
|
entry.is_at_goal = is_at_goal
|
|||
|
|
|
|||
|
|
entry.save()
|
|||
|
|
|
|||
|
|
update_time = timezone.now()
|
|||
|
|
|
|||
|
|
logger.info(f"Competition status updated for zekken {zekken_number} in event {event_code}")
|
|||
|
|
|
|||
|
|
return Response({
|
|||
|
|
"status": "OK",
|
|||
|
|
"message": "Competition status updated successfully",
|
|||
|
|
"updated_at": update_time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error in update_competition_status: {str(e)}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "サーバーエラーが発生しました"
|
|||
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@api_view(['GET'])
|
|||
|
|
def checkpoint_status(request):
|
|||
|
|
"""
|
|||
|
|
チェックポイント状態取得API
|
|||
|
|
|
|||
|
|
パラメータ:
|
|||
|
|
- event_code: イベントコード
|
|||
|
|
- zekken_number: ゼッケン番号
|
|||
|
|
- cp_number: チェックポイント番号
|
|||
|
|
|
|||
|
|
レスポンス:
|
|||
|
|
{
|
|||
|
|
"status": "OK",
|
|||
|
|
"data": {
|
|||
|
|
"cp_number": -2,
|
|||
|
|
"is_checked_in": true,
|
|||
|
|
"checkin_time": "2025-09-04T09:00:00+09:00",
|
|||
|
|
"status": "競技中", // "未", "競技中", "競技終了"
|
|||
|
|
"points_earned": 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
logger.info("checkpoint_status API called")
|
|||
|
|
|
|||
|
|
event_code = request.query_params.get('event_code')
|
|||
|
|
zekken_number = request.query_params.get('zekken_number')
|
|||
|
|
cp_number = request.query_params.get('cp_number')
|
|||
|
|
|
|||
|
|
logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}, cp_number={cp_number}")
|
|||
|
|
|
|||
|
|
# パラメータ検証
|
|||
|
|
if not all([event_code, zekken_number, cp_number]):
|
|||
|
|
logger.warning("Missing required parameters")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "イベントコード、ゼッケン番号、チェックポイント番号が必要です"
|
|||
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# イベントの存在確認
|
|||
|
|
event = NewEvent2.objects.filter(event_name=event_code).first()
|
|||
|
|
if not event:
|
|||
|
|
logger.warning(f"Event not found: {event_code}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "指定されたイベントが見つかりません"
|
|||
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|||
|
|
|
|||
|
|
# エントリーの存在確認
|
|||
|
|
entry = Entry.objects.filter(
|
|||
|
|
event=event,
|
|||
|
|
zekken_number=zekken_number
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
if not entry:
|
|||
|
|
logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "指定されたゼッケン番号のエントリーが見つかりません"
|
|||
|
|
}, status=status.HTTP_404_NOT_FOUND)
|
|||
|
|
|
|||
|
|
# チェックイン状況の確認
|
|||
|
|
checkin_record = GpsLog.objects.filter(
|
|||
|
|
zekken_number=zekken_number,
|
|||
|
|
event_code=event_code,
|
|||
|
|
cp_number=cp_number
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
# チェックポイント情報の取得
|
|||
|
|
checkpoint = Location2025.objects.filter(
|
|||
|
|
event=event,
|
|||
|
|
cp_number=cp_number
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
# 競技状況の判定
|
|||
|
|
competition_status = "未"
|
|||
|
|
if entry.is_at_goal:
|
|||
|
|
competition_status = "競技終了"
|
|||
|
|
elif entry.is_in_rog:
|
|||
|
|
competition_status = "競技中"
|
|||
|
|
|
|||
|
|
# レスポンスデータ構築
|
|||
|
|
data = {
|
|||
|
|
"cp_number": int(cp_number),
|
|||
|
|
"is_checked_in": bool(checkin_record),
|
|||
|
|
"checkin_time": checkin_record.checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") if checkin_record else None,
|
|||
|
|
"status": competition_status,
|
|||
|
|
"points_earned": checkpoint.point if checkpoint else 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.info(f"Checkpoint status retrieved for CP {cp_number}, zekken {zekken_number} in event {event_code}")
|
|||
|
|
|
|||
|
|
return Response({
|
|||
|
|
"status": "OK",
|
|||
|
|
"data": data
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Error in checkpoint_status: {str(e)}")
|
|||
|
|
return Response({
|
|||
|
|
"status": "ERROR",
|
|||
|
|
"message": "サーバーエラーが発生しました"
|
|||
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|