almost finish migrate new circumstances

This commit is contained in:
2025-08-24 19:44:36 +09:00
parent 1ba305641e
commit fe5a044c82
67 changed files with 1194889 additions and 467 deletions

View File

@ -4,7 +4,7 @@ from django.shortcuts import render,redirect
from leaflet.admin import LeafletGeoAdmin
from leaflet.admin import LeafletGeoAdminMixin
from leaflet_admin_list.admin import LeafletAdminListMixin
from .models import RogUser, Location, SystemSettings, JoinedEvent, Favorite, TravelList, TravelPoint, ShapeLayers, Event, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, CustomUser, GifuAreas, UserTracks, templocation, UserUpload, EventUser, GoalImages, CheckinImages, NewEvent2, Team, NewCategory, Entry, Member, TempUser,GifurogeRegister
from .models import RogUser, Location, Location2025, SystemSettings, JoinedEvent, Favorite, TravelList, TravelPoint, ShapeLayers, Event, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, CustomUser, GifuAreas, UserTracks, templocation, UserUpload, EventUser, GoalImages, CheckinImages, NewEvent2, Team, NewCategory, Entry, Member, TempUser, GifurogeRegister, GpsLog, GpsCheckin, Checkpoint, Waypoint
from django.contrib.auth.admin import UserAdmin
from django.urls import path,reverse
from django.shortcuts import render
@ -1007,3 +1007,170 @@ admin.site.register(templocation, TempLocationAdmin)
admin.site.register(GoalImages, admin.ModelAdmin)
admin.site.register(CheckinImages, admin.ModelAdmin)
# GpsLogとその他の新しいモデルの登録
@admin.register(GpsLog)
class GpsLogAdmin(admin.ModelAdmin):
list_display = ['id', 'serial_number', 'zekken_number', 'event_code', 'cp_number', 'checkin_time']
list_filter = ['event_code', 'checkin_time', 'buy_flag', 'is_service_checked']
search_fields = ['zekken_number', 'event_code', 'cp_number']
readonly_fields = ['checkin_time', 'create_at', 'update_at']
@admin.register(GpsCheckin)
class GpsCheckinAdmin(admin.ModelAdmin):
list_display = ['id', 'zekken_number', 'event_code', 'cp_number', 'create_at']
list_filter = ['event_code', 'create_at', 'validate_location']
search_fields = ['zekken_number', 'event_code', 'cp_number']
readonly_fields = ['create_at']
@admin.register(Checkpoint)
class CheckpointAdmin(admin.ModelAdmin):
list_display = ['id', 'cp_name', 'cp_number', 'photo_point', 'buy_point']
search_fields = ['cp_name', 'cp_number']
list_filter = ['photo_point', 'buy_point']
readonly_fields = ['created_at', 'updated_at']
@admin.register(Waypoint)
class WaypointAdmin(admin.ModelAdmin):
list_display = ['id', 'entry', 'latitude', 'longitude', 'recorded_at']
search_fields = ['entry__team_name']
list_filter = ['recorded_at']
readonly_fields = ['created_at']
@admin.register(Location2025)
class Location2025Admin(LeafletGeoAdmin):
"""Location2025の管理画面"""
list_display = [
'cp_number', 'cp_name', 'event', 'total_point', 'is_active',
'csv_upload_date', 'created_at'
]
list_filter = [
'event', 'is_active', 'shop_closed', 'shop_shutdown',
'csv_upload_date', 'created_at'
]
search_fields = ['cp_name', 'address', 'description']
readonly_fields = [
'csv_source_file', 'csv_upload_date', 'csv_upload_user',
'created_at', 'updated_at', 'created_by', 'updated_by'
]
fieldsets = (
('基本情報', {
'fields': ('cp_number', 'event', 'cp_name', 'is_active', 'sort_order')
}),
('位置情報', {
'fields': ('latitude', 'longitude', 'location', 'address')
}),
('ポイント設定', {
'fields': ('cp_point', 'photo_point', 'buy_point')
}),
('チェックイン設定', {
'fields': ('checkin_radius', 'auto_checkin')
}),
('営業情報', {
'fields': ('shop_closed', 'shop_shutdown', 'opening_hours')
}),
('詳細情報', {
'fields': ('phone', 'website', 'description')
}),
('CSV情報', {
'fields': ('csv_source_file', 'csv_upload_date', 'csv_upload_user'),
'classes': ('collapse',)
}),
('管理情報', {
'fields': ('created_at', 'updated_at', 'created_by', 'updated_by'),
'classes': ('collapse',)
}),
)
# CSV一括アップロード機能
change_list_template = 'admin/location2025/change_list.html'
def get_urls(self):
from django.urls import path
urls = super().get_urls()
custom_urls = [
path('upload-csv/', self.upload_csv_view, name='location2025_upload_csv'),
path('export-csv/', self.export_csv_view, name='location2025_export_csv'),
]
return custom_urls + urls
def upload_csv_view(self, request):
"""CSVアップロード画面"""
if request.method == 'POST':
if 'csv_file' in request.FILES and 'event' in request.POST:
csv_file = request.FILES['csv_file']
event_id = request.POST['event']
try:
from .models import NewEvent2
event = NewEvent2.objects.get(id=event_id)
# CSVインポート実行
result = Location2025.import_from_csv(
csv_file,
event,
user=request.user
)
# 結果メッセージ
if result['errors']:
messages.warning(
request,
f"インポート完了: 作成{result['created']}件, 更新{result['updated']}件, "
f"エラー{len(result['errors'])}件 - {'; '.join(result['errors'][:5])}"
)
else:
messages.success(
request,
f"CSVインポートが完了しました。作成: {result['created']}件, 更新: {result['updated']}"
)
except Exception as e:
messages.error(request, f"CSVインポートエラー: {str(e)}")
return redirect('..')
# フォーム表示
from .models import NewEvent2
events = NewEvent2.objects.filter(event_active=True).order_by('-created_at')
return render(request, 'admin/location2025/upload_csv.html', {
'events': events,
'title': 'チェックポイントCSVアップロード'
})
def export_csv_view(self, request):
"""CSVエクスポート"""
import csv
from django.http import HttpResponse
from django.utils import timezone
response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = f'attachment; filename="checkpoints_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"'
# BOM付きUTF-8で出力
response.write('\ufeff')
writer = csv.writer(response)
writer.writerow([
'cp_number', 'cp_name', 'latitude', 'longitude', 'cp_point',
'photo_point', 'buy_point', 'address', 'phone', 'description'
])
queryset = self.get_queryset(request)
for obj in queryset:
writer.writerow([
obj.cp_number, obj.cp_name, obj.latitude, obj.longitude,
obj.cp_point, obj.photo_point, obj.buy_point,
obj.address, obj.phone, obj.description
])
return response
def save_model(self, request, obj, form, change):
"""保存時にユーザー情報を自動設定"""
if not change: # 新規作成時
obj.created_by = request.user
obj.updated_by = request.user
super().save_model(request, obj, form, change)

View File

@ -0,0 +1,354 @@
import logging
from django.core.management.base import BaseCommand
from django.db import connections, transaction, connection
from django.db.models import Q
from django.contrib.gis.geos import Point
from django.utils import timezone
from rog.models import Team, NewEvent2, Checkpoint, GpsCheckin, GpsLog, Entry
from datetime import datetime
import psycopg2
from psycopg2.extras import execute_values
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'MobServerデータベースからDjangoモデルにデータを移行します'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
dest='dry_run',
help='実際の移行を行わず、処理内容のみを表示します',
)
parser.add_argument(
'--batch-size',
type=int,
default=100,
help='バッチサイズ(デフォルト: 100'
)
def handle(self, *args, **options):
dry_run = options['dry_run']
batch_size = options['batch_size']
if dry_run:
self.stdout.write(self.style.WARNING('ドライランモードで実行中...'))
# MobServerデータベース接続を取得
mobserver_conn = connections['mobserver']
try:
with transaction.atomic():
self.migrate_events(mobserver_conn, dry_run, batch_size)
self.migrate_teams(mobserver_conn, dry_run, batch_size)
self.migrate_checkpoints(mobserver_conn, dry_run, batch_size)
self.migrate_gps_logs(mobserver_conn, dry_run, batch_size)
if dry_run:
raise transaction.TransactionManagementError("ドライランのためロールバックします")
except transaction.TransactionManagementError:
if dry_run:
self.stdout.write(self.style.SUCCESS('ドライランが完了しました(変更は保存されていません)'))
else:
raise
except Exception as e:
logger.error(f'データ移行エラー: {e}')
self.stdout.write(self.style.ERROR(f'エラーが発生しました: {e}'))
raise
def migrate_events(self, conn, dry_run, batch_size):
"""イベント情報を移行"""
self.stdout.write('イベント情報を移行中...')
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM event_table")
rows = cursor.fetchall()
events_migrated = 0
batch_data = []
for row in rows:
(event_code, event_name, start_time, event_day) = row
# start_timeのデータクリーニング
cleaned_start_time = start_time
if start_time and isinstance(start_time, str):
# セミコロンをコロンに置換
cleaned_start_time = start_time.replace(';', ':')
# タイムゾーン情報を含む場合は時間部分のみ抽出
if '+' in cleaned_start_time or 'T' in cleaned_start_time:
try:
from datetime import datetime
dt = datetime.fromisoformat(cleaned_start_time.replace('Z', '+00:00'))
cleaned_start_time = dt.strftime('%H:%M:%S')
except:
cleaned_start_time = None
if not dry_run:
batch_data.append(NewEvent2(
event_code=event_code,
event_name=event_name,
event_day=event_day,
start_time=cleaned_start_time,
))
events_migrated += 1
# バッチ処理
if len(batch_data) >= batch_size:
if not dry_run:
NewEvent2.objects.bulk_create(batch_data, ignore_conflicts=True)
batch_data = []
# 残りのデータを処理
if batch_data and not dry_run:
NewEvent2.objects.bulk_create(batch_data, ignore_conflicts=True)
self.stdout.write(f'{events_migrated}件のイベントを移行しました')
def migrate_teams(self, conn, dry_run, batch_size):
"""チーム情報を移行"""
self.stdout.write('チーム情報を移行中...')
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM team_table")
rows = cursor.fetchall()
teams_migrated = 0
batch_data = []
for row in rows:
(zekken_number, event_code, team_name, class_name, password, trial) = row
# 対応するイベントを取得
try:
event = NewEvent2.objects.get(event_code=event_code)
except NewEvent2.DoesNotExist:
self.stdout.write(self.style.WARNING(f' 警告: イベント {event_code} が見つかりません。スキップします。'))
continue
if not dry_run:
batch_data.append(Team(
zekken_number=zekken_number,
team_name=team_name,
event=event,
class_name=class_name,
password=password,
trial=trial,
))
teams_migrated += 1
# バッチ処理
if len(batch_data) >= batch_size:
if not dry_run:
Team.objects.bulk_create(batch_data, ignore_conflicts=True)
batch_data = []
# 残りのデータを処理
if batch_data and not dry_run:
Team.objects.bulk_create(batch_data, ignore_conflicts=True)
self.stdout.write(f'{teams_migrated}件のチームを移行しました')
def migrate_checkpoints(self, conn, dry_run, batch_size):
"""チェックポイント情報を移行"""
self.stdout.write('チェックポイント情報を移行中...')
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM checkpoint_table")
rows = cursor.fetchall()
checkpoints_migrated = 0
batch_data = []
for row in rows:
(cp_number, event_code, cp_name, latitude, longitude,
photo_point, buy_point, sample_photo, colabo_company_memo) = row
# 対応するイベントを取得
try:
event = NewEvent2.objects.get(event_code=event_code)
except NewEvent2.DoesNotExist:
continue
# 位置情報の処理
location = None
if latitude is not None and longitude is not None:
try:
location = Point(longitude, latitude) # Pointは(longitude, latitude)の順序
except (ValueError, AttributeError):
pass
if not dry_run:
batch_data.append(Checkpoint(
cp_number=cp_number,
event=event,
cp_name=cp_name,
location=location,
photo_point=photo_point or 0,
buy_point=buy_point or 0,
sample_photo=sample_photo,
colabo_company_memo=colabo_company_memo,
))
checkpoints_migrated += 1
# バッチ処理
if len(batch_data) >= batch_size:
if not dry_run:
Checkpoint.objects.bulk_create(batch_data, ignore_conflicts=True)
batch_data = []
# 残りのデータを処理
if batch_data and not dry_run:
Checkpoint.objects.bulk_create(batch_data, ignore_conflicts=True)
self.stdout.write(f'{checkpoints_migrated}件のチェックポイントを移行しました')
def migrate_gps_logs(self, conn, dry_run, batch_size):
"""GPS位置情報を移行"""
print('GPS位置情報を移行中...')
# チームとイベントのマッピングを作成
team_to_event_map = {}
for team in Team.objects.select_related('event'):
if team.event: # eventがNoneでないことを確認
team_to_event_map[team.zekken_number] = team.event.id
# チェックポイントのマッピングを作成
checkpoint_id_map = {}
for checkpoint in Checkpoint.objects.select_related('event'):
if checkpoint.event: # eventがNoneでないことを確認
key = (checkpoint.event.event_code, checkpoint.cp_number)
checkpoint_id_map[key] = checkpoint.id
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM gps_information")
rows = cursor.fetchall()
logs_migrated = 0
batch_data = []
for row in rows:
(serial_number, zekken_number, event_code, cp_number,
image_address, goal_time, late_point, create_at, create_user,
update_at, update_user, buy_flag, minus_photo_flag, colabo_company_memo) = row
# 対応するチームを取得
try:
event = NewEvent2.objects.get(event_code=event_code)
team = Team.objects.get(zekken_number=zekken_number, event=event)
# teamが存在し、eventも存在することを確認
if not team or not team.event:
continue
except (NewEvent2.DoesNotExist, Team.DoesNotExist):
continue
# 対応するチェックポイントを取得(存在する場合)
checkpoint = None
if cp_number is not None and cp_number != -1:
try:
checkpoint = Checkpoint.objects.get(cp_number=cp_number, event=event)
except Checkpoint.DoesNotExist:
pass
# checkin_timeの設定必須フィールド
checkin_time = timezone.now() # デフォルト値
if goal_time:
try:
# goal_timeはHH:MM形式と仮定
from datetime import datetime, time
parsed_time = datetime.strptime(goal_time, '%H:%M').time()
if create_at:
checkin_time = timezone.make_aware(datetime.combine(create_at.date(), parsed_time))
else:
checkin_time = timezone.make_aware(datetime.combine(datetime.now().date(), parsed_time))
except:
checkin_time = timezone.make_aware(create_at) if create_at else timezone.now()
elif create_at:
checkin_time = timezone.make_aware(create_at) if timezone.is_naive(create_at) else create_at
if not dry_run:
# GpsCheckinテーブル用のデータ
batch_data.append({
'event_code': event_code,
'zekken': zekken_number,
'serial_number': serial_number,
'cp_number': cp_number or 0,
'lat': None, # 実際のMobServerデータベースから取得
'lng': None, # 実際のMobServerデータベースから取得
'checkin_time': checkin_time,
'record_time': timezone.make_aware(create_at) if create_at and timezone.is_naive(create_at) else (create_at or timezone.now()),
'location': "", # PostGISポイントは後で設定
'mobserver_id': serial_number,
'event_id': team_to_event_map.get(zekken_number),
'team_id': team.id,
'checkpoint_id': checkpoint.id if checkpoint else None
})
logs_migrated += 1
# バッチ処理
if len(batch_data) >= batch_size:
if not dry_run:
self.bulk_insert_gps_logs(batch_data)
batch_data = []
# 残りのデータを処理
if batch_data and not dry_run:
self.bulk_insert_gps_logs(batch_data)
print(f'{logs_migrated}件のGPS位置情報を移行しました')
def bulk_insert_gps_logs(self, batch_data):
"""
GpsCheckinテーブルに直接SQLを使って挿入
"""
if not batch_data:
return
with connection.cursor() as cursor:
# DjangoのGpsCheckinテーブルに挿入
insert_sql = """
INSERT INTO rog_gpscheckin (
event_code, zekken, serial_number, cp_number, lat, lng,
checkin_time, record_time, location, mobserver_id,
event_id, team_id, checkpoint_id
) VALUES %s
ON CONFLICT DO NOTHING
"""
# locationフィールドを除外してバリューを準備
clean_values = []
for data in batch_data:
# lat/lngがある場合はPostGISポイントを作成、ない場合はNULL
if data['lat'] is not None and data['lng'] is not None:
location_point = f"ST_GeomFromText('POINT({data['lng']} {data['lat']})', 4326)"
else:
location_point = None
clean_values.append((
data['event_code'],
data['zekken'],
data['serial_number'],
data['cp_number'],
data['lat'],
data['lng'],
data['checkin_time'],
data['record_time'],
location_point,
data['mobserver_id'],
data['event_id'],
data['team_id'],
data['checkpoint_id']
))
try:
execute_values(cursor, insert_sql, clean_values, template=None, page_size=100)
except Exception as e:
logger.error(f"データ移行エラー: {e}")
raise

View File

@ -0,0 +1,331 @@
"""
修正版データ移行スクリプト
gifurogeデータベースからrogdbデータベースへの正確な移行を行う
UTCからJSTに変換して移行
"""
import psycopg2
from PIL import Image
import PIL.ExifTags
from datetime import datetime, timedelta
import pytz
import os
import re
def get_gps_from_image(image_path):
"""
画像ファイルからGPS情報を抽出する
Returns: (latitude, longitude) または取得できない場合は (None, None)
"""
try:
with Image.open(image_path) as img:
exif = {
PIL.ExifTags.TAGS[k]: v
for k, v in img._getexif().items()
if k in PIL.ExifTags.TAGS
}
if 'GPSInfo' in exif:
gps_info = exif['GPSInfo']
# 緯度の計算
lat = gps_info[2]
lat = lat[0] + lat[1]/60 + lat[2]/3600
if gps_info[1] == 'S':
lat = -lat
# 経度の計算
lon = gps_info[4]
lon = lon[0] + lon[1]/60 + lon[2]/3600
if gps_info[3] == 'W':
lon = -lon
return lat, lon
except Exception as e:
print(f"GPS情報の抽出に失敗: {e}")
return None, None
def convert_utc_to_jst(utc_datetime):
"""
UTCタイムスタンプをJSTに変換する
Args:
utc_datetime: UTC時刻のdatetimeオブジェクト
Returns:
JST時刻のdatetimeオブジェクト
"""
if utc_datetime is None:
return None
# UTCタイムゾーンを設定
if utc_datetime.tzinfo is None:
utc_datetime = pytz.UTC.localize(utc_datetime)
# JSTに変換
jst = pytz.timezone('Asia/Tokyo')
jst_datetime = utc_datetime.astimezone(jst)
# タイムゾーン情報を削除してnaive datetimeとして返す
return jst_datetime.replace(tzinfo=None)
def parse_goal_time(goal_time_str, event_date, create_at=None):
"""
goal_time文字列を正しいdatetimeに変換する
Args:
goal_time_str: "14:58" 形式の時刻文字列
event_date: イベント日付
create_at: goal_timeが空の場合に使用するタイムスタンプ
Returns:
datetime object または None
"""
# goal_timeが空の場合はcreate_atを使用UTCからJSTに変換
if not goal_time_str or goal_time_str.strip() == '':
if create_at:
return convert_utc_to_jst(create_at)
return None
try:
# "HH:MM" または "HH:MM:SS" 形式の時刻をパースJST時刻として扱う
if re.match(r'^\d{1,2}:\d{2}(:\d{2})?$', goal_time_str.strip()):
time_parts = goal_time_str.strip().split(':')
hour = int(time_parts[0])
minute = int(time_parts[1])
second = int(time_parts[2]) if len(time_parts) > 2 else 0
# イベント日付と結合JST時刻として扱うため変換なし
result_datetime = event_date.replace(hour=hour, minute=minute, second=second, microsecond=0)
# 深夜の場合は翌日に調整
if hour < 6: # 午前6時以前は翌日とみなす
result_datetime += timedelta(days=1)
return result_datetime
# すでにdatetime形式の場合
elif 'T' in goal_time_str or ' ' in goal_time_str:
return datetime.fromisoformat(goal_time_str.replace('T', ' ').replace('Z', ''))
except Exception as e:
print(f"時刻パースエラー: {goal_time_str} -> {e}")
return None
def get_event_date(event_code, target_cur):
"""
イベントコードからイベント開催日を取得する
"""
# イベントコード別の実際の開催日を定義
event_dates = {
'FC岐阜': datetime(2024, 10, 25).date(),
'美濃加茂': datetime(2024, 5, 19).date(),
'岐阜市': datetime(2023, 11, 19).date(),
'大垣2': datetime(2023, 5, 14).date(),
'各務原': datetime(2023, 10, 15).date(),
'郡上': datetime(2023, 10, 22).date(),
'中津川': datetime(2024, 4, 14).date(),
'下呂': datetime(2024, 1, 21).date(),
'多治見': datetime(2023, 11, 26).date(),
'大垣': datetime(2023, 4, 16).date(),
'揖斐川': datetime(2023, 12, 3).date(),
'養老ロゲ': datetime(2023, 4, 23).date(),
'高山': datetime(2024, 3, 10).date(),
'大垣3': datetime(2024, 8, 4).date(),
'各務原2': datetime(2024, 11, 10).date(),
'多治見2': datetime(2024, 12, 15).date(),
'下呂2': datetime(2024, 12, 1).date(),
'美濃加茂2': datetime(2024, 11, 3).date(),
'郡上2': datetime(2024, 12, 8).date(),
'関ケ原2': datetime(2024, 9, 29).date(),
'養老2': datetime(2024, 11, 24).date(),
'高山2': datetime(2024, 12, 22).date(),
}
if event_code in event_dates:
return event_dates[event_code]
# デフォルト日付
return datetime(2024, 1, 1).date()
def get_foreign_keys(zekken_number, event_code, cp_number, target_cur):
"""
team_id, event_id, checkpoint_idを取得する
"""
team_id = None
event_id = None
checkpoint_id = None
# team_id を取得
try:
target_cur.execute("""
SELECT t.id, t.event_id
FROM rog_team t
JOIN rog_newevent2 e ON t.event_id = e.id
WHERE t.zekken_number = %s AND e.event_code = %s
""", (zekken_number, event_code))
result = target_cur.fetchone()
if result:
team_id, event_id = result
except Exception as e:
print(f"Team ID取得エラー: {e}")
# checkpoint_id を取得
try:
target_cur.execute("""
SELECT c.id
FROM rog_checkpoint c
JOIN rog_newevent2 e ON c.event_id = e.id
WHERE c.cp_number = %s AND e.event_code = %s
""", (str(cp_number), event_code))
result = target_cur.fetchone()
if result:
checkpoint_id = result[0]
except Exception as e:
print(f"Checkpoint ID取得エラー: {e}")
return team_id, event_id, checkpoint_id
def migrate_gps_data():
"""
GPSチェックインデータの移行
"""
# コンテナ環境用の接続情報
source_db = {
'dbname': 'gifuroge',
'user': 'admin',
'password': 'admin123456',
'host': 'postgres-db', # Dockerサービス名
'port': '5432'
}
target_db = {
'dbname': 'rogdb',
'user': 'admin',
'password': 'admin123456',
'host': 'postgres-db', # Dockerサービス名
'port': '5432'
}
source_conn = None
target_conn = None
source_cur = None
target_cur = None
try:
print("ソースDBへの接続を試みています...")
source_conn = psycopg2.connect(**source_db)
source_cur = source_conn.cursor()
print("ソースDBへの接続が成功しました")
print("ターゲットDBへの接続を試みています...")
target_conn = psycopg2.connect(**target_db)
target_cur = target_conn.cursor()
print("ターゲットDBへの接続が成功しました")
# 既存のrog_gpscheckinデータをクリア
print("既存のGPSチェックインデータをクリアしています...")
target_cur.execute("DELETE FROM rog_gpscheckin")
target_conn.commit()
print("既存データのクリアが完了しました")
print("データの取得を開始します...")
source_cur.execute("""
SELECT serial_number, zekken_number, event_code, cp_number, image_address,
goal_time, late_point, create_at, create_user,
update_at, update_user, buy_flag, colabo_company_memo
FROM gps_information
ORDER BY event_code, zekken_number, serial_number
""")
rows = source_cur.fetchall()
print(f"取得したレコード数: {len(rows)}")
processed_count = 0
error_count = 0
for row in rows:
(serial_number, zekken_number, event_code, cp_number, image_address,
goal_time, late_point, create_at, create_user,
update_at, update_user, buy_flag, colabo_company_memo) = row
try:
# 関連IDを取得
team_id, event_id, checkpoint_id = get_foreign_keys(zekken_number, event_code, cp_number, target_cur)
if not team_id or not event_id:
print(f"スキップ: team_id={team_id}, event_id={event_id} for {zekken_number}/{event_code}")
error_count += 1
continue
# イベント日付を取得
event_date = get_event_date(event_code, target_cur)
# 時刻を正しく変換create_atも渡す
checkin_time = None
record_time = None
if goal_time:
parsed_time = parse_goal_time(goal_time, datetime.combine(event_date, datetime.min.time()), create_at)
if parsed_time:
checkin_time = parsed_time
record_time = parsed_time
# goal_timeがない場合はcreate_atを使用UTCからJSTに変換
if not checkin_time and create_at:
checkin_time = convert_utc_to_jst(create_at)
record_time = convert_utc_to_jst(create_at)
elif not checkin_time:
# 最後の手段としてデフォルト時刻
checkin_time = datetime.combine(event_date, datetime.min.time()) + timedelta(hours=12)
record_time = checkin_time
# GPS座標を取得
latitude, longitude = None, None
if image_address and os.path.exists(image_address):
latitude, longitude = get_gps_from_image(image_address)
# rog_gpscheckinテーブルに挿入
target_cur.execute("""
INSERT INTO rog_gpscheckin (
event_code, zekken, serial_number, cp_number,
lat, lng, checkin_time, record_time,
mobserver_id, event_id, team_id, checkpoint_id
) VALUES (
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
)
""", (
event_code, zekken_number, serial_number, str(cp_number),
latitude, longitude, checkin_time, record_time,
serial_number, event_id, team_id, checkpoint_id
))
processed_count += 1
if processed_count % 100 == 0:
print(f"処理済みレコード数: {processed_count}")
target_conn.commit()
except Exception as e:
print(f"レコード処理エラー: {e} - {row}")
error_count += 1
continue
target_conn.commit()
print(f"移行完了: {processed_count}件のレコードを処理しました")
print(f"エラー件数: {error_count}")
except Exception as e:
print(f"エラーが発生しました: {e}")
if target_conn:
target_conn.rollback()
finally:
if source_cur:
source_cur.close()
if target_cur:
target_cur.close()
if source_conn:
source_conn.close()
if target_conn:
target_conn.close()
print("すべての接続をクローズしました")
if __name__ == "__main__":
migrate_gps_data()

View File

@ -309,10 +309,11 @@ class TempUser(models.Model):
return timezone.now() <= self.expires_at
class NewEvent2(models.Model):
# 既存フィールド
event_name = models.CharField(max_length=255, unique=True)
event_description=models.TextField(max_length=255,blank=True, null=True)
start_datetime = models.DateTimeField(default=timezone.now)
end_datetime = models.DateTimeField()
end_datetime = models.DateTimeField(null=True, blank=True)
deadlineDateTime = models.DateTimeField(null=True, blank=True)
#// Added @2024-10-21
@ -325,8 +326,19 @@ class NewEvent2(models.Model):
class_solo_female = models.BooleanField(default=True)
self_rogaining = models.BooleanField(default=False)
# MobServer統合フィールド
event_code = models.CharField(max_length=50, unique=True, blank=True, null=True) # event_table.event_code
start_time = models.CharField(max_length=20, blank=True, null=True) # event_table.start_time
event_day = models.CharField(max_length=20, blank=True, null=True) # event_table.event_day
# 会場情報統合
venue_location = models.PointField(null=True, blank=True, srid=4326)
venue_address = models.CharField(max_length=500, blank=True, null=True)
def __str__(self):
if self.event_code:
return f"{self.event_code} - {self.event_name}"
return f"{self.event_name} - From:{self.start_datetime} To:{self.end_datetime}"
def save(self, *args, **kwargs):
@ -347,16 +359,38 @@ def get_default_category():
class Team(models.Model):
# zekken_number = models.CharField(max_length=255, unique=True)
# 既存フィールド
team_name = models.CharField(max_length=255)
owner = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='owned_teams', blank=True, null=True)
category = models.ForeignKey('NewCategory', on_delete=models.SET_DEFAULT, default=get_default_category)
# MobServer統合フィールド
zekken_number = models.CharField(max_length=20, blank=True, null=True) # team_table.zekken_number
event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE, blank=True, null=True) # team_table.event_code
password = models.CharField(max_length=100, blank=True, null=True) # team_table.password
class_name = models.CharField(max_length=100, blank=True, null=True) # team_table.class_name
trial = models.BooleanField(default=False) # team_table.trial
# 地理情報
location = models.PointField(null=True, blank=True, srid=4326)
# 統合管理フィールド
created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True, null=True, blank=True)
# class Meta:
# unique_together = ('zekken_number', 'category')
class Meta:
constraints = [
models.UniqueConstraint(
fields=['zekken_number', 'event'],
name='unique_team_per_event',
condition=models.Q(zekken_number__isnull=False, event__isnull=False)
)
]
def __str__(self):
return f"{self.team_name}, owner:{self.owner.lastname} {self.owner.firstname}"
if self.zekken_number and self.event:
return f"{self.zekken_number}-{self.team_name} ({self.event.event_name})"
return f"{self.team_name}, owner:{self.owner.lastname if self.owner else 'None'} {self.owner.firstname if self.owner else ''}"
class Member(models.Model):
@ -540,14 +574,53 @@ class CheckinImages(models.Model):
event_code = models.CharField(_("event code"), max_length=255)
cp_number = models.IntegerField(_("CP numner"))
class Checkpoint(models.Model):
"""チェックポイント管理モデルMobServer統合"""
# MobServer完全統合
cp_number = models.IntegerField() # checkpoint_table.cp_number
event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE, blank=True, null=True)
cp_name = models.CharField(max_length=200, blank=True, null=True) # checkpoint_table.cp_name
# 位置情報PostGIS対応
location = models.PointField(srid=4326, blank=True, null=True) # latitude, longitude統合
# ポイント情報
photo_point = models.IntegerField(default=0) # checkpoint_table.photo_point
buy_point = models.IntegerField(default=0) # checkpoint_table.buy_point
# サンプル・メモ
sample_photo = models.CharField(max_length=500, blank=True, null=True)
colabo_company_memo = models.TextField(blank=True, null=True)
# 統合管理フィールド
created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True, null=True, blank=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['cp_number', 'event'],
name='unique_cp_per_event'
)
]
indexes = [
models.Index(fields=['event', 'cp_number'], name='idx_checkpoint_event_cp'),
GistIndex(fields=['location'], name='idx_checkpoint_location'),
]
def __str__(self):
return f"CP{self.cp_number} - {self.cp_name} ({self.event.event_code if self.event.event_code else self.event.event_name})"
class GpsCheckin(models.Model):
id = models.AutoField(primary_key=True) # 明示的にidフィールドを追加
path_order = models.IntegerField(
null=False,
default=0,
help_text="チェックポイントの順序番号"
)
zekken_number = models.TextField(
null=False,
default='',
help_text="ゼッケン番号"
)
event_id = models.IntegerField(
@ -557,6 +630,7 @@ class GpsCheckin(models.Model):
)
event_code = models.TextField(
null=False,
default='',
help_text="イベントコード"
)
cp_number = models.IntegerField(
@ -637,6 +711,76 @@ class GpsCheckin(models.Model):
blank=True,
help_text="ポイント:このチェックインによる獲得ポイント。通常ポイントと買い物ポイントは分離される。ゴールの場合には減点なども含む。"
)
# MobServer統合フィールド
serial_number = models.IntegerField(
null=True,
blank=True,
help_text="MobServer gps_information.serial_number"
)
team = models.ForeignKey(
'Team',
on_delete=models.CASCADE,
null=True,
blank=True,
help_text="統合チームリレーション"
)
checkpoint = models.ForeignKey(
'Checkpoint',
on_delete=models.CASCADE,
null=True,
blank=True,
help_text="統合チェックポイントリレーション"
)
minus_photo_flag = models.BooleanField(
default=False,
help_text="MobServer gps_information.minus_photo_flag"
)
# 通過審査管理フィールド
validation_status = models.CharField(
max_length=20,
choices=[
('PENDING', '審査待ち'),
('APPROVED', '承認'),
('REJECTED', '却下'),
('AUTO_APPROVED', '自動承認')
],
default='PENDING',
help_text="通過審査ステータス"
)
validation_comment = models.TextField(
null=True,
blank=True,
help_text="審査コメント"
)
validated_at = models.DateTimeField(
null=True,
blank=True,
help_text="審査実施日時"
)
validated_by = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="審査実施者"
)
validation_comment = models.TextField(
null=True,
blank=True,
help_text="審査コメント・理由"
)
validated_by = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="審査者"
)
validated_at = models.DateTimeField(
null=True,
blank=True,
help_text="審査日時"
)
class Meta:
db_table = 'gps_checkins'
@ -800,6 +944,161 @@ class templocation(models.Model):
def __str__(self):
return self.location_name
class Location2025(models.Model):
"""
2025年版チェックポイント管理モデル
CSVアップロード対応の新しいチェックポイント管理システム
"""
# 基本情報
cp_number = models.IntegerField(_('CP番号'), db_index=True)
event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE, verbose_name=_('イベント'))
cp_name = models.CharField(_('CP名'), max_length=255)
# 位置情報
latitude = models.FloatField(_('緯度'), null=True, blank=True)
longitude = models.FloatField(_('経度'), null=True, blank=True)
location = models.PointField(_('位置'), srid=4326, null=True, blank=True)
# ポイント情報
cp_point = models.IntegerField(_('チェックポイント得点'), default=10)
photo_point = models.IntegerField(_('写真ポイント'), default=0)
buy_point = models.IntegerField(_('買い物ポイント'), default=0)
# チェックイン設定
checkin_radius = models.FloatField(_('チェックイン範囲(m)'), default=15.0)
auto_checkin = models.BooleanField(_('自動チェックイン'), default=False)
# 営業情報
shop_closed = models.BooleanField(_('休業中'), default=False)
shop_shutdown = models.BooleanField(_('閉業'), default=False)
opening_hours = models.TextField(_('営業時間'), blank=True, null=True)
# 詳細情報
address = models.CharField(_('住所'), max_length=512, blank=True, null=True)
phone = models.CharField(_('電話番号'), max_length=32, blank=True, null=True)
website = models.URLField(_('ウェブサイト'), blank=True, null=True)
description = models.TextField(_('説明'), blank=True, null=True)
# 管理情報
is_active = models.BooleanField(_('有効'), default=True, db_index=True)
sort_order = models.IntegerField(_('表示順'), default=0)
# CSVアップロード関連
csv_source_file = models.CharField(_('CSVファイル名'), max_length=255, blank=True, null=True)
csv_upload_date = models.DateTimeField(_('CSVアップロード日時'), null=True, blank=True)
csv_upload_user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True,
related_name='location2025_csv_uploads', verbose_name=_('CSVアップロードユーザー'))
# タイムスタンプ
created_at = models.DateTimeField(_('作成日時'), auto_now_add=True)
updated_at = models.DateTimeField(_('更新日時'), auto_now=True)
created_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True,
related_name='location2025_created', verbose_name=_('作成者'))
updated_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, blank=True,
related_name='location2025_updated', verbose_name=_('更新者'))
class Meta:
db_table = 'rog_location2025'
verbose_name = _('チェックポイント2025')
verbose_name_plural = _('チェックポイント2025')
unique_together = ['cp_number', 'event']
ordering = ['event', 'sort_order', 'cp_number']
indexes = [
models.Index(fields=['event', 'cp_number'], name='location2025_event_cp_idx'),
models.Index(fields=['event', 'is_active'], name='location2025_event_active_idx'),
models.Index(fields=['csv_upload_date'], name='location2025_csv_date_idx'),
GistIndex(fields=['location'], name='location2025_location_gist_idx'),
]
def save(self, *args, **kwargs):
# 緯度経度からLocationフィールドを自動生成
if self.latitude and self.longitude:
from django.contrib.gis.geos import Point
self.location = Point(self.longitude, self.latitude, srid=4326)
super().save(*args, **kwargs)
def __str__(self):
return f"{self.event.event_name} - CP{self.cp_number}: {self.cp_name}"
@property
def total_point(self):
"""総得点を計算"""
return self.cp_point + self.photo_point + self.buy_point
@classmethod
def import_from_csv(cls, csv_file, event, user=None):
"""
CSVファイルからチェックポイントデータをインポート
CSV形式:
cp_number,cp_name,latitude,longitude,cp_point,photo_point,buy_point,address,phone,description
"""
import csv
import io
from django.utils import timezone
if isinstance(csv_file, str):
# ファイルパスの場合
with open(csv_file, 'r', encoding='utf-8') as f:
csv_content = f.read()
else:
# アップロードされたファイルの場合
csv_content = csv_file.read().decode('utf-8')
csv_reader = csv.DictReader(io.StringIO(csv_content))
created_count = 0
updated_count = 0
errors = []
for row_num, row in enumerate(csv_reader, start=2):
try:
cp_number = int(row.get('cp_number', 0))
if cp_number <= 0:
errors.append(f"{row_num}: CP番号が無効です")
continue
defaults = {
'cp_name': row.get('cp_name', f'CP{cp_number}'),
'latitude': float(row['latitude']) if row.get('latitude') else None,
'longitude': float(row['longitude']) if row.get('longitude') else None,
'cp_point': int(row.get('cp_point', 10)),
'photo_point': int(row.get('photo_point', 0)),
'buy_point': int(row.get('buy_point', 0)),
'address': row.get('address', ''),
'phone': row.get('phone', ''),
'description': row.get('description', ''),
'csv_source_file': getattr(csv_file, 'name', 'uploaded_file.csv'),
'csv_upload_date': timezone.now(),
'csv_upload_user': user,
'updated_by': user,
}
if user:
defaults['created_by'] = user
obj, created = cls.objects.update_or_create(
cp_number=cp_number,
event=event,
defaults=defaults
)
if created:
created_count += 1
else:
updated_count += 1
except (ValueError, KeyError) as e:
errors.append(f"{row_num}: {str(e)}")
continue
return {
'created': created_count,
'updated': updated_count,
'errors': errors
}
class Location_line(models.Model):
location_id=models.IntegerField(_('Location id'), blank=True, null=True)
location_name=models.CharField(_('Location Name'), max_length=255)
@ -1409,6 +1708,39 @@ def publish_data(sender, instance, created, **kwargs):
# 既存のモデルに追加=> 通過記録に相応しい名称に変更すべき
def get_default_entry():
"""
デフォルトのEntryを取得または作成する
"""
try:
# NewEvent2のデフォルトイベントを取得
default_event = NewEvent2.objects.first()
if default_event:
# デフォルトチームを取得
default_team = Team.objects.first()
if default_team:
# 既存のEntryを取得
entry = Entry.objects.filter(
teams=default_team,
event=default_event
).first()
if entry:
return entry.id
# 新しいEntryを作成
from django.contrib.auth import get_user_model
User = get_user_model()
default_user = User.objects.first()
if default_user:
entry = Entry.objects.create(
event=default_event,
main_user=default_user
)
entry.teams.add(default_team)
return entry.id
except:
pass
return None
class GpsLog(models.Model):
"""
@ -1417,12 +1749,9 @@ class GpsLog(models.Model):
"""
serial_number = models.IntegerField(null=False)
# 新規追加
entry = models.ForeignKey(Entry, on_delete=models.CASCADE, related_name='checkpoints')
# Entry へ移行
zekken_number = models.TextField(null=False)
event_code = models.TextField(null=False)
zekken_number = models.TextField(null=False, default='')
event_code = models.TextField(null=False, default='')
cp_number = models.TextField(null=True, blank=True)
image_address = models.TextField(null=True, blank=True)

222
rog/services/s3_service.py Normal file
View File

@ -0,0 +1,222 @@
"""
S3 Service for managing uploads and standard images
"""
import boto3
import uuid
import base64
from datetime import datetime
from django.conf import settings
from django.core.files.uploadedfile import InMemoryUploadedFile
import logging
logger = logging.getLogger(__name__)
class S3Service:
"""AWS S3 service for handling image uploads and management"""
def __init__(self):
self.s3_client = boto3.client(
's3',
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_S3_REGION_NAME
)
self.bucket_name = settings.AWS_STORAGE_BUCKET_NAME
self.custom_domain = settings.AWS_S3_CUSTOM_DOMAIN
def upload_checkin_image(self, image_file, event_code, team_code, cp_number, is_goal=False):
"""
チェックイン画像またはゴール画像をS3にアップロード
Args:
image_file: アップロードする画像ファイル
event_code: イベントコード
team_code: チームコード
cp_number: チェックポイント番号
is_goal: ゴール画像かどうか(デフォルト: False
"""
try:
# ファイル名を生成UUID + タイムスタンプ)
file_extension = image_file.name.split('.')[-1] if '.' in image_file.name else 'jpg'
filename = f"{uuid.uuid4()}-{datetime.now().strftime('%Y-%m-%dT%H-%M-%S')}.{file_extension}"
# S3キーを生成イベント/チーム/ファイル名)
# ゴール画像の場合は専用フォルダに保存
if is_goal:
s3_key = f"{event_code}/goals/{team_code}/{filename}"
else:
s3_key = f"{event_code}/{team_code}/{filename}"
# メタデータをBase64エンコードして設定S3メタデータはASCIIのみ対応
metadata = {
'event_b64': base64.b64encode(event_code.encode('utf-8')).decode('ascii'),
'team_b64': base64.b64encode(team_code.encode('utf-8')).decode('ascii'),
'cp_number': str(cp_number),
'uploaded_at': datetime.now().strftime('%Y-%m-%dT%H-%M-%S'),
'image_type': 'goal' if is_goal else 'checkin'
}
# S3にアップロード
self.s3_client.upload_fileobj(
image_file,
self.bucket_name,
s3_key,
ExtraArgs={
'ContentType': f'image/{file_extension}',
'Metadata': metadata
}
)
# S3 URLを生成
s3_url = f"https://{self.bucket_name}.s3.{settings.AWS_S3_REGION_NAME}.amazonaws.com/{s3_key}"
logger.info(f"{'Goal' if is_goal else 'Checkin'} image uploaded to S3: {s3_url}")
return s3_url
except Exception as e:
logger.error(f"Failed to upload image to S3: {e}")
raise
def upload_standard_image(self, image_file, event_code, image_type):
"""
規定画像をS3にアップロード
"""
try:
# ファイル拡張子を取得
file_extension = image_file.name.split('.')[-1] if '.' in image_file.name else 'jpg'
# S3キーを生成イベント/standards/タイプ.拡張子)
s3_key = f"{event_code}/standards/{image_type}.{file_extension}"
# メタデータをBase64エンコードして設定
metadata = {
'event_b64': base64.b64encode(event_code.encode('utf-8')).decode('ascii'),
'image_type': image_type,
'uploaded_at': datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
}
# S3にアップロード
self.s3_client.upload_fileobj(
image_file,
self.bucket_name,
s3_key,
ExtraArgs={
'ContentType': f'image/{file_extension}',
'Metadata': metadata
}
)
# S3 URLを生成
s3_url = f"https://{self.bucket_name}.s3.{settings.AWS_S3_REGION_NAME}.amazonaws.com/{s3_key}"
logger.info(f"Standard image uploaded to S3: {s3_url}")
return s3_url
except Exception as e:
logger.error(f"Failed to upload standard image to S3: {e}")
raise
def get_standard_image_url(self, event_code, image_type):
"""
Get URL for standard image
Args:
event_code: Event code
image_type: Type of image (goal, start, checkpoint, etc.)
Returns:
str: S3 URL of standard image or None if not found
"""
# Try common image extensions
extensions = ['.jpg', '.jpeg', '.png', '.gif']
for ext in extensions:
s3_key = f"{event_code}/standards/{image_type}{ext}"
try:
# Check if object exists
self.s3_client.head_object(Bucket=self.bucket_name, Key=s3_key)
return f"https://{self.custom_domain}/{s3_key}"
except self.s3_client.exceptions.NoSuchKey:
continue
except Exception as e:
logger.error(f"Error checking standard image: {e}")
continue
return None
def delete_image(self, s3_url):
"""
Delete image from S3
Args:
s3_url: Full S3 URL of the image
Returns:
bool: True if deleted successfully
"""
try:
# Extract S3 key from URL
s3_key = self._extract_s3_key_from_url(s3_url)
if not s3_key:
logger.error(f"Invalid S3 URL: {s3_url}")
return False
# Delete from S3
self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key)
logger.info(f"Image deleted from S3: {s3_url}")
return True
except Exception as e:
logger.error(f"Failed to delete image from S3: {e}")
return False
def list_event_images(self, event_code, limit=100):
"""
List all images for an event
Args:
event_code: Event code
limit: Maximum number of images to return
Returns:
list: List of S3 URLs
"""
try:
response = self.s3_client.list_objects_v2(
Bucket=self.bucket_name,
Prefix=f"{event_code}/",
MaxKeys=limit
)
urls = []
if 'Contents' in response:
for obj in response['Contents']:
url = f"https://{self.custom_domain}/{obj['Key']}"
urls.append(url)
return urls
except Exception as e:
logger.error(f"Failed to list event images: {e}")
return []
def _get_file_extension(self, filename):
"""Get file extension from filename"""
if '.' in filename:
return '.' + filename.split('.')[-1].lower()
return '.jpg' # default extension
def _get_timestamp(self):
"""Get current timestamp string"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
def _extract_s3_key_from_url(self, s3_url):
"""Extract S3 key from full S3 URL"""
try:
# Remove domain part to get key
if self.custom_domain in s3_url:
return s3_url.split(self.custom_domain + '/')[-1]
return None
except Exception:
return None

View File

@ -12,8 +12,11 @@ from .views_apis.api_routes import top_users_routes,generate_route_image
from .views_apis.api_events import get_start_point,analyze_point
from .views_apis.api_monitor import realtime_monitor, realtime_monitor_zekken_narrow
from .views_apis.api_ranking import get_ranking,all_ranking_top3
from .views_apis.api_photos import get_photo_list, get_photo_list_prod
from .views_apis.api_photos import get_photo_list, get_photo_list_prod, get_team_photos
from .views_apis.s3_views import upload_checkin_image, upload_standard_image, get_standard_image, list_event_images, delete_image
from .views_apis.api_scoreboard import get_scoreboard,download_scoreboard,reprint,make_all_scoreboard,make_cp_list_sheet
from .views_apis.api_bulk_upload import bulk_upload_photos, confirm_checkin_validation
from .views_apis.api_admin_validation import get_event_participants_ranking, get_participant_validation_details, get_event_zekken_list
from .views_apis.api_simulator import rogaining_simulator
from .views_apis.api_test import test_gifuroge,practice
@ -202,6 +205,7 @@ urlpatterns += [
## PhotoList
path('get_photo_list', get_photo_list, name='get_photo_list'),
path('get_photo_list_prod', get_photo_list_prod, name='get_photo_list_prod'),
path('get_team_photos', get_team_photos, name='get_team_photos'),
path('getCheckpointList', get_checkpoint_list, name='get_checkpoint_list'),
path('makeCpListSheet', make_cp_list_sheet, name='make_cp_list_sheet'),
@ -218,6 +222,19 @@ urlpatterns += [
path('test_gifuroge', test_gifuroge, name='test_gifuroge'),
path('practice', practice, name='practice'),
## S3 Image Management
path('upload-checkin-image/', upload_checkin_image, name='upload_checkin_image'),
path('upload-standard-image/', upload_standard_image, name='upload_standard_image'),
path('get-standard-image/', get_standard_image, name='get_standard_image'),
path('list-event-images/', list_event_images, name='list_event_images'),
path('delete-image/', delete_image, name='delete_image'),
## Bulk Upload and Validation Management
path('bulk-upload-photos/', bulk_upload_photos, name='bulk_upload_photos'),
path('confirm-checkin-validation/', confirm_checkin_validation, name='confirm_checkin_validation'),
path('event-participants-ranking/', get_event_participants_ranking, name='get_event_participants_ranking'),
path('participant-validation-details/', get_participant_validation_details, name='get_participant_validation_details'),
path('event-zekken-list/', get_event_zekken_list, name='get_event_zekken_list'),
]

View File

@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
User = get_user_model()
import traceback
from django.contrib.auth.hashers import make_password
from urllib.parse import unquote # URLデコード用
import subprocess # subprocessモジュールを追加
import tempfile # tempfileモジュールを追加
@ -242,37 +243,19 @@ class LocationViewSet(viewsets.ModelViewSet):
def get_queryset(self):
queryset = Location.objects.all()
logger.info("=== Location API Called ===")
# リクエストパラメータの確認
group_filter = self.request.query_params.get('group__contains')
logger.info(f"Request params: {dict(self.request.query_params)}")
logger.info(f"Group filter: {group_filter}")
if group_filter:
# フィルタ適用前のデータ数
total_count = queryset.count()
logger.info(f"Total locations before filter: {total_count}")
# フィルタの適用
queryset = queryset.filter(group__contains=group_filter)
# フィルタ適用後のデータ数
filtered_count = queryset.count()
logger.info(f"Filtered locations count: {filtered_count}")
# フィルタされたデータのサンプル最初の5件
sample_data = queryset[:5]
logger.info("Sample of filtered data:")
for loc in sample_data:
logger.info(f"ID: {loc.id}, Name: {loc.location_name}, Group: {loc.group}")
return queryset
def list(self, request, *args, **kwargs):
try:
response = super().list(request, *args, **kwargs)
logger.info(f"Response data count: {len(response.data['features'])}")
return response
except Exception as e:
logger.error(f"Error in list method: {str(e)}", exc_info=True)
@ -862,7 +845,12 @@ class LoginAPI(generics.GenericAPIView):
logger.info(f"Login attempt for identifier: {request.data.get('identifier', 'identifier not provided')}")
logger.debug(f"Request data: {request.data}")
serializer = self.get_serializer(data=request.data)
# フロントエンドの 'identifier' フィールドを 'email' にマッピング
data = request.data.copy()
if 'identifier' in data and 'email' not in data:
data['email'] = data['identifier']
serializer = self.get_serializer(data=data)
try:
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
@ -2491,11 +2479,59 @@ def get_events(request):
)
@api_view(['GET'])
def get_zekken_numbers(request, event_code):
entries = Entry.objects.filter(
event__event_name=event_code,
is_active=True
).order_by('zekken_number')
return Response([entry.zekken_number for entry in entries])
# 通過審査画面用: GpsCheckinテーブルから過去の移行データと新規Entryテーブルの両方をサポート
try:
print(f"=== get_zekken_numbers called with event_code: {event_code} ===")
# event_codeからNewEvent2のIDを取得
try:
event_obj = NewEvent2.objects.get(event_code=event_code)
event_id = event_obj.id
print(f"Found event: {event_obj.event_name} (ID: {event_id})")
except NewEvent2.DoesNotExist:
print(f"No event found with event_code: {event_code}, trying legacy data only")
event_id = None
entry_list = []
# 新規のEntryテーブルから取得event_idが見つかった場合のみ
if event_id:
entries = Entry.objects.filter(
event_id=event_id,
zekken_number__gt=0
).values_list('zekken_number', flat=True).order_by('zekken_number')
if event_id:
entries = Entry.objects.filter(
event_id=event_id,
zekken_number__gt=0
).values_list('zekken_number', flat=True).order_by('zekken_number')
entry_list = list(entries)
print(f"Entry table found {len(entry_list)} records: {entry_list[:10]}")
# GpsCheckinテーブルからも検索過去の移行データ
gps_checkins = GpsCheckin.objects.filter(
event_code=event_code,
zekken_number__gt=0
).values_list('zekken_number', flat=True).distinct().order_by('zekken_number')
gps_list = list(gps_checkins)
print(f"GpsCheckin table found {len(gps_list)} records: {gps_list[:10]}")
# 両方の結果をマージして重複を除去
all_zekken_numbers = entry_list + gps_list
unique_zekken_numbers = sorted(set(all_zekken_numbers))
print(f"Final result: {unique_zekken_numbers[:10]}")
return Response(unique_zekken_numbers)
except Exception as e:
print(f"Error in get_zekken_numbers: {str(e)}")
import traceback
traceback.print_exc()
return Response({"error": str(e)}, status=500)
@api_view(['GET'])
def get_team_info(request, zekken_number):
@ -2550,16 +2586,61 @@ def get_team_info(request, zekken_number):
team = Team.objects.get(id=self.kwargs['team_id'])
def get_image_url(image_path):
"""画像URLを生成する補助関数"""
"""画像URLを生成する補助関数 - S3とローカルメディアの両方に対応"""
if not image_path:
return None
# 開発環境用のパス生成
if settings.DEBUG:
return os.path.join(settings.MEDIA_URL, str(image_path))
image_path_str = str(image_path)
# 本番環境用のパス生成
return f"{settings.MEDIA_URL}{image_path}"
# シミュレーション画像の場合はローカルメディアパスを返す
if image_path_str in ['simulation_image.jpg', '/media/simulation_image.jpg']:
return f"{settings.MEDIA_URL}simulation_image.jpg"
# ローカルメディアのフルパス(/media/で始まるの場合は、S3パスを抽出
if image_path_str.startswith('/media/'):
# /media/を削除してS3パスを取得
s3_path = image_path_str[7:] # "/media/"を除去
if hasattr(settings, 'AWS_STORAGE_BUCKET_NAME') and settings.AWS_STORAGE_BUCKET_NAME:
# S3 URLを直接生成
return f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{s3_path}"
# S3のファイルパスcheckin/で始まる、画像拡張子を含む)かどうかを判定
if (any(keyword in image_path_str.lower() for keyword in ['jpg', 'jpeg', 'png', 'gif', 'webp']) and
('checkin/' in image_path_str or not image_path_str.startswith('/'))):
# S3 URLを生成
if hasattr(settings, 'AWS_STORAGE_BUCKET_NAME') and settings.AWS_STORAGE_BUCKET_NAME:
return f"https://{settings.AWS_S3_CUSTOM_DOMAIN}/{image_path_str}"
# その他の場合はローカルメディアURL
return f"{settings.MEDIA_URL}{image_path_str}"
def get_standard_image_url(event_code, image_type):
"""規定画像URLを取得S3優先、フォールバック対応"""
try:
# S3から規定画像を取得
from .services.s3_service import S3Service
s3_service = S3Service()
s3_url = s3_service.get_standard_image_url(event_code, image_type)
if s3_url:
return s3_url
# S3に規定画像がない場合はデフォルト画像を返す
default_images = {
'goal': 'https://rogaining.sumasen.net/images/gifuRoge/asset/goal.png',
'start': 'https://rogaining.sumasen.net/images/gifuRoge/asset/start.png',
'checkpoint': 'https://rogaining.sumasen.net/images/gifuRoge/asset/checkpoint.png',
'finish': 'https://rogaining.sumasen.net/images/gifuRoge/asset/finish.png',
'photo_none': 'https://rogaining.sumasen.net/images/gifuRoge/asset/photo_none.png'
}
return default_images.get(image_type, default_images['photo_none'])
except Exception as e:
logger.error(f"Error getting standard image: {e}")
# エラー時はデフォルト画像を返す
return 'https://rogaining.sumasen.net/images/gifuRoge/asset/photo_none.png'
@api_view(['GET'])
@ -2584,9 +2665,6 @@ def get_checkins(request, *args, **kwargs):
event_code=event_code
).order_by('path_order')
# すべてのフィールドを確実に取得できるようにデバッグログを追加
logger.debug(f"Found {checkins.count()} checkins for zekken_number {zekken_number} and event_code {event_code}")
data = []
for c in checkins:
location = Location.objects.filter(cp=c.cp_number,group=event_code).first()
@ -3111,11 +3189,21 @@ def export_excel(request, zekken_number, event_code):
s3.upload_file(pdf_path, f'{event_code}/scoreboard/certificate_{zekken_number}.pdf')
s3.upload_file(excel_path, f'{event_code}/scoreboard_excel/certificate_{zekken_number}.xlsx')
# PDFファイルを読み込んでレスポンスとして返す
with open(pdf_path, 'rb') as pdf_file:
pdf_content = pdf_file.read()
os.remove(temp_excel_path)
os.remove(excel_path)
os.remove(pdf_path)
return Response( status=status.HTTP_200_OK )
# PDFファイルをレスポンスとして返す
response = HttpResponse(
pdf_content,
content_type='application/pdf'
)
response['Content-Disposition'] = f'inline; filename="certificate_{zekken_number}_{event_code}.pdf"'
return response
except subprocess.CalledProcessError as e:
@ -3781,3 +3869,26 @@ def all_ranking_top3(request, event_code):
{"error": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# ルートページ用のビュー
def index_view(request):
"""ルートページをスーパーバイザー画面にリダイレクト"""
from django.shortcuts import render
from django.http import HttpResponse
# supervisor/html/index.htmlを読み込んで返す
try:
import os
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
supervisor_path = os.path.join(base_dir, 'supervisor', 'html', 'index.html')
with open(supervisor_path, 'r', encoding='utf-8') as file:
content = file.read()
return HttpResponse(content, content_type='text/html')
except Exception as e:
logger.error(f"Error loading supervisor page: {str(e)}")
return HttpResponse(
"<h1>System Error</h1><p>Failed to load supervisor interface</p>",
status=500
)

View File

@ -0,0 +1,352 @@
"""
通過審査管理画面用API
参加者全体の得点とクラス別ランキング表示機能
"""
import logging
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.models import Sum, Q, Count
from django.db import models
from rog.models import NewEvent2, Entry, GpsCheckin, NewCategory
logger = logging.getLogger(__name__)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_event_participants_ranking(request):
"""
イベント参加者全体のクラス別得点ランキング取得
GET /api/event-participants-ranking/?event_code=FC岐阜
"""
try:
event_code = request.GET.get('event_code')
if not event_code:
return Response({
'error': 'event_code parameter is required'
}, status=status.HTTP_400_BAD_REQUEST)
# イベントの検索(完全一致を優先)
event = None
if event_code:
# まず完全一致でイベント名検索
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
# 次にイベントコードで検索
event = NewEvent2.objects.filter(event_code=event_code).first()
if not event:
return Response({
'error': 'Event not found'
}, status=status.HTTP_404_NOT_FOUND)
# イベント参加者の取得と得点計算
entries = Entry.objects.filter(
event=event,
is_active=True
).select_related('category', 'team').prefetch_related('team__members')
ranking_data = []
for entry in entries:
# このエントリーのチェックイン記録を取得
checkins = GpsCheckin.objects.filter(
zekken_number=str(entry.zekken_number),
event_code=event_code
)
# 得点計算
total_points = 0
cp_points = 0
buy_points = 0
confirmed_points = 0 # 確定済み得点
unconfirmed_points = 0 # 未確定得点
late_penalty = 0
for checkin in checkins:
if checkin.points:
if checkin.validate_location: # 確定済み
confirmed_points += checkin.points
if checkin.buy_flag:
buy_points += checkin.points
else:
cp_points += checkin.points
else: # 未確定
unconfirmed_points += checkin.points
if checkin.late_point:
late_penalty += checkin.late_point
total_points = confirmed_points - late_penalty
# チェックイン確定状況
total_checkins = checkins.count()
confirmed_checkins = checkins.filter(validate_location=True).count()
confirmation_rate = (confirmed_checkins / total_checkins * 100) if total_checkins > 0 else 0
# チームメンバー情報
team_members = []
if entry.team and entry.team.members.exists():
team_members = [
{
'name': f"{member.user.firstname} {member.user.lastname}" if member.user else member.name,
'age': member.age if hasattr(member, 'age') else None
}
for member in entry.team.members.all()
]
ranking_data.append({
'rank': 0, # 後で設定
'zekken_number': entry.zekken_number,
'zekken_label': entry.zekken_label or f"{entry.zekken_number}",
'team_name': entry.team.team_name if entry.team else "チーム名不明",
'category': {
'name': entry.category.category_name,
'class_name': entry.category.category_name # class_nameプロパティがない場合はcategory_nameを使用
},
'members': team_members,
'points': {
'total': total_points,
'cp_points': cp_points,
'buy_points': buy_points,
'confirmed_points': confirmed_points,
'unconfirmed_points': unconfirmed_points,
'late_penalty': late_penalty
},
'checkin_status': {
'total_checkins': total_checkins,
'confirmed_checkins': confirmed_checkins,
'unconfirmed_checkins': total_checkins - confirmed_checkins,
'confirmation_rate': round(confirmation_rate, 1)
},
'entry_status': {
'has_participated': entry.hasParticipated,
'has_goaled': entry.hasGoaled
}
})
# クラス別にソートしてランキング設定
ranking_data.sort(key=lambda x: (x['category']['class_name'], -x['points']['total']))
# クラス別ランキングの設定
current_class = None
current_rank = 0
for i, item in enumerate(ranking_data):
if item['category']['class_name'] != current_class:
current_class = item['category']['class_name']
current_rank = 1
else:
current_rank += 1
item['rank'] = current_rank
item['class_rank'] = current_rank
# クラス別にグループ化
classes_ranking = {}
for item in ranking_data:
class_name = item['category']['class_name']
if class_name not in classes_ranking:
classes_ranking[class_name] = []
classes_ranking[class_name].append(item)
return Response({
'status': 'success',
'event': {
'event_code': event_code,
'event_name': event.event_name,
'total_participants': len(ranking_data)
},
'classes_ranking': classes_ranking,
'all_participants': ranking_data,
'participants': ranking_data # JavaScript互換性のため
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in get_event_participants_ranking: {str(e)}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_participant_validation_details(request):
"""
参加者の通過情報詳細と確定状況の取得
GET /api/participant-validation-details/?event_code=FC岐阜&zekken_number=123
"""
try:
event_code = request.GET.get('event_code')
zekken_number = request.GET.get('zekken_number')
if not all([event_code, zekken_number]):
return Response({
'error': 'event_code and zekken_number parameters 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': 'Participant not found'
}, status=status.HTTP_404_NOT_FOUND)
# チェックイン記録の取得
checkins = GpsCheckin.objects.filter(
zekken_number=str(zekken_number),
event_code=event_code
).order_by('path_order')
checkin_details = []
for checkin in checkins:
checkin_details.append({
'id': checkin.id,
'path_order': checkin.path_order,
'cp_number': checkin.cp_number,
'checkin_time': checkin.create_at.isoformat() if checkin.create_at else None,
'image_url': checkin.image_address,
'gps_location': {
'latitude': checkin.lattitude,
'longitude': checkin.longitude
} if checkin.lattitude and checkin.longitude else None,
'validation': {
'is_confirmed': checkin.validate_location,
'buy_flag': checkin.buy_flag,
'points': checkin.points or 0
},
'metadata': {
'create_user': checkin.create_user,
'update_user': checkin.update_user,
'update_time': checkin.update_at.isoformat() if checkin.update_at else None
}
})
# 統計情報
stats = {
'total_checkins': len(checkin_details),
'confirmed_checkins': sum(1 for c in checkin_details if c['validation']['is_confirmed']),
'unconfirmed_checkins': sum(1 for c in checkin_details if not c['validation']['is_confirmed']),
'total_points': sum(c['validation']['points'] for c in checkin_details if c['validation']['is_confirmed']),
'potential_points': sum(c['validation']['points'] for c in checkin_details)
}
return Response({
'status': 'success',
'participant': {
'zekken_number': entry.zekken_number,
'team_name': entry.team_name,
'category': entry.category.name,
'class_name': entry.class_name
},
'statistics': stats,
'checkins': checkin_details
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in get_participant_validation_details: {str(e)}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def get_event_zekken_list(request):
"""
イベントのゼッケン番号リスト取得ALLオプション付き
POST /api/event-zekken-list/
{
"event_code": "FC岐阜"
}
"""
try:
import json
data = json.loads(request.body)
event_code = data.get('event_code')
if event_code is None:
return Response({
'error': 'event_code parameter is required'
}, status=status.HTTP_400_BAD_REQUEST)
# イベントの確認 - event_code=Noneの場合の処理を追加
try:
if event_code == '' or event_code is None:
# event_code=Noneまたは空文字列の場合
event = NewEvent2.objects.filter(event_code=None).first()
else:
# まずevent_nameで正確な検索を試す
try:
event = NewEvent2.objects.get(event_name=event_code)
except NewEvent2.DoesNotExist:
# event_nameで見つからない場合はevent_codeで検索
event = NewEvent2.objects.get(event_code=event_code)
if not event:
return Response({
'error': 'Event not found'
}, status=status.HTTP_404_NOT_FOUND)
except NewEvent2.DoesNotExist:
return Response({
'error': 'Event not found'
}, status=status.HTTP_404_NOT_FOUND)
# 参加エントリーの取得
entries = Entry.objects.filter(
event=event,
is_active=True
).order_by('zekken_number')
zekken_list = []
# ALLオプションを最初に追加
zekken_list.append({
'value': 'ALL',
'label': 'ALL全参加者',
'team_name': '全参加者表示',
'category': '全クラス'
})
# 各参加者のゼッケン番号を追加
for entry in entries:
team_name = entry.team.team_name if entry.team else 'チーム名未設定'
category_name = entry.category.category_name if entry.category else 'クラス未設定'
zekken_list.append({
'value': str(entry.zekken_number),
'label': f"{entry.zekken_number} - {team_name}",
'team_name': team_name,
'category': category_name
})
return Response({
'status': 'success',
'event_code': event_code,
'zekken_options': zekken_list
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in get_event_zekken_list: {str(e)}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

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

View File

@ -1,12 +1,15 @@
# 既存のインポート部分に追加
from datetime import datetime, timezone
from sqlalchemy import Transaction
# from sqlalchemy import Transaction # 削除 - SQLAlchemy 2.0では利用不可
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import Location, NewEvent2, Entry, GpsLog
import logging
import uuid
import os
from django.db.models import F, Q
from django.db import transaction
from django.conf import settings
import os
from urllib.parse import urljoin
@ -755,7 +758,7 @@ def goal_checkin(request):
goal_time = timezone.now()
# トランザクション開始
with Transaction.atomic():
with transaction.atomic():
# スコアの計算
score = calculate_team_score(entry)

View File

@ -2,7 +2,7 @@
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import NewEvent2, Entry,Location, GpsLog
from rog.models import NewEvent2, Entry, Location2025, GpsLog
import logging
from django.db.models import F, Q
from django.conf import settings
@ -332,7 +332,7 @@ def analyze_point(request):
try:
# イベントのチェックポイント定義を取得
event_cps = Location.objects.filter(event=event)
event_cps = Location2025.objects.filter(event=event)
# チームが通過したチェックポイントを取得
team_cps = GpsLog.objects.filter(entry=entry)

View File

@ -1,18 +1,49 @@
# 既存のインポート部分に追加
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import NewEvent2, Entry, GpsLog
from rog.models import NewEvent2, Entry, GpsCheckin, Team
import logging
from django.db.models import F, Q
from django.conf import settings
import os
from urllib.parse import urljoin
from urllib.parse import urljoin, quote
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
import boto3
from botocore.exceptions import ClientError
logger = logging.getLogger(__name__)
def generate_image_url(image_address, event_code, zekken_number):
"""
画像アドレスからS3 URLまたは適切なURLを生成
"""
if not image_address:
return None
# 既にHTTP URLの場合はそのまま返す
if image_address.startswith('http'):
return image_address
# simulation_image.jpgなどのテスト画像の場合はS3にないのでスキップ
if image_address in ['simulation_image.jpg', 'test_image']:
return f"/media/{image_address}"
# S3パスを構築してURLを生成
s3_key = f"{event_code}/{zekken_number}/{image_address}"
try:
# S3 URLを生成
s3_url = f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_S3_REGION_NAME}.amazonaws.com/{quote(s3_key)}"
return s3_url
except Exception as e:
# S3設定に問題がある場合はmediaパスを返す
return f"/media/{image_address}"
"""
解説
この実装では以下の処理を行っています:
@ -113,7 +144,7 @@ def get_photo_list_prod(request):
# パスワード検証
if not hasattr(entry, 'password') or entry.password != password:
logger.warning(f"Invalid password for team: {entry.team_name}")
logger.warning(f"Invalid password for team: {entry.team.team_name if entry.team else 'Unknown'}")
return Response({
"status": "ERROR",
"message": "パスワードが一致しません"
@ -128,154 +159,49 @@ def get_photo_list_prod(request):
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_team_photos(zekken_number, event_code):
def get_team_photos(request):
"""
チームの写真とレポートURLを取得する共通関数
チームの写真データを取得するAPI
"""
try:
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"Event not found: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# チームの存在確認
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# チームの基本情報を取得
team_info = {
"team_name": entry.team_name,
"zekken_number": entry.zekken_number,
"class_name": entry.class_name,
"event_name": event.event_name
}
# チェックポイント通過情報(写真を含む)を取得
checkpoints = GpsLog.objects.filter(
entry=entry
).order_by('checkin_time')
# 写真リストを作成
photos = []
for cp in checkpoints:
# 写真URLがある場合のみ追加
if hasattr(cp, 'image') and cp.image:
photo_data = {
"cp_number": cp.cp_number,
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
"image_url": request.build_absolute_uri(cp.image.url) if hasattr(request, 'build_absolute_uri') else cp.image.url
}
# サービスチェックの情報があれば追加
if hasattr(cp, 'is_service_checked'):
photo_data["is_service_checked"] = cp.is_service_checked
photos.append(photo_data)
# スタート写真があれば追加
if hasattr(entry, 'start_info') and hasattr(entry.start_info, 'start_image') and entry.start_info.start_image:
start_image = {
"cp_number": "START",
"checkin_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S") if entry.start_info.start_time else None,
"image_url": request.build_absolute_uri(entry.start_info.start_image.url) if hasattr(request, 'build_absolute_uri') else entry.start_info.start_image.url
}
photos.insert(0, start_image) # リストの先頭に追加
# ゴール写真があれば追加
if hasattr(entry, 'goal_info') and hasattr(entry.goal_info, 'goal_image') and entry.goal_info.goal_image:
goal_image = {
"cp_number": "GOAL",
"checkin_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S") if entry.goal_info.goal_time else None,
"image_url": request.build_absolute_uri(entry.goal_info.goal_image.url) if hasattr(request, 'build_absolute_uri') else entry.goal_info.goal_image.url
}
photos.append(goal_image) # リストの末尾に追加
# チームレポートURLを生成
# レポートURLは「/レポートディレクトリ/イベント名/ゼッケン番号.pdf」のパターンを想定
report_directory = getattr(settings, 'REPORT_DIRECTORY', 'reports')
report_base_url = getattr(settings, 'REPORT_BASE_URL', '/media/reports/')
# レポートファイルの物理パスをチェック
report_path = os.path.join(
settings.MEDIA_ROOT,
report_directory,
event_code,
f"{zekken_number}.pdf"
)
# レポートURLを生成
has_report = os.path.exists(report_path)
report_url = None
if has_report:
report_url = urljoin(
report_base_url,
f"{event_code}/{zekken_number}.pdf"
)
# 絶対URLに変換
if hasattr(request, 'build_absolute_uri'):
report_url = request.build_absolute_uri(report_url)
# スコアボードURLを生成
scoreboard_path = os.path.join(
settings.MEDIA_ROOT,
'scoreboards',
event_code,
f"scoreboard_{zekken_number}.pdf"
)
has_scoreboard = os.path.exists(scoreboard_path)
scoreboard_url = None
if has_scoreboard:
scoreboard_url = urljoin(
'/media/scoreboards/',
f"{event_code}/scoreboard_{zekken_number}.pdf"
)
# 絶対URLに変換
if hasattr(request, 'build_absolute_uri'):
scoreboard_url = request.build_absolute_uri(scoreboard_url)
# チームのスコア情報
score = None
if hasattr(entry, 'goal_info') and hasattr(entry.goal_info, 'score'):
score = entry.goal_info.score
# レスポンスデータ
response_data = {
"status": "OK",
"team": team_info,
"photos": photos,
"photo_count": len(photos),
"has_report": has_report,
"report_url": report_url,
"has_scoreboard": has_scoreboard,
"scoreboard_url": scoreboard_url,
"score": score
}
return Response(response_data)
zekken = request.GET.get('zekken')
event = request.GET.get('event')
if not zekken or not event:
return JsonResponse({
'error': 'zekken and event parameters are required'
}, status=400)
try:
# GpsCheckinからチームの画像データを取得
gps_checkins = GpsCheckin.objects.filter(
zekken_number=zekken,
event_code=event
).exclude(
image_address__isnull=True
).exclude(
image_address=''
).order_by('create_at')
photos = []
for gps in gps_checkins:
# image_addressを処理してS3 URLまたは既存URLを生成
image_url = generate_image_url(gps.image_address, event, zekken)
photos.append({
'id': gps.id,
'image_url': image_url,
'created_at': gps.create_at.strftime('%Y-%m-%d %H:%M:%S') if gps.create_at else None,
'point_name': gps.checkpoint_id,
'latitude': float(gps.lattitude) if gps.lattitude else None,
'longitude': float(gps.longitude) if gps.longitude else None,
})
return JsonResponse({
'photos': photos,
'count': len(photos)
})
except Exception as e:
logger.error(f"Error in get_team_photos: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return JsonResponse({
'error': f'Error retrieving photos: {str(e)}'
}, status=500)

View File

@ -0,0 +1,239 @@
# 既存のインポート部分に追加
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import NewEvent2, Entry, GpsCheckin, Team
import logging
from django.db.models import F, Q
from django.conf import settings
import os
from urllib.parse import urljoin
logger = logging.getLogger(__name__)
"""
解説
この実装では以下の処理を行っています:
1.2つのエンドポイントを提供しています:
- /get_photo_list - 認証なしで写真とレポートURLを取得
- /get_photo_list_prod - パスワード認証付きで同じ情報を取得
2.共通のロジックは get_team_photos 関数に集約し、以下の情報を取得します:
- チームの基本情報(名前、ゼッケン番号、クラス名)
- チェックポイント通過時の写真(時間順、サービスチェック情報含む)
- スタート写真とゴール写真(あれば)
- チームレポートのURL存在する場合
- スコアボードのURL存在する場合
- チームのスコア(ゴール済みの場合)
3.レポートとスコアボードのファイルパスを実際に確認し、存在する場合のみURLを提供します
4.写真の表示順はスタート→チェックポイント(時間順)→ゴールとなっており、チェックポイントについてはそれぞれ番号、撮影時間、サービスチェック状態などの情報も含めています
この実装により、チームは自分たちの競技中の写真やレポートを簡単に確認できます。
本番環境_prod版ではパスワード認証によりセキュリティを確保しています。
"""
@api_view(['GET'])
def get_photo_list(request):
"""
チームの写真とレポートURLを取得認証なし版
パラメータ:
- zekken: ゼッケン番号
- event: イベントコード
"""
logger.info("get_photo_list called")
# リクエストからパラメータを取得
zekken_number = request.query_params.get('zekken')
event_code = request.query_params.get('event')
logger.debug(f"Parameters: zekken={zekken_number}, event={event_code}")
# パラメータ検証
if not all([zekken_number, event_code]):
logger.warning("Missing required parameters")
return Response({
"status": "ERROR",
"message": "ゼッケン番号とイベントコードが必要です"
}, status=status.HTTP_400_BAD_REQUEST)
return get_team_photos(zekken_number, event_code)
@api_view(['GET'])
def get_photo_list_prod(request):
"""
チームの写真とレポートURLを取得認証あり版
パラメータ:
- zekken: ゼッケン番号
- pw: パスワード
- event: イベントコード
"""
logger.info("get_photo_list_prod called")
# リクエストからパラメータを取得
zekken_number = request.query_params.get('zekken')
password = request.query_params.get('pw')
event_code = request.query_params.get('event')
logger.debug(f"Parameters: zekken={zekken_number}, event={event_code}, has_password={bool(password)}")
# パラメータ検証
if not all([zekken_number, password, event_code]):
logger.warning("Missing required parameters")
return Response({
"status": "ERROR",
"message": "ゼッケン番号、パスワード、イベントコードが必要です"
}, status=status.HTTP_400_BAD_REQUEST)
try:
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"Event not found: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# チームの存在確認とパスワード検証
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# パスワード検証
if not hasattr(entry, 'password') or entry.password != password:
logger.warning(f"Invalid password for team: {entry.team.team_name if entry.team else 'Unknown'}")
return Response({
"status": "ERROR",
"message": "パスワードが一致しません"
}, status=status.HTTP_401_UNAUTHORIZED)
return get_team_photos(zekken_number, event_code)
except Exception as e:
logger.error(f"Error in get_photo_list_prod: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_team_photos(zekken_number, event_code):
"""
チームの写真とレポートURLを取得する共通関数
"""
try:
logger.info(f"Getting photos for zekken: {zekken_number}, event: {event_code}")
# イベントの存在確認event_codeで検索
event = NewEvent2.objects.filter(event_code=event_code).first()
if not event:
logger.warning(f"Event not found with event_code: {event_code}")
# event_nameでも試してみる
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"Event not found with event_name: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"Found event: {event.event_name} (ID: {event.id})")
# まずEntryテーブルを確認
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).first()
team_name = "Unknown Team"
if entry and entry.team:
team_name = entry.team.team_name
logger.info(f"Found team in Entry: {team_name}")
else:
logger.info(f"No Entry found for zekken {zekken_number}, checking GpsCheckin for legacy data")
# GpsCheckinテーブルからチーム情報を取得レガシーデータ対応
gps_checkin_sample = GpsCheckin.objects.filter(
event_code=event_code,
zekken_number=str(zekken_number)
).first()
if gps_checkin_sample and gps_checkin_sample.team:
team_name = gps_checkin_sample.team.team_name
logger.info(f"Found team in GpsCheckin: {team_name}")
else:
team_name = f"Team {zekken_number}"
logger.info(f"No team found, using default: {team_name}")
# GpsCheckinテーブルから写真データを取得
gps_checkins = GpsCheckin.objects.filter(
event_code=event_code,
zekken_number=str(zekken_number),
image_address__isnull=False
).exclude(
image_address=''
).order_by('path_order', 'create_at')
logger.info(f"Found {gps_checkins.count()} GPS checkins with images")
# 写真リストを作成
photos = []
for gps in gps_checkins:
if gps.image_address:
# 画像URLを構築
if gps.image_address.startswith('http'):
# 絶対URLの場合はそのまま使用
image_url = gps.image_address
else:
# 相対パスの場合はベースURLと結合
# settings.MEDIA_URLやstatic fileの設定に基づいて調整
image_url = f"/media/{gps.image_address}" if not gps.image_address.startswith('/') else gps.image_address
photo_data = {
"cp_number": gps.cp_number if gps.cp_number is not None else 0,
"photo_url": image_url,
"checkin_time": gps.create_at.strftime("%Y-%m-%d %H:%M:%S") if gps.create_at else None,
"path_order": gps.path_order,
"buy_flag": gps.buy_flag,
"validate_location": gps.validate_location,
"points": gps.points
}
photos.append(photo_data)
logger.debug(f"Added photo: CP {gps.cp_number}, URL: {image_url}")
# チームの基本情報
team_info = {
"team_name": team_name,
"zekken_number": zekken_number,
"event_name": event.event_name,
"photo_count": len(photos)
}
# レスポンス構築
response_data = {
"status": "SUCCESS",
"message": f"写真を{len(photos)}枚取得しました",
"team_info": team_info,
"photo_list": photos
}
logger.info(f"Successfully retrieved {len(photos)} photos for team {team_name}")
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error in get_team_photos: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -2,7 +2,7 @@
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import NewEvent2, Entry, Location
from rog.models import NewEvent2, Entry, Location2025
from rog.models import GpsLog
import logging
from django.db.models import F, Q
@ -172,7 +172,7 @@ def get_checkpoint_list(request):
}, status=status.HTTP_404_NOT_FOUND)
# イベントのチェックポイント情報を取得
checkpoints = Location.objects.filter(event=event).order_by('cp_number')
checkpoints = Location2025.objects.filter(event=event).order_by('cp_number')
checkpoint_list = []
for cp in checkpoints:
@ -398,12 +398,12 @@ def checkin_from_rogapp(request):
# イベントのチェックポイント定義を確認(存在する場合)
event_cp = None
try:
event_cp = Location.objects.filter(
event_cp = Location2025.objects.filter(
event=event,
cp_number=cp_number
).first()
except:
logger.info(f"Location model not available or CP {cp_number} not defined for event")
logger.info(f"Location2025 model not available or CP {cp_number} not defined for event")
# トランザクション開始
with transaction.atomic():
@ -595,8 +595,8 @@ def calculate_team_score(entry):
# チェックポイントの得点を取得
cp_point = 0
try:
# Location
event_cp = Location.objects.filter(
# Location2025
event_cp = Location2025.objects.filter(
event=entry.event,
cp_number=cp.cp_number
).first()

235
rog/views_apis/s3_views.py Normal file
View File

@ -0,0 +1,235 @@
"""
API views for S3 image management
"""
import json
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from ..services.s3_service import S3Service
import logging
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@csrf_exempt
def upload_checkin_image(request):
"""
Upload checkin image to S3
POST /api/upload-checkin-image/
{
"event_code": "FC岐阜",
"team_code": "3432",
"cp_number": 10,
"image": <file>
}
"""
try:
# Validate required fields
event_code = request.data.get('event_code')
team_code = request.data.get('team_code')
image_file = request.FILES.get('image')
cp_number = request.data.get('cp_number')
if not all([event_code, team_code, image_file]):
return Response({
'error': 'event_code, team_code, and image are required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Upload image
s3_url = s3_service.upload_checkin_image(
image_file=image_file,
event_code=event_code,
team_code=team_code,
cp_number=cp_number
)
return Response({
'success': True,
'image_url': s3_url,
'message': 'Image uploaded successfully'
}, status=status.HTTP_201_CREATED)
except Exception as e:
logger.error(f"Error uploading checkin image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
@csrf_exempt
def upload_standard_image(request):
"""
Upload standard image (goal, start, etc.) to S3
POST /api/upload-standard-image/
{
"event_code": "FC岐阜",
"image_type": "goal", # goal, start, checkpoint, etc.
"image": <file>
}
"""
try:
# Validate required fields
event_code = request.data.get('event_code')
image_type = request.data.get('image_type')
image_file = request.FILES.get('image')
if not all([event_code, image_type, image_file]):
return Response({
'error': 'event_code, image_type, and image are required'
}, status=status.HTTP_400_BAD_REQUEST)
# Validate image_type
valid_types = ['goal', 'start', 'checkpoint', 'finish']
if image_type not in valid_types:
return Response({
'error': f'image_type must be one of: {valid_types}'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Upload standard image
s3_url = s3_service.upload_standard_image(
image_file=image_file,
event_code=event_code,
image_type=image_type
)
return Response({
'success': True,
'image_url': s3_url,
'message': 'Standard image uploaded successfully'
}, status=status.HTTP_201_CREATED)
except Exception as e:
logger.error(f"Error uploading standard image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
def get_standard_image(request):
"""
Get standard image URL
GET /api/get-standard-image/?event_code=FC岐阜&image_type=goal
"""
try:
event_code = request.GET.get('event_code')
image_type = request.GET.get('image_type')
if not all([event_code, image_type]):
return Response({
'error': 'event_code and image_type are required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Get standard image URL
image_url = s3_service.get_standard_image_url(event_code, image_type)
if image_url:
return Response({
'success': True,
'image_url': image_url
}, status=status.HTTP_200_OK)
else:
return Response({
'success': False,
'message': 'Standard image not found',
'image_url': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"Error getting standard image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
def list_event_images(request):
"""
List all images for an event
GET /api/list-event-images/?event_code=FC岐阜&limit=50
"""
try:
event_code = request.GET.get('event_code')
limit = int(request.GET.get('limit', 100))
if not event_code:
return Response({
'error': 'event_code is required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# List event images
image_urls = s3_service.list_event_images(event_code, limit)
return Response({
'success': True,
'event_code': event_code,
'image_count': len(image_urls),
'images': image_urls
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error listing event images: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
@csrf_exempt
def delete_image(request):
"""
Delete image from S3
DELETE /api/delete-image/
{
"image_url": "https://sumasenrogaining.s3.us-west-2.amazonaws.com/FC岐阜/3432/image.jpg"
}
"""
try:
image_url = request.data.get('image_url')
if not image_url:
return Response({
'error': 'image_url is required'
}, status=status.HTTP_400_BAD_REQUEST)
# Initialize S3 service
s3_service = S3Service()
# Delete image
success = s3_service.delete_image(image_url)
if success:
return Response({
'success': True,
'message': 'Image deleted successfully'
}, status=status.HTTP_200_OK)
else:
return Response({
'success': False,
'message': 'Failed to delete image'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error deleting image: {e}")
return Response({
'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)