316 lines
14 KiB
Python
316 lines
14 KiB
Python
"""
|
||
写真一括アップロード・通過履歴校正API
|
||
スマホアルバムから複数の写真をアップロードし、EXIF情報から自動的に通過履歴を生成・校正する
|
||
"""
|
||
|
||
import logging
|
||
import uuid
|
||
import os
|
||
from datetime import datetime
|
||
from django.http import JsonResponse
|
||
from django.utils import timezone
|
||
from django.db import transaction
|
||
from django.core.files.storage import default_storage
|
||
from django.conf import settings
|
||
from rest_framework.decorators import api_view, permission_classes, parser_classes
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.parsers import MultiPartParser, FormParser
|
||
from knox.auth import TokenAuthentication
|
||
|
||
from ..models import NewEvent2, Entry, GpsLog, Location2025
|
||
|
||
# ログ設定
|
||
logger = logging.getLogger(__name__)
|
||
|
||
@api_view(['POST', 'GET'])
|
||
@permission_classes([IsAuthenticated])
|
||
@parser_classes([MultiPartParser, FormParser])
|
||
def bulk_upload_checkin_photos(request):
|
||
"""
|
||
スマホアルバムから複数の写真を一括アップロードし、EXIF情報から通過履歴を校正する
|
||
|
||
パラメータ:
|
||
- event_code: イベントコード (必須)
|
||
- zekken_number: ゼッケン番号 (必須)
|
||
- photos: アップロードする写真ファイルのリスト (必須)
|
||
- auto_process: 自動処理を行うかどうか (デフォルト: true)
|
||
"""
|
||
|
||
# リクエストID生成(デバッグ用)
|
||
request_id = str(uuid.uuid4())[:8]
|
||
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
|
||
|
||
logger.info(f"[BULK_UPLOAD] 🎯 API ACCESS CONFIRMED - bulk_upload_checkin_photos called successfully - ID: {request_id}, Method: {request.method}, User: {request.user.email if request.user.is_authenticated else 'Anonymous'}, Client IP: {client_ip}")
|
||
logger.info(f"[BULK_UPLOAD] 🔍 Request details - Content-Type: {request.content_type}, POST data keys: {list(request.POST.keys())}, FILES count: {len(request.FILES)}")
|
||
|
||
# GETリクエストの場合は、APIが動作していることを確認するための情報を返す
|
||
if request.method == 'GET':
|
||
logger.info(f"[BULK_UPLOAD] 📋 GET request received - returning API status")
|
||
return Response({
|
||
"status": "ACTIVE",
|
||
"message": "一括写真アップロードAPIが動作中です",
|
||
"endpoint": "/api/bulk_upload_checkin_photos/",
|
||
"method": "POST",
|
||
"required_params": ["event_code", "zekken_number", "photos"],
|
||
"optional_params": ["auto_process"],
|
||
"user": request.user.email if request.user.is_authenticated else 'Not authenticated'
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
try:
|
||
# リクエストデータの取得
|
||
event_code = request.POST.get('event_code')
|
||
zekken_number = request.POST.get('zekken_number')
|
||
auto_process = request.POST.get('auto_process', 'true').lower() == 'true'
|
||
|
||
# アップロードされた写真ファイルの取得
|
||
uploaded_files = request.FILES.getlist('photos')
|
||
|
||
logger.info(f"[BULK_UPLOAD] 📝 Request data - ID: {request_id}, event_code: '{event_code}', zekken_number: '{zekken_number}', files_count: {len(uploaded_files)}, auto_process: {auto_process}")
|
||
|
||
# 必須パラメータの検証
|
||
if not all([event_code, zekken_number]):
|
||
logger.warning(f"[BULK_UPLOAD] ❌ Missing required parameters - ID: {request_id}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "イベントコードとゼッケン番号が必要です"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not uploaded_files:
|
||
logger.warning(f"[BULK_UPLOAD] ❌ No files uploaded - ID: {request_id}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "アップロードする写真が必要です"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# ファイル数制限の確認
|
||
max_files = getattr(settings, 'BULK_UPLOAD_MAX_FILES', 50)
|
||
if len(uploaded_files) > max_files:
|
||
logger.warning(f"[BULK_UPLOAD] ❌ Too many files - ID: {request_id}, count: {len(uploaded_files)}, max: {max_files}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": f"一度にアップロードできる写真は最大{max_files}枚です"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# イベントの存在確認
|
||
event = NewEvent2.objects.filter(event_name=event_code).first()
|
||
if not event:
|
||
logger.warning(f"[BULK_UPLOAD] ❌ Event not found - ID: {request_id}, event_code: '{event_code}'")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "指定されたイベントが見つかりません"
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
logger.info(f"[BULK_UPLOAD] ✅ Event found - ID: {request_id}, event: '{event_code}', event_id: {event.id}")
|
||
|
||
# チームの存在確認とオーナー権限の検証
|
||
entry = Entry.objects.filter(
|
||
event=event,
|
||
team__zekken_number=zekken_number
|
||
).first()
|
||
|
||
if not entry:
|
||
logger.warning(f"[BULK_UPLOAD] ❌ Team not found - ID: {request_id}, zekken_number: '{zekken_number}', event_code: '{event_code}'")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
logger.info(f"[BULK_UPLOAD] ✅ Team found - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}, entry_id: {entry.id}")
|
||
|
||
# オーナー権限の確認
|
||
if entry.owner != request.user:
|
||
logger.warning(f"[BULK_UPLOAD] ❌ Permission denied - ID: {request_id}, user: {request.user.email}, team_owner: {entry.owner.email}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "このチームの写真をアップロードする権限がありません"
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 写真処理の準備
|
||
processed_files = []
|
||
successful_uploads = 0
|
||
failed_uploads = 0
|
||
|
||
# アップロードディレクトリの準備
|
||
upload_dir = f"bulk_checkin_photos/{event_code}/{zekken_number}/"
|
||
os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_dir), exist_ok=True)
|
||
|
||
with transaction.atomic():
|
||
for index, uploaded_file in enumerate(uploaded_files):
|
||
file_result = {
|
||
"filename": uploaded_file.name,
|
||
"file_index": index + 1,
|
||
"file_size": uploaded_file.size,
|
||
"upload_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
}
|
||
|
||
try:
|
||
# ファイル形式の確認
|
||
allowed_extensions = ['.jpg', '.jpeg', '.png', '.heic']
|
||
file_extension = os.path.splitext(uploaded_file.name)[1].lower()
|
||
|
||
if file_extension not in allowed_extensions:
|
||
file_result.update({
|
||
"status": "failed",
|
||
"error": f"サポートされていないファイル形式: {file_extension}"
|
||
})
|
||
failed_uploads += 1
|
||
processed_files.append(file_result)
|
||
continue
|
||
|
||
# ファイルサイズの確認
|
||
max_size = getattr(settings, 'BULK_UPLOAD_MAX_FILE_SIZE', 10 * 1024 * 1024) # 10MB
|
||
if uploaded_file.size > max_size:
|
||
file_result.update({
|
||
"status": "failed",
|
||
"error": f"ファイルサイズが大きすぎます: {uploaded_file.size / (1024*1024):.1f}MB"
|
||
})
|
||
failed_uploads += 1
|
||
processed_files.append(file_result)
|
||
continue
|
||
|
||
# ファイルの保存
|
||
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
|
||
safe_filename = f"{timestamp}_{index+1:03d}_{uploaded_file.name}"
|
||
file_path = os.path.join(upload_dir, safe_filename)
|
||
|
||
# ファイル保存
|
||
saved_path = default_storage.save(file_path, uploaded_file)
|
||
full_path = os.path.join(settings.MEDIA_ROOT, saved_path)
|
||
|
||
file_result.update({
|
||
"status": "uploaded",
|
||
"saved_path": saved_path,
|
||
"file_url": f"{settings.MEDIA_URL}{saved_path}"
|
||
})
|
||
|
||
# EXIF情報の抽出(今後実装予定)
|
||
if auto_process:
|
||
# TODO: EXIF情報の抽出とGPS座標取得
|
||
# TODO: 撮影時刻の取得
|
||
# TODO: 近接チェックポイントの検索
|
||
# TODO: 自動チェックイン処理
|
||
|
||
file_result.update({
|
||
"auto_process_status": "pending",
|
||
"auto_process_message": "EXIF解析機能は今後実装予定です"
|
||
})
|
||
|
||
successful_uploads += 1
|
||
logger.info(f"[BULK_UPLOAD] ✅ File uploaded - ID: {request_id}, filename: {uploaded_file.name}, size: {uploaded_file.size}")
|
||
|
||
except Exception as file_error:
|
||
file_result.update({
|
||
"status": "failed",
|
||
"error": f"ファイル処理エラー: {str(file_error)}"
|
||
})
|
||
failed_uploads += 1
|
||
logger.error(f"[BULK_UPLOAD] ❌ File processing error - ID: {request_id}, filename: {uploaded_file.name}, error: {str(file_error)}")
|
||
|
||
processed_files.append(file_result)
|
||
|
||
# 処理結果のサマリー
|
||
logger.info(f"[BULK_UPLOAD] ✅ Upload completed - ID: {request_id}, successful: {successful_uploads}, failed: {failed_uploads}")
|
||
|
||
# 成功レスポンス
|
||
return Response({
|
||
"status": "OK",
|
||
"message": "写真の一括アップロードが完了しました",
|
||
"upload_summary": {
|
||
"total_files": len(uploaded_files),
|
||
"successful_uploads": successful_uploads,
|
||
"failed_uploads": failed_uploads,
|
||
"upload_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
},
|
||
"team_info": {
|
||
"team_name": entry.team.team_name,
|
||
"zekken_number": zekken_number,
|
||
"event_code": event_code
|
||
},
|
||
"processed_files": processed_files,
|
||
"auto_process_enabled": auto_process,
|
||
"next_steps": [
|
||
"アップロードされた写真のEXIF情報解析",
|
||
"GPS座標からチェックポイント自動判定",
|
||
"通過履歴の自動生成と校正",
|
||
"ユーザーによる確認と承認"
|
||
]
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"[BULK_UPLOAD] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
@api_view(['GET'])
|
||
@permission_classes([IsAuthenticated])
|
||
def get_bulk_upload_status(request):
|
||
"""
|
||
一括アップロードの処理状況を取得する
|
||
|
||
パラメータ:
|
||
- event_code: イベントコード (必須)
|
||
- zekken_number: ゼッケン番号 (必須)
|
||
"""
|
||
|
||
request_id = str(uuid.uuid4())[:8]
|
||
logger.info(f"[BULK_STATUS] 🎯 API call started - ID: {request_id}, User: {request.user.email}")
|
||
|
||
try:
|
||
event_code = request.GET.get('event_code')
|
||
zekken_number = request.GET.get('zekken_number')
|
||
|
||
if not all([event_code, zekken_number]):
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "イベントコードとゼッケン番号が必要です"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# チーム権限の確認
|
||
event = NewEvent2.objects.filter(event_name=event_code).first()
|
||
if not event:
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "指定されたイベントが見つかりません"
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
entry = Entry.objects.filter(event=event, team__zekken_number=zekken_number).first()
|
||
if not entry or entry.owner != request.user:
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "このチームの情報にアクセスする権限がありません"
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# TODO: 実際の処理状況を取得
|
||
# TODO: アップロードされたファイル一覧
|
||
# TODO: EXIF解析状況
|
||
# TODO: 自動チェックイン生成状況
|
||
|
||
return Response({
|
||
"status": "OK",
|
||
"team_info": {
|
||
"team_name": entry.team.team_name,
|
||
"zekken_number": zekken_number,
|
||
"event_code": event_code
|
||
},
|
||
"upload_status": {
|
||
"total_uploaded_files": 0,
|
||
"processed_files": 0,
|
||
"pending_files": 0,
|
||
"auto_checkins_generated": 0,
|
||
"manual_review_required": 0
|
||
},
|
||
"implementation_status": "基本機能実装完了、詳細処理は今後実装予定"
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"[BULK_STATUS] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|