From 596b7313dde50eba01155c95b0722ea3290614af Mon Sep 17 00:00:00 2001 From: Akira Date: Sat, 30 Aug 2025 03:48:07 +0900 Subject: [PATCH] add location migrate --- migrate_location_to_location2025_complete.py | 120 ++++++ migrate_location_to_location2025_enhanced.py | 176 ++++++++ migrate_location_to_location2025_final.py | 150 +++++++ ...ocation_to_location2025_with_validation.py | 397 ++++++++++++++++++ migrate_sub_fields_to_location2025.py | 64 +++ rog/admin.py | 8 +- rog/migrations/0008_add_status_field.py | 22 + rog/migrations/0009_add_fields_to_models.py | 34 ++ ...0010_add_missing_fields_to_location2025.py | 43 ++ rog/migrations/__init__.py | 0 rog/models.py | 10 + 11 files changed, 1021 insertions(+), 3 deletions(-) create mode 100644 migrate_location_to_location2025_complete.py create mode 100644 migrate_location_to_location2025_enhanced.py create mode 100644 migrate_location_to_location2025_final.py create mode 100644 migrate_location_to_location2025_with_validation.py create mode 100644 migrate_sub_fields_to_location2025.py create mode 100644 rog/migrations/0008_add_status_field.py create mode 100644 rog/migrations/0009_add_fields_to_models.py create mode 100644 rog/migrations/0010_add_missing_fields_to_location2025.py create mode 100644 rog/migrations/__init__.py diff --git a/migrate_location_to_location2025_complete.py b/migrate_location_to_location2025_complete.py new file mode 100644 index 0000000..7d208c3 --- /dev/null +++ b/migrate_location_to_location2025_complete.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +""" +LocationからLocation2025への完全データ移行スクリプト + +条件: +- NewEvent2ごとにlocation.groupにそのevent_codeが含まれているものを抽出 +- location.cpをlocation2025.cp_numberに変換 +- location2025.event_idにはnewevent2.idを代入 + +実行前にlocation2025のデータを削除してから実行 +""" + +from rog.models import Location, Location2025, NewEvent2 + +def main(): + print("=== Location から Location2025 への完全データ移行 ===") + + # 1. Location2025の既存データを削除 + print("\n1. Location2025の既存データを削除中...") + deleted_count = Location2025.objects.count() + Location2025.objects.all().delete() + print(f" 削除済み: {deleted_count}件") + + # 2. NewEvent2のevent_codeマップを作成 + print("\n2. NewEvent2のevent_codeマップを作成中...") + + events = NewEvent2.objects.filter(event_code__isnull=False).exclude(event_code='') + event_code_map = {} + for event in events: + event_code_map[event.event_code] = event + print(f" Event_code: '{event.event_code}' -> ID: {event.id} ({event.event_name})") + + print(f" 有効なevent_code数: {len(event_code_map)}件") + + # 3. 全Locationを取得 + print("\n3. 移行対象のLocationレコードを取得中...") + locations = Location.objects.all() + print(f" 総Location数: {locations.count()}件") + + # 4. 条件に合致するLocationを移行 + print("\n4. データ移行中...") + + migrated_count = 0 + skipped_count = 0 + error_count = 0 + + for location in locations: + try: + # groupが空の場合はスキップ + if not location.group: + skipped_count += 1 + continue + + # location.groupに含まれるevent_codeを検索 + matched_event = None + matched_event_code = None + + for event_code, event in event_code_map.items(): + if event_code in location.group: + matched_event = event + matched_event_code = event_code + break + + # マッチするevent_codeがない場合はスキップ + if not matched_event: + skipped_count += 1 + continue + + # Location2025レコードを作成 + location2025 = Location2025( + cp_number=location.cp, # cpをcp_numberに代入 + name=location.location_name, # location_nameを使用 + description=location.address or '', # addressをdescriptionとして使用 + latitude=location.latitude, + longitude=location.longitude, + point=location.checkin_point, # checkin_pointをpointとして使用 + geom=location.geom, + sub_loc_id=location.sub_loc_id, + subcategory=location.subcategory, + event_id=matched_event.id, # NewEvent2のIDを設定 + created_at=location.created_at, + updated_at=location.last_updated_at, + ) + + location2025.save() + + print(f" ✅ 移行完了: {location.cp} -> {location2025.cp_number} ({location.location_name}) [Event: {matched_event_code}]") + migrated_count += 1 + + except Exception as e: + print(f" ❌ エラー: {location.cp} - {str(e)}") + error_count += 1 + + # 5. 結果サマリー + print(f"\n=== 移行結果サマリー ===") + print(f"移行完了: {migrated_count}件") + print(f"スキップ: {skipped_count}件") + print(f"エラー: {error_count}件") + print(f"総処理: {migrated_count + skipped_count + error_count}件") + + # 6. Location2025の最終件数確認 + final_count = Location2025.objects.count() + print(f"\nLocation2025最終件数: {final_count}件") + + # 7. event_id別の統計 + print(f"\n=== event_id別統計 ===") + for event_code, event in event_code_map.items(): + count = Location2025.objects.filter(event_id=event.id).count() + print(f" Event '{event_code}' (ID: {event.id}): {count}件") + + if migrated_count > 0: + print("\n✅ データ移行が正常に完了しました") + else: + print("\n⚠️ 移行されたデータがありません") + +if __name__ == "__main__": + main() + +if __name__ == "__main__": + main() diff --git a/migrate_location_to_location2025_enhanced.py b/migrate_location_to_location2025_enhanced.py new file mode 100644 index 0000000..da93a3b --- /dev/null +++ b/migrate_location_to_location2025_enhanced.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +""" +LocationからLocation2025への完全データ移行スクリプト(フィールド追加版) + +更新内容: +- photos, videos, remark, tags, evaluation_value, hidden_location フィールドを追加 +- cp_pointとphoto_pointは同じもので、checkin_pointとして移行 +- location.cpを直接location2025.cp_numberに書き込み +""" + +import os +import django + +# Django設定 +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from rog.models import Location, Location2025, NewEvent2 +from django.contrib.auth import get_user_model +from django.contrib.gis.geos import Point +from collections import defaultdict + +def main(): + User = get_user_model() + default_user = User.objects.first() + + print('=== Location から Location2025 への完全データ移行(フィールド追加版) ===') + + # 1. Location2025の既存データを削除 + print('\n1. Location2025の既存データを削除中...') + deleted_count = Location2025.objects.count() + Location2025.objects.all().delete() + print(f' 削除済み: {deleted_count}件') + + # 2. NewEvent2のevent_codeマップを作成 + print('\n2. NewEvent2のevent_codeマップを作成中...') + events = NewEvent2.objects.filter(event_code__isnull=False).exclude(event_code='') + event_code_map = {} + for event in events: + event_code_map[event.event_code] = event + print(f' 有効なevent_code数: {len(event_code_map)}件') + + # 3. 全Locationを取得し、cp_number+event_idのユニークな組み合わせのみを処理 + print('\n3. ユニークなcp_number+event_idの組み合わせで移行中...') + + locations = Location.objects.all() + processed_combinations = set() + migrated_count = 0 + skipped_count = 0 + error_count = 0 + event_stats = defaultdict(int) + + for location in locations: + try: + # groupが空の場合はスキップ + if not location.group: + skipped_count += 1 + continue + + # location.groupに含まれるevent_codeを検索 + matched_event = None + matched_event_code = None + + for event_code, event in event_code_map.items(): + if event_code in location.group: + matched_event = event + matched_event_code = event_code + break + + # マッチするevent_codeがない場合はスキップ + if not matched_event: + skipped_count += 1 + continue + + # cp_number + event_idの組み合わせを確認 + combination_key = (location.cp, matched_event.id) + if combination_key in processed_combinations: + skipped_count += 1 + continue + + # この組み合わせを処理済みとしてマーク + processed_combinations.add(combination_key) + + # MultiPointからPointに変換 + point_location = None + if location.geom and len(location.geom) > 0: + first_point = location.geom[0] + point_location = Point(first_point.x, first_point.y) + elif location.longitude and location.latitude: + point_location = Point(location.longitude, location.latitude) + + # Location2025レコードを作成(update_or_create使用) + location2025, created = Location2025.objects.update_or_create( + cp_number=location.cp, # location.cpを直接使用 + event=matched_event, + defaults={ + 'cp_name': location.location_name or '', + 'sub_loc_id': location.sub_loc_id or '', + 'subcategory': location.subcategory or '', + 'latitude': location.latitude or 0.0, + 'longitude': location.longitude or 0.0, + 'location': point_location, + # cp_pointとphoto_pointは同じもので、checkin_pointとして移行 + 'cp_point': int(location.checkin_point) if location.checkin_point else 0, + 'photo_point': int(location.checkin_point) if location.checkin_point else 0, + 'buy_point': int(location.buy_point) if location.buy_point else 0, + 'checkin_radius': location.checkin_radius or 100.0, + 'auto_checkin': location.auto_checkin or False, + 'shop_closed': location.shop_closed or False, + 'shop_shutdown': location.shop_shutdown or False, + 'opening_hours': '', + 'address': location.address or '', + 'phone': location.phone or '', + 'website': '', + 'description': location.remark or '', + # 追加フィールド + 'photos': location.photos or '', + 'videos': location.videos or '', + 'remark': location.remark or '', + 'tags': location.tags or '', + 'evaluation_value': location.evaluation_value or '', + 'hidden_location': location.hidden_location or False, + # 管理情報 + 'is_active': True, + 'sort_order': 0, + 'csv_source_file': 'migration_from_location', + 'created_by': default_user, + 'updated_by': default_user, + } + ) + + if created: + migrated_count += 1 + event_stats[matched_event_code] += 1 + + if migrated_count % 100 == 0: + print(f' 進捗: {migrated_count}件完了') + + except Exception as e: + print(f' ❌ エラー: CP {location.cp} - {str(e)}') + error_count += 1 + + # 4. 結果サマリー + print(f'\n=== 移行結果サマリー ===') + print(f'移行完了: {migrated_count}件') + print(f'スキップ: {skipped_count}件') + print(f'エラー: {error_count}件') + print(f'総処理: {migrated_count + skipped_count + error_count}件') + + # 5. Location2025の最終件数確認 + final_count = Location2025.objects.count() + print(f'\nLocation2025最終件数: {final_count}件') + + # 6. event_code別の統計 + print(f'\n=== event_code別統計 ===') + for event_code, count in event_stats.items(): + print(f' Event "{event_code}": {count}件') + + # 7. 移行されたフィールドの確認 + if migrated_count > 0: + print('\n=== 移行フィールド確認(サンプル) ===') + sample = Location2025.objects.first() + print(f' CP番号: {sample.cp_number}') + print(f' CP名: {sample.cp_name}') + print(f' CPポイント: {sample.cp_point}') + print(f' フォトポイント: {sample.photo_point}') + print(f' 写真: {sample.photos[:50]}...' if sample.photos else ' 写真: (空)') + print(f' 動画: {sample.videos[:50]}...' if sample.videos else ' 動画: (空)') + print(f' タグ: {sample.tags[:50]}...' if sample.tags else ' タグ: (空)') + print(f' 評価値: {sample.evaluation_value}') + print(f' 隠しロケーション: {sample.hidden_location}') + + print('\n✅ 全フィールド対応のデータ移行が正常に完了しました') + +if __name__ == "__main__": + main() diff --git a/migrate_location_to_location2025_final.py b/migrate_location_to_location2025_final.py new file mode 100644 index 0000000..04f9102 --- /dev/null +++ b/migrate_location_to_location2025_final.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +""" +LocationからLocation2025への完全データ移行スクリプト(最終版) +""" + +from rog.models import Location, Location2025, NewEvent2 +from django.contrib.auth import get_user_model +from django.contrib.gis.geos import Point + +def main(): + User = get_user_model() + default_user = User.objects.first() + + print('=== Location から Location2025 への完全データ移行(最終版) ===') + + # 1. Location2025の既存データを削除 + print('\n1. Location2025の既存データを削除中...') + deleted_count = Location2025.objects.count() + Location2025.objects.all().delete() + print(f' 削除済み: {deleted_count}件') + + # 2. NewEvent2のevent_codeマップを作成 + print('\n2. NewEvent2のevent_codeマップを作成中...') + events = NewEvent2.objects.filter(event_code__isnull=False).exclude(event_code='') + event_code_map = {} + for event in events: + event_code_map[event.event_code] = event + print(f' 有効なevent_code数: {len(event_code_map)}件') + + # 3. 全Locationを取得 + print('\n3. 移行対象のLocationレコードを取得中...') + locations = Location.objects.all() + print(f' 総Location数: {locations.count()}件') + + # 4. 条件に合致するLocationを移行 + print('\n4. データ移行中...') + + migrated_count = 0 + skipped_count = 0 + error_count = 0 + cp_number_counter = {} # event_id別のcp_numberカウンター + + for i, location in enumerate(locations): + try: + # 進捗表示(1000件ごと) + if i % 1000 == 0: + print(f' 処理中: {i}/{locations.count()}件') + + # groupが空の場合はスキップ + if not location.group: + skipped_count += 1 + continue + + # location.groupに含まれるevent_codeを検索 + matched_event = None + matched_event_code = None + + for event_code, event in event_code_map.items(): + if event_code in location.group: + matched_event = event + matched_event_code = event_code + break + + # マッチするevent_codeがない場合はスキップ + if not matched_event: + skipped_count += 1 + continue + + # cp_numberの処理(0の場合は自動採番) + cp_number = int(location.cp) if location.cp else 0 + if cp_number == 0: + # event_id別に自動採番 + if matched_event.id not in cp_number_counter: + cp_number_counter[matched_event.id] = 10000 # 10000から開始 + cp_number = cp_number_counter[matched_event.id] + cp_number_counter[matched_event.id] += 1 + + # MultiPointからPointに変換 + point_location = None + if location.geom and len(location.geom) > 0: + first_point = location.geom[0] + point_location = Point(first_point.x, first_point.y) + elif location.longitude and location.latitude: + point_location = Point(location.longitude, location.latitude) + + # Location2025レコードを作成 + location2025 = Location2025( + cp_number=cp_number, + event=matched_event, + cp_name=location.location_name, + sub_loc_id=location.sub_loc_id or '', + subcategory=location.subcategory or '', + latitude=location.latitude or 0.0, + longitude=location.longitude or 0.0, + location=point_location, + cp_point=int(location.checkin_point) if location.checkin_point else 0, + photo_point=0, + buy_point=int(location.buy_point) if location.buy_point else 0, + checkin_radius=location.checkin_radius or 100.0, + auto_checkin=location.auto_checkin or False, + shop_closed=location.shop_closed or False, + shop_shutdown=location.shop_shutdown or False, + opening_hours='', + address=location.address or '', + phone=location.phone or '', + website='', + description=location.remark or '', + is_active=True, + sort_order=0, + csv_source_file='migration_from_location', + created_by=default_user, + updated_by=default_user, + ) + + location2025.save() + migrated_count += 1 + + # 最初の10件は詳細ログ + if migrated_count <= 10: + print(f' ✅ 移行完了: {location.cp} -> {location2025.cp_number} ({location.location_name}) [Event: {matched_event_code}]') + + except Exception as e: + print(f' ❌ エラー: {location.cp} - {str(e)}') + error_count += 1 + + # 5. 結果サマリー + print(f'\n=== 移行結果サマリー ===') + print(f'移行完了: {migrated_count}件') + print(f'スキップ: {skipped_count}件') + print(f'エラー: {error_count}件') + print(f'総処理: {migrated_count + skipped_count + error_count}件') + + # 6. Location2025の最終件数確認 + final_count = Location2025.objects.count() + print(f'\nLocation2025最終件数: {final_count}件') + + # 7. event_id別の統計 + print(f'\n=== event_id別統計 ===') + for event_code, event in event_code_map.items(): + count = Location2025.objects.filter(event=event).count() + if count > 0: + print(f' Event "{event_code}" (ID: {event.id}): {count}件') + + if migrated_count > 0: + print('\n✅ データ移行が正常に完了しました') + else: + print('\n⚠️ 移行されたデータがありません') + +if __name__ == "__main__": + main() diff --git a/migrate_location_to_location2025_with_validation.py b/migrate_location_to_location2025_with_validation.py new file mode 100644 index 0000000..aaf5af8 --- /dev/null +++ b/migrate_location_to_location2025_with_validation.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python +""" +LocationからLocation2025への完全データ移行スクリプト(統計検証付き) + +機能: +- 全フィールド対応の完全データ移行 +- リアルタイム統計検証 +- データ品質チェック +- 移行前後の比較 +- 詳細レポート生成 +""" + +import os +import django +from collections import defaultdict, Counter + +# Django設定 +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from rog.models import Location, Location2025, NewEvent2 +from django.contrib.auth import get_user_model +from django.contrib.gis.geos import Point + +def analyze_source_data(): + """移行前のデータ分析""" + print('=== 移行前データ分析 ===') + + total_locations = Location.objects.count() + print(f'総Location件数: {total_locations}件') + + # グループ別統計 + with_group = Location.objects.exclude(group__isnull=True).exclude(group='').count() + without_group = total_locations - with_group + print(f'groupありLocation: {with_group}件') + print(f'groupなしLocation: {without_group}件') + + # 座標データ統計 + with_geom = Location.objects.exclude(geom__isnull=True).count() + with_lat_lng = Location.objects.exclude(longitude__isnull=True).exclude(latitude__isnull=True).count() + print(f'geom座標あり: {with_geom}件') + print(f'lat/lng座標あり: {with_lat_lng}件') + + # フィールド統計 + fields_stats = {} + text_fields = ['photos', 'videos', 'remark', 'tags', 'evaluation_value', 'sub_loc_id', 'subcategory'] + numeric_fields = ['checkin_point', 'buy_point'] + boolean_fields = ['hidden_location'] + + for field in text_fields: + if hasattr(Location, field): + count = Location.objects.exclude(**{f'{field}__isnull': True}).exclude(**{field: ''}).count() + fields_stats[field] = count + print(f'{field}データあり: {count}件') + + for field in numeric_fields: + if hasattr(Location, field): + count = Location.objects.exclude(**{f'{field}__isnull': True}).exclude(**{field: 0}).count() + fields_stats[field] = count + print(f'{field}データあり: {count}件') + + for field in boolean_fields: + if hasattr(Location, field): + count = Location.objects.filter(**{field: True}).count() + fields_stats[field] = count + print(f'{field}データあり: {count}件') + + return { + 'total': total_locations, + 'with_group': with_group, + 'without_group': without_group, + 'with_geom': with_geom, + 'with_lat_lng': with_lat_lng, + 'fields': fields_stats + } + +def validate_migration_data(source_stats): + """移行後データ検証""" + print('\n=== 移行後データ検証 ===') + + total_migrated = Location2025.objects.count() + print(f'移行完了件数: {total_migrated}件') + + # フィールド検証 + migrated_stats = {} + field_mapping = { + 'photos': 'photos', + 'videos': 'videos', + 'remark': 'remark', + 'tags': 'tags', + 'evaluation_value': 'evaluation_value', + 'hidden_location': 'hidden_location', + 'sub_loc_id': 'sub_loc_id', + 'subcategory': 'subcategory' + } + + for source_field, target_field in field_mapping.items(): + if source_field == 'hidden_location': + count = Location2025.objects.filter(**{target_field: True}).count() + else: + count = Location2025.objects.exclude(**{f'{target_field}__isnull': True}).exclude(**{target_field: ''}).count() + migrated_stats[source_field] = count + print(f'{target_field}データあり: {count}件') + + # 座標検証 + with_location = Location2025.objects.exclude(location__isnull=True).count() + with_lat_lng = Location2025.objects.exclude(longitude__isnull=True).exclude(latitude__isnull=True).count() + print(f'location座標あり: {with_location}件') + print(f'lat/lng座標あり: {with_lat_lng}件') + + # 必須フィールド検証 + with_event = Location2025.objects.exclude(event__isnull=True).count() + with_cp_name = Location2025.objects.exclude(cp_name__isnull=True).exclude(cp_name='').count() + print(f'eventリンクあり: {with_event}件') + print(f'cp_nameあり: {with_cp_name}件') + + return { + 'total': total_migrated, + 'fields': migrated_stats, + 'with_location': with_location, + 'with_lat_lng': with_lat_lng, + 'with_event': with_event, + 'with_cp_name': with_cp_name + } + +def generate_comparison_report(source_stats, migrated_stats): + """移行前後比較レポート""" + print('\n=== 移行前後比較レポート ===') + + print(f'総件数比較:') + print(f' 移行前: {source_stats["total"]:,}件') + print(f' 移行後: {migrated_stats["total"]:,}件') + print(f' 移行率: {(migrated_stats["total"] / source_stats["total"] * 100):.1f}%') + + print(f'\nフィールド別データ保持率:') + for field in source_stats['fields']: + if field in migrated_stats['fields']: + source_count = source_stats['fields'][field] + migrated_count = migrated_stats['fields'][field] + if source_count > 0: + retention_rate = (migrated_count / source_count * 100) + print(f' {field}: {migrated_count:,}/{source_count:,}件 ({retention_rate:.1f}%)') + else: + print(f' {field}: {migrated_count:,}/0件 (N/A)') + +def analyze_event_distribution(): + """イベント別分布分析""" + print('\n=== イベント別分布分析 ===') + + event_stats = {} + for location in Location2025.objects.select_related('event'): + event_name = location.event.event_name if location.event else 'No Event' + event_code = location.event.event_code if location.event else 'No Code' + key = f"{event_code} ({event_name})" + event_stats[key] = event_stats.get(key, 0) + 1 + + # 件数順でソート + sorted_events = sorted(event_stats.items(), key=lambda x: x[1], reverse=True) + + print(f'総イベント数: {len(sorted_events)}件') + print(f'上位イベント:') + for i, (event_key, count) in enumerate(sorted_events[:10], 1): + print(f' {i:2d}. {event_key}: {count:,}件') + + return event_stats + +def sample_data_verification(): + """サンプルデータ検証""" + print('\n=== サンプルデータ検証 ===') + + # 各種データパターンのサンプルを取得 + samples = [] + + # 写真データありのサンプル + photo_sample = Location2025.objects.filter(photos__isnull=False).exclude(photos='').first() + if photo_sample: + samples.append(('写真データあり', photo_sample)) + + # remarkデータありのサンプル + remark_sample = Location2025.objects.filter(remark__isnull=False).exclude(remark='').first() + if remark_sample: + samples.append(('詳細説明あり', remark_sample)) + + # 高ポイントのサンプル + high_point_sample = Location2025.objects.filter(cp_point__gt=50).first() + if high_point_sample: + samples.append(('高ポイント', high_point_sample)) + + # 通常サンプル + if not samples: + normal_sample = Location2025.objects.first() + if normal_sample: + samples.append(('通常データ', normal_sample)) + + for sample_type, sample in samples[:3]: + print(f'\n【{sample_type}サンプル】') + print(f' CP番号: {sample.cp_number}') + print(f' CP名: {sample.cp_name}') + print(f' CPポイント: {sample.cp_point}') + print(f' フォトポイント: {sample.photo_point}') + print(f' sub_loc_id: {sample.sub_loc_id}') + print(f' subcategory: {sample.subcategory}') + + # データ長を制限して表示 + def truncate_text(text, max_len=30): + if not text: + return '(空)' + return text[:max_len] + '...' if len(text) > max_len else text + + print(f' 写真: {truncate_text(sample.photos)}') + print(f' 動画: {truncate_text(sample.videos)}') + print(f' 詳細: {truncate_text(sample.remark)}') + print(f' タグ: {truncate_text(sample.tags)}') + print(f' 評価値: {truncate_text(sample.evaluation_value)}') + print(f' 隠し: {sample.hidden_location}') + print(f' イベント: {sample.event.event_name if sample.event else "None"}') + +def main(): + """メイン実行関数""" + User = get_user_model() + default_user = User.objects.first() + + print('='*60) + print('Location → Location2025 完全移行スクリプト(統計検証付き)') + print('='*60) + + # 1. 移行前データ分析 + source_stats = analyze_source_data() + + # 2. 既存Location2025データ削除 + print('\n=== 既存データクリア ===') + deleted_count = Location2025.objects.count() + Location2025.objects.all().delete() + print(f'削除済み: {deleted_count}件') + + # 3. NewEvent2のevent_codeマップ作成 + print('\n=== Event Code マッピング ===') + events = NewEvent2.objects.filter(event_code__isnull=False).exclude(event_code='') + event_code_map = {} + for event in events: + event_code_map[event.event_code] = event + print(f'有効なevent_code数: {len(event_code_map)}件') + + # 4. データ移行実行 + print('\n=== データ移行実行 ===') + locations = Location.objects.all() + processed_combinations = set() + migrated_count = 0 + skipped_count = 0 + error_count = 0 + event_migration_stats = defaultdict(int) + + for location in locations: + try: + # groupが空の場合はスキップ + if not location.group: + skipped_count += 1 + continue + + # location.groupに含まれるevent_codeを検索 + matched_event = None + matched_event_code = None + + for event_code, event in event_code_map.items(): + if event_code in location.group: + matched_event = event + matched_event_code = event_code + break + + # マッチするevent_codeがない場合はスキップ + if not matched_event: + skipped_count += 1 + continue + + # cp_number + event_idの組み合わせを確認 + combination_key = (location.cp, matched_event.id) + if combination_key in processed_combinations: + skipped_count += 1 + continue + + # この組み合わせを処理済みとしてマーク + processed_combinations.add(combination_key) + + # MultiPointからPointに変換 + point_location = None + if location.geom and len(location.geom) > 0: + first_point = location.geom[0] + point_location = Point(first_point.x, first_point.y) + elif location.longitude and location.latitude: + point_location = Point(location.longitude, location.latitude) + + # Location2025レコードを作成 + location2025, created = Location2025.objects.update_or_create( + cp_number=location.cp, + event=matched_event, + defaults={ + 'cp_name': location.location_name or '', + 'sub_loc_id': location.sub_loc_id or '', + 'subcategory': location.subcategory or '', + 'latitude': location.latitude or 0.0, + 'longitude': location.longitude or 0.0, + 'location': point_location, + 'cp_point': int(location.checkin_point) if location.checkin_point else 0, + 'photo_point': int(location.checkin_point) if location.checkin_point else 0, + 'buy_point': int(location.buy_point) if location.buy_point else 0, + 'checkin_radius': location.checkin_radius or 100.0, + 'auto_checkin': location.auto_checkin or False, + 'shop_closed': location.shop_closed or False, + 'shop_shutdown': location.shop_shutdown or False, + 'opening_hours': '', + 'address': location.address or '', + 'phone': location.phone or '', + 'website': '', + 'description': location.remark or '', + # 追加フィールド + 'photos': location.photos or '', + 'videos': location.videos or '', + 'remark': location.remark or '', + 'tags': location.tags or '', + 'evaluation_value': location.evaluation_value or '', + 'hidden_location': location.hidden_location or False, + # 管理情報 + 'is_active': True, + 'sort_order': 0, + 'csv_source_file': 'migration_from_location', + 'created_by': default_user, + 'updated_by': default_user, + } + ) + + if created: + migrated_count += 1 + event_migration_stats[matched_event_code] += 1 + + if migrated_count % 100 == 0: + print(f'進捗: {migrated_count:,}件完了') + + except Exception as e: + print(f'❌ エラー: CP {location.cp} - {str(e)}') + error_count += 1 + + # 5. 移行結果サマリー + print(f'\n=== 移行結果サマリー ===') + print(f'移行完了: {migrated_count:,}件') + print(f'スキップ: {skipped_count:,}件') + print(f'エラー: {error_count:,}件') + print(f'総処理: {migrated_count + skipped_count + error_count:,}件') + + # 6. 移行後データ検証 + migrated_stats = validate_migration_data(source_stats) + + # 7. 比較レポート生成 + generate_comparison_report(source_stats, migrated_stats) + + # 8. イベント別分布分析 + event_distribution = analyze_event_distribution() + + # 9. サンプルデータ検証 + sample_data_verification() + + # 10. 最終検証サマリー + print('\n' + '='*60) + print('🎯 移行完了検証サマリー') + print('='*60) + + success_rate = (migrated_count / source_stats['total'] * 100) if source_stats['total'] > 0 else 0 + print(f'✅ 総移行成功率: {success_rate:.1f}% ({migrated_count:,}/{source_stats["total"]:,}件)') + print(f'✅ エラー率: {(error_count / source_stats["total"] * 100):.1f}% ({error_count:,}件)') + print(f'✅ 最終Location2025件数: {Location2025.objects.count():,}件') + print(f'✅ 対応イベント数: {len(event_distribution)}件') + + # データ品質スコア算出 + quality_score = 0 + if migrated_stats['with_event'] == migrated_stats['total']: + quality_score += 25 # 全てにイベントがリンクされている + if migrated_stats['with_cp_name'] >= migrated_stats['total'] * 0.95: + quality_score += 25 # 95%以上にCP名がある + if migrated_stats['fields']['photos'] >= migrated_stats['total'] * 0.8: + quality_score += 25 # 80%以上に写真データがある + if migrated_stats['fields']['remark'] >= migrated_stats['total'] * 0.8: + quality_score += 25 # 80%以上に詳細説明がある + + print(f'✅ データ品質スコア: {quality_score}/100点') + + if quality_score >= 90: + print('🏆 優秀:本格運用準備完了') + elif quality_score >= 70: + print('🥉 良好:運用可能レベル') + elif quality_score >= 50: + print('⚠️ 要改善:一部データ補完推奨') + else: + print('❌ 要対応:データ品質に課題あり') + + print('\n✅ 全フィールド対応の完全データ移行が正常に完了しました') + +if __name__ == "__main__": + main() diff --git a/migrate_sub_fields_to_location2025.py b/migrate_sub_fields_to_location2025.py new file mode 100644 index 0000000..d1556bb --- /dev/null +++ b/migrate_sub_fields_to_location2025.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +LocationからLocation2025へsub_loc_idとsubcategoryを移行するスクリプト +""" + +import os +import sys +import django + +# Djangoの設定 +sys.path.append('/app') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from rog.models import Location, Location2025 + +def migrate_sub_fields(): + """LocationからLocation2025にsub_loc_idとsubcategoryを移行""" + + print("LocationからLocation2025への移行を開始します...") + + # Locationデータを取得 + locations = Location.objects.all() + print(f"移行対象のLocationレコード数: {locations.count()}") + + # Location2025データとマッチングして更新 + updated_count = 0 + not_found_count = 0 + + for location in locations: + # cp_numberとcp_nameでLocation2025を検索 + try: + # location_idをcp_numberとして検索 + location2025_records = Location2025.objects.filter( + cp_number=location.location_id, + cp_name__icontains=location.location_name[:50] # 名前の部分一致 + ) + + if location2025_records.exists(): + for location2025 in location2025_records: + # フィールドが空の場合のみ更新 + if not location2025.sub_loc_id and location.sub_loc_id: + location2025.sub_loc_id = location.sub_loc_id + + if not location2025.subcategory and location.subcategory: + location2025.subcategory = location.subcategory + + location2025.save() + updated_count += 1 + print(f"✓ 更新: CP{location.location_id} - {location.location_name[:30]}...") + else: + not_found_count += 1 + print(f"✗ 未発見: CP{location.location_id} - {location.location_name[:30]}") + + except Exception as e: + print(f"エラー (CP{location.location_id}): {str(e)}") + + print(f"\n移行完了:") + print(f" 更新レコード数: {updated_count}") + print(f" 未発見レコード数: {not_found_count}") + print(f" 元レコード数: {locations.count()}") + +if __name__ == "__main__": + migrate_sub_fields() diff --git a/rog/admin.py b/rog/admin.py index bd97452..4cb836c 100755 --- a/rog/admin.py +++ b/rog/admin.py @@ -1002,7 +1002,8 @@ admin.site.register(EventUser, admin.ModelAdmin) #admin.site.register(ShapeFileLocations, admin.ModelAdmin) #admin.site.register(CustomUser, UserAdminConfig) -admin.site.register(templocation, TempLocationAdmin) +# 古いtemplocationは無効化 - Location2025を使用 +#admin.site.register(templocation, TempLocationAdmin) admin.site.register(GoalImages, admin.ModelAdmin) admin.site.register(CheckinImages, admin.ModelAdmin) @@ -1130,9 +1131,10 @@ class Location2025Admin(LeafletGeoAdmin): return redirect('..') - # フォーム表示 + # フォーム表示 - Location2025システム用 from .models import NewEvent2 - events = NewEvent2.objects.filter(event_active=True).order_by('-created_at') + # statusフィールドベースでアクティブなイベントを取得 + events = NewEvent2.objects.filter(status='public').order_by('-start_datetime') return render(request, 'admin/location2025/upload_csv.html', { 'events': events, diff --git a/rog/migrations/0008_add_status_field.py b/rog/migrations/0008_add_status_field.py new file mode 100644 index 0000000..dba20eb --- /dev/null +++ b/rog/migrations/0008_add_status_field.py @@ -0,0 +1,22 @@ +# Generated manually to add status field to NewEvent2 +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rog', '0007_auto_20250829_1836'), + ] + + operations = [ + migrations.AddField( + model_name='newevent2', + name='status', + field=models.CharField( + choices=[('public', 'Public'), ('private', 'Private'), ('draft', 'Draft'), ('closed', 'Closed')], + default='draft', + help_text='イベントステータス', + max_length=20 + ), + ), + ] diff --git a/rog/migrations/0009_add_fields_to_models.py b/rog/migrations/0009_add_fields_to_models.py new file mode 100644 index 0000000..351ae81 --- /dev/null +++ b/rog/migrations/0009_add_fields_to_models.py @@ -0,0 +1,34 @@ +# Generated manually to add missing timestamp fields to gpscheckin +from django.db import migrations, models +from django.utils import timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('rog', '0008_add_status_field'), + ] + + operations = [ + migrations.AddField( + model_name='gpscheckin', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='gpscheckin', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='location2025', + name='sub_loc_id', + field=models.CharField(blank=True, max_length=2048, null=True, verbose_name='サブロケーションID'), + ), + migrations.AddField( + model_name='location2025', + name='subcategory', + field=models.CharField(blank=True, max_length=2048, null=True, verbose_name='サブカテゴリ'), + ), + ] diff --git a/rog/migrations/0010_add_missing_fields_to_location2025.py b/rog/migrations/0010_add_missing_fields_to_location2025.py new file mode 100644 index 0000000..7c3adf3 --- /dev/null +++ b/rog/migrations/0010_add_missing_fields_to_location2025.py @@ -0,0 +1,43 @@ +# Generated manually on 2025-08-30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rog', '0009_add_fields_to_models'), + ] + + operations = [ + migrations.AddField( + model_name='location2025', + name='photos', + field=models.CharField(max_length=2048, blank=True, null=True, verbose_name='写真'), + ), + migrations.AddField( + model_name='location2025', + name='videos', + field=models.CharField(max_length=2048, blank=True, null=True, verbose_name='動画'), + ), + migrations.AddField( + model_name='location2025', + name='remark', + field=models.TextField(blank=True, null=True, verbose_name='備考'), + ), + migrations.AddField( + model_name='location2025', + name='tags', + field=models.CharField(max_length=2048, blank=True, null=True, verbose_name='タグ'), + ), + migrations.AddField( + model_name='location2025', + name='evaluation_value', + field=models.CharField(max_length=255, blank=True, null=True, verbose_name='評価値'), + ), + migrations.AddField( + model_name='location2025', + name='hidden_location', + field=models.BooleanField(default=False, verbose_name='隠しロケーション'), + ), + ] diff --git a/rog/migrations/__init__.py b/rog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rog/models.py b/rog/models.py index e4679d8..0d63737 100755 --- a/rog/models.py +++ b/rog/models.py @@ -1091,6 +1091,8 @@ class Location2025(models.Model): 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) + sub_loc_id = models.CharField(_('サブロケーションID'), max_length=2048, blank=True, null=True) + subcategory = models.CharField(_('サブカテゴリ'), max_length=2048, blank=True, null=True) # 位置情報 latitude = models.FloatField(_('緯度'), null=True, blank=True) @@ -1117,6 +1119,14 @@ class Location2025(models.Model): website = models.URLField(_('ウェブサイト'), blank=True, null=True) description = models.TextField(_('説明'), blank=True, null=True) + # 追加フィールド(Locationテーブルから移行) + photos = models.CharField(_('写真'), max_length=2048, blank=True, null=True) + videos = models.CharField(_('動画'), max_length=2048, blank=True, null=True) + remark = models.TextField(_('備考'), blank=True, null=True) + tags = models.CharField(_('タグ'), max_length=2048, blank=True, null=True) + evaluation_value = models.CharField(_('評価値'), max_length=255, blank=True, null=True) + hidden_location = models.BooleanField(_('隠しロケーション'), default=False) + # 管理情報 is_active = models.BooleanField(_('有効'), default=True, db_index=True) sort_order = models.IntegerField(_('表示順'), default=0)