Files
rogaining_srv/rog/views_apis/api_bulk_photo_upload.py

316 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
写真一括アップロード・通過履歴校正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)