almost finish migrate new circumstances

This commit is contained in:
2025-08-24 19:44:36 +09:00
parent 1ba305641e
commit fe5a044c82
67 changed files with 1194889 additions and 467 deletions

View File

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

View File

@ -0,0 +1,304 @@
"""
写真一括アップロード機能
写真の位置情報と撮影時刻を使用してチェックイン処理を行う
"""
import os
import json
import logging
from datetime import datetime
from django.conf import settings
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
from django.utils import timezone
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance
from PIL import Image
from PIL.ExifTags import TAGS
import piexif
from rog.models import NewEvent2, Entry, GpsCheckin, Location2025, Checkpoint
from rog.services.s3_service import S3Service
logger = logging.getLogger(__name__)
def extract_gps_from_image(image_file):
"""
画像からGPS情報と撮影時刻を抽出
"""
try:
image = Image.open(image_file)
exif_data = piexif.load(image.info.get('exif', b''))
gps_info = {}
datetime_info = None
# GPS情報の抽出
if 'GPS' in exif_data:
gps_data = exif_data['GPS']
# 緯度の取得
if piexif.GPSIFD.GPSLatitude in gps_data and piexif.GPSIFD.GPSLatitudeRef in gps_data:
lat = gps_data[piexif.GPSIFD.GPSLatitude]
lat_ref = gps_data[piexif.GPSIFD.GPSLatitudeRef].decode('utf-8')
latitude = lat[0][0]/lat[0][1] + lat[1][0]/lat[1][1]/60 + lat[2][0]/lat[2][1]/3600
if lat_ref == 'S':
latitude = -latitude
gps_info['latitude'] = latitude
# 経度の取得
if piexif.GPSIFD.GPSLongitude in gps_data and piexif.GPSIFD.GPSLongitudeRef in gps_data:
lon = gps_data[piexif.GPSIFD.GPSLongitude]
lon_ref = gps_data[piexif.GPSIFD.GPSLongitudeRef].decode('utf-8')
longitude = lon[0][0]/lon[0][1] + lon[1][0]/lon[1][1]/60 + lon[2][0]/lon[2][1]/3600
if lon_ref == 'W':
longitude = -longitude
gps_info['longitude'] = longitude
# 撮影時刻の抽出
if 'Exif' in exif_data:
exif_ifd = exif_data['Exif']
if piexif.ExifIFD.DateTimeOriginal in exif_ifd:
datetime_str = exif_ifd[piexif.ExifIFD.DateTimeOriginal].decode('utf-8')
datetime_info = datetime.strptime(datetime_str, '%Y:%m:%d %H:%M:%S')
return gps_info, datetime_info
except Exception as e:
logger.error(f"Error extracting GPS/datetime from image: {str(e)}")
return {}, None
def find_nearby_checkpoint(latitude, longitude, event, radius_meters=50):
"""
位置情報から近くのチェックポイントを検索
"""
try:
point = Point(longitude, latitude, srid=4326)
# Location2025モデルからチェックポイントを検索
nearby_checkpoints = Location2025.objects.filter(
event=event,
location__distance_lte=(point, Distance(m=radius_meters))
).order_by('location__distance')
if nearby_checkpoints.exists():
return nearby_checkpoints.first()
# 従来のCheckpointモデルからも検索
nearby_legacy_checkpoints = Checkpoint.objects.filter(
event=event,
location__distance_lte=(point, Distance(m=radius_meters))
).order_by('location__distance')
if nearby_legacy_checkpoints.exists():
return nearby_legacy_checkpoints.first()
return None
except Exception as e:
logger.error(f"Error finding nearby checkpoint: {str(e)}")
return None
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def bulk_upload_photos(request):
"""
写真一括アップロード処理
POST /api/bulk-upload-photos/
{
"event_code": "FC岐阜",
"zekken_number": "123",
"images": [<files>]
}
"""
try:
event_code = request.data.get('event_code')
zekken_number = request.data.get('zekken_number')
images = request.FILES.getlist('images')
if not all([event_code, zekken_number, images]):
return Response({
'error': 'event_code, zekken_number, and images are required'
}, status=status.HTTP_400_BAD_REQUEST)
# イベントの確認
try:
event = NewEvent2.objects.get(event_code=event_code)
except NewEvent2.DoesNotExist:
return Response({
'error': 'Event not found'
}, status=status.HTTP_404_NOT_FOUND)
# エントリーの確認
try:
entry = Entry.objects.get(event=event, zekken_number=zekken_number)
except Entry.DoesNotExist:
return Response({
'error': 'Team entry not found'
}, status=status.HTTP_404_NOT_FOUND)
results = {
'successful_uploads': [],
'failed_uploads': [],
'created_checkins': [],
'failed_checkins': []
}
s3_service = S3Service()
with transaction.atomic():
for image in images:
image_result = {
'filename': image.name,
'status': 'processing'
}
try:
# GPS情報と撮影時刻を抽出
gps_info, capture_time = extract_gps_from_image(image)
# S3にアップロード
s3_url = s3_service.upload_checkin_image(
image_file=image,
event_code=event_code,
team_code=str(zekken_number),
cp_number=0 # 後で更新
)
image_result['s3_url'] = s3_url
image_result['gps_info'] = gps_info
image_result['capture_time'] = capture_time.isoformat() if capture_time else None
# GPS情報がある場合、近くのチェックポイントを検索
if gps_info.get('latitude') and gps_info.get('longitude'):
checkpoint = find_nearby_checkpoint(
gps_info['latitude'],
gps_info['longitude'],
event
)
if checkpoint:
# チェックイン記録の作成
# 既存のチェックイン記録数を取得して順序を決定
existing_count = GpsCheckin.objects.filter(
zekken_number=str(zekken_number),
event_code=event_code
).count()
checkin = GpsCheckin.objects.create(
zekken_number=str(zekken_number),
event_code=event_code,
cp_number=checkpoint.cp_number,
path_order=existing_count + 1,
lattitude=gps_info['latitude'],
longitude=gps_info['longitude'],
image_address=s3_url,
create_at=capture_time or timezone.now(),
validate_location=False, # 初期状態では未確定
buy_flag=False,
points=checkpoint.cp_point if hasattr(checkpoint, 'cp_point') else 0,
create_user=request.user.email if request.user.is_authenticated else None
)
image_result['checkpoint'] = {
'cp_number': checkpoint.cp_number,
'cp_name': checkpoint.cp_name,
'points': checkin.points
}
image_result['checkin_id'] = checkin.id
results['created_checkins'].append(image_result)
else:
image_result['error'] = 'No nearby checkpoint found'
results['failed_checkins'].append(image_result)
else:
image_result['error'] = 'No GPS information in image'
results['failed_checkins'].append(image_result)
image_result['status'] = 'success'
results['successful_uploads'].append(image_result)
except Exception as e:
image_result['status'] = 'failed'
image_result['error'] = str(e)
results['failed_uploads'].append(image_result)
logger.error(f"Error processing image {image.name}: {str(e)}")
return Response({
'status': 'completed',
'summary': {
'total_images': len(images),
'successful_uploads': len(results['successful_uploads']),
'failed_uploads': len(results['failed_uploads']),
'created_checkins': len(results['created_checkins']),
'failed_checkins': len(results['failed_checkins'])
},
'results': results
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in bulk_upload_photos: {str(e)}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def confirm_checkin_validation(request):
"""
通過情報の確定・否確定処理
POST /api/confirm-checkin-validation/
{
"checkin_ids": [1, 2, 3],
"validation_status": true/false
}
"""
try:
checkin_ids = request.data.get('checkin_ids', [])
validation_status = request.data.get('validation_status')
if not checkin_ids or validation_status is None:
return Response({
'error': 'checkin_ids and validation_status are required'
}, status=status.HTTP_400_BAD_REQUEST)
updated_checkins = []
with transaction.atomic():
for checkin_id in checkin_ids:
try:
checkin = GpsCheckin.objects.get(id=checkin_id)
checkin.validate_location = validation_status
checkin.update_user = request.user.email if request.user.is_authenticated else None
checkin.update_at = timezone.now()
checkin.save()
updated_checkins.append({
'id': checkin.id,
'cp_number': checkin.cp_number,
'validation_status': checkin.validate_location
})
except GpsCheckin.DoesNotExist:
logger.warning(f"Checkin with ID {checkin_id} not found")
continue
return Response({
'status': 'success',
'message': f'{len(updated_checkins)} checkins updated',
'updated_checkins': updated_checkins
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in confirm_checkin_validation: {str(e)}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -1,12 +1,15 @@
# 既存のインポート部分に追加
from datetime import datetime, timezone
from sqlalchemy import Transaction
# from sqlalchemy import Transaction # 削除 - SQLAlchemy 2.0では利用不可
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import Location, NewEvent2, Entry, GpsLog
import logging
import uuid
import os
from django.db.models import F, Q
from django.db import transaction
from django.conf import settings
import os
from urllib.parse import urljoin
@ -755,7 +758,7 @@ def goal_checkin(request):
goal_time = timezone.now()
# トランザクション開始
with Transaction.atomic():
with transaction.atomic():
# スコアの計算
score = calculate_team_score(entry)

View File

@ -2,7 +2,7 @@
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
from rog.models import NewEvent2, Entry, Location2025, GpsLog
import logging
from django.db.models import F, Q
from django.conf import settings
@ -332,7 +332,7 @@ def analyze_point(request):
try:
# イベントのチェックポイント定義を取得
event_cps = Location.objects.filter(event=event)
event_cps = Location2025.objects.filter(event=event)
# チームが通過したチェックポイントを取得
team_cps = GpsLog.objects.filter(entry=entry)

View File

@ -1,18 +1,49 @@
# 既存のインポート部分に追加
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
from rog.models import NewEvent2, Entry, GpsCheckin, Team
import logging
from django.db.models import F, Q
from django.conf import settings
import os
from urllib.parse import urljoin
from urllib.parse import urljoin, quote
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
import boto3
from botocore.exceptions import ClientError
logger = logging.getLogger(__name__)
def generate_image_url(image_address, event_code, zekken_number):
"""
画像アドレスからS3 URLまたは適切なURLを生成
"""
if not image_address:
return None
# 既にHTTP URLの場合はそのまま返す
if image_address.startswith('http'):
return image_address
# simulation_image.jpgなどのテスト画像の場合はS3にないのでスキップ
if image_address in ['simulation_image.jpg', 'test_image']:
return f"/media/{image_address}"
# S3パスを構築してURLを生成
s3_key = f"{event_code}/{zekken_number}/{image_address}"
try:
# S3 URLを生成
s3_url = f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_S3_REGION_NAME}.amazonaws.com/{quote(s3_key)}"
return s3_url
except Exception as e:
# S3設定に問題がある場合はmediaパスを返す
return f"/media/{image_address}"
"""
解説
この実装では以下の処理を行っています:
@ -113,7 +144,7 @@ def get_photo_list_prod(request):
# パスワード検証
if not hasattr(entry, 'password') or entry.password != password:
logger.warning(f"Invalid password for team: {entry.team_name}")
logger.warning(f"Invalid password for team: {entry.team.team_name if entry.team else 'Unknown'}")
return Response({
"status": "ERROR",
"message": "パスワードが一致しません"
@ -128,154 +159,49 @@ def get_photo_list_prod(request):
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_team_photos(zekken_number, event_code):
def get_team_photos(request):
"""
チームの写真とレポートURLを取得する共通関数
チームの写真データを取得するAPI
"""
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)
zekken = request.GET.get('zekken')
event = request.GET.get('event')
if not zekken or not event:
return JsonResponse({
'error': 'zekken and event parameters are required'
}, status=400)
try:
# GpsCheckinからチームの画像データを取得
gps_checkins = GpsCheckin.objects.filter(
zekken_number=zekken,
event_code=event
).exclude(
image_address__isnull=True
).exclude(
image_address=''
).order_by('create_at')
photos = []
for gps in gps_checkins:
# image_addressを処理してS3 URLまたは既存URLを生成
image_url = generate_image_url(gps.image_address, event, zekken)
photos.append({
'id': gps.id,
'image_url': image_url,
'created_at': gps.create_at.strftime('%Y-%m-%d %H:%M:%S') if gps.create_at else None,
'point_name': gps.checkpoint_id,
'latitude': float(gps.lattitude) if gps.lattitude else None,
'longitude': float(gps.longitude) if gps.longitude else None,
})
return JsonResponse({
'photos': photos,
'count': len(photos)
})
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)
return JsonResponse({
'error': f'Error retrieving photos: {str(e)}'
}, status=500)

View File

@ -0,0 +1,239 @@
# 既存のインポート部分に追加
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import NewEvent2, Entry, GpsCheckin, Team
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.team_name if entry.team else 'Unknown'}")
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:
logger.info(f"Getting photos for zekken: {zekken_number}, event: {event_code}")
# イベントの存在確認event_codeで検索
event = NewEvent2.objects.filter(event_code=event_code).first()
if not event:
logger.warning(f"Event not found with event_code: {event_code}")
# event_nameでも試してみる
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"Event not found with event_name: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"Found event: {event.event_name} (ID: {event.id})")
# まずEntryテーブルを確認
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).first()
team_name = "Unknown Team"
if entry and entry.team:
team_name = entry.team.team_name
logger.info(f"Found team in Entry: {team_name}")
else:
logger.info(f"No Entry found for zekken {zekken_number}, checking GpsCheckin for legacy data")
# GpsCheckinテーブルからチーム情報を取得レガシーデータ対応
gps_checkin_sample = GpsCheckin.objects.filter(
event_code=event_code,
zekken_number=str(zekken_number)
).first()
if gps_checkin_sample and gps_checkin_sample.team:
team_name = gps_checkin_sample.team.team_name
logger.info(f"Found team in GpsCheckin: {team_name}")
else:
team_name = f"Team {zekken_number}"
logger.info(f"No team found, using default: {team_name}")
# GpsCheckinテーブルから写真データを取得
gps_checkins = GpsCheckin.objects.filter(
event_code=event_code,
zekken_number=str(zekken_number),
image_address__isnull=False
).exclude(
image_address=''
).order_by('path_order', 'create_at')
logger.info(f"Found {gps_checkins.count()} GPS checkins with images")
# 写真リストを作成
photos = []
for gps in gps_checkins:
if gps.image_address:
# 画像URLを構築
if gps.image_address.startswith('http'):
# 絶対URLの場合はそのまま使用
image_url = gps.image_address
else:
# 相対パスの場合はベースURLと結合
# settings.MEDIA_URLやstatic fileの設定に基づいて調整
image_url = f"/media/{gps.image_address}" if not gps.image_address.startswith('/') else gps.image_address
photo_data = {
"cp_number": gps.cp_number if gps.cp_number is not None else 0,
"photo_url": image_url,
"checkin_time": gps.create_at.strftime("%Y-%m-%d %H:%M:%S") if gps.create_at else None,
"path_order": gps.path_order,
"buy_flag": gps.buy_flag,
"validate_location": gps.validate_location,
"points": gps.points
}
photos.append(photo_data)
logger.debug(f"Added photo: CP {gps.cp_number}, URL: {image_url}")
# チームの基本情報
team_info = {
"team_name": team_name,
"zekken_number": zekken_number,
"event_name": event.event_name,
"photo_count": len(photos)
}
# レスポンス構築
response_data = {
"status": "SUCCESS",
"message": f"写真を{len(photos)}枚取得しました",
"team_info": team_info,
"photo_list": photos
}
logger.info(f"Successfully retrieved {len(photos)} photos for team {team_name}")
return Response(response_data, status=status.HTTP_200_OK)
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)

View File

@ -2,7 +2,7 @@
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 NewEvent2, Entry, Location2025
from rog.models import GpsLog
import logging
from django.db.models import F, Q
@ -172,7 +172,7 @@ def get_checkpoint_list(request):
}, status=status.HTTP_404_NOT_FOUND)
# イベントのチェックポイント情報を取得
checkpoints = Location.objects.filter(event=event).order_by('cp_number')
checkpoints = Location2025.objects.filter(event=event).order_by('cp_number')
checkpoint_list = []
for cp in checkpoints:
@ -398,12 +398,12 @@ def checkin_from_rogapp(request):
# イベントのチェックポイント定義を確認(存在する場合)
event_cp = None
try:
event_cp = Location.objects.filter(
event_cp = Location2025.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")
logger.info(f"Location2025 model not available or CP {cp_number} not defined for event")
# トランザクション開始
with transaction.atomic():
@ -595,8 +595,8 @@ def calculate_team_score(entry):
# チェックポイントの得点を取得
cp_point = 0
try:
# Location
event_cp = Location.objects.filter(
# Location2025
event_cp = Location2025.objects.filter(
event=entry.event,
cp_number=cp.cp_number
).first()

235
rog/views_apis/s3_views.py Normal file
View File

@ -0,0 +1,235 @@
"""
API views for S3 image management
"""
import json
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from ..services.s3_service import S3Service
import logging
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@csrf_exempt
def upload_checkin_image(request):
"""
Upload checkin image to S3
POST /api/upload-checkin-image/
{
"event_code": "FC岐阜",
"team_code": "3432",
"cp_number": 10,
"image": <file>
}
"""
try:
# Validate required fields
event_code = request.data.get('event_code')
team_code = request.data.get('team_code')
image_file = request.FILES.get('image')
cp_number = request.data.get('cp_number')
if not all([event_code, team_code, image_file]):
return Response({
'error': 'event_code, team_code, and image are required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Upload image
s3_url = s3_service.upload_checkin_image(
image_file=image_file,
event_code=event_code,
team_code=team_code,
cp_number=cp_number
)
return Response({
'success': True,
'image_url': s3_url,
'message': 'Image uploaded successfully'
}, status=status.HTTP_201_CREATED)
except Exception as e:
logger.error(f"Error uploading checkin image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@csrf_exempt
def upload_standard_image(request):
"""
Upload standard image (goal, start, etc.) to S3
POST /api/upload-standard-image/
{
"event_code": "FC岐阜",
"image_type": "goal", # goal, start, checkpoint, etc.
"image": <file>
}
"""
try:
# Validate required fields
event_code = request.data.get('event_code')
image_type = request.data.get('image_type')
image_file = request.FILES.get('image')
if not all([event_code, image_type, image_file]):
return Response({
'error': 'event_code, image_type, and image are required'
}, status=status.HTTP_400_BAD_REQUEST)
# Validate image_type
valid_types = ['goal', 'start', 'checkpoint', 'finish']
if image_type not in valid_types:
return Response({
'error': f'image_type must be one of: {valid_types}'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Upload standard image
s3_url = s3_service.upload_standard_image(
image_file=image_file,
event_code=event_code,
image_type=image_type
)
return Response({
'success': True,
'image_url': s3_url,
'message': 'Standard image uploaded successfully'
}, status=status.HTTP_201_CREATED)
except Exception as e:
logger.error(f"Error uploading standard image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
def get_standard_image(request):
"""
Get standard image URL
GET /api/get-standard-image/?event_code=FC岐阜&image_type=goal
"""
try:
event_code = request.GET.get('event_code')
image_type = request.GET.get('image_type')
if not all([event_code, image_type]):
return Response({
'error': 'event_code and image_type are required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Get standard image URL
image_url = s3_service.get_standard_image_url(event_code, image_type)
if image_url:
return Response({
'success': True,
'image_url': image_url
}, status=status.HTTP_200_OK)
else:
return Response({
'success': False,
'message': 'Standard image not found',
'image_url': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"Error getting standard image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
def list_event_images(request):
"""
List all images for an event
GET /api/list-event-images/?event_code=FC岐阜&limit=50
"""
try:
event_code = request.GET.get('event_code')
limit = int(request.GET.get('limit', 100))
if not event_code:
return Response({
'error': 'event_code is required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# List event images
image_urls = s3_service.list_event_images(event_code, limit)
return Response({
'success': True,
'event_code': event_code,
'image_count': len(image_urls),
'images': image_urls
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error listing event images: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
@csrf_exempt
def delete_image(request):
"""
Delete image from S3
DELETE /api/delete-image/
{
"image_url": "https://sumasenrogaining.s3.us-west-2.amazonaws.com/FC岐阜/3432/image.jpg"
}
"""
try:
image_url = request.data.get('image_url')
if not image_url:
return Response({
'error': 'image_url is required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Delete image
success = s3_service.delete_image(image_url)
if success:
return Response({
'success': True,
'message': 'Image deleted successfully'
}, status=status.HTTP_200_OK)
else:
return Response({
'success': False,
'message': 'Failed to delete image'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error deleting image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)