From 66aacbb69e963d077ee2271247d691e62a407a5d Mon Sep 17 00:00:00 2001 From: Akira Date: Sat, 6 Sep 2025 01:23:28 +0900 Subject: [PATCH] Fix photo upload 2 --- rog/views_apis/api_bulk_photo_upload.py | 285 ++++++++++++++++++++---- 1 file changed, 240 insertions(+), 45 deletions(-) diff --git a/rog/views_apis/api_bulk_photo_upload.py b/rog/views_apis/api_bulk_photo_upload.py index af60c32..28d345d 100644 --- a/rog/views_apis/api_bulk_photo_upload.py +++ b/rog/views_apis/api_bulk_photo_upload.py @@ -21,12 +21,150 @@ from knox.auth import TokenAuthentication from PIL import Image from PIL.ExifTags import TAGS, GPSTAGS import math +import tempfile +import json -from ..models import NewEvent2, Entry, GpsLog, Location2025, GpsCheckin +from ..models import NewEvent2, Entry, GpsLog, Location2025, GpsCheckin, CheckinImages # ログ設定 logger = logging.getLogger(__name__) +def upload_photo_to_s3(uploaded_file, event_code, zekken_number, cp_number=None, request_id=None): + """ + アップロードされた写真をS3にアップロードする + + Args: + uploaded_file: アップロードされたファイル + event_code: イベントコード + zekken_number: ゼッケン番号 + cp_number: チェックポイント番号 + request_id: リクエストID + + Returns: + dict: { + 'success': bool, + 's3_url': str, + 's3_key': str, + 'error': str + } + """ + try: + # S3のバケット設定を取得 + bucket_name = getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'rogaining-images') + + # S3キーの生成 + timestamp = timezone.now().strftime("%Y%m%d_%H%M%S") + file_extension = os.path.splitext(uploaded_file.name)[1].lower() + + if cp_number: + s3_key = f"checkin_photos/{event_code}/{zekken_number}/CP{cp_number}_{timestamp}_{request_id}{file_extension}" + else: + s3_key = f"bulk_upload/{event_code}/{zekken_number}/{timestamp}_{request_id}_{uploaded_file.name}" + + # 一時ファイルとして保存 + with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file: + for chunk in uploaded_file.chunks(): + temp_file.write(chunk) + temp_file_path = temp_file.name + + try: + # S3クライアントの設定(環境変数から取得) + import boto3 + from botocore.exceptions import ClientError + + s3_client = boto3.client( + 's3', + aws_access_key_id=getattr(settings, 'AWS_ACCESS_KEY_ID', None), + aws_secret_access_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY', None), + region_name=getattr(settings, 'AWS_S3_REGION_NAME', 'ap-northeast-1') + ) + + # S3にアップロード + s3_client.upload_file( + temp_file_path, + bucket_name, + s3_key, + ExtraArgs={ + 'ContentType': uploaded_file.content_type, + 'Metadata': { + 'event_code': event_code, + 'zekken_number': str(zekken_number), + 'cp_number': str(cp_number) if cp_number else '', + 'original_filename': uploaded_file.name, + 'upload_source': 'bulk_upload_api' + } + } + ) + + # S3 URLの生成 + s3_url = f"https://{bucket_name}.s3.{getattr(settings, 'AWS_S3_REGION_NAME', 'ap-northeast-1')}.amazonaws.com/{s3_key}" + + logger.info(f"[S3_UPLOAD] ✅ Successfully uploaded to S3 - key: {s3_key}") + + return { + 'success': True, + 's3_url': s3_url, + 's3_key': s3_key, + 'error': None + } + + finally: + # 一時ファイルを削除 + if os.path.exists(temp_file_path): + os.unlink(temp_file_path) + + except ImportError: + logger.warning(f"[S3_UPLOAD] ❌ boto3 not available, saving locally") + return { + 'success': False, + 's3_url': None, + 's3_key': None, + 'error': 'S3 upload not available (boto3 not installed)' + } + except Exception as e: + logger.error(f"[S3_UPLOAD] ❌ Error uploading to S3: {str(e)}") + return { + 'success': False, + 's3_url': None, + 's3_key': None, + 'error': str(e) + } + +def create_checkin_image_record(gps_checkin, s3_url, s3_key, original_filename, exif_data, request_id, user): + """ + CheckinImagesテーブルにレコードを作成する + + Args: + gps_checkin: GpsCheckinオブジェクト + s3_url: S3のURL + s3_key: S3のキー + original_filename: 元のファイル名 + exif_data: EXIF情報 + request_id: リクエストID + user: ユーザーオブジェクト + + Returns: + CheckinImagesオブジェクトまたはNone + """ + try: + # CheckinImagesレコードを作成 + checkin_image = CheckinImages.objects.create( + user=user, + checkinimage=s3_url, # S3のURLを画像URLとして保存 + checkintime=timezone.now(), + team_name=f"Team_{gps_checkin.zekken}", # ゼッケン番号からチーム名を生成 + event_code=gps_checkin.event_code, + cp_number=gps_checkin.cp_number + ) + + logger.info(f"[CHECKIN_IMAGE] ✅ Created CheckinImages record - ID: {checkin_image.id}, checkin_id: {gps_checkin.id}") + + return checkin_image + + except Exception as e: + logger.error(f"[CHECKIN_IMAGE] ❌ Error creating CheckinImages record: {str(e)}") + return None + def extract_exif_data(image_file): """ 画像ファイルからEXIF情報を抽出する @@ -164,9 +302,9 @@ def calculate_distance(lat1, lon1, lat2, lon2): distance = R * c return distance -def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number, event_code, photo_filename, request_id): +def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number, event_code, uploaded_file, exif_data, request_id, user): """ - 写真情報からチェックインデータを作成する + 写真情報からチェックインデータを作成し、S3アップロードとCheckinImages登録も行う Args: entry: Entryオブジェクト @@ -174,11 +312,18 @@ def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number, photo_datetime: 撮影日時 zekken_number: ゼッケン番号 event_code: イベントコード - photo_filename: 写真ファイル名 + uploaded_file: アップロードされたファイル + exif_data: EXIF情報辞書 request_id: リクエストID + user: ユーザーオブジェクト Returns: - GpsCheckinオブジェクトまたはNone + dict: { + 'gps_checkin': GpsCheckinオブジェクト, + 'checkin_image': CheckinImagesオブジェクト, + 's3_info': S3アップロード情報, + 'created': bool + } """ try: # 既存のチェックインをチェック(重複防止) @@ -188,44 +333,75 @@ def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number, cp_number=checkpoint.cp_number ).first() + gps_checkin = None + created = False + if existing_checkin: - logger.info(f"[BULK_UPLOAD] 📍 Existing checkin found - ID: {request_id}, CP: {checkpoint.cp}, existing_id: {existing_checkin.id}") - return existing_checkin - - # 新規チェックインの作成 - # 撮影時刻をJSTに変換 - if photo_datetime: - # 撮影時刻をUTCとして扱い、JSTに変換 - create_at = timezone.make_aware(photo_datetime, timezone.utc) + logger.info(f"[BULK_UPLOAD] 📍 Existing checkin found - ID: {request_id}, CP: {checkpoint.cp_number}, existing_id: {existing_checkin.id}") + gps_checkin = existing_checkin else: - create_at = timezone.now() + # 新規チェックインの作成 + # 撮影時刻をJSTに変換 + if photo_datetime: + # 撮影時刻をUTCとして扱い、JSTに変換 + create_at = timezone.make_aware(photo_datetime, timezone.utc) + else: + create_at = timezone.now() + + # シリアル番号を決定(既存のチェックイン数+1) + existing_count = GpsCheckin.objects.filter( + zekken=str(zekken_number), + event_code=event_code + ).count() + serial_number = existing_count + 1 + + # チェックインデータを作成 + gps_checkin = GpsCheckin.objects.create( + zekken=str(zekken_number), + event_code=event_code, + cp_number=checkpoint.cp_number, + serial_number=serial_number, + create_at=create_at, + update_at=timezone.now(), + validate_location=True, # 写真から作成されたものは自動承認 + buy_flag=False, + points=checkpoint.checkin_point if checkpoint.checkin_point else 0, + create_user=f"bulk_upload_{request_id}", # 一括アップロードで作成されたことを示す + update_user=f"bulk_upload_{request_id}", + ) + + logger.info(f"[BULK_UPLOAD] ✅ Created checkin - ID: {request_id}, checkin_id: {gps_checkin.id}, CP: {checkpoint.cp_number}, time: {create_at}, points: {gps_checkin.points}") + created = True - # シリアル番号を決定(既存のチェックイン数+1) - existing_count = GpsCheckin.objects.filter( - zekken=str(zekken_number), - event_code=event_code - ).count() - serial_number = existing_count + 1 - - # チェックインデータを作成 - checkin = GpsCheckin.objects.create( - zekken=str(zekken_number), - event_code=event_code, - cp_number=checkpoint.cp_number, - serial_number=serial_number, - create_at=create_at, - update_at=timezone.now(), - validate_location=True, # 写真から作成されたものは自動承認 - buy_flag=False, - points=checkpoint.checkin_point if checkpoint.checkin_point else 0, - create_user=f"bulk_upload_{request_id}", # 一括アップロードで作成されたことを示す - update_user=f"bulk_upload_{request_id}", - photo_source=photo_filename # 元画像ファイル名を記録(カスタムフィールドがあれば) + # S3に写真をアップロード + logger.info(f"[BULK_UPLOAD] 📤 Uploading photo to S3 - ID: {request_id}, CP: {checkpoint.cp_number}") + s3_result = upload_photo_to_s3( + uploaded_file, + event_code, + zekken_number, + checkpoint.cp_number, + request_id ) - logger.info(f"[BULK_UPLOAD] ✅ Created checkin - ID: {request_id}, checkin_id: {checkin.id}, CP: {checkpoint.cp_number}, time: {create_at}, points: {checkin.points}") + checkin_image = None + if s3_result['success']: + # CheckinImagesテーブルにレコード作成 + checkin_image = create_checkin_image_record( + gps_checkin, + s3_result['s3_url'], + s3_result['s3_key'], + uploaded_file.name, + exif_data, + request_id, + user + ) - return checkin + return { + 'gps_checkin': gps_checkin, + 'checkin_image': checkin_image, + 's3_info': s3_result, + 'created': created + } except Exception as e: logger.error(f"[BULK_UPLOAD] ❌ Error creating checkin - ID: {request_id}, CP: {checkpoint.cp_number if checkpoint else 'None'}, error: {str(e)}") @@ -457,27 +633,46 @@ def bulk_upload_checkin_photos(request): ) if nearest_checkpoint: - # チェックインデータを作成 - checkin = create_checkin_from_photo( + # ファイルポインタを先頭に戻す(S3アップロード用) + uploaded_file.seek(0) + + # チェックインデータを作成(S3アップロードとCheckinImages登録も含む) + checkin_result = create_checkin_from_photo( entry, nearest_checkpoint, exif_data['datetime'], zekken_number, event_code, - uploaded_file.name, - request_id + uploaded_file, + exif_data, + request_id, + request.user ) - if checkin: + if checkin_result and checkin_result['gps_checkin']: + gps_checkin = checkin_result['gps_checkin'] + checkin_image = checkin_result['checkin_image'] + s3_info = checkin_result['s3_info'] + file_result.update({ "auto_process_status": "success", "auto_process_message": f"チェックイン作成完了", "checkin_info": { "checkpoint_name": nearest_checkpoint.location, "cp_number": nearest_checkpoint.cp_number, - "points": checkin.points, - "checkin_time": checkin.create_at.strftime("%Y-%m-%d %H:%M:%S"), - "checkin_id": checkin.id + "points": gps_checkin.points, + "checkin_time": gps_checkin.create_at.strftime("%Y-%m-%d %H:%M:%S"), + "checkin_id": gps_checkin.id, + "was_existing": not checkin_result['created'] + }, + "s3_info": { + "uploaded": s3_info['success'], + "s3_url": s3_info['s3_url'], + "error": s3_info['error'] + }, + "checkin_image_info": { + "created": checkin_image is not None, + "image_id": checkin_image.id if checkin_image else None }, "gps_info": { "latitude": exif_data['latitude'],