""" 写真一括アップロード機能 写真の位置情報と撮影時刻を使用してチェックイン処理を行う """ 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": [] } """ 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)