Fix photo upload 2
This commit is contained in:
@ -21,12 +21,150 @@ from knox.auth import TokenAuthentication
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
from PIL.ExifTags import TAGS, GPSTAGS
|
from PIL.ExifTags import TAGS, GPSTAGS
|
||||||
import math
|
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__)
|
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):
|
def extract_exif_data(image_file):
|
||||||
"""
|
"""
|
||||||
画像ファイルからEXIF情報を抽出する
|
画像ファイルからEXIF情報を抽出する
|
||||||
@ -164,9 +302,9 @@ def calculate_distance(lat1, lon1, lat2, lon2):
|
|||||||
distance = R * c
|
distance = R * c
|
||||||
return distance
|
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:
|
Args:
|
||||||
entry: Entryオブジェクト
|
entry: Entryオブジェクト
|
||||||
@ -174,11 +312,18 @@ def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number,
|
|||||||
photo_datetime: 撮影日時
|
photo_datetime: 撮影日時
|
||||||
zekken_number: ゼッケン番号
|
zekken_number: ゼッケン番号
|
||||||
event_code: イベントコード
|
event_code: イベントコード
|
||||||
photo_filename: 写真ファイル名
|
uploaded_file: アップロードされたファイル
|
||||||
|
exif_data: EXIF情報辞書
|
||||||
request_id: リクエストID
|
request_id: リクエストID
|
||||||
|
user: ユーザーオブジェクト
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
GpsCheckinオブジェクトまたはNone
|
dict: {
|
||||||
|
'gps_checkin': GpsCheckinオブジェクト,
|
||||||
|
'checkin_image': CheckinImagesオブジェクト,
|
||||||
|
's3_info': S3アップロード情報,
|
||||||
|
'created': bool
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 既存のチェックインをチェック(重複防止)
|
# 既存のチェックインをチェック(重複防止)
|
||||||
@ -188,10 +333,13 @@ def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number,
|
|||||||
cp_number=checkpoint.cp_number
|
cp_number=checkpoint.cp_number
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing_checkin:
|
gps_checkin = None
|
||||||
logger.info(f"[BULK_UPLOAD] 📍 Existing checkin found - ID: {request_id}, CP: {checkpoint.cp}, existing_id: {existing_checkin.id}")
|
created = False
|
||||||
return existing_checkin
|
|
||||||
|
|
||||||
|
if existing_checkin:
|
||||||
|
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:
|
||||||
# 新規チェックインの作成
|
# 新規チェックインの作成
|
||||||
# 撮影時刻をJSTに変換
|
# 撮影時刻をJSTに変換
|
||||||
if photo_datetime:
|
if photo_datetime:
|
||||||
@ -208,7 +356,7 @@ def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number,
|
|||||||
serial_number = existing_count + 1
|
serial_number = existing_count + 1
|
||||||
|
|
||||||
# チェックインデータを作成
|
# チェックインデータを作成
|
||||||
checkin = GpsCheckin.objects.create(
|
gps_checkin = GpsCheckin.objects.create(
|
||||||
zekken=str(zekken_number),
|
zekken=str(zekken_number),
|
||||||
event_code=event_code,
|
event_code=event_code,
|
||||||
cp_number=checkpoint.cp_number,
|
cp_number=checkpoint.cp_number,
|
||||||
@ -220,12 +368,40 @@ def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number,
|
|||||||
points=checkpoint.checkin_point if checkpoint.checkin_point else 0,
|
points=checkpoint.checkin_point if checkpoint.checkin_point else 0,
|
||||||
create_user=f"bulk_upload_{request_id}", # 一括アップロードで作成されたことを示す
|
create_user=f"bulk_upload_{request_id}", # 一括アップロードで作成されたことを示す
|
||||||
update_user=f"bulk_upload_{request_id}",
|
update_user=f"bulk_upload_{request_id}",
|
||||||
photo_source=photo_filename # 元画像ファイル名を記録(カスタムフィールドがあれば)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"[BULK_UPLOAD] ✅ Created checkin - ID: {request_id}, checkin_id: {checkin.id}, CP: {checkpoint.cp_number}, time: {create_at}, points: {checkin.points}")
|
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
|
||||||
|
|
||||||
return checkin
|
# 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
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
'gps_checkin': gps_checkin,
|
||||||
|
'checkin_image': checkin_image,
|
||||||
|
's3_info': s3_result,
|
||||||
|
'created': created
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
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)}")
|
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:
|
if nearest_checkpoint:
|
||||||
# チェックインデータを作成
|
# ファイルポインタを先頭に戻す(S3アップロード用)
|
||||||
checkin = create_checkin_from_photo(
|
uploaded_file.seek(0)
|
||||||
|
|
||||||
|
# チェックインデータを作成(S3アップロードとCheckinImages登録も含む)
|
||||||
|
checkin_result = create_checkin_from_photo(
|
||||||
entry,
|
entry,
|
||||||
nearest_checkpoint,
|
nearest_checkpoint,
|
||||||
exif_data['datetime'],
|
exif_data['datetime'],
|
||||||
zekken_number,
|
zekken_number,
|
||||||
event_code,
|
event_code,
|
||||||
uploaded_file.name,
|
uploaded_file,
|
||||||
request_id
|
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({
|
file_result.update({
|
||||||
"auto_process_status": "success",
|
"auto_process_status": "success",
|
||||||
"auto_process_message": f"チェックイン作成完了",
|
"auto_process_message": f"チェックイン作成完了",
|
||||||
"checkin_info": {
|
"checkin_info": {
|
||||||
"checkpoint_name": nearest_checkpoint.location,
|
"checkpoint_name": nearest_checkpoint.location,
|
||||||
"cp_number": nearest_checkpoint.cp_number,
|
"cp_number": nearest_checkpoint.cp_number,
|
||||||
"points": checkin.points,
|
"points": gps_checkin.points,
|
||||||
"checkin_time": checkin.create_at.strftime("%Y-%m-%d %H:%M:%S"),
|
"checkin_time": gps_checkin.create_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"checkin_id": checkin.id
|
"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": {
|
"gps_info": {
|
||||||
"latitude": exif_data['latitude'],
|
"latitude": exif_data['latitude'],
|
||||||
|
|||||||
Reference in New Issue
Block a user