almost finish migrate new circumstances
This commit is contained in:
352
rog/views_apis/api_admin_validation.py
Normal file
352
rog/views_apis/api_admin_validation.py
Normal 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)
|
||||
304
rog/views_apis/api_bulk_upload.py
Normal file
304
rog/views_apis/api_bulk_upload.py
Normal 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)
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
239
rog/views_apis/api_photos_fixed.py
Normal file
239
rog/views_apis/api_photos_fixed.py
Normal 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)
|
||||
@ -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
235
rog/views_apis/s3_views.py
Normal 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)
|
||||
Reference in New Issue
Block a user