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,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)