initial setting at 20-Aug-2025
This commit is contained in:
0
rog/views_apis/__init__.py
Executable file
0
rog/views_apis/__init__.py
Executable file
72
rog/views_apis/api_auth.py
Executable file
72
rog/views_apis/api_auth.py
Executable file
@ -0,0 +1,72 @@
|
||||
# 既存のインポート部分を変更
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry # Eventから変更
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from django.conf import settings
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
これで/check_event_codeエンドポイントがDjangoに実装されました。
|
||||
このエンドポイントは元のRubyコードと同様に、ゼッケン番号とパスワードの認証を行い、
|
||||
正しい場合はイベントコードを返します。
|
||||
|
||||
※注意: パスワード検証部分は、Djangoのcheck_passwordメソッドを使用していますが、
|
||||
元のRubyコードでの検証方法と異なる可能性があります。
|
||||
データベースの実際の構造や認証方法に合わせて調整が必要かもしれません。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def check_event_code(request):
|
||||
"""
|
||||
ゼッケン番号とパスワードの組み合わせが正しいか確認し、イベントコードを返す
|
||||
パラメータ:
|
||||
- zekken_number: ゼッケン番号
|
||||
- pw: パスワード
|
||||
"""
|
||||
logger.info("check_event_code called")
|
||||
|
||||
zekken_number = request.query_params.get('zekken_number')
|
||||
password = request.query_params.get('pw')
|
||||
|
||||
logger.debug(f"Parameters: zekken_number={zekken_number}, password={'*' * len(password) if password else None}")
|
||||
|
||||
if not zekken_number or not password:
|
||||
logger.warning("Missing required parameters")
|
||||
return Response({"status": "ERROR", "message": "ゼッケン番号とパスワードが必要です"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
# ゼッケン番号とパスワードでユーザーを検索
|
||||
entry = Entry.objects.filter(zekken_number=zekken_number).first()
|
||||
|
||||
if not entry:
|
||||
logger.warning(f"No entry found for zekken_number: {zekken_number}")
|
||||
return Response({"status": "ERROR", "message": "ゼッケン番号が見つかりません"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
user = entry.owner
|
||||
|
||||
# パスワード検証
|
||||
if not user.check_password(password):
|
||||
logger.warning(f"Invalid password for zekken_number: {zekken_number}")
|
||||
return Response({"status": "ERROR", "message": "パスワードが正しくありません"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
# NewEvent2のイベント参照に対応
|
||||
# Entryモデルがnew_event2などの別フィールド名でNewEvent2を参照している場合は変更が必要
|
||||
new_event = NewEvent2.objects.filter(id=entry.event.id).first()
|
||||
event_code = new_event.event_name if new_event else None
|
||||
|
||||
if not event_code:
|
||||
logger.warning(f"No event associated with zekken_number: {zekken_number}")
|
||||
return Response({"status": "ERROR", "message": "イベントが見つかりません"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
logger.info(f"Authentication successful for zekken_number: {zekken_number}, event_code: {event_code}")
|
||||
return Response({"status": "OK", "event_code": event_code})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in check_event_code: {str(e)}")
|
||||
return Response({"status": "ERROR", "message": "サーバーエラーが発生しました"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
1366
rog/views_apis/api_edit.py
Executable file
1366
rog/views_apis/api_edit.py
Executable file
File diff suppressed because it is too large
Load Diff
401
rog/views_apis/api_events.py
Executable file
401
rog/views_apis/api_events.py
Executable file
@ -0,0 +1,401 @@
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry,Location, GpsLog
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from django.conf import settings
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
get_start_point()
|
||||
|
||||
解説:
|
||||
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコードのパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントの存在を確認します
|
||||
4. イベントのスタートポイント情報を取得します:
|
||||
- 緯度・経度
|
||||
- スタート地点の名前
|
||||
- スタート地点の説明
|
||||
5.GeoJSON形式のデータも提供します(座標情報がある場合)
|
||||
- スタート地点をPointとして表現
|
||||
- プロパティとして名前や説明も含む
|
||||
|
||||
このエンドポイントは、アプリでイベントのスタート地点を地図上に表示したり、
|
||||
スタート地点の詳細情報を表示したりするために使用できます。
|
||||
これにより、参加者はどこに集合すればよいかを正確に把握できます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_start_point(request):
|
||||
"""
|
||||
イベントのスタートポイント情報を取得
|
||||
|
||||
パラメータ:
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("get_start_point called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: event={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not event_code:
|
||||
logger.warning("Missing required event parameter")
|
||||
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)
|
||||
|
||||
# スタートポイント情報の取得
|
||||
start_point = {
|
||||
"event_code": event_code,
|
||||
"event_name": event.event_name
|
||||
}
|
||||
|
||||
# 座標情報があれば追加
|
||||
if event.start_latitude is not None and event.start_longitude is not None:
|
||||
start_point["latitude"] = event.start_latitude
|
||||
start_point["longitude"] = event.start_longitude
|
||||
|
||||
# 名前や説明があれば追加
|
||||
if hasattr(event, 'start_name') and event.start_name:
|
||||
start_point["name"] = event.start_name
|
||||
|
||||
if hasattr(event, 'start_description') and event.start_description:
|
||||
start_point["description"] = event.start_description
|
||||
|
||||
# GeoJSON形式のデータも作成
|
||||
geo_json = None
|
||||
if event.start_latitude is not None and event.start_longitude is not None:
|
||||
geo_json = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [event.start_longitude, event.start_latitude]
|
||||
},
|
||||
"properties": {
|
||||
"name": event.start_name if hasattr(event, 'start_name') else "スタート地点",
|
||||
"description": event.start_description if hasattr(event, 'start_description') else "",
|
||||
"event_code": event_code
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"start_point": start_point,
|
||||
"geo_json": geo_json
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_start_point: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.緯度、経度、チーム名、イベントコードのパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとチームの存在を確認します
|
||||
4.チームのウェイポイントデータを取得し、指定された座標からの距離を計算します
|
||||
5.最も近いウェイポイントを特定し、以下の情報を含む分析結果を作成します:
|
||||
- 最近接ポイントの詳細(位置、時間、標高、精度、速度など)
|
||||
- 速度分析(前後のポイントから計算した速度と移動タイプの推測)
|
||||
- 周辺ポイント(時間的・空間的に近いウェイポイント)
|
||||
- 最も近いチェックポイントとの関係(オプション)
|
||||
|
||||
実装のポイント:
|
||||
|
||||
- ハバーサイン公式を使用して、2点間の地理的距離を正確に計算
|
||||
- 速度から移動タイプを推測(静止、歩行、ジョギング、ランニングなど)
|
||||
- チェックポイントとの近接性分析も行い、チームが通過済みかどうかも確認
|
||||
- 周辺ポイントも提供することで、より広い範囲での動きを把握可能
|
||||
|
||||
このエンドポイントは、特定地点での移動状況を詳細に分析したい場合に役立ちます。
|
||||
例えば、チームの移動戦略の分析や、特定地点でのパフォーマンス評価などに使用できます。
|
||||
|
||||
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def analyze_point(request):
|
||||
"""
|
||||
指定地点の情報を分析(速度、移動タイプなど)
|
||||
|
||||
パラメータ:
|
||||
- lat: 緯度
|
||||
- lng: 経度
|
||||
- team_name: チーム名
|
||||
- event_code: イベントコード
|
||||
"""
|
||||
logger.info("analyze_point called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
latitude = request.query_params.get('lat')
|
||||
longitude = request.query_params.get('lng')
|
||||
team_name = request.query_params.get('team_name')
|
||||
event_code = request.query_params.get('event_code')
|
||||
|
||||
logger.debug(f"Parameters: lat={latitude}, lng={longitude}, team_name={team_name}, event_code={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([latitude, longitude, team_name, event_code]):
|
||||
logger.warning("Missing required parameters")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "緯度、経度、チーム名、イベントコードが全て必要です"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
# 数値型に変換
|
||||
try:
|
||||
lat = float(latitude)
|
||||
lng = float(longitude)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid coordinate values: lat={latitude}, lng={longitude}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "緯度と経度は数値である必要があります"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# イベントの存在確認
|
||||
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,
|
||||
team_name=team_name
|
||||
).first()
|
||||
|
||||
if not entry:
|
||||
logger.warning(f"Team '{team_name}' not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# 最も近い位置にあるウェイポイントを見つける
|
||||
# 指定座標からの距離を計算するヘルパー関数
|
||||
def calculate_distance(waypoint):
|
||||
wp_lat = waypoint.latitude
|
||||
wp_lng = waypoint.longitude
|
||||
|
||||
# ハバーサイン公式で距離を計算
|
||||
R = 6371.0 # 地球の半径(km)
|
||||
|
||||
lat1_rad = radians(lat)
|
||||
lon1_rad = radians(lng)
|
||||
lat2_rad = radians(wp_lat)
|
||||
lon2_rad = radians(wp_lng)
|
||||
|
||||
dlon = lon2_rad - lon1_rad
|
||||
dlat = lat2_rad - lat1_rad
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = R * c * 1000 # メートル単位に変換
|
||||
return distance
|
||||
|
||||
# チームの全ウェイポイントを取得
|
||||
all_waypoints = Waypoint.objects.filter(entry=entry).order_by('recorded_at')
|
||||
|
||||
if not all_waypoints.exists():
|
||||
logger.warning(f"No waypoints found for team {team_name}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "このチームのウェイポイントデータがありません",
|
||||
"team_name": team_name,
|
||||
"event_code": event_code,
|
||||
"latitude": lat,
|
||||
"longitude": lng
|
||||
})
|
||||
|
||||
# 各ウェイポイントについて指定座標からの距離を計算
|
||||
waypoints_with_distance = [(wp, calculate_distance(wp)) for wp in all_waypoints]
|
||||
|
||||
# 距離でソート
|
||||
waypoints_with_distance.sort(key=lambda x: x[1])
|
||||
|
||||
# 最も近いウェイポイント
|
||||
nearest_waypoint, nearest_distance = waypoints_with_distance[0]
|
||||
|
||||
# 分析結果
|
||||
analysis = {
|
||||
"nearest_point": {
|
||||
"latitude": nearest_waypoint.latitude,
|
||||
"longitude": nearest_waypoint.longitude,
|
||||
"timestamp": nearest_waypoint.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"distance": round(nearest_distance, 2), # メートル単位
|
||||
"altitude": nearest_waypoint.altitude,
|
||||
"accuracy": nearest_waypoint.accuracy,
|
||||
"speed": nearest_waypoint.speed
|
||||
}
|
||||
}
|
||||
|
||||
# 前後のウェイポイントを取得して速度分析
|
||||
waypoint_index = list(all_waypoints).index(nearest_waypoint)
|
||||
|
||||
# 前のウェイポイント(あれば)
|
||||
if waypoint_index > 0:
|
||||
prev_waypoint = all_waypoints[waypoint_index - 1]
|
||||
time_diff = (nearest_waypoint.recorded_at - prev_waypoint.recorded_at).total_seconds()
|
||||
|
||||
if time_diff > 0:
|
||||
# 2点間の距離を計算
|
||||
prev_distance = calculate_distance(prev_waypoint)
|
||||
|
||||
# 速度を計算(m/s)
|
||||
speed_mps = abs(nearest_distance - prev_distance) / time_diff
|
||||
|
||||
analysis["speed_analysis"] = {
|
||||
"speed_mps": round(speed_mps, 2),
|
||||
"speed_kmh": round(speed_mps * 3.6, 2), # km/h に変換
|
||||
"time_diff_seconds": time_diff
|
||||
}
|
||||
|
||||
# 移動タイプを推測
|
||||
if speed_mps < 1.5:
|
||||
mobility_type = "静止またはゆっくり歩行"
|
||||
elif speed_mps < 2.5:
|
||||
mobility_type = "歩行"
|
||||
elif speed_mps < 4.0:
|
||||
mobility_type = "速歩き"
|
||||
elif speed_mps < 7.0:
|
||||
mobility_type = "ジョギング"
|
||||
elif speed_mps < 12.0:
|
||||
mobility_type = "ランニング"
|
||||
else:
|
||||
mobility_type = "自転車または車両"
|
||||
|
||||
analysis["speed_analysis"]["mobility_type"] = mobility_type
|
||||
|
||||
# 周辺の他のウェイポイントも取得(時間範囲内で)
|
||||
# 前後30分以内のウェイポイントを検索
|
||||
time_range = timedelta(minutes=30)
|
||||
nearby_waypoints = []
|
||||
|
||||
for wp in all_waypoints:
|
||||
if abs((wp.recorded_at - nearest_waypoint.recorded_at).total_seconds()) <= time_range.total_seconds():
|
||||
# 距離も近いものだけ(1km以内)
|
||||
distance = calculate_distance(wp)
|
||||
if distance <= 1000:
|
||||
nearby_waypoints.append({
|
||||
"latitude": wp.latitude,
|
||||
"longitude": wp.longitude,
|
||||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"distance": round(distance, 2),
|
||||
"altitude": wp.altitude,
|
||||
"speed": wp.speed
|
||||
})
|
||||
|
||||
analysis["nearby_points"] = nearby_waypoints
|
||||
analysis["nearby_points_count"] = len(nearby_waypoints)
|
||||
|
||||
# チェックポイントとの関係を分析(オプション)
|
||||
try:
|
||||
|
||||
# イベントのチェックポイント定義を取得
|
||||
event_cps = Location.objects.filter(event=event)
|
||||
|
||||
# チームが通過したチェックポイントを取得
|
||||
team_cps = GpsLog.objects.filter(entry=entry)
|
||||
|
||||
if event_cps.exists():
|
||||
# 指定地点から最も近いチェックポイントを見つける
|
||||
closest_cp = None
|
||||
closest_cp_distance = float('inf')
|
||||
|
||||
for cp in event_cps:
|
||||
if cp.latitude is not None and cp.longitude is not None:
|
||||
# チェックポイントからの距離を計算
|
||||
cp_lat = cp.latitude
|
||||
cp_lng = cp.longitude
|
||||
|
||||
R = 6371.0
|
||||
lat1_rad = radians(lat)
|
||||
lon1_rad = radians(lng)
|
||||
lat2_rad = radians(cp_lat)
|
||||
lon2_rad = radians(cp_lng)
|
||||
|
||||
dlon = lon2_rad - lon1_rad
|
||||
dlat = lat2_rad - lat1_rad
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = R * c * 1000 # メートル単位
|
||||
|
||||
if distance < closest_cp_distance:
|
||||
closest_cp = cp
|
||||
closest_cp_distance = distance
|
||||
|
||||
if closest_cp:
|
||||
# チームがこのチェックポイントを通過したかを確認
|
||||
team_cp = team_cps.filter(cp_number=closest_cp.cp_number).first()
|
||||
visited = team_cp is not None
|
||||
|
||||
analysis["nearest_checkpoint"] = {
|
||||
"cp_number": closest_cp.cp_number,
|
||||
"cp_name": closest_cp.cp_name,
|
||||
"cp_point": closest_cp.cp_point,
|
||||
"distance": round(closest_cp_distance, 2),
|
||||
"visited": visited,
|
||||
"checkin_time": team_cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if visited and team_cp.checkin_time else None
|
||||
}
|
||||
except:
|
||||
# Location モデルがない場合などはスキップ
|
||||
pass
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"team_name": team_name,
|
||||
"event_code": event_code,
|
||||
"latitude": lat,
|
||||
"longitude": lng,
|
||||
"analysis": analysis
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in analyze_point: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
491
rog/views_apis/api_monitor.py
Executable file
491
rog/views_apis/api_monitor.py
Executable file
@ -0,0 +1,491 @@
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, Waypoint, GpsLog
|
||||
import logging
|
||||
from django.db.models import F, Q, Max, Count
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では2つのエンドポイントを提供しています:
|
||||
|
||||
1. /realtimeMonitor (全チーム対象)
|
||||
|
||||
すべてのチームの最新位置情報を取得するエンドポイントです。
|
||||
|
||||
- パラメータ:
|
||||
- event_code:イベントコード(必須)
|
||||
- class:クラス名(省略可)
|
||||
|
||||
主な機能:
|
||||
|
||||
- スタート済みの全チームの最新位置情報を返す
|
||||
- クラス名でフィルタリングできる
|
||||
- 各チームの状態(未スタート、競技中、ゴール済)、経過時間、スコア、チェックポイント数などの情報を提供
|
||||
- 位置情報の「鮮度」を評価(5分以内は「新鮮」、5分~30分は「通常」、30分以上は「古い」)
|
||||
- GeoJSON形式のデータも提供し、地図表示に使いやすくしている
|
||||
|
||||
2. /realtimeMonitor_zekken_narrow (特定チーム対象)
|
||||
|
||||
特定のゼッケン番号を持つチームの詳細な位置情報を取得するエンドポイントです。
|
||||
|
||||
- パラメータ:
|
||||
- event_code:イベントコード(必須)
|
||||
- zekken:ゼッケン番号(必須)
|
||||
- class:クラス名(省略可)
|
||||
|
||||
- 主な機能:
|
||||
- 指定されたゼッケン番号を持つチームの詳細データを返す
|
||||
- 最新の位置情報だけでなく、複数の過去の位置情報も提供(最新10件)
|
||||
- クラス名が指定された場合、そのクラスに所属しているか確認する
|
||||
- チームの状態、経過時間、スコア、チェックポイント数などの詳細情報を提供
|
||||
- GeoJSON形式で以下のデータを提供:
|
||||
- 最新位置(特別なマーカー)
|
||||
- 過去の位置(別のマーカー)
|
||||
- 移動経路(ライン)
|
||||
|
||||
この2つのエンドポイントにより、ウェブ上でリアルタイムのモニタリングダッシュボードを実現できます。
|
||||
全チームの概要を一覧表示し、特定チームを選択すると詳細な移動履歴を確認できる仕組みです。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def realtime_monitor(request):
|
||||
"""
|
||||
リアルタイムのチーム位置情報を取得
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- class: クラス名(省略可)
|
||||
"""
|
||||
logger.info("realtime_monitor called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event_code')
|
||||
class_name = request.query_params.get('class')
|
||||
|
||||
# パラメータの別名対応
|
||||
if not event_code:
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, class={class_name}")
|
||||
|
||||
# パラメータ検証
|
||||
if not event_code:
|
||||
logger.warning("Missing required event_code parameter")
|
||||
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)
|
||||
|
||||
# スタート済みのチームをクエリ
|
||||
entries_query = Entry.objects.filter(
|
||||
event=event,
|
||||
start_info__isnull=False
|
||||
)
|
||||
|
||||
# クラス名でフィルタリング(指定があれば)
|
||||
if class_name:
|
||||
entries_query = entries_query.filter(class_name=class_name)
|
||||
|
||||
# 各チームの最新ウェイポイントを取得するためのサブクエリ
|
||||
subquery = Waypoint.objects.filter(
|
||||
entry=OuterRef('pk')
|
||||
).order_by('-recorded_at').values('latitude', 'longitude', 'recorded_at', 'speed', 'altitude')[:1]
|
||||
|
||||
# 各チームのチェックポイント数を取得するためのサブクエリ
|
||||
cp_count_subquery = GpsLog.objects.filter(
|
||||
entry=OuterRef('pk')
|
||||
).values('entry').annotate(count=Count('id')).values('count')
|
||||
|
||||
# アノテーションを追加してサブクエリの結果をメインクエリに結合
|
||||
entries_with_latest_waypoint = entries_query.annotate(
|
||||
latest_latitude=Subquery(subquery.values('latitude')[:1]),
|
||||
latest_longitude=Subquery(subquery.values('longitude')[:1]),
|
||||
latest_recorded_at=Subquery(subquery.values('recorded_at')[:1]),
|
||||
latest_speed=Subquery(subquery.values('speed')[:1]),
|
||||
latest_altitude=Subquery(subquery.values('altitude')[:1]),
|
||||
checkpoint_count=Coalesce(Subquery(cp_count_subquery), 0)
|
||||
).filter(
|
||||
# 最新ウェイポイントがあるチームのみ
|
||||
latest_latitude__isnull=False
|
||||
)
|
||||
|
||||
# 現在時刻を取得
|
||||
now = timezone.now()
|
||||
|
||||
# 各チームのリアルタイム情報を収集
|
||||
teams_data = []
|
||||
|
||||
for entry in entries_with_latest_waypoint:
|
||||
# ウェイポイントがないチームはスキップ
|
||||
if not hasattr(entry, 'latest_latitude') or not entry.latest_latitude:
|
||||
continue
|
||||
|
||||
# 最新の位置情報の取得時刻
|
||||
location_time = entry.latest_recorded_at
|
||||
location_age_seconds = (now - location_time).total_seconds() if location_time else None
|
||||
|
||||
# 位置情報の鮮度を評価(5分以内は新鮮、30分以上は古い)
|
||||
location_freshness = "fresh" if location_age_seconds and location_age_seconds < 300 else \
|
||||
"normal" if location_age_seconds and location_age_seconds < 1800 else "stale"
|
||||
|
||||
# スタート、ゴール情報を取得
|
||||
start_time = entry.start_info.start_time if hasattr(entry, 'start_info') else None
|
||||
goal_time = entry.goal_info.goal_time if hasattr(entry, 'goal_info') else None
|
||||
score = entry.goal_info.score if hasattr(entry, 'goal_info') and hasattr(entry.goal_info, 'score') else None
|
||||
|
||||
# 経過時間を計算
|
||||
elapsed_time = None
|
||||
if start_time:
|
||||
end_time = goal_time if goal_time else now
|
||||
elapsed_seconds = (end_time - start_time).total_seconds()
|
||||
|
||||
# 時間:分:秒の形式にフォーマット
|
||||
hours, remainder = divmod(int(elapsed_seconds), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
elapsed_time = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
|
||||
# チームステータスを判定
|
||||
if goal_time:
|
||||
status_text = "ゴール済"
|
||||
elif start_time:
|
||||
status_text = "競技中"
|
||||
else:
|
||||
status_text = "未スタート"
|
||||
|
||||
team_data = {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"status": status_text,
|
||||
"start_time": start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
|
||||
"goal_time": goal_time.strftime("%Y-%m-%d %H:%M:%S") if goal_time else None,
|
||||
"elapsed_time": elapsed_time,
|
||||
"score": score,
|
||||
"checkpoint_count": entry.checkpoint_count,
|
||||
"location": {
|
||||
"latitude": entry.latest_latitude,
|
||||
"longitude": entry.latest_longitude,
|
||||
"timestamp": location_time.strftime("%Y-%m-%d %H:%M:%S") if location_time else None,
|
||||
"age_seconds": int(location_age_seconds) if location_age_seconds else None,
|
||||
"freshness": location_freshness,
|
||||
"speed": entry.latest_speed,
|
||||
"altitude": entry.latest_altitude
|
||||
}
|
||||
}
|
||||
|
||||
teams_data.append(team_data)
|
||||
|
||||
# GeoJSON形式のデータを作成
|
||||
geo_json_features = []
|
||||
|
||||
# 各チームのポイントフィーチャーを追加
|
||||
for team in teams_data:
|
||||
# 位置情報がないチームはスキップ
|
||||
if not team['location']['latitude'] or not team['location']['longitude']:
|
||||
continue
|
||||
|
||||
# 位置情報の鮮度に基づいて色を変更
|
||||
color = {
|
||||
"fresh": "#00FF00", # 緑
|
||||
"normal": "#FFFF00", # 黄
|
||||
"stale": "#FF0000" # 赤
|
||||
}.get(team['location']['freshness'], "#AAAAAA") # デフォルトはグレー
|
||||
|
||||
# ゴール済みのチームは異なるマーカーを使用
|
||||
is_goal = team['status'] == "ゴール済"
|
||||
|
||||
geo_json_features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [team['location']['longitude'], team['location']['latitude']]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": team['team_name'],
|
||||
"zekken_number": team['zekken_number'],
|
||||
"class_name": team['class_name'],
|
||||
"status": team['status'],
|
||||
"elapsed_time": team['elapsed_time'],
|
||||
"score": team['score'],
|
||||
"checkpoint_count": team['checkpoint_count'],
|
||||
"timestamp": team['location']['timestamp'],
|
||||
"age_seconds": team['location']['age_seconds'],
|
||||
"color": color,
|
||||
"icon": "flag-checkered" if is_goal else "map-marker-alt",
|
||||
"iconColor": color
|
||||
}
|
||||
})
|
||||
|
||||
geo_json = {
|
||||
"type": "FeatureCollection",
|
||||
"features": geo_json_features
|
||||
}
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event_code": event_code,
|
||||
"class_name": class_name,
|
||||
"timestamp": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"total_teams": len(teams_data),
|
||||
"teams": teams_data,
|
||||
"geo_json": geo_json
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in realtime_monitor: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@api_view(['GET'])
|
||||
def realtime_monitor_zekken_narrow(request):
|
||||
"""
|
||||
指定ゼッケン番号のチームの位置情報を取得
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- class: クラス名(省略可)
|
||||
- zekken: ゼッケン番号
|
||||
"""
|
||||
logger.info("realtime_monitor_zekken_narrow called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event_code')
|
||||
class_name = request.query_params.get('class')
|
||||
zekken_number = request.query_params.get('zekken')
|
||||
|
||||
# パラメータの別名対応
|
||||
if not event_code:
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, class={class_name}, zekken={zekken_number}")
|
||||
|
||||
# パラメータ検証
|
||||
if not event_code:
|
||||
logger.warning("Missing required event_code parameter")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "イベントコードが必要です"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not zekken_number:
|
||||
logger.warning("Missing required zekken parameter")
|
||||
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"Team with zekken number {zekken_number} not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# クラス名でフィルタリング(指定があれば)
|
||||
if class_name and entry.class_name != class_name:
|
||||
logger.warning(f"Team {zekken_number} is not in class: {class_name}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたクラスに所属していません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# ウェイポイントデータを取得(最新の複数件)
|
||||
waypoints = Waypoint.objects.filter(
|
||||
entry=entry
|
||||
).order_by('-recorded_at')[:10] # 最新10件
|
||||
|
||||
# チェックポイント数を取得
|
||||
checkpoint_count = GpsLog.objects.filter(entry=entry).count()
|
||||
|
||||
# 現在時刻を取得
|
||||
now = timezone.now()
|
||||
|
||||
# スタート、ゴール情報を取得
|
||||
start_time = entry.start_info.start_time if hasattr(entry, 'start_info') else None
|
||||
goal_time = entry.goal_info.goal_time if hasattr(entry, 'goal_info') and entry.goal_info else None
|
||||
score = entry.goal_info.score if hasattr(entry, 'goal_info') and entry.goal_info and hasattr(entry.goal_info, 'score') else None
|
||||
|
||||
# 経過時間を計算
|
||||
elapsed_time = None
|
||||
if start_time:
|
||||
end_time = goal_time if goal_time else now
|
||||
elapsed_seconds = (end_time - start_time).total_seconds()
|
||||
|
||||
# 時間:分:秒の形式にフォーマット
|
||||
hours, remainder = divmod(int(elapsed_seconds), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
elapsed_time = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
|
||||
# チームステータスを判定
|
||||
if goal_time:
|
||||
status_text = "ゴール済"
|
||||
elif start_time:
|
||||
status_text = "競技中"
|
||||
else:
|
||||
status_text = "未スタート"
|
||||
|
||||
# ウェイポイントデータを処理
|
||||
location_data = []
|
||||
latest_location = None
|
||||
|
||||
for i, wp in enumerate(waypoints):
|
||||
location = {
|
||||
"latitude": wp.latitude,
|
||||
"longitude": wp.longitude,
|
||||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"age_seconds": int((now - wp.recorded_at).total_seconds()),
|
||||
"speed": wp.speed,
|
||||
"altitude": wp.altitude,
|
||||
"accuracy": wp.accuracy
|
||||
}
|
||||
|
||||
# 位置情報の鮮度を評価
|
||||
location["freshness"] = "fresh" if location["age_seconds"] < 300 else \
|
||||
"normal" if location["age_seconds"] < 1800 else "stale"
|
||||
|
||||
location_data.append(location)
|
||||
|
||||
# 最新位置情報を保存
|
||||
if i == 0:
|
||||
latest_location = location
|
||||
|
||||
# GeoJSON形式のデータを作成
|
||||
geo_json_features = []
|
||||
|
||||
# 最新位置のポイントフィーチャー
|
||||
if latest_location:
|
||||
color = {
|
||||
"fresh": "#00FF00", # 緑
|
||||
"normal": "#FFFF00", # 黄
|
||||
"stale": "#FF0000" # 赤
|
||||
}.get(latest_location['freshness'], "#AAAAAA") # デフォルトはグレー
|
||||
|
||||
is_goal = status_text == "ゴール済"
|
||||
|
||||
geo_json_features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [latest_location['longitude'], latest_location['latitude']]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"status": status_text,
|
||||
"timestamp": latest_location['timestamp'],
|
||||
"color": color,
|
||||
"icon": "flag-checkered" if is_goal else "map-marker-alt",
|
||||
"iconColor": color,
|
||||
"isLatest": True
|
||||
}
|
||||
})
|
||||
|
||||
# 過去位置のポイントフィーチャー
|
||||
for i, location in enumerate(location_data):
|
||||
if i == 0: # 最新位置は既に追加済み
|
||||
continue
|
||||
|
||||
geo_json_features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [location['longitude'], location['latitude']]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": zekken_number,
|
||||
"timestamp": location['timestamp'],
|
||||
"color": "#AAAAAA", # 過去の位置はグレー
|
||||
"icon": "circle",
|
||||
"iconColor": "#AAAAAA",
|
||||
"isLatest": False
|
||||
}
|
||||
})
|
||||
|
||||
# 移動経路を表すラインフィーチャー
|
||||
if len(location_data) >= 2:
|
||||
geo_json_features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[location['longitude'], location['latitude']]
|
||||
for location in reversed(location_data) # 時系列順
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": zekken_number,
|
||||
"color": "#0000FF" # 青色の線
|
||||
}
|
||||
})
|
||||
|
||||
geo_json = {
|
||||
"type": "FeatureCollection",
|
||||
"features": geo_json_features
|
||||
}
|
||||
|
||||
team_data = {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"status": status_text,
|
||||
"start_time": start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
|
||||
"goal_time": goal_time.strftime("%Y-%m-%d %H:%M:%S") if goal_time else None,
|
||||
"elapsed_time": elapsed_time,
|
||||
"score": score,
|
||||
"checkpoint_count": checkpoint_count,
|
||||
"latest_location": latest_location,
|
||||
"locations": location_data,
|
||||
"locations_count": len(location_data)
|
||||
}
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event_code": event_code,
|
||||
"timestamp": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"team": team_data,
|
||||
"geo_json": geo_json
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in realtime_monitor_zekken_narrow: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
281
rog/views_apis/api_photos.py
Executable file
281
rog/views_apis/api_photos.py
Executable file
@ -0,0 +1,281 @@
|
||||
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, GpsLog
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from django.conf import settings
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.2つのエンドポイントを提供しています:
|
||||
- /get_photo_list - 認証なしで写真とレポートURLを取得
|
||||
- /get_photo_list_prod - パスワード認証付きで同じ情報を取得
|
||||
2.共通のロジックは get_team_photos 関数に集約し、以下の情報を取得します:
|
||||
- チームの基本情報(名前、ゼッケン番号、クラス名)
|
||||
- チェックポイント通過時の写真(時間順、サービスチェック情報含む)
|
||||
- スタート写真とゴール写真(あれば)
|
||||
- チームレポートのURL(存在する場合)
|
||||
- スコアボードのURL(存在する場合)
|
||||
- チームのスコア(ゴール済みの場合)
|
||||
3.レポートとスコアボードのファイルパスを実際に確認し、存在する場合のみURLを提供します
|
||||
4.写真の表示順はスタート→チェックポイント(時間順)→ゴールとなっており、チェックポイントについてはそれぞれ番号、撮影時間、サービスチェック状態などの情報も含めています
|
||||
|
||||
この実装により、チームは自分たちの競技中の写真やレポートを簡単に確認できます。
|
||||
本番環境(_prod版)ではパスワード認証によりセキュリティを確保しています。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_photo_list(request):
|
||||
"""
|
||||
チームの写真とレポートURLを取得(認証なし版)
|
||||
|
||||
パラメータ:
|
||||
- zekken: ゼッケン番号
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("get_photo_list called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
zekken_number = request.query_params.get('zekken')
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: zekken={zekken_number}, event={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, event_code]):
|
||||
logger.warning("Missing required parameters")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "ゼッケン番号とイベントコードが必要です"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return get_team_photos(zekken_number, event_code)
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_photo_list_prod(request):
|
||||
"""
|
||||
チームの写真とレポートURLを取得(認証あり版)
|
||||
|
||||
パラメータ:
|
||||
- zekken: ゼッケン番号
|
||||
- pw: パスワード
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("get_photo_list_prod called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
zekken_number = request.query_params.get('zekken')
|
||||
password = request.query_params.get('pw')
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: zekken={zekken_number}, event={event_code}, has_password={bool(password)}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, password, event_code]):
|
||||
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"Team with zekken number {zekken_number} not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# パスワード検証
|
||||
if not hasattr(entry, 'password') or entry.password != password:
|
||||
logger.warning(f"Invalid password for team: {entry.team_name}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "パスワードが一致しません"
|
||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
return get_team_photos(zekken_number, event_code)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_photo_list_prod: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def get_team_photos(zekken_number, event_code):
|
||||
"""
|
||||
チームの写真とレポートURLを取得する共通関数
|
||||
"""
|
||||
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"Team with zekken number {zekken_number} not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# チームの基本情報を取得
|
||||
team_info = {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"event_name": event.event_name
|
||||
}
|
||||
|
||||
# チェックポイント通過情報(写真を含む)を取得
|
||||
checkpoints = GpsLog.objects.filter(
|
||||
entry=entry
|
||||
).order_by('checkin_time')
|
||||
|
||||
# 写真リストを作成
|
||||
photos = []
|
||||
|
||||
for cp in checkpoints:
|
||||
# 写真URLがある場合のみ追加
|
||||
if hasattr(cp, 'image') and cp.image:
|
||||
photo_data = {
|
||||
"cp_number": cp.cp_number,
|
||||
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
|
||||
"image_url": request.build_absolute_uri(cp.image.url) if hasattr(request, 'build_absolute_uri') else cp.image.url
|
||||
}
|
||||
|
||||
# サービスチェックの情報があれば追加
|
||||
if hasattr(cp, 'is_service_checked'):
|
||||
photo_data["is_service_checked"] = cp.is_service_checked
|
||||
|
||||
photos.append(photo_data)
|
||||
|
||||
# スタート写真があれば追加
|
||||
if hasattr(entry, 'start_info') and hasattr(entry.start_info, 'start_image') and entry.start_info.start_image:
|
||||
start_image = {
|
||||
"cp_number": "START",
|
||||
"checkin_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S") if entry.start_info.start_time else None,
|
||||
"image_url": request.build_absolute_uri(entry.start_info.start_image.url) if hasattr(request, 'build_absolute_uri') else entry.start_info.start_image.url
|
||||
}
|
||||
photos.insert(0, start_image) # リストの先頭に追加
|
||||
|
||||
# ゴール写真があれば追加
|
||||
if hasattr(entry, 'goal_info') and hasattr(entry.goal_info, 'goal_image') and entry.goal_info.goal_image:
|
||||
goal_image = {
|
||||
"cp_number": "GOAL",
|
||||
"checkin_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S") if entry.goal_info.goal_time else None,
|
||||
"image_url": request.build_absolute_uri(entry.goal_info.goal_image.url) if hasattr(request, 'build_absolute_uri') else entry.goal_info.goal_image.url
|
||||
}
|
||||
photos.append(goal_image) # リストの末尾に追加
|
||||
|
||||
# チームレポートURLを生成
|
||||
# レポートURLは「/レポートディレクトリ/イベント名/ゼッケン番号.pdf」のパターンを想定
|
||||
report_directory = getattr(settings, 'REPORT_DIRECTORY', 'reports')
|
||||
report_base_url = getattr(settings, 'REPORT_BASE_URL', '/media/reports/')
|
||||
|
||||
# レポートファイルの物理パスをチェック
|
||||
report_path = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
report_directory,
|
||||
event_code,
|
||||
f"{zekken_number}.pdf"
|
||||
)
|
||||
|
||||
# レポートURLを生成
|
||||
has_report = os.path.exists(report_path)
|
||||
report_url = None
|
||||
|
||||
if has_report:
|
||||
report_url = urljoin(
|
||||
report_base_url,
|
||||
f"{event_code}/{zekken_number}.pdf"
|
||||
)
|
||||
|
||||
# 絶対URLに変換
|
||||
if hasattr(request, 'build_absolute_uri'):
|
||||
report_url = request.build_absolute_uri(report_url)
|
||||
|
||||
# スコアボードURLを生成
|
||||
scoreboard_path = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
'scoreboards',
|
||||
event_code,
|
||||
f"scoreboard_{zekken_number}.pdf"
|
||||
)
|
||||
|
||||
has_scoreboard = os.path.exists(scoreboard_path)
|
||||
scoreboard_url = None
|
||||
|
||||
if has_scoreboard:
|
||||
scoreboard_url = urljoin(
|
||||
'/media/scoreboards/',
|
||||
f"{event_code}/scoreboard_{zekken_number}.pdf"
|
||||
)
|
||||
|
||||
# 絶対URLに変換
|
||||
if hasattr(request, 'build_absolute_uri'):
|
||||
scoreboard_url = request.build_absolute_uri(scoreboard_url)
|
||||
|
||||
# チームのスコア情報
|
||||
score = None
|
||||
if hasattr(entry, 'goal_info') and hasattr(entry.goal_info, 'score'):
|
||||
score = entry.goal_info.score
|
||||
|
||||
# レスポンスデータ
|
||||
response_data = {
|
||||
"status": "OK",
|
||||
"team": team_info,
|
||||
"photos": photos,
|
||||
"photo_count": len(photos),
|
||||
"has_report": has_report,
|
||||
"report_url": report_url,
|
||||
"has_scoreboard": has_scoreboard,
|
||||
"scoreboard_url": scoreboard_url,
|
||||
"score": score
|
||||
}
|
||||
|
||||
return Response(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_team_photos: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
612
rog/views_apis/api_play.py
Executable file
612
rog/views_apis/api_play.py
Executable file
@ -0,0 +1,612 @@
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, Location
|
||||
from rog.models import GpsLog
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from django.conf import settings
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.ゼッケン番号、イベントコード、チェックポイント番号、画像アドレスのパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとチームの存在を確認します
|
||||
4.既に同じチェックポイントが登録されていないかチェックします
|
||||
- 既に登録されている場合は警告メッセージを返します
|
||||
5.新しいチェックポイント情報を登録します
|
||||
6.成功した場合、登録情報と共に成功メッセージを返します
|
||||
|
||||
GpsLog モデルは、チェックポイント通過情報を保存するための独自のモデルです。
|
||||
既存のシステムに類似のモデルがある場合は、そちらを使用してください。
|
||||
"""
|
||||
@api_view(['POST'])
|
||||
def input_cp(request):
|
||||
"""
|
||||
チェックポイント通過情報を登録
|
||||
|
||||
パラメータ:
|
||||
- zekken_number: ゼッケン番号
|
||||
- event_code: イベントコード
|
||||
- cp_number: チェックポイント番号
|
||||
- image_address: 画像アドレス
|
||||
"""
|
||||
logger.info("input_cp called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
zekken_number = request.data.get('zekken_number')
|
||||
event_code = request.data.get('event_code')
|
||||
cp_number = request.data.get('cp_number')
|
||||
image_address = request.data.get('image_address')
|
||||
|
||||
logger.debug(f"Parameters: zekken_number={zekken_number}, event_code={event_code}, "
|
||||
f"cp_number={cp_number}, image_address={image_address}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, event_code, 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"Team not found with zekken: {zekken_number} in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# 既に同じCPを登録済みかチェック
|
||||
existing_checkpoint = GpsLog.objects.filter(
|
||||
entry=entry,
|
||||
cp_number=cp_number
|
||||
).first()
|
||||
|
||||
if existing_checkpoint:
|
||||
logger.warning(f"Checkpoint {cp_number} already registered for team: {entry.team_name}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "このチェックポイントは既に登録されています",
|
||||
"checkpoint_id": existing_checkpoint.id
|
||||
})
|
||||
|
||||
# トランザクション開始
|
||||
with transaction.atomic():
|
||||
# チェックポイント登録
|
||||
checkpoint = GpsLog.objects.create(
|
||||
entry=entry,
|
||||
cp_number=cp_number,
|
||||
image_address=image_address,
|
||||
checkin_time=timezone.now()
|
||||
)
|
||||
|
||||
logger.info(f"Successfully registered CP {cp_number} for team: {entry.team_name} "
|
||||
f"with zekken: {zekken_number}")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "チェックポイントが正常に登録されました",
|
||||
"checkpoint_id": checkpoint.id,
|
||||
"checkin_time": checkpoint.checkin_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in input_cp: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.eventパラメータを受け取り、イベントコードを指定します
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントコードでイベントを検索します
|
||||
- イベントが存在しない場合はエラーを返します
|
||||
4.そのイベントに関連する全てのチェックポイント情報を取得します
|
||||
5.各チェックポイントの詳細情報をリスト形式で整理します
|
||||
6.チェックポイントリストをJSON形式で返します
|
||||
|
||||
EventCheckpoint()=>Location)モデルは、イベントごとのチェックポイント設定を保存するためのモデルです。
|
||||
実際のシステムでは、このモデルと同等の機能を持つモデルがすでに存在している可能性があります。
|
||||
その場合は、そのモデルを使用して実装してください。
|
||||
|
||||
このエンドポイントは、ロゲイニングアプリがイベントのチェックポイント情報を取得するのに役立ちます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_checkpoint_list(request):
|
||||
"""
|
||||
指定イベントの全チェックポイント情報を取得
|
||||
|
||||
パラメータ:
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("get_checkpoint_list called")
|
||||
|
||||
event_code = request.query_params.get('event')
|
||||
logger.debug(f"Parameters: event={event_code}")
|
||||
|
||||
if not event_code:
|
||||
logger.warning("Event code not provided")
|
||||
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)
|
||||
|
||||
# イベントのチェックポイント情報を取得
|
||||
checkpoints = Location.objects.filter(event=event).order_by('cp_number')
|
||||
|
||||
checkpoint_list = []
|
||||
for cp in checkpoints:
|
||||
checkpoint_info = {
|
||||
"cp_number": cp.cp_number,
|
||||
"cp_name": cp.cp_name,
|
||||
"cp_point": cp.cp_point,
|
||||
"latitude": cp.latitude,
|
||||
"longitude": cp.longitude,
|
||||
"cp_description": cp.description,
|
||||
"is_service_cp": cp.is_service_cp
|
||||
}
|
||||
checkpoint_list.append(checkpoint_info)
|
||||
|
||||
logger.info(f"Successfully retrieved {len(checkpoint_list)} checkpoints for event {event_code}")
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"checkpoints": checkpoint_list,
|
||||
"total_count": len(checkpoint_list)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_checkpoint_list: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコードとチーム名のパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとチームの存在を確認します
|
||||
4.チームが既にスタート済みかどうかをチェックします
|
||||
- 既にスタート済みの場合は警告メッセージを返します
|
||||
5.スタート情報を登録します
|
||||
6.成功した場合、スタート時間と共に成功メッセージを返します
|
||||
|
||||
このエンドポイントにより、ロゲイニングアプリからチームのスタート処理を行うことができ、開始時間が正確に記録されます。
|
||||
"""
|
||||
|
||||
@api_view(['POST'])
|
||||
def start_from_rogapp(request):
|
||||
"""
|
||||
アプリからスタート処理を実行
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- team_name: チーム名
|
||||
"""
|
||||
logger.info("start_from_rogapp called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.data.get('event_code')
|
||||
team_name = request.data.get('team_name')
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([event_code, team_name]):
|
||||
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,
|
||||
team_name=team_name
|
||||
).first()
|
||||
|
||||
if not entry:
|
||||
logger.warning(f"Team not found: {team_name} in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# 既にスタート済みかチェック
|
||||
if hasattr(entry, 'start_info'):
|
||||
logger.warning(f"Team {team_name} already started at {entry.start_info.start_time}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "このチームは既にスタートしています",
|
||||
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
|
||||
# トランザクション開始
|
||||
with transaction.atomic():
|
||||
# スタート情報を登録
|
||||
start_info = TeamStart.objects.create(
|
||||
entry=entry,
|
||||
start_time=timezone.now()
|
||||
)
|
||||
|
||||
logger.info(f"Team {team_name} started at {start_info.start_time}")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "スタート処理が完了しました",
|
||||
"team_name": team_name,
|
||||
"event_code": event_code,
|
||||
"start_time": start_info.start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in start_from_rogapp: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコード、チーム名、チェックポイント番号、画像URLのパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとチームの存在を確認します
|
||||
4.チームがスタートしているかを確認します
|
||||
- スタートしていない場合はエラーを返します
|
||||
5.既に同じチェックポイントが登録されていないかチェックします
|
||||
- 既に登録されている場合は警告メッセージを返します
|
||||
6.イベントのチェックポイント定義があれば、そのデータを取得します
|
||||
7.チェックポイント通過情報を登録します
|
||||
8.成功した場合、登録情報と獲得ポイントを含む成功メッセージを返します
|
||||
|
||||
このエンドポイントは、ロゲイニングアプリからチェックポイント通過情報を登録するためのもので、
|
||||
/input_cpエンドポイントと類似していますが、チームをゼッケン番号ではなくチーム名で指定する点や、
|
||||
チェックポイントのポイント情報も返す点が異なります。
|
||||
"""
|
||||
|
||||
@api_view(['POST'])
|
||||
def checkin_from_rogapp(request):
|
||||
"""
|
||||
アプリからチェックイン処理を実行
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- team_name: チーム名
|
||||
- cp_number: チェックポイント番号
|
||||
- image: 画像URL
|
||||
"""
|
||||
logger.info("checkin_from_rogapp called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.data.get('event_code')
|
||||
team_name = request.data.get('team_name')
|
||||
cp_number = request.data.get('cp_number')
|
||||
image_url = request.data.get('image')
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}, "
|
||||
f"cp_number={cp_number}, image={image_url}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([event_code, team_name, 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,
|
||||
team_name=team_name
|
||||
).first()
|
||||
|
||||
if not entry:
|
||||
logger.warning(f"Team not found: {team_name} in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# チームがスタートしているか確認
|
||||
if not hasattr(entry, 'start_info'):
|
||||
logger.warning(f"Team {team_name} has not started yet")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "このチームはまだスタートしていません。先にスタート処理を行ってください。"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 既に同じCPを登録済みかチェック
|
||||
existing_checkpoint = GpsLog.objects.filter(
|
||||
entry=entry,
|
||||
cp_number=cp_number
|
||||
).first()
|
||||
|
||||
if existing_checkpoint:
|
||||
logger.warning(f"Checkpoint {cp_number} already registered for team: {team_name}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "このチェックポイントは既に登録されています",
|
||||
"checkpoint_id": existing_checkpoint.id,
|
||||
"checkin_time": existing_checkpoint.checkin_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
|
||||
# イベントのチェックポイント定義を確認(存在する場合)
|
||||
event_cp = None
|
||||
try:
|
||||
event_cp = Location.objects.filter(
|
||||
event=event,
|
||||
cp_number=cp_number
|
||||
).first()
|
||||
except:
|
||||
logger.info(f"Location model not available or CP {cp_number} not defined for event")
|
||||
|
||||
# トランザクション開始
|
||||
with transaction.atomic():
|
||||
# チェックポイント登録
|
||||
checkpoint = GpsLog.objects.create(
|
||||
entry=entry,
|
||||
cp_number=cp_number,
|
||||
image_address=image_url,
|
||||
checkin_time=timezone.now(),
|
||||
is_service_checked=event_cp.is_service_cp if event_cp else False
|
||||
)
|
||||
|
||||
logger.info(f"Successfully registered CP {cp_number} for team: {team_name} in event: {event_code}")
|
||||
|
||||
# 獲得ポイントの計算(イベントCPが定義されている場合)
|
||||
point_value = event_cp.cp_point if event_cp else 0
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "チェックポイントが正常に登録されました",
|
||||
"team_name": team_name,
|
||||
"cp_number": cp_number,
|
||||
"checkpoint_id": checkpoint.id,
|
||||
"checkin_time": checkpoint.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"point_value": point_value
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in checkin_from_rogapp: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコード、チーム名、画像URL、ゴール時間のパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとチームの存在を確認します
|
||||
4.チームがスタートしているかを確認します
|
||||
- スタートしていない場合はエラーを返します
|
||||
5.既にゴールしているかチェックします
|
||||
- ゴール済みの場合は警告メッセージを返します
|
||||
6.ゴール時間を処理します(提供されていない場合は現在時刻を使用)
|
||||
7.チームのスコアを計算します
|
||||
8.スコアボードを生成します(実際の生成ロジックは実装によって異なります)
|
||||
9.ゴール情報を登録します
|
||||
10.成功した場合、ゴール情報、スコア、スコアボードURLを含む成功メッセージを返します
|
||||
|
||||
スコアボードの生成部分は、実際のシステムの要件に合わせて詳細に実装する必要があります。
|
||||
この例では、単純にPDFファイルのパスとURLを生成していますが、
|
||||
実際にはPDF生成ライブラリ(例:ReportLab、WeasyPrintなど)を使用してスコアボードを生成する必要があります。
|
||||
"""
|
||||
|
||||
@api_view(['POST'])
|
||||
def goal_from_rogapp(request):
|
||||
"""
|
||||
アプリからゴール処理を実行し、スコアボードを生成
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- team_name: チーム名
|
||||
- image: 画像URL
|
||||
- goal_time: ゴール時間
|
||||
"""
|
||||
logger.info("goal_from_rogapp called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.data.get('event_code')
|
||||
team_name = request.data.get('team_name')
|
||||
image_url = request.data.get('image')
|
||||
goal_time_str = request.data.get('goal_time')
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}, "
|
||||
f"image={image_url}, goal_time={goal_time_str}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([event_code, team_name]):
|
||||
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,
|
||||
team_name=team_name
|
||||
).first()
|
||||
|
||||
if not entry:
|
||||
logger.warning(f"Team not found: {team_name} in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# チームがスタートしているか確認
|
||||
if not hasattr(entry, 'start_info'):
|
||||
logger.warning(f"Team {team_name} has not started yet")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "このチームはまだスタートしていません。先にスタート処理を行ってください。"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 既にゴールしているかチェック
|
||||
if hasattr(entry, 'goal_info'):
|
||||
logger.warning(f"Team {team_name} already reached goal at {entry.goal_info.goal_time}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "このチームは既にゴールしています",
|
||||
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"scoreboard_url": entry.goal_info.scoreboard_url
|
||||
})
|
||||
|
||||
# ゴール時間の処理
|
||||
if goal_time_str:
|
||||
try:
|
||||
goal_time = datetime.strptime(goal_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid goal_time format: {goal_time_str}")
|
||||
goal_time = timezone.now()
|
||||
else:
|
||||
goal_time = timezone.now()
|
||||
|
||||
# トランザクション開始
|
||||
with transaction.atomic():
|
||||
# スコアの計算
|
||||
score = calculate_team_score(entry)
|
||||
|
||||
# スコアボードの生成
|
||||
scoreboard_filename = f"scoreboard_{entry.zekken_number}_{uuid.uuid4().hex[:8]}.pdf"
|
||||
scoreboard_path = os.path.join(settings.MEDIA_ROOT, 'scoreboards', scoreboard_filename)
|
||||
os.makedirs(os.path.dirname(scoreboard_path), exist_ok=True)
|
||||
|
||||
# ここでスコアボードを実際に生成する処理を実装
|
||||
# 例: generate_scoreboard(entry, score, scoreboard_path)
|
||||
|
||||
# スコアボードへのURL
|
||||
scoreboard_url = f"{settings.MEDIA_URL}scoreboards/{scoreboard_filename}"
|
||||
|
||||
# ゴール情報を登録
|
||||
goal_info = TeamGoal.objects.create(
|
||||
entry=entry,
|
||||
goal_time=goal_time,
|
||||
image_url=image_url,
|
||||
score=score,
|
||||
scoreboard_url=scoreboard_url
|
||||
)
|
||||
|
||||
logger.info(f"Team {team_name} reached goal at {goal_time} with score {score}")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "ゴール処理が正常に完了しました",
|
||||
"team_name": team_name,
|
||||
"goal_time": goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"score": score,
|
||||
"scoreboard_url": scoreboard_url
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in goal_from_rogapp: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def calculate_team_score(entry):
|
||||
"""チームのスコアを計算する補助関数"""
|
||||
# チームが通過したチェックポイントを取得
|
||||
checkpoints = GpsLog.objects.filter(entry=entry)
|
||||
|
||||
total_score = 0
|
||||
|
||||
for cp in checkpoints:
|
||||
# チェックポイントの得点を取得
|
||||
cp_point = 0
|
||||
try:
|
||||
# Location
|
||||
event_cp = Location.objects.filter(
|
||||
event=entry.event,
|
||||
cp_number=cp.cp_number
|
||||
).first()
|
||||
if event_cp:
|
||||
cp_point = event_cp.cp_point
|
||||
except:
|
||||
# モデルが存在しない場合はデフォルト値を使用
|
||||
cp_point = 10
|
||||
|
||||
total_score += cp_point
|
||||
|
||||
return total_score
|
||||
|
||||
374
rog/views_apis/api_ranking.py
Executable file
374
rog/views_apis/api_ranking.py
Executable file
@ -0,0 +1,374 @@
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry
|
||||
import logging
|
||||
from django.db.models import F, Count
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコードとクラス名のパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントの存在を確認します
|
||||
4.指定クラスに属する、ゴール済みのチームをスコア降順で取得します
|
||||
5.各チームについて以下の情報を収集します:
|
||||
- 順位(1から始まる)
|
||||
- チーム基本情報(名前、ゼッケン番号)
|
||||
- スコア
|
||||
- レース時間(スタートからゴールまでの時間)
|
||||
- スタート時間・ゴール時間
|
||||
- チェックポイント通過数
|
||||
- オーナー情報(ユーザー名、メールアドレス)
|
||||
6.ランキングの要約情報も提供します:
|
||||
- イベント情報
|
||||
- 指定クラスのチーム総数
|
||||
- ゴール済みチーム数
|
||||
- 未ゴールチーム数(スタート済みだがゴールしていないチーム)
|
||||
|
||||
この実装により、イベント管理者やユーザーはリアルタイムのランキング情報を確認できます。
|
||||
特に結果発表や途中経過の確認に有用です。スコア順にソートされているため、現在の順位が一目でわかります。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_ranking(request):
|
||||
"""
|
||||
指定クラスのランキングを取得
|
||||
|
||||
パラメータ:
|
||||
- class: クラス名
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("get_ranking called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
class_name = request.query_params.get('class')
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: class={class_name}, event={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([class_name, event_code]):
|
||||
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)
|
||||
|
||||
# 指定クラスのゴール済みチームを取得(スコア降順)
|
||||
teams = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name,
|
||||
goal_info__isnull=False
|
||||
).order_by('-goal_info__score')
|
||||
|
||||
ranking_data = []
|
||||
|
||||
for i, team in enumerate(teams):
|
||||
# チームのスタート情報を取得
|
||||
start_time = None
|
||||
if hasattr(team, 'start_info') and team.start_info:
|
||||
start_time = team.start_info.start_time
|
||||
|
||||
# チームのゴール情報を取得
|
||||
goal_time = None
|
||||
score = 0
|
||||
if hasattr(team, 'goal_info') and team.goal_info:
|
||||
goal_time = team.goal_info.goal_time
|
||||
score = team.goal_info.score or 0
|
||||
|
||||
# レース時間を計算
|
||||
race_time = None
|
||||
if start_time and goal_time:
|
||||
race_seconds = (goal_time - start_time).total_seconds()
|
||||
|
||||
# 時間:分:秒の形式にフォーマット
|
||||
hours, remainder = divmod(int(race_seconds), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
race_time = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
|
||||
# チェックポイント数を取得
|
||||
cp_count = 0
|
||||
try:
|
||||
from rog.models import GpsLog
|
||||
cp_count = GpsLog.objects.filter(entry=team).count()
|
||||
except:
|
||||
pass
|
||||
|
||||
# ランキングデータに追加
|
||||
team_data = {
|
||||
"rank": i + 1, # 1-basedのランキング
|
||||
"team_name": team.team_name,
|
||||
"zekken_number": team.zekken_number,
|
||||
"score": score,
|
||||
"race_time": race_time,
|
||||
"start_time": start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
|
||||
"goal_time": goal_time.strftime("%Y-%m-%d %H:%M:%S") if goal_time else None,
|
||||
"checkpoint_count": cp_count
|
||||
}
|
||||
|
||||
# オーナー情報があれば追加
|
||||
if hasattr(team, 'owner') and team.owner:
|
||||
team_data["owner_name"] = team.owner.username
|
||||
team_data["owner_email"] = team.owner.email
|
||||
|
||||
ranking_data.append(team_data)
|
||||
|
||||
# 未ゴールチームの数を取得
|
||||
not_finished_count = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name,
|
||||
start_info__isnull=False,
|
||||
goal_info__isnull=True
|
||||
).count()
|
||||
|
||||
# 登録チーム総数を取得
|
||||
total_teams = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name
|
||||
).count()
|
||||
|
||||
# イベント情報を取得
|
||||
event_info = {
|
||||
"event_name": event.event_name,
|
||||
"event_description": getattr(event, 'description', None),
|
||||
"event_date": getattr(event, 'event_date', None)
|
||||
}
|
||||
|
||||
if hasattr(event, 'event_date') and event.event_date:
|
||||
event_info["event_date"] = event.event_date.strftime("%Y-%m-%d")
|
||||
|
||||
# 現在の日時
|
||||
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event": event_info,
|
||||
"class_name": class_name,
|
||||
"total_teams": total_teams,
|
||||
"finished_teams": len(ranking_data),
|
||||
"not_finished_teams": not_finished_count,
|
||||
"rankings": ranking_data,
|
||||
"timestamp": current_time
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_ranking: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry
|
||||
import logging
|
||||
from django.db.models import F, Count, Q
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコードのパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントの存在を確認します
|
||||
4.イベント内の全クラスを取得します
|
||||
5.各クラスごとに以下の処理を行います:
|
||||
- ゴール済みチームをスコア降順で取得し、上位3件に絞り込みます
|
||||
- 各チームの基本情報(ランク、名前、ゼッケン番号、スコア、レース時間、チェックポイント数)を収集します
|
||||
- クラスの統計情報(総チーム数、ゴール済みチーム数)を計算します
|
||||
- クラスデータとチームデータを階層化して格納します
|
||||
6.イベント全体の統計情報も提供します:
|
||||
- 全クラス数
|
||||
- 全チーム数
|
||||
- ゴール済みチーム数
|
||||
- 未ゴールチーム数(スタート済みだがゴールしていないチーム)
|
||||
この実装により、リアルタイムで全クラスのトップ3ランキングを確認できます。
|
||||
大会のスコアボードやリザルト公開ページなどに利用できます。
|
||||
1つのリクエストで全クラスの情報が取得できるため、複数のクラスを表示するページで効率的に情報を取得できます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def all_ranking_top3(request):
|
||||
"""
|
||||
指定イベントの全クラスにおけるトップ3選手のランキングを取得
|
||||
|
||||
パラメータ:
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("all_ranking_top3 called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: event={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not event_code:
|
||||
logger.warning("Missing required event parameter")
|
||||
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)
|
||||
|
||||
# イベント内の全クラスを取得
|
||||
all_classes = Entry.objects.filter(
|
||||
event=event
|
||||
).values_list('class_name', flat=True).distinct()
|
||||
|
||||
class_rankings = []
|
||||
|
||||
for class_name in all_classes:
|
||||
# 指定クラスのゴール済みチームを取得(スコア降順、上位3件)
|
||||
teams = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name,
|
||||
goal_info__isnull=False
|
||||
).order_by('-goal_info__score')[:3] # 上位3件
|
||||
|
||||
team_rankings = []
|
||||
|
||||
for i, team in enumerate(teams):
|
||||
# チームのスタート情報を取得
|
||||
start_time = None
|
||||
if hasattr(team, 'start_info') and team.start_info:
|
||||
start_time = team.start_info.start_time
|
||||
|
||||
# チームのゴール情報を取得
|
||||
goal_time = None
|
||||
score = 0
|
||||
if hasattr(team, 'goal_info') and team.goal_info:
|
||||
goal_time = team.goal_info.goal_time
|
||||
score = team.goal_info.score or 0
|
||||
|
||||
# レース時間を計算
|
||||
race_time = None
|
||||
if start_time and goal_time:
|
||||
race_seconds = (goal_time - start_time).total_seconds()
|
||||
|
||||
# 時間:分:秒の形式にフォーマット
|
||||
hours, remainder = divmod(int(race_seconds), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
race_time = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
|
||||
# チェックポイント数を取得
|
||||
cp_count = 0
|
||||
try:
|
||||
from rog.models import GpsLog
|
||||
cp_count = GpsLog.objects.filter(entry=team).count()
|
||||
except:
|
||||
pass
|
||||
|
||||
# チームデータ
|
||||
team_data = {
|
||||
"rank": i + 1, # 1-basedのランキング
|
||||
"team_name": team.team_name,
|
||||
"zekken_number": team.zekken_number,
|
||||
"score": score,
|
||||
"race_time": race_time,
|
||||
"checkpoint_count": cp_count
|
||||
}
|
||||
|
||||
team_rankings.append(team_data)
|
||||
|
||||
# クラスの総チーム数
|
||||
total_teams_in_class = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name
|
||||
).count()
|
||||
|
||||
# ゴール済みチーム数
|
||||
finished_teams_in_class = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name,
|
||||
goal_info__isnull=False
|
||||
).count()
|
||||
|
||||
class_data = {
|
||||
"class_name": class_name,
|
||||
"total_teams": total_teams_in_class,
|
||||
"finished_teams": finished_teams_in_class,
|
||||
"top_teams": team_rankings
|
||||
}
|
||||
|
||||
class_rankings.append(class_data)
|
||||
|
||||
# イベント情報を取得
|
||||
event_info = {
|
||||
"event_name": event.event_name,
|
||||
"event_description": getattr(event, 'description', None),
|
||||
"event_date": getattr(event, 'event_date', None)
|
||||
}
|
||||
|
||||
if hasattr(event, 'event_date') and event.event_date:
|
||||
event_info["event_date"] = event.event_date.strftime("%Y-%m-%d")
|
||||
|
||||
# 未ゴールチームの数をイベント全体で取得
|
||||
not_finished_count = Entry.objects.filter(
|
||||
event=event,
|
||||
start_info__isnull=False,
|
||||
goal_info__isnull=True
|
||||
).count()
|
||||
|
||||
# ゴール済みチームの数をイベント全体で取得
|
||||
finished_count = Entry.objects.filter(
|
||||
event=event,
|
||||
goal_info__isnull=False
|
||||
).count()
|
||||
|
||||
# 登録チーム総数をイベント全体で取得
|
||||
total_teams = Entry.objects.filter(
|
||||
event=event
|
||||
).count()
|
||||
|
||||
# 現在の日時
|
||||
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event": event_info,
|
||||
"total_classes": len(class_rankings),
|
||||
"total_teams": total_teams,
|
||||
"finished_teams": finished_count,
|
||||
"not_finished_teams": not_finished_count,
|
||||
"rankings_by_class": class_rankings,
|
||||
"timestamp": current_time
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in all_ranking_top3: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
634
rog/views_apis/api_routes.py
Executable file
634
rog/views_apis/api_routes.py
Executable file
@ -0,0 +1,634 @@
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, Location, GpsLog
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from django.conf import settings
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコードとクラス名のパラメータを受け取ります。省略可能なパラメータとして limit(表示するトップ選手の数)も受け取ります。
|
||||
2.パラメータが不足している場合はエラーを返します。
|
||||
3.指定されたイベントの存在を確認します。
|
||||
4.指定されたクラスに所属する、ゴール済みのチームをスコア降順で取得します。
|
||||
5.上位 limit 件(デフォルトは3件)のチームについて、詳細なルート情報を収集します:
|
||||
- チームの基本情報(名前、ゼッケン番号、クラス、順位、スコア)
|
||||
- スタート情報とゴール情報
|
||||
- ウェイポイントとチェックポイントのデータ
|
||||
- ルートの統計情報(総距離、ウェイポイント数、チェックポイント数)
|
||||
6.各チームに特徴的な色を割り当て(1位: 金色、2位: 銀色、3位: 銅色)、ルートを区別しやすくします。
|
||||
7.全チームの情報と、GeoJSON形式のデータを含む応答を返します。
|
||||
|
||||
実装のポイント:
|
||||
|
||||
- /getAllRoutes とは異なり、上位チームのみを対象としています。
|
||||
- 上位チームは詳細なルートポイント情報も含めることで、より詳しい分析が可能です。
|
||||
- 各チームのルートを視覚的に区別しやすくするための色情報を追加しています。
|
||||
- チェックポイントもGeoJSON内のポイントフィーチャーとして追加し、各チームがどのチェックポイントを通過したかを可視化しやすくしています。
|
||||
|
||||
このエンドポイントは、結果分析や表彰式でのプレゼンテーション、上位選手のルート戦略の研究などに役立ちます。
|
||||
"""
|
||||
|
||||
from math import sin, cos, sqrt, atan2, radians
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@api_view(['GET'])
|
||||
def top_users_routes(request):
|
||||
"""
|
||||
指定クラスのトップ選手のルート情報を取得
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- class_name: クラス名
|
||||
- limit: 取得するトップ選手の数(省略時は3)
|
||||
"""
|
||||
logger.info("top_users_routes called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event_code')
|
||||
class_name = request.query_params.get('class_name')
|
||||
|
||||
# クエリパラメータの別名対応(互換性のため)
|
||||
if not event_code:
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
# 省略可能なパラメータ
|
||||
try:
|
||||
limit = int(request.query_params.get('limit', 3))
|
||||
except ValueError:
|
||||
limit = 3
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, class_name={class_name}, limit={limit}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([event_code, class_name]):
|
||||
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)
|
||||
|
||||
# ゴール済みで指定クラスに属する上位チームを取得
|
||||
top_entries = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name,
|
||||
goal_info__isnull=False # ゴール済みのチームのみ
|
||||
).order_by('-goal_info__score')[:limit] # スコア降順で上位limit件
|
||||
|
||||
if not top_entries.exists():
|
||||
logger.warning(f"No teams with goal info found for event: {event_code}, class: {class_name}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "ゴール済みのチームが見つかりません",
|
||||
"teams": [],
|
||||
"total_teams": 0
|
||||
})
|
||||
|
||||
# トップチームの情報とルートを取得
|
||||
teams_data = []
|
||||
geo_json_features = []
|
||||
|
||||
for entry in top_entries:
|
||||
try:
|
||||
# ウェイポイントデータを取得(時間順)
|
||||
waypoints = Waypoint.objects.filter(
|
||||
entry=entry
|
||||
).order_by('recorded_at')
|
||||
|
||||
# チェックポイント通過情報を取得(時間順)
|
||||
checkpoints = GpsLog.objects.filter(
|
||||
entry=entry
|
||||
).order_by('checkin_time')
|
||||
|
||||
# スタート情報を取得
|
||||
start_info = None
|
||||
if hasattr(entry, 'start_info'):
|
||||
start_info = {
|
||||
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
# ゴール情報を取得
|
||||
goal_info = None
|
||||
if hasattr(entry, 'goal_info'):
|
||||
goal_info = {
|
||||
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"score": entry.goal_info.score
|
||||
}
|
||||
|
||||
# ウェイポイントとチェックポイントを処理
|
||||
route_points = []
|
||||
|
||||
# ウェイポイントを処理
|
||||
for wp in waypoints:
|
||||
point = {
|
||||
"latitude": wp.latitude,
|
||||
"longitude": wp.longitude,
|
||||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"type": "waypoint"
|
||||
}
|
||||
|
||||
if wp.altitude is not None:
|
||||
point["altitude"] = wp.altitude
|
||||
if wp.accuracy is not None:
|
||||
point["accuracy"] = wp.accuracy
|
||||
if wp.speed is not None:
|
||||
point["speed"] = wp.speed
|
||||
|
||||
route_points.append(point)
|
||||
|
||||
# チェックポイントの座標を追加(イベントのチェックポイント定義がある場合)
|
||||
checkpoint_data = []
|
||||
for cp in checkpoints:
|
||||
cp_data = {
|
||||
"cp_number": cp.cp_number,
|
||||
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
|
||||
"is_service_checked": getattr(cp, 'is_service_checked', False)
|
||||
}
|
||||
|
||||
# チェックポイントの座標情報を取得
|
||||
try:
|
||||
event_cp = Location.objects.filter(
|
||||
event=event,
|
||||
cp_number=cp.cp_number
|
||||
).first()
|
||||
|
||||
if event_cp and event_cp.latitude and event_cp.longitude:
|
||||
cp_data["latitude"] = event_cp.latitude
|
||||
cp_data["longitude"] = event_cp.longitude
|
||||
cp_data["cp_name"] = event_cp.cp_name
|
||||
cp_data["cp_point"] = event_cp.cp_point
|
||||
|
||||
# ルートポイントとしても追加
|
||||
route_points.append({
|
||||
"latitude": event_cp.latitude,
|
||||
"longitude": event_cp.longitude,
|
||||
"timestamp": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
|
||||
"type": "checkpoint",
|
||||
"cp_number": cp.cp_number
|
||||
})
|
||||
except:
|
||||
# Locationモデルが存在しないか、座標が設定されていない場合
|
||||
pass
|
||||
|
||||
checkpoint_data.append(cp_data)
|
||||
|
||||
# ルートの統計情報を計算
|
||||
total_distance = 0
|
||||
if len(route_points) >= 2:
|
||||
def calculate_distance(lat1, lon1, lat2, lon2):
|
||||
# 2点間の距離をハバーサイン公式で計算(km単位)
|
||||
R = 6371.0 # 地球の半径(km)
|
||||
|
||||
lat1_rad = radians(lat1)
|
||||
lon1_rad = radians(lon1)
|
||||
lat2_rad = radians(lat2)
|
||||
lon2_rad = radians(lon2)
|
||||
|
||||
dlon = lon2_rad - lon1_rad
|
||||
dlat = lat2_rad - lat1_rad
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = R * c
|
||||
return distance
|
||||
|
||||
for i in range(len(route_points) - 1):
|
||||
p1 = route_points[i]
|
||||
p2 = route_points[i + 1]
|
||||
|
||||
if 'latitude' in p1 and 'longitude' in p1 and 'latitude' in p2 and 'longitude' in p2:
|
||||
distance = calculate_distance(
|
||||
p1['latitude'], p1['longitude'],
|
||||
p2['latitude'], p2['longitude']
|
||||
)
|
||||
total_distance += distance
|
||||
|
||||
# チームデータを追加
|
||||
team_data = {
|
||||
"rank": list(top_entries).index(entry) + 1, # 順位
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"score": goal_info["score"] if goal_info else None,
|
||||
"start_info": start_info,
|
||||
"goal_info": goal_info,
|
||||
"route_statistics": {
|
||||
"total_distance": round(total_distance, 2),
|
||||
"total_waypoints": waypoints.count(),
|
||||
"total_checkpoints": checkpoints.count()
|
||||
},
|
||||
"checkpoints": checkpoint_data,
|
||||
"route_points": route_points # トップチームなので詳細なルートデータも含める
|
||||
}
|
||||
|
||||
teams_data.append(team_data)
|
||||
|
||||
# GeoJSONフィーチャーを追加
|
||||
if route_points:
|
||||
# 特徴的な色をチームに割り当て(上位3チームの場合、金・銀・銅のイメージ)
|
||||
colors = ["#FFD700", "#C0C0C0", "#CD7F32"] # Gold, Silver, Bronze
|
||||
rank = list(top_entries).index(entry)
|
||||
color = colors[rank] if rank < len(colors) else f"#{(rank * 50) % 256:02x}{100:02x}{150:02x}"
|
||||
|
||||
geo_json_features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[point["longitude"], point["latitude"]]
|
||||
for point in route_points
|
||||
if "latitude" in point and "longitude" in point
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"rank": list(top_entries).index(entry) + 1,
|
||||
"score": goal_info["score"] if goal_info else None,
|
||||
"total_distance": round(total_distance, 2),
|
||||
"color": color,
|
||||
"waypoint_count": waypoints.count(),
|
||||
"checkpoint_count": checkpoints.count()
|
||||
}
|
||||
})
|
||||
|
||||
# チェックポイントをポイントフィーチャーとして追加
|
||||
for cp in checkpoint_data:
|
||||
if 'latitude' in cp and 'longitude' in cp:
|
||||
geo_json_features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [cp["longitude"], cp["latitude"]]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"type": "checkpoint",
|
||||
"cp_number": cp["cp_number"],
|
||||
"cp_name": cp.get("cp_name", cp["cp_number"]),
|
||||
"color": color,
|
||||
"rank": list(top_entries).index(entry) + 1
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing route for team {entry.team_name}: {str(e)}")
|
||||
# エラーが発生したチームはスキップ
|
||||
continue
|
||||
|
||||
# 対象クラスの全チーム数(ゴール済み)を取得
|
||||
total_teams_in_class = Entry.objects.filter(
|
||||
event=event,
|
||||
class_name=class_name,
|
||||
goal_info__isnull=False
|
||||
).count()
|
||||
|
||||
# GeoJSON形式のデータを作成
|
||||
geo_json = {
|
||||
"type": "FeatureCollection",
|
||||
"features": geo_json_features
|
||||
}
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event_code": event_code,
|
||||
"class_name": class_name,
|
||||
"top_limit": limit,
|
||||
"total_teams_in_class": total_teams_in_class,
|
||||
"total_top_teams": len(teams_data),
|
||||
"teams": teams_data,
|
||||
"geo_json": geo_json
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in top_users_routes: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
pip install folium selenium webdriver-manager Pillow
|
||||
"""
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, Waypoint, GpsLog
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import folium
|
||||
from folium.plugins import MarkerCluster
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
from PIL import Image
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from django.utils import timezone
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコードとゼッケン番号のパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとチームの存在を確認します
|
||||
4.チームのウェイポイントとチェックポイントデータを取得します
|
||||
5.Foliumライブラリを使用して地図を生成し、以下の情報を描画します:
|
||||
- ルート(青い線)
|
||||
- スタート地点(緑のマーカー)
|
||||
- 現在地点/ゴール地点(赤のマーカー)
|
||||
- チェックポイント(緑のフラグ:通過済み、グレーのフラグ:未通過)
|
||||
- チーム情報(名前、ゼッケン番号、クラス、スタート時間、ゴール時間、スコア)
|
||||
6.生成した地図をHTMLとして一時保存します
|
||||
7.Seleniumを使用して、HTMLのスクリーンショットを撮影しPNG画像を生成します
|
||||
8.画像をメディアディレクトリに保存し、URLを生成します
|
||||
9.画像のURLを含むレスポンスを返します
|
||||
|
||||
実装のポイント:
|
||||
|
||||
- Foliumライブラリを使用することで、OpenStreetMapをベースにした高品質な地図を生成できます
|
||||
- Seleniumでのスクリーンショット撮影により、インタラクティブな地図を静的画像として保存できます
|
||||
- チェックポイントの通過状況に応じてマーカーの色を変えることで、視覚的な情報を提供します
|
||||
- チーム情報を地図上に表示することで、画像だけで必要な情報が分かるようにしています
|
||||
|
||||
注意点:
|
||||
|
||||
- この実装にはSeleniumとChromeドライバーが必要です。サーバー環境によっては追加の設定が必要になる場合があります
|
||||
- 画像生成は比較的重い処理なので、リクエストが多い場合はキャッシュ機能の実装を検討するとよいでしょう
|
||||
|
||||
このエンドポイントにより、チームの移動ルートを可視化した画像を生成し、ウェブページに埋め込んだり、
|
||||
SNSで共有したりすることができます。
|
||||
"""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@api_view(['GET'])
|
||||
def generate_route_image(request):
|
||||
"""
|
||||
チームのルートを可視化した画像を生成
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- zekken_number: ゼッケン番号
|
||||
"""
|
||||
logger.info("generate_route_image called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event_code')
|
||||
zekken_number = request.query_params.get('zekken_number')
|
||||
|
||||
# 別名のパラメータ対応
|
||||
if not event_code:
|
||||
event_code = request.query_params.get('event')
|
||||
if not zekken_number:
|
||||
zekken_number = request.query_params.get('zekken')
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([event_code, 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"Team with zekken number {zekken_number} not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# ウェイポイントデータを取得(時間順)
|
||||
waypoints = Waypoint.objects.filter(
|
||||
entry=entry
|
||||
).order_by('recorded_at')
|
||||
|
||||
if not waypoints.exists():
|
||||
logger.warning(f"No waypoints found for team: {entry.team_name}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "このチームのウェイポイントデータがありません"
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
# チェックポイント通過情報を取得(時間順)
|
||||
checkpoints = GpsLog.objects.filter(
|
||||
entry=entry
|
||||
).order_by('checkin_time')
|
||||
|
||||
# チェックポイント情報の取得(座標)
|
||||
cp_locations = {}
|
||||
try:
|
||||
event_cps = Location.objects.filter(event=event)
|
||||
|
||||
for cp in event_cps:
|
||||
if cp.latitude is not None and cp.longitude is not None:
|
||||
cp_locations[cp.cp_number] = {
|
||||
'lat': cp.latitude,
|
||||
'lon': cp.longitude,
|
||||
'name': cp.cp_name,
|
||||
'point': cp.cp_point
|
||||
}
|
||||
except:
|
||||
# Locationモデルが存在しない場合
|
||||
pass
|
||||
|
||||
# チームが通過したチェックポイント番号のリスト
|
||||
visited_cps = set([cp.cp_number for cp in checkpoints])
|
||||
|
||||
# 地図の中心座標を計算(全ウェイポイントの中央値)
|
||||
lats = [wp.latitude for wp in waypoints]
|
||||
lons = [wp.longitude for wp in waypoints]
|
||||
center_lat = sum(lats) / len(lats)
|
||||
center_lon = sum(lons) / len(lons)
|
||||
|
||||
# スタート地点と終了地点
|
||||
start_point = (waypoints.first().latitude, waypoints.first().longitude)
|
||||
end_point = (waypoints.last().latitude, waypoints.last().longitude)
|
||||
|
||||
# Folium地図を作成
|
||||
m = folium.Map(location=[center_lat, center_lon], zoom_start=13)
|
||||
|
||||
# ルートを描画
|
||||
route_points = [(wp.latitude, wp.longitude) for wp in waypoints]
|
||||
folium.PolyLine(
|
||||
route_points,
|
||||
color='blue',
|
||||
weight=3,
|
||||
opacity=0.7
|
||||
).add_to(m)
|
||||
|
||||
# スタート地点をマーク
|
||||
folium.Marker(
|
||||
start_point,
|
||||
popup="スタート",
|
||||
icon=folium.Icon(color='green', icon='play', prefix='fa')
|
||||
).add_to(m)
|
||||
|
||||
# 終了地点をマーク
|
||||
folium.Marker(
|
||||
end_point,
|
||||
popup="現在位置 / ゴール",
|
||||
icon=folium.Icon(color='red', icon='stop', prefix='fa')
|
||||
).add_to(m)
|
||||
|
||||
# チェックポイントを描画
|
||||
for cp_number, cp_info in cp_locations.items():
|
||||
visited = cp_number in visited_cps
|
||||
color = 'green' if visited else 'gray'
|
||||
|
||||
folium.Marker(
|
||||
[cp_info['lat'], cp_info['lon']],
|
||||
popup=f"{cp_info['name']} ({cp_info['point']}点) - {'通過済' if visited else '未通過'}",
|
||||
icon=folium.Icon(color=color, icon='flag', prefix='fa')
|
||||
).add_to(m)
|
||||
|
||||
# チーム情報を追加
|
||||
team_info = f"""
|
||||
<div style="font-size: 12px; font-family: Arial;">
|
||||
<h3>チーム情報</h3>
|
||||
<p>チーム名: {entry.team_name}</p>
|
||||
<p>ゼッケン番号: {entry.zekken_number}</p>
|
||||
<p>クラス: {entry.class_name}</p>
|
||||
"""
|
||||
|
||||
if hasattr(entry, 'start_info'):
|
||||
team_info += f"<p>スタート時間: {entry.start_info.start_time.strftime('%Y-%m-%d %H:%M:%S')}</p>"
|
||||
|
||||
if hasattr(entry, 'goal_info'):
|
||||
team_info += f"""
|
||||
<p>ゴール時間: {entry.goal_info.goal_time.strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
<p>スコア: {entry.goal_info.score}点</p>
|
||||
"""
|
||||
|
||||
team_info += "</div>"
|
||||
|
||||
# チーム情報をマップに追加
|
||||
folium.Element(team_info).add_to(m)
|
||||
|
||||
# 一時的にHTMLファイルを保存
|
||||
timestamp = timezone.now().strftime("%Y%m%d%H%M%S")
|
||||
html_file = f"route_map_{event_code}_{zekken_number}_{timestamp}.html"
|
||||
|
||||
# 一時ディレクトリを作成
|
||||
tmp_dir = os.path.join(settings.MEDIA_ROOT, 'tmp', 'route_maps')
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
html_path = os.path.join(tmp_dir, html_file)
|
||||
|
||||
m.save(html_path)
|
||||
|
||||
# HTMLをPNGに変換
|
||||
png_file = f"route_map_{event_code}_{zekken_number}_{timestamp}.png"
|
||||
png_path = os.path.join(tmp_dir, png_file)
|
||||
|
||||
# Seleniumを使用してスクリーンショットを撮影
|
||||
try:
|
||||
# Chromeオプションを設定
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument('--headless')
|
||||
options.add_argument('--no-sandbox')
|
||||
options.add_argument('--disable-dev-shm-usage')
|
||||
options.add_argument('--disable-gpu')
|
||||
|
||||
# ウィンドウサイズを設定
|
||||
options.add_argument('--window-size=1200,900')
|
||||
|
||||
# WebDriverを初期化
|
||||
service = Service(ChromeDriverManager().install())
|
||||
driver = webdriver.Chrome(service=service, options=options)
|
||||
|
||||
# HTMLファイルを開く
|
||||
driver.get(f"file://{html_path}")
|
||||
|
||||
# 地図の読み込みを待機
|
||||
time.sleep(2)
|
||||
|
||||
# スクリーンショットを撮影
|
||||
driver.save_screenshot(png_path)
|
||||
driver.quit()
|
||||
|
||||
# メディアディレクトリに移動
|
||||
final_path = os.path.join('route_maps', png_file)
|
||||
with open(png_path, 'rb') as f:
|
||||
default_storage.save(final_path, ContentFile(f.read()))
|
||||
|
||||
# 画像のURLを生成
|
||||
image_url = settings.MEDIA_URL + final_path
|
||||
|
||||
# 一時ファイルを削除
|
||||
if os.path.exists(html_path):
|
||||
os.remove(html_path)
|
||||
if os.path.exists(png_path):
|
||||
os.remove(png_path)
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"team_name": entry.team_name,
|
||||
"event_code": event_code,
|
||||
"zekken_number": zekken_number,
|
||||
"image_url": image_url
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating screenshot: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "地図の画像生成に失敗しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in generate_route_image: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
1417
rog/views_apis/api_scoreboard.py
Executable file
1417
rog/views_apis/api_scoreboard.py
Executable file
File diff suppressed because it is too large
Load Diff
546
rog/views_apis/api_simulator.py
Executable file
546
rog/views_apis/api_simulator.py
Executable file
@ -0,0 +1,546 @@
|
||||
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2
|
||||
import logging
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import math
|
||||
from haversine import haversine
|
||||
import folium
|
||||
import json
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では、ロゲイニングコースのルートシミュレーションを行う機能を提供しています:
|
||||
|
||||
1.データ入力とパラメータ設定:
|
||||
- イベントコード、コース時間、停止時間などのパラメータを受け取ります
|
||||
- 訪問すべき必須ノード(無料ノードと有料ノード)を指定できます
|
||||
- 目標速度を設定し、移動時間の計算に使用します
|
||||
2.グラフモデル構築:
|
||||
- NetworkXライブラリを使用してグラフを構築します
|
||||
- チェックポイントをノードとして、移動経路をエッジとして表現
|
||||
- 各エッジには距離と所要時間の属性を付与
|
||||
- Haversine公式を使用して緯度・経度から実際の距離を計算
|
||||
3.ルート最適化アルゴリズム:
|
||||
- 必須ノードを優先的に訪問し、残りの時間で効率良く他のノードを訪問
|
||||
- ポイント/時間の比率に基づいて効率の良いチェックポイントを選択
|
||||
- 制限時間内にゴールに戻ることを保証
|
||||
4.結果の可視化:
|
||||
- Foliumライブラリを使用して地図上にルートを可視化
|
||||
- 訪問順序、ポイント情報などを地図上に表示
|
||||
- 結果はHTML形式で保存され、ブラウザで確認可能
|
||||
5.レスポンスデータ:
|
||||
- 合計ポイント、総移動距離、総所要時間などの統計情報
|
||||
- 訪問ノード数や必須ノードの完了状況
|
||||
- 詳細なルート情報(各ステップの移動距離、所要時間、獲得ポイントなど)
|
||||
- 生成された地図へのURL
|
||||
|
||||
このシミュレーターは、ロゲイニング参加者がコース計画を立てる際に役立ちます。
|
||||
自分のペースや優先するチェックポイントに基づいて、最適なルートを事前に検討できます。
|
||||
また、イベント主催者がコースデザインの評価や、ポイント配分の調整に活用することもできます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def rogaining_simulator(request):
|
||||
"""
|
||||
ロゲイニングのルートシミュレーションを実行
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- course_time: コース時間(分)
|
||||
- pause_time_free: 無料CP停止時間(分)
|
||||
- pause_time_paid: 有料CP停止時間(分)
|
||||
- spare_time: 予備時間(分)
|
||||
- target_velocity: 目標速度(km/h)
|
||||
- free_node_to_visit: 訪問する無料ノード
|
||||
- paid_node_to_visit: 訪問する有料ノード
|
||||
"""
|
||||
logger.info("rogaining_simulator called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event_code')
|
||||
|
||||
try:
|
||||
course_time = float(request.query_params.get('course_time', 180)) # デフォルト3時間
|
||||
pause_time_free = float(request.query_params.get('pause_time_free', 2)) # デフォルト2分
|
||||
pause_time_paid = float(request.query_params.get('pause_time_paid', 5)) # デフォルト5分
|
||||
spare_time = float(request.query_params.get('spare_time', 30)) # デフォルト30分
|
||||
target_velocity = float(request.query_params.get('target_velocity', 5.0)) # デフォルト5km/h
|
||||
|
||||
free_node_to_visit = request.query_params.get('free_node_to_visit', '')
|
||||
paid_node_to_visit = request.query_params.get('paid_node_to_visit', '')
|
||||
|
||||
# カンマ区切りの文字列をリストに変換
|
||||
free_nodes = [n.strip() for n in free_node_to_visit.split(',') if n.strip()]
|
||||
paid_nodes = [n.strip() for n in paid_node_to_visit.split(',') if n.strip()]
|
||||
except ValueError as e:
|
||||
logger.error(f"Parameter conversion error: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": f"パラメータの変換に失敗しました: {str(e)}"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, course_time={course_time}, "
|
||||
f"pause_time_free={pause_time_free}, pause_time_paid={pause_time_paid}, "
|
||||
f"spare_time={spare_time}, target_velocity={target_velocity}, "
|
||||
f"free_nodes={free_nodes}, paid_nodes={paid_nodes}")
|
||||
|
||||
# パラメータ検証
|
||||
if not event_code:
|
||||
logger.warning("Missing required event_code parameter")
|
||||
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)
|
||||
|
||||
# チェックポイント情報の取得
|
||||
checkpoints = []
|
||||
try:
|
||||
event_cps = Location.objects.filter(event=event)
|
||||
|
||||
for cp in event_cps:
|
||||
if not cp.latitude or not cp.longitude:
|
||||
continue
|
||||
|
||||
checkpoints.append({
|
||||
'id': cp.cp_number,
|
||||
'name': cp.cp_name or cp.cp_number,
|
||||
'point': cp.cp_point or 0,
|
||||
'latitude': float(cp.latitude),
|
||||
'longitude': float(cp.longitude),
|
||||
'is_paid': cp.cp_number in paid_nodes,
|
||||
'is_free': cp.cp_number in free_nodes,
|
||||
'pause_time': pause_time_paid if cp.cp_number in paid_nodes else pause_time_free
|
||||
})
|
||||
except:
|
||||
# Locationモデルが存在しない場合、ダミーデータを作成
|
||||
logger.warning("Location model not found, using dummy data")
|
||||
checkpoints = []
|
||||
for i in range(1, 11):
|
||||
cp_number = f"CP{i}"
|
||||
checkpoints.append({
|
||||
'id': cp_number,
|
||||
'name': f"チェックポイント{i}",
|
||||
'point': i * 10,
|
||||
'latitude': 35.0 + i * 0.01,
|
||||
'longitude': 135.0 + i * 0.01,
|
||||
'is_paid': cp_number in paid_nodes,
|
||||
'is_free': cp_number in free_nodes,
|
||||
'pause_time': pause_time_paid if cp_number in paid_nodes else pause_time_free
|
||||
})
|
||||
|
||||
# スタートとゴールポイントの取得
|
||||
start_point = None
|
||||
goal_point = None
|
||||
|
||||
# 設定からスタート/ゴールポイントを取得(実際のシステムに合わせて調整)
|
||||
try:
|
||||
# イベント設定モデルからスタート/ゴール情報を取得
|
||||
from rog.models import EventSetting
|
||||
event_setting = EventSetting.objects.filter(event=event).first()
|
||||
|
||||
if event_setting:
|
||||
if all([event_setting.start_latitude, event_setting.start_longitude]):
|
||||
start_point = {
|
||||
'id': 'START',
|
||||
'name': 'スタート地点',
|
||||
'point': 0,
|
||||
'latitude': float(event_setting.start_latitude),
|
||||
'longitude': float(event_setting.start_longitude),
|
||||
'is_paid': False,
|
||||
'is_free': False,
|
||||
'pause_time': 0
|
||||
}
|
||||
|
||||
if all([event_setting.goal_latitude, event_setting.goal_longitude]):
|
||||
goal_point = {
|
||||
'id': 'GOAL',
|
||||
'name': 'ゴール地点',
|
||||
'point': 0,
|
||||
'latitude': float(event_setting.goal_latitude),
|
||||
'longitude': float(event_setting.goal_longitude),
|
||||
'is_paid': False,
|
||||
'is_free': False,
|
||||
'pause_time': 0
|
||||
}
|
||||
except:
|
||||
logger.warning("EventSetting model not found, using first CP as start/goal")
|
||||
|
||||
# スタート/ゴールが設定されていない場合、最初のCPをスタートとゴールに設定
|
||||
if not start_point and checkpoints:
|
||||
start_point = checkpoints[0].copy()
|
||||
start_point['id'] = 'START'
|
||||
start_point['name'] = 'スタート地点'
|
||||
start_point['point'] = 0
|
||||
start_point['is_paid'] = False
|
||||
start_point['is_free'] = False
|
||||
start_point['pause_time'] = 0
|
||||
|
||||
if not goal_point and checkpoints:
|
||||
goal_point = checkpoints[0].copy()
|
||||
goal_point['id'] = 'GOAL'
|
||||
goal_point['name'] = 'ゴール地点'
|
||||
goal_point['point'] = 0
|
||||
goal_point['is_paid'] = False
|
||||
goal_point['is_free'] = False
|
||||
goal_point['pause_time'] = 0
|
||||
|
||||
# ルートシミュレーション実行
|
||||
result = simulate_rogaining_route(
|
||||
checkpoints,
|
||||
start_point,
|
||||
goal_point,
|
||||
course_time=course_time,
|
||||
spare_time=spare_time,
|
||||
target_velocity=target_velocity,
|
||||
free_nodes=free_nodes,
|
||||
paid_nodes=paid_nodes
|
||||
)
|
||||
|
||||
# 結果を返す
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event_code": event_code,
|
||||
"simulation_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"parameters": {
|
||||
"course_time": course_time,
|
||||
"pause_time_free": pause_time_free,
|
||||
"pause_time_paid": pause_time_paid,
|
||||
"spare_time": spare_time,
|
||||
"target_velocity": target_velocity,
|
||||
"free_node_count": len(free_nodes),
|
||||
"paid_node_count": len(paid_nodes)
|
||||
},
|
||||
"result": result
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in rogaining_simulator: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "シミュレーション中にエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def simulate_rogaining_route(checkpoints, start_point, goal_point, course_time, spare_time, target_velocity, free_nodes, paid_nodes):
|
||||
"""
|
||||
ロゲイニングのルートをシミュレーション
|
||||
"""
|
||||
# グラフを作成
|
||||
G = nx.Graph()
|
||||
|
||||
# ノードとエッジを追加
|
||||
all_points = [start_point] + checkpoints + [goal_point]
|
||||
all_points_dict = {p['id']: p for p in all_points}
|
||||
|
||||
# ノードを追加
|
||||
for point in all_points:
|
||||
G.add_node(
|
||||
point['id'],
|
||||
name=point['name'],
|
||||
point=point['point'],
|
||||
lat=point['latitude'],
|
||||
lng=point['longitude'],
|
||||
is_paid=point['is_paid'],
|
||||
is_free=point['is_free'],
|
||||
pause_time=point['pause_time']
|
||||
)
|
||||
|
||||
# エッジを追加(全ノード間)
|
||||
for i, point1 in enumerate(all_points):
|
||||
for j, point2 in enumerate(all_points):
|
||||
if i != j:
|
||||
# 2点間の距離を計算(km)
|
||||
dist = haversine(
|
||||
(point1['latitude'], point1['longitude']),
|
||||
(point2['latitude'], point2['longitude'])
|
||||
)
|
||||
|
||||
# 時間を計算(分)
|
||||
time_min = (dist / target_velocity) * 60
|
||||
|
||||
G.add_edge(point1['id'], point2['id'], distance=dist, time=time_min)
|
||||
|
||||
# 訪問必須ノードの集合
|
||||
must_visit = set(free_nodes + paid_nodes)
|
||||
|
||||
# シミュレーション実行
|
||||
available_time = course_time - spare_time # 利用可能時間(分)
|
||||
|
||||
# 最適化アルゴリズム - ここでは貪欲法の例
|
||||
current_node = 'START'
|
||||
visited = [current_node]
|
||||
total_time = 0
|
||||
total_points = 0
|
||||
route_details = []
|
||||
|
||||
route_details.append({
|
||||
'node': current_node,
|
||||
'name': all_points_dict[current_node]['name'],
|
||||
'point': 0,
|
||||
'distance_from_prev': 0,
|
||||
'time_from_prev': 0,
|
||||
'pause_time': 0,
|
||||
'cumulative_time': 0,
|
||||
'cumulative_points': 0,
|
||||
'lat': all_points_dict[current_node]['lat'],
|
||||
'lng': all_points_dict[current_node]['lng']
|
||||
})
|
||||
|
||||
# 訪問必須ノードを先に処理
|
||||
remaining_must_visit = must_visit.copy()
|
||||
while remaining_must_visit and total_time < available_time:
|
||||
best_node = None
|
||||
best_ratio = -1
|
||||
best_time = float('inf')
|
||||
|
||||
for node in remaining_must_visit:
|
||||
if node in visited:
|
||||
continue
|
||||
|
||||
# 現在のノードから次のノードへの移動時間
|
||||
edge_time = G.edges[current_node, node]['time']
|
||||
# ノードでの停止時間
|
||||
pause_time = G.nodes[node]['pause_time']
|
||||
# 合計所要時間
|
||||
node_time = edge_time + pause_time
|
||||
|
||||
# このノードの得点
|
||||
node_point = G.nodes[node]['point']
|
||||
|
||||
# 時間あたりの得点比率(高いほど効率が良い)
|
||||
ratio = node_point / node_time if node_time > 0 else 0
|
||||
|
||||
if ratio > best_ratio:
|
||||
best_ratio = ratio
|
||||
best_node = node
|
||||
best_time = node_time
|
||||
|
||||
if best_node and total_time + best_time <= available_time:
|
||||
# 移動可能な場合、訪問
|
||||
edge_time = G.edges[current_node, best_node]['time']
|
||||
pause_time = G.nodes[best_node]['pause_time']
|
||||
distance = G.edges[current_node, best_node]['distance']
|
||||
node_point = G.nodes[best_node]['point']
|
||||
|
||||
total_time += edge_time + pause_time
|
||||
total_points += node_point
|
||||
|
||||
route_details.append({
|
||||
'node': best_node,
|
||||
'name': all_points_dict[best_node]['name'],
|
||||
'point': node_point,
|
||||
'distance_from_prev': distance,
|
||||
'time_from_prev': edge_time,
|
||||
'pause_time': pause_time,
|
||||
'cumulative_time': total_time,
|
||||
'cumulative_points': total_points,
|
||||
'lat': all_points_dict[best_node]['lat'],
|
||||
'lng': all_points_dict[best_node]['lng']
|
||||
})
|
||||
|
||||
visited.append(best_node)
|
||||
current_node = best_node
|
||||
remaining_must_visit.remove(best_node)
|
||||
else:
|
||||
# これ以上訪問できない
|
||||
break
|
||||
|
||||
# 残りの時間で他のノードを訪問(必須でないノードも含めて効率の良い順に)
|
||||
remaining_nodes = [node for node in G.nodes if node not in visited and node != 'GOAL']
|
||||
|
||||
while remaining_nodes and total_time < available_time:
|
||||
best_node = None
|
||||
best_ratio = -1
|
||||
best_time = float('inf')
|
||||
|
||||
for node in remaining_nodes:
|
||||
# 現在のノードから次のノードへの移動時間
|
||||
edge_time = G.edges[current_node, node]['time']
|
||||
# ノードでの停止時間
|
||||
pause_time = G.nodes[node]['pause_time']
|
||||
# 合計所要時間
|
||||
node_time = edge_time + pause_time
|
||||
|
||||
# このノードから最終的にゴールに行くのに必要な時間
|
||||
to_goal_time = G.edges[node, 'GOAL']['time']
|
||||
|
||||
# このノードの得点
|
||||
node_point = G.nodes[node]['point']
|
||||
|
||||
# 時間あたりの得点比率(高いほど効率が良い)
|
||||
ratio = node_point / node_time if node_time > 0 else 0
|
||||
|
||||
# ゴールまで行ける場合のみ考慮
|
||||
if total_time + node_time + to_goal_time <= available_time and ratio > best_ratio:
|
||||
best_ratio = ratio
|
||||
best_node = node
|
||||
best_time = node_time
|
||||
|
||||
if best_node:
|
||||
# 移動可能な場合、訪問
|
||||
edge_time = G.edges[current_node, best_node]['time']
|
||||
pause_time = G.nodes[best_node]['pause_time']
|
||||
distance = G.edges[current_node, best_node]['distance']
|
||||
node_point = G.nodes[best_node]['point']
|
||||
|
||||
total_time += edge_time + pause_time
|
||||
total_points += node_point
|
||||
|
||||
route_details.append({
|
||||
'node': best_node,
|
||||
'name': all_points_dict[best_node]['name'],
|
||||
'point': node_point,
|
||||
'distance_from_prev': distance,
|
||||
'time_from_prev': edge_time,
|
||||
'pause_time': pause_time,
|
||||
'cumulative_time': total_time,
|
||||
'cumulative_points': total_points,
|
||||
'lat': all_points_dict[best_node]['lat'],
|
||||
'lng': all_points_dict[best_node]['lng']
|
||||
})
|
||||
|
||||
visited.append(best_node)
|
||||
current_node = best_node
|
||||
remaining_nodes.remove(best_node)
|
||||
else:
|
||||
# これ以上訪問できない
|
||||
break
|
||||
|
||||
# ゴールまでの移動
|
||||
edge_time = G.edges[current_node, 'GOAL']['time']
|
||||
distance = G.edges[current_node, 'GOAL']['distance']
|
||||
|
||||
total_time += edge_time
|
||||
|
||||
route_details.append({
|
||||
'node': 'GOAL',
|
||||
'name': all_points_dict['GOAL']['name'],
|
||||
'point': 0,
|
||||
'distance_from_prev': distance,
|
||||
'time_from_prev': edge_time,
|
||||
'pause_time': 0,
|
||||
'cumulative_time': total_time,
|
||||
'cumulative_points': total_points,
|
||||
'lat': all_points_dict['GOAL']['lat'],
|
||||
'lng': all_points_dict['GOAL']['lng']
|
||||
})
|
||||
|
||||
visited.append('GOAL')
|
||||
|
||||
# ルートの可視化
|
||||
map_file = visualize_route(route_details, event_code=start_point['name'])
|
||||
|
||||
# 結果まとめ
|
||||
total_distance = sum(step['distance_from_prev'] for step in route_details)
|
||||
|
||||
# 必須ノードの訪問状況チェック
|
||||
unvisited_must = must_visit - set(visited)
|
||||
|
||||
# マップへのURLを生成
|
||||
map_url = f"/media/simulations/{os.path.basename(map_file)}" if map_file else None
|
||||
|
||||
result = {
|
||||
'total_points': total_points,
|
||||
'total_time': total_time,
|
||||
'total_distance': total_distance,
|
||||
'visited_nodes_count': len(visited) - 2, # スタートとゴールを除く
|
||||
'must_visit_completed': len(unvisited_must) == 0,
|
||||
'unvisited_must_nodes': list(unvisited_must),
|
||||
'route': route_details,
|
||||
'map_url': map_url
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def visualize_route(route_details, event_code='rogaining'):
|
||||
"""
|
||||
ルートを可視化して地図を生成
|
||||
"""
|
||||
try:
|
||||
# ルートの中心位置を計算
|
||||
lats = [step['lat'] for step in route_details]
|
||||
lngs = [step['lng'] for step in route_details]
|
||||
center_lat = sum(lats) / len(lats)
|
||||
center_lng = sum(lngs) / len(lngs)
|
||||
|
||||
# 地図を作成
|
||||
m = folium.Map(location=[center_lat, center_lng], zoom_start=13)
|
||||
|
||||
# ルートをプロット
|
||||
route_points = [(step['lat'], step['lng']) for step in route_details]
|
||||
folium.PolyLine(
|
||||
route_points,
|
||||
color='blue',
|
||||
weight=3,
|
||||
opacity=0.7
|
||||
).add_to(m)
|
||||
|
||||
# スタート地点にマーカーを追加
|
||||
folium.Marker(
|
||||
[route_details[0]['lat'], route_details[0]['lng']],
|
||||
popup='スタート',
|
||||
icon=folium.Icon(color='green', icon='play')
|
||||
).add_to(m)
|
||||
|
||||
# ゴール地点にマーカーを追加
|
||||
folium.Marker(
|
||||
[route_details[-1]['lat'], route_details[-1]['lng']],
|
||||
popup='ゴール',
|
||||
icon=folium.Icon(color='red', icon='stop')
|
||||
).add_to(m)
|
||||
|
||||
# 各チェックポイントにマーカーを追加
|
||||
for i, step in enumerate(route_details[1:-1], 1):
|
||||
folium.Marker(
|
||||
[step['lat'], step['lng']],
|
||||
popup=f"{i}. {step['name']} ({step['point']}pts)",
|
||||
icon=folium.Icon(color='blue')
|
||||
).add_to(m)
|
||||
|
||||
# 訪問順序を表示
|
||||
folium.Circle(
|
||||
[step['lat'], step['lng']],
|
||||
radius=10,
|
||||
color='black',
|
||||
fill=True,
|
||||
fill_color='white',
|
||||
fill_opacity=0.7,
|
||||
popup=str(i)
|
||||
).add_to(m)
|
||||
|
||||
# 保存先ディレクトリを確保
|
||||
sim_dir = os.path.join(settings.MEDIA_ROOT, 'simulations')
|
||||
os.makedirs(sim_dir, exist_ok=True)
|
||||
|
||||
# タイムスタンプを含むファイル名
|
||||
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"route_sim_{event_code}_{timestamp}.html"
|
||||
file_path = os.path.join(sim_dir, filename)
|
||||
|
||||
# 地図を保存
|
||||
m.save(file_path)
|
||||
|
||||
return file_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error visualizing route: {str(e)}")
|
||||
return None
|
||||
695
rog/views_apis/api_teams.py
Executable file
695
rog/views_apis/api_teams.py
Executable file
@ -0,0 +1,695 @@
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, GpsLog
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from django.conf import settings
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
このコードでは、以下の処理を行っています:
|
||||
|
||||
1.event_codeパラメータを受け取ります(オプショナル)
|
||||
2.event_codeが指定されている場合:
|
||||
- そのイベントに所属するチームのみを抽出します
|
||||
- イベントが存在しない場合はエラーを返します
|
||||
3.event_codeが指定されていない場合:
|
||||
- すべてのイベントのすべてのチームを抽出します
|
||||
- チーム情報を整形してJSONレスポンスとして返します
|
||||
|
||||
このコードはDjangoの既存モデルを使用し、
|
||||
Entryモデルと関連するEventおよびOwner(CustomUser)を使用してチーム情報を取得します。
|
||||
必要に応じて、実際のモデル構造に合わせてフィールド名などを調整してください。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_team_list(request):
|
||||
"""
|
||||
指定されたイベントのチームリスト、または全イベントのチームリストを取得
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード(省略可)
|
||||
"""
|
||||
logger.info("get_team_list called")
|
||||
|
||||
event_code = request.query_params.get('event_code')
|
||||
logger.debug(f"Parameters: event_code={event_code}")
|
||||
|
||||
try:
|
||||
# イベントコードが指定された場合、そのイベントのチームのみを抽出
|
||||
if event_code:
|
||||
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)
|
||||
|
||||
# 指定されたイベントに所属するチームを取得
|
||||
entries = Entry.objects.filter(event=event).select_related('owner')
|
||||
|
||||
else:
|
||||
# イベントコードが指定されていない場合は全てのエントリーを取得
|
||||
entries = Entry.objects.all().select_related('owner', 'event')
|
||||
|
||||
# チーム情報をJSON形式に整形
|
||||
team_list = []
|
||||
for entry in entries:
|
||||
try:
|
||||
team_info = {
|
||||
"zekken_number": entry.zekken_number,
|
||||
"team_name": entry.team_name,
|
||||
"class_name": entry.class_name,
|
||||
"event_code": entry.event.event_name if entry.event else "",
|
||||
"owner_name": entry.owner.username if entry.owner else "",
|
||||
"registration_date": entry.created_at.strftime("%Y-%m-%d %H:%M:%S") if hasattr(entry, 'created_at') else ""
|
||||
}
|
||||
team_list.append(team_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing entry {entry.id}: {str(e)}")
|
||||
|
||||
logger.info(f"Successfully retrieved {len(team_list)} teams")
|
||||
return Response({"status": "OK", "teams": team_list})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_team_list: {str(e)}")
|
||||
return Response({"status": "ERROR", "message": "サーバーエラーが発生しました"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
"""
|
||||
解説
|
||||
このコードでは以下の処理を行っています:
|
||||
|
||||
1.eventパラメータを受け取り、イベントコードの指定を確認します
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントコードでイベントを検索します
|
||||
- イベントが存在しない場合はエラーを返します
|
||||
4.見つかったイベントに関連付けられたすべてのエントリーからゼッケン番号を抽出します
|
||||
5,ゼッケン番号のリストをJSON形式で返します
|
||||
|
||||
このエンドポイントは、指定したイベントに参加している全チームのゼッケン番号を一覧で取得するのに役立ちます。
|
||||
"""
|
||||
@api_view(['GET'])
|
||||
def get_zekken_list(request):
|
||||
"""
|
||||
指定されたイベントの全ゼッケン番号を取得
|
||||
|
||||
パラメータ:
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("get_zekken_list called")
|
||||
|
||||
event_code = request.query_params.get('event')
|
||||
logger.debug(f"Parameters: event={event_code}")
|
||||
|
||||
if not event_code:
|
||||
logger.warning("Event code not provided")
|
||||
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)
|
||||
|
||||
# 指定されたイベントに所属するエントリーからゼッケン番号を抽出
|
||||
entries = Entry.objects.filter(event=event)
|
||||
zekken_list = [entry.zekken_number for entry in entries if entry.zekken_number]
|
||||
|
||||
logger.info(f"Successfully retrieved {len(zekken_list)} zekken numbers for event {event_code}")
|
||||
return Response({"status": "OK", "zekken_list": zekken_list})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_zekken_list: {str(e)}")
|
||||
return Response({"status": "ERROR", "message": "サーバーエラーが発生しました"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では次のような処理を行っています:
|
||||
|
||||
1.必要なパラメータ(ゼッケン番号、イベントコード、チーム名、クラス名、パスワード)を受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントが存在するか確認します
|
||||
4.指定されたゼッケン番号が既に使用されていないか確認します
|
||||
5.以下の処理をトランザクションで実行します:
|
||||
- 新しいユーザーアカウントを作成します(ユーザー名は「チーム名_ゼッケン番号」の形式)
|
||||
- ユーザーのパスワードをハッシュ化して保存します
|
||||
- 新しいチームエントリーを作成し、ユーザーとイベントに紐付けます
|
||||
6.成功した場合、登録成功のメッセージとチームIDを返します
|
||||
|
||||
これにより、チームの登録を安全かつ一貫性を保って行うことができます。
|
||||
"""
|
||||
@api_view(['POST'])
|
||||
def register_team(request):
|
||||
"""
|
||||
新しいチームを登録
|
||||
|
||||
パラメータ:
|
||||
- zekken_number: ゼッケン番号
|
||||
- event_code: イベントコード
|
||||
- team_name: チーム名
|
||||
- class_name: クラス名
|
||||
- password: パスワード
|
||||
"""
|
||||
logger.info("register_team called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
zekken_number = request.data.get('zekken_number')
|
||||
event_code = request.data.get('event_code')
|
||||
team_name = request.data.get('team_name')
|
||||
class_name = request.data.get('class_name')
|
||||
password = request.data.get('password')
|
||||
|
||||
logger.debug(f"Parameters: zekken_number={zekken_number}, event_code={event_code}, "
|
||||
f"team_name={team_name}, class_name={class_name}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, event_code, team_name, class_name, password]):
|
||||
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)
|
||||
|
||||
# ゼッケン番号の重複チェック
|
||||
existing_entry = Entry.objects.filter(
|
||||
event=event,
|
||||
zekken_number=zekken_number
|
||||
).first()
|
||||
|
||||
if existing_entry:
|
||||
logger.warning(f"Duplicate zekken number: {zekken_number} for event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "このゼッケン番号は既に使用されています"
|
||||
}, status=status.HTTP_409_CONFLICT)
|
||||
|
||||
# トランザクション開始
|
||||
with transaction.atomic():
|
||||
# ユーザー作成(ユーザー名はチーム名+ゼッケン番号とする)
|
||||
username = f"{team_name}_{zekken_number}"
|
||||
|
||||
# 同じユーザー名が存在するか確認
|
||||
if CustomUser.objects.filter(username=username).exists():
|
||||
username = f"{team_name}_{zekken_number}_{event_code}"
|
||||
|
||||
user = CustomUser.objects.create(
|
||||
username=username,
|
||||
password=make_password(password) # パスワードをハッシュ化
|
||||
)
|
||||
|
||||
# チーム登録
|
||||
entry = Entry.objects.create(
|
||||
owner=user,
|
||||
zekken_number=zekken_number,
|
||||
team_name=team_name,
|
||||
class_name=class_name,
|
||||
event=event
|
||||
)
|
||||
|
||||
logger.info(f"Successfully registered team: {team_name} with zekken: {zekken_number} for event: {event_code}")
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "チームが正常に登録されました",
|
||||
"team_id": entry.id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in register_team: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.必要なパラメータ(ゼッケン番号、新しいチーム名、イベントコード)を受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントが存在するか確認します
|
||||
4.指定されたゼッケン番号とイベントの組み合わせでエントリーを検索します
|
||||
- 該当するエントリーが見つからない場合はエラーを返します
|
||||
5.エントリーのチーム名を新しい値に更新します
|
||||
6.成功した場合、元のチーム名と新しいチーム名を含む成功メッセージを返します
|
||||
|
||||
これにより、既存のチームの名前を安全に更新することができます。
|
||||
"""
|
||||
@api_view(['POST'])
|
||||
def update_team_name(request):
|
||||
"""
|
||||
チーム名を更新
|
||||
|
||||
パラメータ:
|
||||
- zekken_number: ゼッケン番号
|
||||
- new_team_name: 新しいチーム名
|
||||
- event_code: イベントコード
|
||||
"""
|
||||
logger.info("update_team_name called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
zekken_number = request.data.get('zekken_number')
|
||||
new_team_name = request.data.get('new_team_name')
|
||||
event_code = request.data.get('event_code')
|
||||
|
||||
logger.debug(f"Parameters: zekken_number={zekken_number}, "
|
||||
f"new_team_name={new_team_name}, event_code={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, new_team_name, event_code]):
|
||||
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 for zekken number: {zekken_number} in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# チーム名を更新
|
||||
old_team_name = entry.team_name
|
||||
entry.team_name = new_team_name
|
||||
entry.save()
|
||||
|
||||
logger.info(f"Successfully updated team name from '{old_team_name}' to '{new_team_name}' "
|
||||
f"for zekken: {zekken_number} in event: {event_code}")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "チーム名が正常に更新されました",
|
||||
"old_team_name": old_team_name,
|
||||
"new_team_name": new_team_name
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in update_team_name: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.必要なパラメータ(ゼッケン番号、イベントコード、新しいクラス名)を受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとゼッケン番号のチームを検索します
|
||||
4.チームのクラス名を更新します
|
||||
5.成功した場合、元のクラス名と新しいクラス名を含む成功メッセージを返します
|
||||
|
||||
オリジナルの仕様通り、このエンドポイントは GET メソッドを使用していますが、
|
||||
データを更新する操作なのでRESTfulなAPIデザインでは通常は POST または PUT メソッドを使用するべきです。
|
||||
"""
|
||||
@api_view(['GET'])
|
||||
def team_class_changer(request):
|
||||
"""
|
||||
チームのクラスを変更
|
||||
|
||||
パラメータ:
|
||||
- zekken: ゼッケン番号
|
||||
- event: イベントコード
|
||||
- new_class: 新しいクラス名
|
||||
"""
|
||||
logger.info("team_class_changer called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
zekken_number = request.query_params.get('zekken')
|
||||
event_code = request.query_params.get('event')
|
||||
new_class = request.query_params.get('new_class')
|
||||
|
||||
logger.debug(f"Parameters: zekken={zekken_number}, "
|
||||
f"event={event_code}, new_class={new_class}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, event_code, new_class]):
|
||||
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 for zekken number: {zekken_number} in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# クラス名を更新
|
||||
old_class = entry.class_name
|
||||
entry.class_name = new_class
|
||||
entry.save()
|
||||
|
||||
logger.info(f"Successfully updated class from '{old_class}' to '{new_class}' "
|
||||
f"for team: {entry.team_name} (zekken: {zekken_number}) in event: {event_code}")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "クラスが正常に更新されました",
|
||||
"team_name": entry.team_name,
|
||||
"old_class": old_class,
|
||||
"new_class": new_class
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in team_class_changer: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
管理者用のチーム登録エンドポイントを実装します。
|
||||
これは先ほど実装した /register_team と類似していますが、GETメソッドを使用し、パラメータ名が異なります。
|
||||
|
||||
解説
|
||||
このエンドポイントは、管理者用のチーム登録機能を提供します。前に実装した /register_team との主な違いは以下の点です:
|
||||
|
||||
1.GETメソッドを使用している点
|
||||
2.パラメータ名が異なる:
|
||||
- event_code → event
|
||||
- class_name → class
|
||||
- zekken_number → zekken
|
||||
- team_name → team
|
||||
- password → pass
|
||||
|
||||
処理の流れは /register_team と同様で、以下のことを行っています:
|
||||
|
||||
- パラメータの検証
|
||||
- イベントの存在確認
|
||||
- ゼッケン番号の重複チェック
|
||||
- 新しいユーザーとチームエントリーの作成
|
||||
|
||||
本来は「管理者用」とされているため、管理者権限の確認も必要かもしれませんが、
|
||||
ここでは簡略のためにアクセス制限は実装していません。
|
||||
実際の運用では、認証・認可の仕組みを追加することをお勧めします。
|
||||
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def team_register(request):
|
||||
"""
|
||||
チームを登録(管理者用)
|
||||
|
||||
パラメータ:
|
||||
- event: イベントコード
|
||||
- class: クラス名
|
||||
- zekken: ゼッケン番号
|
||||
- team: チーム名
|
||||
- pass: パスワード
|
||||
"""
|
||||
logger.info("team_register called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event')
|
||||
class_name = request.query_params.get('class')
|
||||
zekken_number = request.query_params.get('zekken')
|
||||
team_name = request.query_params.get('team')
|
||||
password = request.query_params.get('pass')
|
||||
|
||||
logger.debug(f"Parameters: event={event_code}, class={class_name}, "
|
||||
f"zekken={zekken_number}, team={team_name}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([event_code, class_name, zekken_number, team_name, password]):
|
||||
logger.warning("Missing required parameters")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "すべてのパラメータが必要です (event, class, zekken, team, pass)"
|
||||
}, 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)
|
||||
|
||||
# ゼッケン番号の重複チェック
|
||||
existing_entry = Entry.objects.filter(
|
||||
event=event,
|
||||
zekken_number=zekken_number
|
||||
).first()
|
||||
|
||||
if existing_entry:
|
||||
logger.warning(f"Duplicate zekken number: {zekken_number} for event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "このゼッケン番号は既に使用されています"
|
||||
}, status=status.HTTP_409_CONFLICT)
|
||||
|
||||
# トランザクション開始
|
||||
with transaction.atomic():
|
||||
# ユーザー作成(ユーザー名はチーム名+ゼッケン番号とする)
|
||||
username = f"{team_name}_{zekken_number}"
|
||||
|
||||
# 同じユーザー名が存在するか確認
|
||||
if CustomUser.objects.filter(username=username).exists():
|
||||
username = f"{team_name}_{zekken_number}_{event_code}"
|
||||
|
||||
user = CustomUser.objects.create(
|
||||
username=username,
|
||||
password=make_password(password) # パスワードをハッシュ化
|
||||
)
|
||||
|
||||
# チーム登録
|
||||
entry = Entry.objects.create(
|
||||
owner=user,
|
||||
zekken_number=zekken_number,
|
||||
team_name=team_name,
|
||||
class_name=class_name,
|
||||
event=event
|
||||
)
|
||||
|
||||
logger.info(f"Successfully registered team: {team_name} with zekken: {zekken_number} "
|
||||
f"for event: {event_code} (class: {class_name})")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "チームが正常に登録されました",
|
||||
"team_id": entry.id,
|
||||
"zekken_number": zekken_number,
|
||||
"team_name": team_name
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in team_register: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.eventパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントコードでイベントを検索します
|
||||
- イベントが存在しない場合はエラーを返します
|
||||
4.そのイベントに関連する全エントリーを取得します
|
||||
- エントリーが存在しない場合は0を返します
|
||||
5.各エントリーのゼッケン番号を数値として解釈し、最大値を計算します
|
||||
- 数値に変換できないゼッケン番号は無視されます
|
||||
6.最大ゼッケン番号をJSON形式で返します
|
||||
|
||||
これにより、イベント主催者は既に使用されているゼッケン番号の最大値を知り、
|
||||
新しいチーム登録時に適切なゼッケン番号を割り当てることができます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def zekken_max_num(request):
|
||||
"""
|
||||
指定イベントで使用されている最大のゼッケン番号を取得
|
||||
|
||||
パラメータ:
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("zekken_max_num called")
|
||||
|
||||
event_code = request.query_params.get('event')
|
||||
logger.debug(f"Parameters: event={event_code}")
|
||||
|
||||
if not event_code:
|
||||
logger.warning("Event code not provided")
|
||||
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)
|
||||
|
||||
# イベントに関連するエントリーからゼッケン番号の最大値を取得
|
||||
entries = Entry.objects.filter(event=event)
|
||||
|
||||
if not entries.exists():
|
||||
# エントリーが存在しない場合は0を返す
|
||||
max_zekken = 0
|
||||
else:
|
||||
# 文字列のゼッケン番号を数値として解釈して最大値を取得
|
||||
# 数値に変換できない場合は除外する
|
||||
numeric_entries = []
|
||||
for entry in entries:
|
||||
try:
|
||||
if entry.zekken_number and entry.zekken_number.isdigit():
|
||||
numeric_entries.append(int(entry.zekken_number))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
max_zekken = max(numeric_entries) if numeric_entries else 0
|
||||
|
||||
logger.info(f"Maximum zekken number for event {event_code}: {max_zekken}")
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"max_zekken": max_zekken
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in zekken_max_num: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.ゼッケン番号(zekken)とイベントコード(event)パラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントコードでイベントを検索します
|
||||
- イベントが存在しない場合はエラーを返します
|
||||
4.指定されたイベント内で、指定ゼッケン番号が既に使用されているかチェックします
|
||||
5.重複の有無と適切なメッセージをJSON形式で返します
|
||||
|
||||
このエンドポイントは、新しいチーム登録時にゼッケン番号の重複を事前にチェックするのに役立ちます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def zekken_double_check(request):
|
||||
"""
|
||||
指定ゼッケン番号が既に使用されているか確認
|
||||
|
||||
パラメータ:
|
||||
- zekken: ゼッケン番号
|
||||
- event: イベントコード
|
||||
"""
|
||||
logger.info("zekken_double_check called")
|
||||
|
||||
zekken_number = request.query_params.get('zekken')
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: zekken={zekken_number}, event={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, event_code]):
|
||||
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)
|
||||
|
||||
# ゼッケン番号の重複チェック
|
||||
is_duplicate = Entry.objects.filter(
|
||||
event=event,
|
||||
zekken_number=zekken_number
|
||||
).exists()
|
||||
|
||||
logger.info(f"Double check for zekken {zekken_number} in event {event_code}: "
|
||||
f"{'Duplicate' if is_duplicate else 'Not duplicate'}")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"is_duplicate": is_duplicate,
|
||||
"message": "このゼッケン番号は既に使用されています" if is_duplicate else "このゼッケン番号は使用可能です"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in zekken_double_check: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
294
rog/views_apis/api_test.py
Executable file
294
rog/views_apis/api_test.py
Executable file
@ -0,0 +1,294 @@
|
||||
|
||||
|
||||
"""
|
||||
pip install psutil
|
||||
"""
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
import logging
|
||||
from django.utils import timezone
|
||||
import platform
|
||||
import sys
|
||||
import os
|
||||
import psutil
|
||||
import django
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では、サーバーの動作状態を確認するための診断エンドポイントを提供しています。このエンドポイントは以下の情報を返します:
|
||||
|
||||
1.基本情報:
|
||||
- サーバーの稼働状態
|
||||
- 現在のタイムスタンプ
|
||||
- アクティブなイベント数
|
||||
2.システム情報:
|
||||
- 使用しているOSやプラットフォーム
|
||||
- Pythonのバージョン
|
||||
- Djangoのバージョン
|
||||
3.リソース使用状況:
|
||||
- CPU使用率
|
||||
- メモリ使用率
|
||||
- ディスク使用率
|
||||
4.環境設定:
|
||||
- デバッグモードの状態
|
||||
- タイムゾーン設定
|
||||
5.サーバー稼働時間:
|
||||
- サーバープロセスの起動からの経過時間
|
||||
6.利用可能なAPIルート:
|
||||
- サーバーで設定されているURL設定から取得したルート情報
|
||||
|
||||
このエンドポイントは主に以下の目的で使用されます:
|
||||
- サーバーの正常性確認(ヘルスチェック)
|
||||
- サーバーの基本的な診断情報の取得
|
||||
- 動作環境の確認
|
||||
- 監視やデバッグのためのメトリクス収集
|
||||
|
||||
実際の運用環境では、このエンドポイントが本番環境で公開されている場合、
|
||||
セキュリティのためアクセス制限を設けることも検討すべきでしょう。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def test_gifuroge(request):
|
||||
"""
|
||||
サーバーの動作テスト用エンドポイント
|
||||
|
||||
機能:
|
||||
- サーバーが動作していることを確認
|
||||
- サーバーの基本情報を返す
|
||||
"""
|
||||
logger.info("test_gifuroge called")
|
||||
|
||||
try:
|
||||
# システム情報を収集
|
||||
server_info = {
|
||||
"status": "OK",
|
||||
"message": "GifuTabi Rogaining Server is running",
|
||||
"timestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"system": {
|
||||
"platform": platform.platform(),
|
||||
"python_version": sys.version,
|
||||
"django_version": django.get_version(),
|
||||
},
|
||||
"resources": {
|
||||
"cpu_usage": f"{psutil.cpu_percent()}%",
|
||||
"memory_usage": f"{psutil.virtual_memory().percent}%",
|
||||
"disk_usage": f"{psutil.disk_usage('/').percent}%"
|
||||
},
|
||||
"env": {
|
||||
"debug_mode": os.environ.get("DEBUG", "False"),
|
||||
"timezone": str(timezone.get_current_timezone())
|
||||
}
|
||||
}
|
||||
|
||||
# アクティブなイベント数をカウント
|
||||
try:
|
||||
from rog.models import NewEvent2
|
||||
active_events = NewEvent2.objects.filter(event_active=True).count()
|
||||
server_info["active_events"] = active_events
|
||||
except:
|
||||
logger.warning("Unable to count active events")
|
||||
server_info["active_events"] = "Unknown"
|
||||
|
||||
# アプリケーション起動時間を計算(サーバープロセスの起動時間)
|
||||
try:
|
||||
process = psutil.Process(os.getpid())
|
||||
start_time = process.create_time()
|
||||
uptime_seconds = int(time.time() - start_time)
|
||||
days, remainder = divmod(uptime_seconds, 86400)
|
||||
hours, remainder = divmod(remainder, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
uptime_str = ""
|
||||
if days > 0:
|
||||
uptime_str += f"{days}日 "
|
||||
if hours > 0 or days > 0:
|
||||
uptime_str += f"{hours}時間 "
|
||||
if minutes > 0 or hours > 0 or days > 0:
|
||||
uptime_str += f"{minutes}分 "
|
||||
uptime_str += f"{seconds}秒"
|
||||
|
||||
server_info["uptime"] = uptime_str
|
||||
except:
|
||||
logger.warning("Unable to calculate uptime")
|
||||
server_info["uptime"] = "Unknown"
|
||||
|
||||
# ルート情報を追加
|
||||
root_paths = []
|
||||
for pattern in request.urlconf.urlpatterns:
|
||||
if hasattr(pattern, 'pattern'):
|
||||
root_paths.append(str(pattern.pattern))
|
||||
|
||||
server_info["available_api_roots"] = root_paths
|
||||
|
||||
# 応答を返す
|
||||
return Response(server_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in test_gifuroge: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーテスト中にエラーが発生しました",
|
||||
"error": str(e)
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
import logging
|
||||
import random
|
||||
import json
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
"""
|
||||
解説
|
||||
この/practiceエンドポイントは、主に以下の目的で使用される練習用・テスト用のエンドポイントです:
|
||||
|
||||
1.学習目的:
|
||||
- APIの基本的な使い方を学ぶための練習用エンドポイント
|
||||
- 異なるレスポンス形式(JSON、プレーンテキスト、HTML)を試すことができる
|
||||
- クエリパラメータの使い方を練習できる
|
||||
2.テスト・デバッグ目的:
|
||||
- APIクライアントのテストに使用できる
|
||||
- エラーレスポンスをシミュレートできる
|
||||
- さまざまなレスポンス形式の処理をテストできる
|
||||
3.機能デモンストレーション:
|
||||
- 岐阜ロゲに関連する簡単なクイズやヒントを提供
|
||||
- ランダムな要素を含むレスポンスを生成
|
||||
|
||||
このエンドポイントは特に実用的な機能を持たないシンプルな実装ですが、
|
||||
APIの使い方を学ぶユーザーや、APIクライアントのテストを行う開発者にとって便利なツールとなります。
|
||||
また、新機能開発時の参考実装としても活用できます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def practice(request):
|
||||
"""
|
||||
練習用エンドポイント
|
||||
|
||||
機能:
|
||||
- APIの基本的な使い方を学ぶための練習用エンドポイント
|
||||
- リクエストパラメータに応じた簡単なレスポンスを返す
|
||||
- テスト・デバッグ用としても使用可能
|
||||
|
||||
パラメータ:
|
||||
- name: 名前 (オプション)
|
||||
- mode: レスポンスモード (json, text, html, random のいずれか。デフォルトは json)
|
||||
- error: エラーをシミュレーションする場合は true (オプション)
|
||||
"""
|
||||
logger.info("practice called")
|
||||
|
||||
# リクエストパラメータを取得
|
||||
name = request.query_params.get('name', 'ゲスト')
|
||||
mode = request.query_params.get('mode', 'json')
|
||||
simulate_error = request.query_params.get('error', 'false').lower() == 'true'
|
||||
|
||||
logger.debug(f"Parameters: name={name}, mode={mode}, error={simulate_error}")
|
||||
|
||||
# エラーシミュレーション
|
||||
if simulate_error:
|
||||
logger.warning("Simulating error response")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "これはリクエストパラメータ 'error=true' によるシミュレーションエラーです"
|
||||
}, status=400)
|
||||
|
||||
# 現在時刻
|
||||
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# ランダムな練習用データ
|
||||
practice_data = {
|
||||
"quiz_questions": [
|
||||
{"id": 1, "question": "岐阜県の県庁所在地は?", "answer": "岐阜市"},
|
||||
{"id": 2, "question": "ロゲイニングで重要なのは?", "answer": "戦略的なルート選択"},
|
||||
{"id": 3, "question": "岐阜県の有名な祭りは?", "answer": "高山祭"}
|
||||
],
|
||||
"tips": [
|
||||
"ロゲイニングでは水分補給が重要です",
|
||||
"地図の縮尺を確認しましょう",
|
||||
"無理なルートは避けて安全第一で"
|
||||
],
|
||||
"random_number": random.randint(1, 100)
|
||||
}
|
||||
|
||||
# モードに応じたレスポンス形式
|
||||
if mode == 'random':
|
||||
# ランダムでモードを選択
|
||||
mode = random.choice(['json', 'text', 'html'])
|
||||
|
||||
if mode == 'text':
|
||||
# テキスト形式のレスポンス
|
||||
response_text = f"""
|
||||
岐阜ロゲAPI練習用エンドポイント
|
||||
|
||||
こんにちは、{name}さん!
|
||||
現在時刻: {current_time}
|
||||
|
||||
今日のランダム数字: {practice_data['random_number']}
|
||||
|
||||
今日のヒント: {random.choice(practice_data['tips'])}
|
||||
|
||||
これはテスト用のレスポンスです。
|
||||
"""
|
||||
return HttpResponse(response_text, content_type='text/plain; charset=utf-8')
|
||||
|
||||
elif mode == 'html':
|
||||
# HTML形式のレスポンス
|
||||
response_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>岐阜ロゲAPI練習ページ</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 20px; }}
|
||||
.container {{ max-width: 800px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }}
|
||||
h1 {{ color: #0066cc; }}
|
||||
.info {{ background-color: #f5f5f5; padding: 10px; border-radius: 5px; }}
|
||||
.tip {{ color: #006600; font-weight: bold; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>岐阜ロゲAPI練習用ページ</h1>
|
||||
<p>こんにちは、<strong>{name}</strong>さん!</p>
|
||||
<div class="info">
|
||||
<p>現在時刻: {current_time}</p>
|
||||
<p>今日のランダム数字: {practice_data['random_number']}</p>
|
||||
</div>
|
||||
<h2>今日のヒント</h2>
|
||||
<p class="tip">{random.choice(practice_data['tips'])}</p>
|
||||
<h2>ロゲイニングクイズ</h2>
|
||||
<div id="quiz">
|
||||
<p><strong>問題:</strong> {random.choice(practice_data['quiz_questions'])['question']}</p>
|
||||
</div>
|
||||
<p><small>これはテスト用のHTMLレスポンスです。</small></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HttpResponse(response_html, content_type='text/html; charset=utf-8')
|
||||
|
||||
else: # mode == 'json' (デフォルト)
|
||||
# JSON形式のレスポンス
|
||||
response_data = {
|
||||
"status": "OK",
|
||||
"message": f"こんにちは、{name}さん!",
|
||||
"timestamp": current_time,
|
||||
"request_info": {
|
||||
"ip": request.META.get('REMOTE_ADDR', 'unknown'),
|
||||
"user_agent": request.META.get('HTTP_USER_AGENT', 'unknown'),
|
||||
"parameters": dict(request.query_params.items())
|
||||
},
|
||||
"practice_data": practice_data
|
||||
}
|
||||
return JsonResponse(response_data)
|
||||
981
rog/views_apis/api_waypoint.py
Executable file
981
rog/views_apis/api_waypoint.py
Executable file
@ -0,0 +1,981 @@
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, Location, GpsLog
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from django.conf import settings
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.ウェイポイントデータを保存するためのモデル Waypoint を定義
|
||||
2.POST リクエストからチーム名、イベントコード、ウェイポイントデータ配列を取得
|
||||
3.データのバリデーションを行う
|
||||
- 必要なパラメータが全て揃っているか
|
||||
- ウェイポイントデータが配列形式か
|
||||
- 各ウェイポイントに必要な情報(緯度、経度、記録時間)が含まれているか
|
||||
4.イベントとチームの存在確認
|
||||
5.各ウェイポイントを処理
|
||||
- 数値型・日時型の変換
|
||||
- 重複チェック(同じ記録時間のデータは無視)
|
||||
- データベースに保存
|
||||
6.処理結果の統計(保存数、スキップ数、無効データ数)を含む応答を返す
|
||||
|
||||
この実装により、アプリからチームの位置情報を送信し、サーバーで保存することができます。
|
||||
保存されたデータは、後でルート表示や分析などに使用できます。
|
||||
"""
|
||||
|
||||
@api_view(['POST'])
|
||||
def get_waypoint_datas_from_rogapp(request):
|
||||
"""
|
||||
アプリからウェイポイントデータを受信し保存
|
||||
|
||||
パラメータ:
|
||||
- team_name: チーム名
|
||||
- event_code: イベントコード
|
||||
- waypoints: ウェイポイントデータの配列
|
||||
"""
|
||||
logger.info("get_waypoint_datas_from_rogapp called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
try:
|
||||
data = request.data
|
||||
team_name = data.get('team_name')
|
||||
event_code = data.get('event_code')
|
||||
waypoints_data = data.get('waypoints')
|
||||
|
||||
# JSONとして送信された場合はデコード
|
||||
if isinstance(waypoints_data, str):
|
||||
try:
|
||||
waypoints_data = json.loads(waypoints_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Invalid JSON format for waypoints")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "ウェイポイントデータのJSONフォーマットが無効です"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
logger.debug(f"Parameters: team_name={team_name}, event_code={event_code}, "
|
||||
f"waypoints_count={len(waypoints_data) if waypoints_data else 0}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing request data: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "リクエストデータの解析に失敗しました"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# パラメータ検証
|
||||
if not all([team_name, event_code, waypoints_data]):
|
||||
logger.warning("Missing required parameters")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "チーム名、イベントコード、ウェイポイントデータが必要です"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# ウェイポイントデータの検証
|
||||
if not isinstance(waypoints_data, list):
|
||||
logger.warning("Waypoints data is not a list")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "ウェイポイントデータはリスト形式である必要があります"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if len(waypoints_data) == 0:
|
||||
logger.warning("Empty waypoints list")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "ウェイポイントリストが空です",
|
||||
"saved_count": 0
|
||||
})
|
||||
|
||||
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,
|
||||
team_name=team_name
|
||||
).first()
|
||||
|
||||
if not entry:
|
||||
logger.warning(f"Team '{team_name}' not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたチーム名のエントリーが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# チームがスタートしているか確認(オプション)
|
||||
if not hasattr(entry, 'start_info'):
|
||||
logger.warning(f"Team {team_name} has not started yet")
|
||||
# 必要に応じてエラーを返すか、自動的にスタート処理を行う
|
||||
|
||||
# トランザクション開始
|
||||
saved_count = 0
|
||||
skipped_count = 0
|
||||
invalid_count = 0
|
||||
|
||||
with transaction.atomic():
|
||||
for waypoint in waypoints_data:
|
||||
try:
|
||||
# 必須フィールドの検証
|
||||
lat = waypoint.get('latitude')
|
||||
lng = waypoint.get('longitude')
|
||||
recorded_at_str = waypoint.get('recorded_at')
|
||||
|
||||
if not all([lat, lng, recorded_at_str]):
|
||||
logger.warning(f"Missing required fields in waypoint: {waypoint}")
|
||||
invalid_count += 1
|
||||
continue
|
||||
|
||||
# 数値型の検証
|
||||
try:
|
||||
lat = float(lat)
|
||||
lng = float(lng)
|
||||
except (ValueError, TypeError):
|
||||
logger.warning(f"Invalid coordinate values: lat={lat}, lng={lng}")
|
||||
invalid_count += 1
|
||||
continue
|
||||
|
||||
# 日時の解析
|
||||
try:
|
||||
# 複数の日付形式に対応
|
||||
recorded_at = None
|
||||
for date_format in [
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ", # ISO形式(ミリ秒付き)
|
||||
"%Y-%m-%dT%H:%M:%SZ", # ISO形式
|
||||
"%Y-%m-%d %H:%M:%S", # 標準形式
|
||||
"%Y/%m/%d %H:%M:%S" # スラッシュ区切り
|
||||
]:
|
||||
try:
|
||||
if recorded_at_str.endswith('Z'):
|
||||
# UTCからローカルタイムに変換する必要がある場合
|
||||
dt = datetime.strptime(recorded_at_str, date_format)
|
||||
# ここでUTCからローカルタイムへの変換を行う場合
|
||||
recorded_at = dt
|
||||
else:
|
||||
recorded_at = datetime.strptime(recorded_at_str, date_format)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if recorded_at is None:
|
||||
raise ValueError(f"No matching date format for: {recorded_at_str}")
|
||||
except ValueError as e:
|
||||
logger.warning(f"Invalid date format: {recorded_at_str} - {str(e)}")
|
||||
invalid_count += 1
|
||||
continue
|
||||
|
||||
# 重複チェック(同じ記録時間のウェイポイントは無視)
|
||||
if Waypoint.objects.filter(
|
||||
entry=entry,
|
||||
recorded_at=recorded_at
|
||||
).exists():
|
||||
logger.debug(f"Skipping duplicate waypoint at: {recorded_at_str}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# オプションフィールドの取得
|
||||
altitude = waypoint.get('altitude')
|
||||
accuracy = waypoint.get('accuracy')
|
||||
speed = waypoint.get('speed')
|
||||
|
||||
# ウェイポイントの保存
|
||||
Waypoint.objects.create(
|
||||
entry=entry,
|
||||
latitude=lat,
|
||||
longitude=lng,
|
||||
altitude=float(altitude) if altitude not in [None, ''] else None,
|
||||
accuracy=float(accuracy) if accuracy not in [None, ''] else None,
|
||||
speed=float(speed) if speed not in [None, ''] else None,
|
||||
recorded_at=recorded_at
|
||||
)
|
||||
|
||||
saved_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing waypoint: {str(e)}")
|
||||
invalid_count += 1
|
||||
|
||||
logger.info(f"Processed waypoints for team {team_name}: "
|
||||
f"saved={saved_count}, skipped={skipped_count}, invalid={invalid_count}")
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"message": "ウェイポイントデータを正常に保存しました",
|
||||
"team_name": team_name,
|
||||
"event_code": event_code,
|
||||
"total_waypoints": len(waypoints_data),
|
||||
"saved_count": saved_count,
|
||||
"skipped_count": skipped_count,
|
||||
"invalid_count": invalid_count
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_waypoint_datas_from_rogapp: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.チーム名とイベントコードのパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントとチームの存在を確認します
|
||||
4.チームの移動経路データを取得します:
|
||||
- ウェイポイントデータ(記録時間順)
|
||||
- チェックポイント通過情報(時間順)
|
||||
- スタート情報とゴール情報
|
||||
5.移動経路の統計情報を計算します:
|
||||
- 総移動距離
|
||||
- ウェイポイント数
|
||||
- チェックポイント数
|
||||
6.GeoJSON形式のルートデータを作成します
|
||||
7.全てのデータを含む応答を返します
|
||||
|
||||
実装のポイント:
|
||||
|
||||
- ウェイポイントとチェックポイントの両方をルートポイントとして統合
|
||||
- チェックポイントの座標はLocationモデルから取得(存在する場合)
|
||||
- ハバーサイン公式を使用して2点間の距離を計算し、総移動距離を算出
|
||||
- GeoJSON形式のデータも含め、様々な用途に対応できる形式でデータを提供
|
||||
|
||||
このエンドポイントにより、チームの移動経路を視覚化したり分析したりすることができます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_route(request):
|
||||
"""
|
||||
指定チームのルート情報を取得
|
||||
|
||||
パラメータ:
|
||||
- team: チーム名
|
||||
- event_code: イベントコード
|
||||
"""
|
||||
logger.info("get_route called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
team_name = request.query_params.get('team')
|
||||
event_code = request.query_params.get('event_code')
|
||||
|
||||
logger.debug(f"Parameters: team={team_name}, event_code={event_code}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([team_name, event_code]):
|
||||
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,
|
||||
team_name=team_name
|
||||
).first()
|
||||
|
||||
if not entry:
|
||||
logger.warning(f"Team '{team_name}' not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# ウェイポイントデータを取得(時間順)
|
||||
waypoints = Waypoint.objects.filter(
|
||||
entry=entry
|
||||
).order_by('recorded_at')
|
||||
|
||||
# チェックポイント通過情報を取得(時間順)
|
||||
checkpoints = GpsLog.objects.filter(
|
||||
entry=entry
|
||||
).order_by('checkin_time')
|
||||
|
||||
# スタート情報を取得
|
||||
start_info = None
|
||||
if hasattr(entry, 'start_info'):
|
||||
start_info = {
|
||||
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
# ゴール情報を取得
|
||||
goal_info = None
|
||||
if hasattr(entry, 'goal_info'):
|
||||
goal_info = {
|
||||
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"score": entry.goal_info.score
|
||||
}
|
||||
|
||||
# ウェイポイントを処理
|
||||
route_points = []
|
||||
for wp in waypoints:
|
||||
point = {
|
||||
"latitude": wp.latitude,
|
||||
"longitude": wp.longitude,
|
||||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"type": "waypoint"
|
||||
}
|
||||
|
||||
# オプションフィールドがあれば追加
|
||||
if wp.altitude is not None:
|
||||
point["altitude"] = wp.altitude
|
||||
if wp.accuracy is not None:
|
||||
point["accuracy"] = wp.accuracy
|
||||
if wp.speed is not None:
|
||||
point["speed"] = wp.speed
|
||||
|
||||
route_points.append(point)
|
||||
|
||||
# チェックポイントの座標を追加(イベントのチェックポイント定義がある場合)
|
||||
checkpoint_data = []
|
||||
for cp in checkpoints:
|
||||
cp_data = {
|
||||
"cp_number": cp.cp_number,
|
||||
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
|
||||
"image_url": cp.image_address,
|
||||
"is_service_checked": getattr(cp, 'is_service_checked', False)
|
||||
}
|
||||
|
||||
# チェックポイントの座標情報を取得(Locationモデルがある場合)
|
||||
try:
|
||||
event_cp = Location.objects.filter(
|
||||
event=event,
|
||||
cp_number=cp.cp_number
|
||||
).first()
|
||||
|
||||
if event_cp and event_cp.latitude and event_cp.longitude:
|
||||
cp_data["latitude"] = event_cp.latitude
|
||||
cp_data["longitude"] = event_cp.longitude
|
||||
cp_data["cp_name"] = event_cp.cp_name
|
||||
cp_data["cp_point"] = event_cp.cp_point
|
||||
cp_data["is_service_cp"] = event_cp.is_service_cp
|
||||
|
||||
# ルートポイントとしても追加
|
||||
route_points.append({
|
||||
"latitude": event_cp.latitude,
|
||||
"longitude": event_cp.longitude,
|
||||
"timestamp": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
|
||||
"type": "checkpoint",
|
||||
"cp_number": cp.cp_number,
|
||||
"cp_name": event_cp.cp_name
|
||||
})
|
||||
except:
|
||||
# Locationモデルが存在しないか、座標が設定されていない場合
|
||||
pass
|
||||
|
||||
checkpoint_data.append(cp_data)
|
||||
|
||||
# ルートの統計情報を計算
|
||||
total_distance = 0
|
||||
if len(route_points) >= 2:
|
||||
from math import sin, cos, sqrt, atan2, radians
|
||||
|
||||
def calculate_distance(lat1, lon1, lat2, lon2):
|
||||
# 2点間の距離をハバーサイン公式で計算(km単位)
|
||||
R = 6371.0 # 地球の半径(km)
|
||||
|
||||
lat1_rad = radians(lat1)
|
||||
lon1_rad = radians(lon1)
|
||||
lat2_rad = radians(lat2)
|
||||
lon2_rad = radians(lon2)
|
||||
|
||||
dlon = lon2_rad - lon1_rad
|
||||
dlat = lat2_rad - lat1_rad
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = R * c
|
||||
return distance
|
||||
|
||||
for i in range(len(route_points) - 1):
|
||||
p1 = route_points[i]
|
||||
p2 = route_points[i + 1]
|
||||
|
||||
if 'latitude' in p1 and 'longitude' in p1 and 'latitude' in p2 and 'longitude' in p2:
|
||||
distance = calculate_distance(
|
||||
p1['latitude'], p1['longitude'],
|
||||
p2['latitude'], p2['longitude']
|
||||
)
|
||||
total_distance += distance
|
||||
|
||||
# GeoJSON形式のルートデータを作成(オプション)
|
||||
geo_json = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[point["longitude"], point["latitude"]]
|
||||
for point in route_points
|
||||
if "latitude" in point and "longitude" in point
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": team_name,
|
||||
"total_distance": round(total_distance, 2), # km単位で小数点以下2桁まで
|
||||
"waypoint_count": waypoints.count(),
|
||||
"checkpoint_count": checkpoints.count()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"team_info": {
|
||||
"team_name": team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"event_code": event_code
|
||||
},
|
||||
"start_info": start_info,
|
||||
"goal_info": goal_info,
|
||||
"route_statistics": {
|
||||
"total_distance": round(total_distance, 2), # km単位
|
||||
"total_waypoints": waypoints.count(),
|
||||
"total_checkpoints": checkpoints.count()
|
||||
},
|
||||
"checkpoints": checkpoint_data,
|
||||
"route_points": route_points,
|
||||
"geo_json": geo_json
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_route: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.ゼッケン番号とイベントコードのパラメータを受け取ります
|
||||
2.オプションで limit(返す位置情報の最大数)と time_range(時間範囲)のパラメータも受け取ります
|
||||
3.パラメータが不足している場合はエラーを返します
|
||||
4.指定されたイベントとチームの存在を確認します
|
||||
5.チームのウェイポイントデータを取得します:
|
||||
- 新しい順(降順)にソート
|
||||
- 時間範囲でフィルタリング(指定された場合)
|
||||
- 件数制限(指定された場合)
|
||||
6.チームの基本情報、最新の位置情報、位置情報履歴を含む応答を作成します
|
||||
7.GeoJSON形式のデータも提供します:
|
||||
- 移動経路を表す LineString フィーチャー
|
||||
- 各位置を表す Point フィーチャー
|
||||
|
||||
この実装は /getRoute エンドポイントに似ていますが、以下の点で異なります:
|
||||
|
||||
- 位置情報の取得に焦点を当てています(チェックポイント情報は含まれません)
|
||||
- より新しいデータを優先的に返します(降順ソート)
|
||||
- 時間範囲や件数制限のオプションがあります
|
||||
- 移動経路の統計情報(距離など)は計算しません
|
||||
- このエンドポイントは、リアルタイムマップ表示やチームの動きを追跡する場合に便利です。
|
||||
"""
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2, Entry, Waypoint
|
||||
import logging
|
||||
from django.db.models import F, Q
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@api_view(['GET'])
|
||||
def fetch_user_locations(request):
|
||||
"""
|
||||
ユーザーの位置情報履歴を取得
|
||||
|
||||
パラメータ:
|
||||
- zekken_number: ゼッケン番号
|
||||
- event_code: イベントコード
|
||||
"""
|
||||
logger.info("fetch_user_locations called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
zekken_number = request.query_params.get('zekken_number')
|
||||
event_code = request.query_params.get('event_code')
|
||||
|
||||
# 追加のオプションパラメータ
|
||||
limit = request.query_params.get('limit')
|
||||
if limit:
|
||||
try:
|
||||
limit = int(limit)
|
||||
except ValueError:
|
||||
limit = None
|
||||
|
||||
time_range = request.query_params.get('time_range') # 例: "1h", "30m", "2d"
|
||||
|
||||
logger.debug(f"Parameters: zekken_number={zekken_number}, event_code={event_code}, "
|
||||
f"limit={limit}, time_range={time_range}")
|
||||
|
||||
# パラメータ検証
|
||||
if not all([zekken_number, event_code]):
|
||||
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"Team with zekken number {zekken_number} not found in event: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# ウェイポイントデータをフィルタリング
|
||||
waypoints_query = Waypoint.objects.filter(entry=entry)
|
||||
|
||||
# 時間範囲でフィルタリング(オプション)
|
||||
if time_range:
|
||||
# 時間範囲をパース (例: "1h", "30m", "2d")
|
||||
time_value = ""
|
||||
time_unit = ""
|
||||
for char in time_range:
|
||||
if char.isdigit():
|
||||
time_value += char
|
||||
else:
|
||||
time_unit += char
|
||||
|
||||
if time_value and time_unit:
|
||||
time_value = int(time_value)
|
||||
now = datetime.now()
|
||||
|
||||
if time_unit.lower() == 'h':
|
||||
start_time = now - timedelta(hours=time_value)
|
||||
elif time_unit.lower() == 'm':
|
||||
start_time = now - timedelta(minutes=time_value)
|
||||
elif time_unit.lower() == 'd':
|
||||
start_time = now - timedelta(days=time_value)
|
||||
else:
|
||||
# デフォルトは1時間
|
||||
start_time = now - timedelta(hours=1)
|
||||
|
||||
waypoints_query = waypoints_query.filter(recorded_at__gte=start_time)
|
||||
|
||||
# 時間順にソート
|
||||
waypoints_query = waypoints_query.order_by('-recorded_at')
|
||||
|
||||
# 件数制限(オプション)
|
||||
if limit:
|
||||
waypoints_query = waypoints_query[:limit]
|
||||
|
||||
# データの取得
|
||||
waypoints = list(waypoints_query)
|
||||
|
||||
# チームのスタート・ゴール情報
|
||||
start_time = None
|
||||
if hasattr(entry, 'start_info'):
|
||||
start_time = entry.start_info.start_time
|
||||
|
||||
goal_time = None
|
||||
if hasattr(entry, 'goal_info'):
|
||||
goal_time = entry.goal_info.goal_time
|
||||
|
||||
# レスポンスデータの構築
|
||||
location_data = []
|
||||
|
||||
for wp in waypoints:
|
||||
location = {
|
||||
"latitude": wp.latitude,
|
||||
"longitude": wp.longitude,
|
||||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
# オプションフィールドがあれば追加
|
||||
if wp.altitude is not None:
|
||||
location["altitude"] = wp.altitude
|
||||
if wp.accuracy is not None:
|
||||
location["accuracy"] = wp.accuracy
|
||||
if wp.speed is not None:
|
||||
location["speed"] = wp.speed
|
||||
|
||||
location_data.append(location)
|
||||
|
||||
# 最後の位置情報
|
||||
latest_location = None
|
||||
if location_data:
|
||||
latest_location = location_data[0] # すでに新しい順にソート済み
|
||||
|
||||
# GeoJSON形式のデータも作成
|
||||
geo_json = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[point["longitude"], point["latitude"]]
|
||||
for point in reversed(location_data) # 時系列順に並べ直す
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": zekken_number,
|
||||
"point_count": len(location_data)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# ポイントを個別のフィーチャーとしても追加
|
||||
for point in location_data:
|
||||
geo_json["features"].append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [point["longitude"], point["latitude"]]
|
||||
},
|
||||
"properties": {
|
||||
"timestamp": point["timestamp"],
|
||||
"speed": point.get("speed"),
|
||||
"altitude": point.get("altitude")
|
||||
}
|
||||
})
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"team_info": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"event_code": event_code,
|
||||
"start_time": start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
|
||||
"goal_time": goal_time.strftime("%Y-%m-%d %H:%M:%S") if goal_time else None
|
||||
},
|
||||
"latest_location": latest_location,
|
||||
"locations": location_data,
|
||||
"total_locations": len(location_data),
|
||||
"geo_json": geo_json
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in fetch_user_locations: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では以下の処理を行っています:
|
||||
|
||||
1.イベントコードとオプションでクラス名のパラメータを受け取ります
|
||||
2.パラメータが不足している場合はエラーを返します
|
||||
3.指定されたイベントの存在を確認します
|
||||
4.イベント内のチームをクエリします:
|
||||
- スタート情報があるチームのみを対象
|
||||
- クラス名が指定された場合はそのクラスに所属するチームのみに限定
|
||||
5.各チームについて以下の情報を取得します:
|
||||
- チームの基本情報(名前、ゼッケン番号、クラス)
|
||||
- スタート情報とゴール情報
|
||||
- ウェイポイントとチェックポイントのデータ
|
||||
- ルートの統計情報(総距離、ウェイポイント数、チェックポイント数)
|
||||
6.全チームの情報と、GeoJSON形式のデータを含む応答を返します
|
||||
|
||||
実装のポイント:
|
||||
|
||||
- レスポンスサイズを抑えるため、各チームのルートポイント詳細は含めず、統計情報のみを提供
|
||||
- GeoJSON形式のデータでは、各チームのルートを1つのフィーチャーとして表現
|
||||
- 各チームのデータ処理中にエラーが発生した場合でも、他のチームの処理は継続する例外処理
|
||||
- オプションでクラス名によるフィルタリングが可能
|
||||
|
||||
このエンドポイントは、管理画面での全チームの一覧表示や、全チームのルートを1つの地図上に表示したい場合に役立ちます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_all_routes(request):
|
||||
"""
|
||||
指定イベントの全チームのルート情報を取得
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- class_name: クラス名(省略可)
|
||||
"""
|
||||
logger.info("get_all_routes called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event_code')
|
||||
class_name = request.query_params.get('class_name')
|
||||
|
||||
# クエリパラメータの別名対応(互換性のため)
|
||||
if not event_code:
|
||||
event_code = request.query_params.get('event')
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, class_name={class_name}")
|
||||
|
||||
# パラメータ検証
|
||||
if not event_code:
|
||||
logger.warning("Missing required event_code parameter")
|
||||
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)
|
||||
|
||||
# 対象チームのクエリ作成(クラス名でフィルタリング)
|
||||
entries_query = Entry.objects.filter(event=event)
|
||||
if class_name:
|
||||
entries_query = entries_query.filter(class_name=class_name)
|
||||
|
||||
# スタート情報があるチームだけ対象にする
|
||||
entries_with_start = entries_query.filter(start_info__isnull=False)
|
||||
|
||||
if not entries_with_start.exists():
|
||||
logger.warning(f"No teams with start info found for event: {event_code}")
|
||||
return Response({
|
||||
"status": "WARNING",
|
||||
"message": "スタート済みのチームが見つかりません",
|
||||
"teams": [],
|
||||
"total_teams": 0
|
||||
})
|
||||
|
||||
# 全チームの情報とルートを取得
|
||||
teams_data = []
|
||||
geo_json_features = []
|
||||
|
||||
for entry in entries_with_start:
|
||||
try:
|
||||
# ウェイポイントデータを取得(時間順)
|
||||
waypoints = Waypoint.objects.filter(
|
||||
entry=entry
|
||||
).order_by('recorded_at')
|
||||
|
||||
# チェックポイント通過情報を取得(時間順)
|
||||
checkpoints = GpsLog.objects.filter(
|
||||
entry=entry
|
||||
).order_by('checkin_time')
|
||||
|
||||
# スタート情報を取得
|
||||
start_info = None
|
||||
if hasattr(entry, 'start_info'):
|
||||
start_info = {
|
||||
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
# ゴール情報を取得
|
||||
goal_info = None
|
||||
if hasattr(entry, 'goal_info'):
|
||||
goal_info = {
|
||||
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"score": entry.goal_info.score
|
||||
}
|
||||
|
||||
# ウェイポイントとチェックポイントを処理
|
||||
route_points = []
|
||||
|
||||
# ウェイポイントを処理
|
||||
for wp in waypoints:
|
||||
point = {
|
||||
"latitude": wp.latitude,
|
||||
"longitude": wp.longitude,
|
||||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"type": "waypoint"
|
||||
}
|
||||
|
||||
if wp.altitude is not None:
|
||||
point["altitude"] = wp.altitude
|
||||
if wp.accuracy is not None:
|
||||
point["accuracy"] = wp.accuracy
|
||||
if wp.speed is not None:
|
||||
point["speed"] = wp.speed
|
||||
|
||||
route_points.append(point)
|
||||
|
||||
# チェックポイントの座標を追加(イベントのチェックポイント定義がある場合)
|
||||
checkpoint_data = []
|
||||
for cp in checkpoints:
|
||||
cp_data = {
|
||||
"cp_number": cp.cp_number,
|
||||
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
|
||||
"is_service_checked": getattr(cp, 'is_service_checked', False)
|
||||
}
|
||||
|
||||
# チェックポイントの座標情報を取得
|
||||
try:
|
||||
event_cp = Location.objects.filter(
|
||||
event=event,
|
||||
cp_number=cp.cp_number
|
||||
).first()
|
||||
|
||||
if event_cp and event_cp.latitude and event_cp.longitude:
|
||||
cp_data["latitude"] = event_cp.latitude
|
||||
cp_data["longitude"] = event_cp.longitude
|
||||
cp_data["cp_name"] = event_cp.cp_name
|
||||
cp_data["cp_point"] = event_cp.cp_point
|
||||
|
||||
# ルートポイントとしても追加
|
||||
route_points.append({
|
||||
"latitude": event_cp.latitude,
|
||||
"longitude": event_cp.longitude,
|
||||
"timestamp": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
|
||||
"type": "checkpoint",
|
||||
"cp_number": cp.cp_number
|
||||
})
|
||||
except:
|
||||
# Locationモデルが存在しないか、座標が設定されていない場合
|
||||
pass
|
||||
|
||||
checkpoint_data.append(cp_data)
|
||||
|
||||
# ルートの統計情報を計算
|
||||
total_distance = 0
|
||||
if len(route_points) >= 2:
|
||||
def calculate_distance(lat1, lon1, lat2, lon2):
|
||||
# 2点間の距離をハバーサイン公式で計算(km単位)
|
||||
R = 6371.0 # 地球の半径(km)
|
||||
|
||||
lat1_rad = radians(lat1)
|
||||
lon1_rad = radians(lon1)
|
||||
lat2_rad = radians(lat2)
|
||||
lon2_rad = radians(lon2)
|
||||
|
||||
dlon = lon2_rad - lon1_rad
|
||||
dlat = lat2_rad - lat1_rad
|
||||
|
||||
a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
|
||||
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
|
||||
distance = R * c
|
||||
return distance
|
||||
|
||||
for i in range(len(route_points) - 1):
|
||||
p1 = route_points[i]
|
||||
p2 = route_points[i + 1]
|
||||
|
||||
if 'latitude' in p1 and 'longitude' in p1 and 'latitude' in p2 and 'longitude' in p2:
|
||||
distance = calculate_distance(
|
||||
p1['latitude'], p1['longitude'],
|
||||
p2['latitude'], p2['longitude']
|
||||
)
|
||||
total_distance += distance
|
||||
|
||||
# チームデータを追加
|
||||
team_data = {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"start_info": start_info,
|
||||
"goal_info": goal_info,
|
||||
"route_statistics": {
|
||||
"total_distance": round(total_distance, 2),
|
||||
"total_waypoints": waypoints.count(),
|
||||
"total_checkpoints": checkpoints.count()
|
||||
},
|
||||
"checkpoints": checkpoint_data,
|
||||
# ルートポイントは量が多いため、必要に応じて個別ルート取得APIを使用する形に
|
||||
"route_points_count": len(route_points)
|
||||
}
|
||||
|
||||
teams_data.append(team_data)
|
||||
|
||||
# GeoJSONフィーチャーを追加
|
||||
if route_points:
|
||||
geo_json_features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[point["longitude"], point["latitude"]]
|
||||
for point in route_points
|
||||
if "latitude" in point and "longitude" in point
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"team_name": entry.team_name,
|
||||
"zekken_number": entry.zekken_number,
|
||||
"class_name": entry.class_name,
|
||||
"total_distance": round(total_distance, 2),
|
||||
"waypoint_count": waypoints.count(),
|
||||
"checkpoint_count": checkpoints.count(),
|
||||
"score": goal_info["score"] if goal_info else None
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing route for team {entry.team_name}: {str(e)}")
|
||||
# エラーが発生したチームはスキップ
|
||||
continue
|
||||
|
||||
# GeoJSON形式のデータを作成
|
||||
geo_json = {
|
||||
"type": "FeatureCollection",
|
||||
"features": geo_json_features
|
||||
}
|
||||
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event_code": event_code,
|
||||
"class_name": class_name,
|
||||
"total_teams": len(teams_data),
|
||||
"teams": teams_data,
|
||||
"geo_json": geo_json
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_all_routes: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "サーバーエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
Reference in New Issue
Block a user