Compare commits
103 Commits
da2a1d64ef
...
extdb-3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e7d547601 | |||
| 45c9b64a78 | |||
| 316cff3f5f | |||
| d7d296c33b | |||
| e65da5fd8f | |||
| 290a5a8c2f | |||
| 67db395c3c | |||
| 023a45f574 | |||
| bcd0bee738 | |||
| a24a0decb9 | |||
| 4761ff9977 | |||
| 33088234f2 | |||
| 49d2aa588b | |||
| 4cd3745812 | |||
| 93768fa4ec | |||
| 6d001bf378 | |||
| 716a0be908 | |||
| 66aacbb69e | |||
| f50d1e1c79 | |||
| 775f77a440 | |||
| efa51b4fcc | |||
| fdc1d66f08 | |||
| 99e4561694 | |||
| 86ea3a4b0c | |||
| 33b122b7e8 | |||
| 00bc1cadc9 | |||
| d8e1b05d41 | |||
| 272269431e | |||
| 9d11685b65 | |||
| 4e1ef7c230 | |||
| 4a5f6273ed | |||
| e0543e2b4e | |||
| 32f860af41 | |||
| 3cb0c2daf7 | |||
| 7abdfbe903 | |||
| 1698776589 | |||
| f55f44013f | |||
| 0d6f9024f4 | |||
| e0635936fe | |||
| cd8f872f1f | |||
| 1c36ece232 | |||
| a0e024b77d | |||
| 4901b44f4a | |||
| 3c28d33ebf | |||
| bbd655955a | |||
| 8ffedc177f | |||
| 9f27357a3b | |||
| 3b28f49959 | |||
| a8dc2ba3b1 | |||
| 0acaa6ea1f | |||
| d6b40bd0f8 | |||
| c95c8713d4 | |||
| 70acda8167 | |||
| 45a29c7b18 | |||
| 05b9432a90 | |||
| a8c0f52860 | |||
| 77acb7c016 | |||
| 104d39a96b | |||
| 619aa4f396 | |||
| aa8b39aa99 | |||
| 03de478b80 | |||
| 58165e825b | |||
| c8c8d264c9 | |||
| bef4af1086 | |||
| 1fe96f6a51 | |||
| e9c6838171 | |||
| 71b073229e | |||
| 0ef0bde5b1 | |||
| cb399f14bf | |||
| 596b7313dd | |||
| cf0adb34f9 | |||
| 9af1e03523 | |||
| 48b09b08da | |||
| 9c0b8932b5 | |||
| 631c7293fc | |||
| 999ce636ac | |||
| d63f205fa3 | |||
| 50ebf8847c | |||
| b4d423aa35 | |||
| 2e3bf14b27 | |||
| 0d3d61a472 | |||
| 84481d9d55 | |||
| 42f6471f73 | |||
| 23a9902885 | |||
| 7593885bbc | |||
| 5fff127faf | |||
| 8a73840298 | |||
| 9472e66ec8 | |||
| b91b522fa3 | |||
| a180c1e258 | |||
| bba0422efb | |||
| 2a962181b2 | |||
| 413bf05042 | |||
| 8ffec240af | |||
| cc9edb9932 | |||
| fff9bce9e7 | |||
| 37ee26c3fd | |||
| 8e3f7024a2 | |||
| 6886ba15c8 | |||
| 961c577ec8 | |||
| 3ed2e6b259 | |||
| fed825a87e | |||
| 65e481de18 |
0
.env.local_akira
Normal file
0
.env.local_akira
Normal file
2
.env.sql
2
.env.sql
@ -2,7 +2,7 @@ POSTGRES_USER=admin
|
|||||||
POSTGRES_PASS=admin123456
|
POSTGRES_PASS=admin123456
|
||||||
POSTGRES_DBNAME=rogdb
|
POSTGRES_DBNAME=rogdb
|
||||||
DATABASE=postgres
|
DATABASE=postgres
|
||||||
PG_HOST=172.31.25.76
|
PG_HOST=postgres-db
|
||||||
PG_PORT=5432
|
PG_PORT=5432
|
||||||
GS_VERSION=2.20.0
|
GS_VERSION=2.20.0
|
||||||
GEOSERVER_PORT=8600
|
GEOSERVER_PORT=8600
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -157,10 +157,6 @@ dmypy.json
|
|||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
# migration files
|
|
||||||
rog/migrations/
|
|
||||||
|
|
||||||
|
|
||||||
# PyCharm
|
# PyCharm
|
||||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
|||||||
264
API_IMPLEMENTATION_REPORT.md
Normal file
264
API_IMPLEMENTATION_REPORT.md
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# サーバーAPI変更要求書 実装報告書
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
2025年8月27日のサーバーAPI変更要求書に基づき、最高優先度および高優先度項目の実装を完了しました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 実装完了項目
|
||||||
|
|
||||||
|
### 🔴 最高優先度(完了)
|
||||||
|
|
||||||
|
#### 1. アプリバージョンチェックAPI
|
||||||
|
**エンドポイント**: `POST /api/app/version-check`
|
||||||
|
|
||||||
|
**実装ファイル**:
|
||||||
|
- `rog/models.py`: `AppVersion`モデル追加
|
||||||
|
- `rog/serializers.py`: `AppVersionSerializer`, `AppVersionCheckSerializer`, `AppVersionResponseSerializer`
|
||||||
|
- `rog/app_version_views.py`: バージョンチェックAPI実装
|
||||||
|
- `rog/urls.py`: URLパターン追加
|
||||||
|
- `create_app_versions_table.sql`: データベーステーブル作成
|
||||||
|
|
||||||
|
**機能**:
|
||||||
|
- セマンティックバージョニング対応
|
||||||
|
- プラットフォーム別管理(Android/iOS)
|
||||||
|
- 強制更新フラグ制御
|
||||||
|
- カスタムメッセージ設定
|
||||||
|
- 管理者向けバージョン管理API
|
||||||
|
|
||||||
|
**使用例**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/app/version-check/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"current_version": "1.2.3",
|
||||||
|
"platform": "android",
|
||||||
|
"build_number": "123"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. イベントステータス管理拡張
|
||||||
|
**エンドポイント**: `GET /newevent2-list/` (既存API拡張)
|
||||||
|
|
||||||
|
**実装ファイル**:
|
||||||
|
- `rog/models.py`: `NewEvent2`モデルに`status`フィールド追加
|
||||||
|
- `rog/serializers.py`: `NewEvent2Serializer`拡張
|
||||||
|
- `api_requirements_migration.sql`: データベース移行スクリプト
|
||||||
|
|
||||||
|
**機能**:
|
||||||
|
- ステータス管理: `public`, `private`, `draft`, `closed`
|
||||||
|
- `deadline_datetime`フィールド追加(API応答統一)
|
||||||
|
- 既存`public`フィールドからの自動移行
|
||||||
|
- ユーザーアクセス権限チェック機能
|
||||||
|
|
||||||
|
**レスポンス例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"event_name": "岐阜ロゲイニング2025",
|
||||||
|
"start_datetime": "2025-09-15T10:00:00Z",
|
||||||
|
"end_datetime": "2025-09-15T16:00:00Z",
|
||||||
|
"deadline_datetime": "2025-09-10T23:59:59Z",
|
||||||
|
"status": "public"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🟡 高優先度(完了)
|
||||||
|
|
||||||
|
#### 3. エントリー情報API拡張
|
||||||
|
**エンドポイント**: `GET /entry/` (既存API拡張)
|
||||||
|
|
||||||
|
**実装ファイル**:
|
||||||
|
- `rog/models.py`: `Entry`モデルにスタッフ権限フィールド追加
|
||||||
|
- `rog/serializers.py`: `EntrySerializer`拡張
|
||||||
|
|
||||||
|
**追加フィールド**:
|
||||||
|
- `staff_privileges`: スタッフ権限フラグ
|
||||||
|
- `can_access_private_events`: 非公開イベント参加権限
|
||||||
|
- `team_validation_status`: チーム承認状況
|
||||||
|
|
||||||
|
#### 4. チェックイン拡張情報システム
|
||||||
|
**実装ファイル**:
|
||||||
|
- `rog/models.py`: `CheckinExtended`モデル追加
|
||||||
|
- `create_checkin_extended_table.sql`: データベーステーブル作成
|
||||||
|
- `rog/views_apis/api_play.py`: `checkin_from_rogapp`API拡張
|
||||||
|
|
||||||
|
**機能**:
|
||||||
|
- GPS精度・座標情報の詳細記録
|
||||||
|
- カメラメタデータ保存
|
||||||
|
- 審査・検証システム
|
||||||
|
- 詳細スコアリング機能
|
||||||
|
- 自動審査フラグ
|
||||||
|
|
||||||
|
**拡張レスポンス例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "OK",
|
||||||
|
"message": "チェックポイントが正常に登録されました",
|
||||||
|
"team_name": "チーム名",
|
||||||
|
"cp_number": 1,
|
||||||
|
"checkpoint_id": 123,
|
||||||
|
"checkin_time": "2025-09-15 11:30:00",
|
||||||
|
"point_value": 10,
|
||||||
|
"bonus_points": 5,
|
||||||
|
"scoring_breakdown": {
|
||||||
|
"base_points": 10,
|
||||||
|
"camera_bonus": 5,
|
||||||
|
"total_points": 15
|
||||||
|
},
|
||||||
|
"validation_status": "pending",
|
||||||
|
"requires_manual_review": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 データベース変更
|
||||||
|
|
||||||
|
### 新規テーブル
|
||||||
|
1. **app_versions**: アプリバージョン管理
|
||||||
|
2. **rog_checkin_extended**: チェックイン拡張情報
|
||||||
|
|
||||||
|
### 既存テーブル拡張
|
||||||
|
1. **rog_newevent2**:
|
||||||
|
- `status` VARCHAR(20): イベントステータス
|
||||||
|
|
||||||
|
2. **rog_entry**:
|
||||||
|
- `staff_privileges` BOOLEAN: スタッフ権限
|
||||||
|
- `can_access_private_events` BOOLEAN: 非公開イベント参加権限
|
||||||
|
- `team_validation_status` VARCHAR(20): チーム承認状況
|
||||||
|
|
||||||
|
### インデックス追加
|
||||||
|
- `idx_app_versions_platform`
|
||||||
|
- `idx_app_versions_latest`
|
||||||
|
- `idx_newevent2_status`
|
||||||
|
- `idx_entry_staff_privileges`
|
||||||
|
- `idx_checkin_extended_gpslog`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 技術的実装詳細
|
||||||
|
|
||||||
|
### セキュリティ機能
|
||||||
|
- アプリバージョンチェックは認証不要(AllowAny)
|
||||||
|
- イベントアクセス権限チェック機能
|
||||||
|
- スタッフ権限による非公開イベント制御
|
||||||
|
|
||||||
|
### パフォーマンス最適化
|
||||||
|
- 適切なデータベースインデックス追加
|
||||||
|
- JSON形式でのスコアリング詳細保存
|
||||||
|
- 最新バージョンフラグによる高速検索
|
||||||
|
|
||||||
|
### エラーハンドリング
|
||||||
|
- 包括的なバリデーション
|
||||||
|
- 詳細なログ出力
|
||||||
|
- ユーザーフレンドリーなエラーメッセージ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 実装ファイル一覧
|
||||||
|
|
||||||
|
### Core Files
|
||||||
|
- `rog/models.py` - モデル定義
|
||||||
|
- `rog/serializers.py` - シリアライザー
|
||||||
|
- `rog/urls.py` - URLパターン
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `rog/app_version_views.py` - バージョンチェックAPI
|
||||||
|
- `create_app_versions_table.sql` - アプリバージョンテーブル
|
||||||
|
- `create_checkin_extended_table.sql` - チェックイン拡張テーブル
|
||||||
|
- `api_requirements_migration.sql` - 全体マイグレーション
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `rog/views_apis/api_play.py` - チェックインAPI拡張
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 デプロイ手順
|
||||||
|
|
||||||
|
### 1. データベース移行
|
||||||
|
```bash
|
||||||
|
# PostgreSQLに接続
|
||||||
|
psql -h localhost -U postgres -d rogdb
|
||||||
|
|
||||||
|
# マイグレーションスクリプト実行
|
||||||
|
\i api_requirements_migration.sql
|
||||||
|
\i create_app_versions_table.sql
|
||||||
|
\i create_checkin_extended_table.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Django設定
|
||||||
|
```bash
|
||||||
|
# モデル変更検出
|
||||||
|
python manage.py makemigrations
|
||||||
|
|
||||||
|
# マイグレーション実行
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
# サーバー再起動
|
||||||
|
sudo systemctl restart rogaining_srv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 動作確認
|
||||||
|
```bash
|
||||||
|
# アプリバージョンチェックテスト
|
||||||
|
curl -X POST http://localhost:8000/api/app/version-check/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"current_version": "1.0.0", "platform": "android"}'
|
||||||
|
|
||||||
|
# イベント一覧確認
|
||||||
|
curl http://localhost:8000/api/newevent2-list/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 パフォーマンス影響
|
||||||
|
|
||||||
|
### 予想される影響
|
||||||
|
- **データベース容量**: 約5-10%増加(新テーブル・フィールド)
|
||||||
|
- **API応答時間**: ほぼ影響なし(適切なインデックス配置)
|
||||||
|
- **メモリ使用量**: 軽微な増加(新モデル定義)
|
||||||
|
|
||||||
|
### 監視項目
|
||||||
|
- アプリバージョンチェックAPI応答時間
|
||||||
|
- チェックイン拡張情報保存成功率
|
||||||
|
- データベース接続プール使用率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事項
|
||||||
|
|
||||||
|
### 後方互換性
|
||||||
|
- 既存API仕様は維持
|
||||||
|
- 新フィールドは全てオプショナル
|
||||||
|
- 段階的移行が可能
|
||||||
|
|
||||||
|
### データ整合性
|
||||||
|
- `public`フィールドと`status`フィールドの整合性チェック実装
|
||||||
|
- トランザクション処理による原子性保証
|
||||||
|
|
||||||
|
### 今後の課題
|
||||||
|
- Location2025テーブルとの完全連携
|
||||||
|
- リアルタイム通知システムの実装
|
||||||
|
- 管理者向けダッシュボード強化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 次のアクション
|
||||||
|
|
||||||
|
### 🟢 中優先度項目(残り)
|
||||||
|
1. **チェックポイント詳細情報API**: Location2025対応
|
||||||
|
2. **管理者向け機能拡張**: 一括操作・リアルタイム監視
|
||||||
|
3. **プッシュ通知システム**: FCM連携
|
||||||
|
|
||||||
|
### 実装予定
|
||||||
|
- **9月3日まで**: 中優先度項目の実装
|
||||||
|
- **9月10日まで**: テスト・検証完了
|
||||||
|
- **9月15日**: 本番リリース
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**実装完了日**: 2025年8月27日
|
||||||
|
**実装者**: サーバー開発チーム
|
||||||
|
**レビュー**: 技術リード
|
||||||
|
**次回進捗確認**: 2025年9月3日
|
||||||
1
CPLIST/input/import_results_None_20250905_162344.csv
Normal file
1
CPLIST/input/import_results_None_20250905_162344.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
|
||||||
|
1
CPLIST/input/import_results_None_20250905_162613.csv
Normal file
1
CPLIST/input/import_results_None_20250905_162613.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
|
||||||
|
1
CPLIST/input/import_results_None_20250905_162727.csv
Normal file
1
CPLIST/input/import_results_None_20250905_162727.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
|
||||||
|
1
CPLIST/input/import_results_None_20250905_163055.csv
Normal file
1
CPLIST/input/import_results_None_20250905_163055.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,リーダー,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
|
||||||
|
25
CPLIST/input/import_results_None_20250905_164643.csv
Normal file
25
CPLIST/input/import_results_None_20250905_164643.csv
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,リーダー,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
|
||||||
|
いなりずし,1,一般-3時間,3.0時間,いなりずし_member_1@dummy.local,児玉優美(1976-12-13),5,優美(1976-12-13); 豊久(1973-11-23); 児玉優美(1976-12-13); 児玉豊久(1973-11-23); 田中広美(1975-10-31),完了,548,2025-09-05 07:46:39
|
||||||
|
Go to the peak!,2,一般-5時間,5.0時間,go_to_the_peak_member_1@dummy.local,柴山晋太郎(1974-12-14),3,柴山晋太郎(1974-12-14); 後藤克弘(1968-04-07); 二村修(1967-06-22),完了,549,2025-09-05 07:46:40
|
||||||
|
きみこうじ,3,一般-3時間,3.0時間,きみこうじ_member_1@dummy.local,齋藤貴美子(1980-07-06),2,齋藤貴美子(1980-07-06); 江口浩次(1968-04-19),完了,550,2025-09-05 07:46:40
|
||||||
|
ウエストサイド,4,一般-5時間,5.0時間,ウエストサイド_member_1@dummy.local,後藤睦子(1961-05-01),8,睦子(2000-01-01); マサトシ(2000-01-01); ショウコ(2000-01-01); ヨシミ(2000-01-01); 後藤睦子(1961-05-01); 後藤正寿(1959-07-23); 大坪照子(1958-11-11); 松村芳美(1964-04-28),完了,551,2025-09-05 07:46:40
|
||||||
|
ベル,5,一般-3時間,3.0時間,ベル_member_1@dummy.local,川村健一(1969-10-08),7,健一(2000-01-01); ショウジ(2000-01-01); チナミ(2000-01-01); 川村健一(1969-10-08); 曽我部知奈美(1973-12-17); 伊藤徳幸(1975-02-06); 筒井勝児(1976-05-31),完了,552,2025-09-05 07:46:40
|
||||||
|
ぐりと愉快な仲間たち,6,一般-3時間,3.0時間,ぐりと愉快な仲間たち_member_1@dummy.local,長屋香代子(1961-10-27),4,(1961-10-27); (1961-05-26); 長屋香代子(1961-10-27); 長屋宣宏(1961-05-26),完了,553,2025-09-05 07:46:40
|
||||||
|
坂本555,7,一般-5時間,5.0時間,坂本555_member_1@dummy.local,坂本正憲(1972-05-30),3,坂本正憲(1972-05-30); 坂本彩子(1976-03-29); 坂本瑠璃子(2003-08-23),完了,554,2025-09-05 07:46:40
|
||||||
|
M sisters with D,8,一般-5時間,5.0時間,m__sisters_with__d_member_1@dummy.local,前田貴代美(1973-01-15),2,前田貴代美(1973-01-15); 中濱智恵美(1969-06-16),完了,555,2025-09-05 07:46:41
|
||||||
|
さなっく,9,一般-5時間,5.0時間,さなっく_member_1@dummy.local,山田朋博(1971-04-23),2,山田朋博(1971-04-23); 眞田尚亮(1982-11-30),完了,556,2025-09-05 07:46:41
|
||||||
|
煮込みラーメン,10,一般-3時間,3.0時間,煮込みラーメン_member_1@dummy.local,西岡嵩倫(1999-01-05),4,(1999-01-05); (1971-02-02); 西岡嵩倫(1999-01-05); 西岡影忠(1971-02-02),完了,557,2025-09-05 07:46:41
|
||||||
|
サウナとビリヤニ,11,一般-3時間,3.0時間,サウナとビリヤニ_member_1@dummy.local,坂口祐生(1992-01-07),3,坂口祐生(1992-01-07); 近藤準(1987-01-25); 圓山大貴(1993-05-10),完了,558,2025-09-05 07:46:41
|
||||||
|
ひろ君と愉快な仲間たち,12,お試し・一般-3時間,3.0時間,ひろ君と愉快な仲間たち_member_1@dummy.local,山脇裕子(1984-01-26),5,山脇裕子(1984-01-26); 高橋美智子(1975-04-21); 樋口博久(1964-01-08); 雨宮功治(1962-05-25); 広瀬貴士(1978-08-17),完了,559,2025-09-05 07:46:41
|
||||||
|
山下和乃,13,女性ソロ-3時間,3.0時間,山下和乃_member_1@dummy.local,山下和乃(2004-04-26),1,山下和乃(2004-04-26),完了,560,2025-09-05 07:46:42
|
||||||
|
Best Wishes,14,女性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,美貴(1973-05-06); 長谷川美貴(1973-05-06),完了,561,2025-09-05 07:46:42
|
||||||
|
しーくん,15,男性ソロ-3時間,3.0時間,しーくん_member_1@dummy.local,水門茂(1962-12-24),2,茂(1962-12-24); 水門茂(1962-12-24),完了,562,2025-09-05 07:46:42
|
||||||
|
風呂の会,16,男性ソロ-5時間,5.0時間,風呂の会_member_1@dummy.local,浅井貴弘(1984-07-11),2,貴弘(2000-01-01); 浅井貴弘(1984-07-11),完了,563,2025-09-05 07:46:42
|
||||||
|
近藤隆,17,男性ソロ-5時間,5.0時間,近藤隆_member_1@dummy.local,近藤隆(1962-06-28),1,近藤隆(1962-06-28),完了,564,2025-09-05 07:46:42
|
||||||
|
日吉将大,18,男性ソロ-3時間,3.0時間,日吉将大_member_1@dummy.local,日吉将大(1995-09-14),2,(1995-09-14); 日吉将大(1995-09-14),完了,565,2025-09-05 07:46:42
|
||||||
|
東京OLクラブ,19,男性ソロ-3時間,3.0時間,東京olクラブ_member_1@dummy.local,阿部昌隆(1956-04-20),1,阿部昌隆(1956-04-20),完了,566,2025-09-05 07:46:42
|
||||||
|
Best Wishes,20,男性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,寿郎(1973-10-26); 長谷川美貴(1973-05-06),完了,567,2025-09-05 07:46:42
|
||||||
|
脇屋貴司,21,男性ソロ-5時間,5.0時間,脇屋貴司_member_1@dummy.local,脇屋貴司(1983-10-26),1,脇屋貴司(1983-10-26),完了,568,2025-09-05 07:46:42
|
||||||
|
うぱうぱアイランド,22,ファミリー-3時間,3.0時間,うぱうぱアイランド_member_1@dummy.local,伊藤由美子(1992-03-28),3,伊藤由美子(1992-03-28); 伊藤嘉仁(1993-08-25); 伊藤嘉利(2022-09-13),完了,569,2025-09-05 07:46:42
|
||||||
|
Team117,23,ファミリー-3時間,3.0時間,team117_member_1@dummy.local,佐々木孝好(1970-12-20),4,佐々木孝好(1970-12-20); 佐々木享子(1977-08-25); 佐々木実希(2012-01-21); 佐々木麻妃(2016-07-01),完了,570,2025-09-05 07:46:43
|
||||||
|
チームしぇいや,24,ファミリー-3時間,3.0時間,チームしぇいや_member_1@dummy.local,山本龍也(1976-03-14),6,聖也(2009-09-09); 輝也(2015-06-03); 龍也(1976-03-14); 山本龍也(1976-03-14); 山本聖也(2009-09-09); 山本輝也(2015-06-03),完了,571,2025-09-05 07:46:43
|
||||||
|
28
CPLIST/input/import_results_None_20250905_165633.csv
Normal file
28
CPLIST/input/import_results_None_20250905_165633.csv
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,リーダー,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
|
||||||
|
いなりずし,1,一般-3時間,3.0時間,いなりずし_member_1@dummy.local,児玉優美(1976-12-13),5,優美(1976-12-13); 豊久(1973-11-23); 児玉優美(1976-12-13); 児玉豊久(1973-11-23); 田中広美(1975-10-31),完了,548,2025-09-05 07:46:39
|
||||||
|
Go to the peak!,2,一般-5時間,5.0時間,go_to_the_peak_member_1@dummy.local,柴山晋太郎(1974-12-14),3,柴山晋太郎(1974-12-14); 後藤克弘(1968-04-07); 二村修(1967-06-22),完了,549,2025-09-05 07:46:40
|
||||||
|
きみこうじ,3,一般-3時間,3.0時間,きみこうじ_member_1@dummy.local,齋藤貴美子(1980-07-06),2,齋藤貴美子(1980-07-06); 江口浩次(1968-04-19),完了,550,2025-09-05 07:46:40
|
||||||
|
ウエストサイド,4,一般-5時間,5.0時間,ウエストサイド_member_1@dummy.local,後藤睦子(1961-05-01),8,睦子(2000-01-01); マサトシ(2000-01-01); ショウコ(2000-01-01); ヨシミ(2000-01-01); 後藤睦子(1961-05-01); 後藤正寿(1959-07-23); 大坪照子(1958-11-11); 松村芳美(1964-04-28),完了,551,2025-09-05 07:46:40
|
||||||
|
ベル,5,一般-3時間,3.0時間,ベル_member_1@dummy.local,川村健一(1969-10-08),7,健一(2000-01-01); ショウジ(2000-01-01); チナミ(2000-01-01); 川村健一(1969-10-08); 曽我部知奈美(1973-12-17); 伊藤徳幸(1975-02-06); 筒井勝児(1976-05-31),完了,552,2025-09-05 07:46:40
|
||||||
|
ぐりと愉快な仲間たち,6,一般-3時間,3.0時間,ぐりと愉快な仲間たち_member_1@dummy.local,長屋香代子(1961-10-27),4,(1961-10-27); (1961-05-26); 長屋香代子(1961-10-27); 長屋宣宏(1961-05-26),完了,553,2025-09-05 07:46:40
|
||||||
|
坂本555,7,一般-5時間,5.0時間,坂本555_member_1@dummy.local,坂本正憲(1972-05-30),3,坂本正憲(1972-05-30); 坂本彩子(1976-03-29); 坂本瑠璃子(2003-08-23),完了,554,2025-09-05 07:46:40
|
||||||
|
M sisters with D,8,一般-5時間,5.0時間,m__sisters_with__d_member_1@dummy.local,前田貴代美(1973-01-15),2,前田貴代美(1973-01-15); 中濱智恵美(1969-06-16),完了,555,2025-09-05 07:46:41
|
||||||
|
さなっく,9,一般-5時間,5.0時間,さなっく_member_1@dummy.local,山田朋博(1971-04-23),2,山田朋博(1971-04-23); 眞田尚亮(1982-11-30),完了,556,2025-09-05 07:46:41
|
||||||
|
煮込みラーメン,10,一般-3時間,3.0時間,煮込みラーメン_member_1@dummy.local,西岡嵩倫(1999-01-05),4,(1999-01-05); (1971-02-02); 西岡嵩倫(1999-01-05); 西岡影忠(1971-02-02),完了,557,2025-09-05 07:46:41
|
||||||
|
サウナとビリヤニ,11,一般-3時間,3.0時間,サウナとビリヤニ_member_1@dummy.local,坂口祐生(1992-01-07),3,坂口祐生(1992-01-07); 近藤準(1987-01-25); 圓山大貴(1993-05-10),完了,558,2025-09-05 07:46:41
|
||||||
|
ひろ君と愉快な仲間たち,12,お試し・一般-3時間,3.0時間,ひろ君と愉快な仲間たち_member_1@dummy.local,山脇裕子(1984-01-26),5,山脇裕子(1984-01-26); 高橋美智子(1975-04-21); 樋口博久(1964-01-08); 雨宮功治(1962-05-25); 広瀬貴士(1978-08-17),完了,559,2025-09-05 07:46:41
|
||||||
|
山下和乃,13,女性ソロ-3時間,3.0時間,山下和乃_member_1@dummy.local,山下和乃(2004-04-26),1,山下和乃(2004-04-26),完了,560,2025-09-05 07:46:42
|
||||||
|
Best Wishes,14,女性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,美貴(1973-05-06); 長谷川美貴(1973-05-06),完了,561,2025-09-05 07:46:42
|
||||||
|
しーくん,15,男性ソロ-3時間,3.0時間,しーくん_member_1@dummy.local,水門茂(1962-12-24),2,茂(1962-12-24); 水門茂(1962-12-24),完了,562,2025-09-05 07:46:42
|
||||||
|
風呂の会,16,男性ソロ-5時間,5.0時間,風呂の会_member_1@dummy.local,浅井貴弘(1984-07-11),2,貴弘(2000-01-01); 浅井貴弘(1984-07-11),完了,563,2025-09-05 07:46:42
|
||||||
|
近藤隆,17,男性ソロ-5時間,5.0時間,近藤隆_member_1@dummy.local,近藤隆(1962-06-28),1,近藤隆(1962-06-28),完了,564,2025-09-05 07:46:42
|
||||||
|
日吉将大,18,男性ソロ-3時間,3.0時間,日吉将大_member_1@dummy.local,日吉将大(1995-09-14),2,(1995-09-14); 日吉将大(1995-09-14),完了,565,2025-09-05 07:46:42
|
||||||
|
東京OLクラブ,19,男性ソロ-3時間,3.0時間,東京olクラブ_member_1@dummy.local,阿部昌隆(1956-04-20),1,阿部昌隆(1956-04-20),完了,566,2025-09-05 07:46:42
|
||||||
|
Best Wishes,20,男性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,寿郎(1973-10-26); 長谷川美貴(1973-05-06),完了,567,2025-09-05 07:46:42
|
||||||
|
脇屋貴司,21,男性ソロ-5時間,5.0時間,脇屋貴司_member_1@dummy.local,脇屋貴司(1983-10-26),1,脇屋貴司(1983-10-26),完了,568,2025-09-05 07:46:42
|
||||||
|
うぱうぱアイランド,22,ファミリー-3時間,3.0時間,うぱうぱアイランド_member_1@dummy.local,伊藤由美子(1992-03-28),3,伊藤由美子(1992-03-28); 伊藤嘉仁(1993-08-25); 伊藤嘉利(2022-09-13),完了,569,2025-09-05 07:46:42
|
||||||
|
Team117,23,ファミリー-3時間,3.0時間,team117_member_1@dummy.local,佐々木孝好(1970-12-20),4,佐々木孝好(1970-12-20); 佐々木享子(1977-08-25); 佐々木実希(2012-01-21); 佐々木麻妃(2016-07-01),完了,570,2025-09-05 07:46:43
|
||||||
|
チームしぇいや,24,ファミリー-3時間,3.0時間,チームしぇいや_member_1@dummy.local,山本龍也(1976-03-14),6,聖也(2009-09-09); 輝也(2015-06-03); 龍也(1976-03-14); 山本龍也(1976-03-14); 山本聖也(2009-09-09); 山本輝也(2015-06-03),完了,571,2025-09-05 07:46:43
|
||||||
|
まゆちー,25,お試し-3時間,3.0時間,まゆちー_member_1@dummy.local,浅田舞子(1986-02-22),4,浅田舞子(1986-02-22); 浅田真結菜(2014-03-30); 森美紀(1988-03-06); 森千晴(2017-08-04),完了,572,2025-09-05 07:56:33
|
||||||
|
ガンバルゾー,26,お試し-3時間,3.0時間,ガンバルゾー_member_1@dummy.local,森祐貴(1985-09-26),4,森祐貴(1985-09-26); 浅田直之(1987-12-12); 浅田晃汰(2014-01-06); 森光喜(2015-04-22),完了,573,2025-09-05 07:56:33
|
||||||
|
ランエンジョン!,27,お試し-5時間,5.0時間,ランエンジョン!_member_1@dummy.local,河合賢次(1972-12-14),2,河合賢次(1972-12-14); 中野真樹(1973-01-23),完了,574,2025-09-05 07:56:33
|
||||||
|
31
CPLIST/input/import_results_岐阜ロゲイニング2025_20250905_173618.csv
Normal file
31
CPLIST/input/import_results_岐阜ロゲイニング2025_20250905_173618.csv
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,リーダー,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
|
||||||
|
いなりずし,1,一般-3時間,3.0時間,いなりずし_member_1@dummy.local,児玉優美(1976-12-13),5,優美(1976-12-13); 豊久(1973-11-23); 児玉優美(1976-12-13); 児玉豊久(1973-11-23); 田中広美(1975-10-31),完了,548,2025-09-05 07:46:39
|
||||||
|
Go to the peak!,2,一般-5時間,5.0時間,go_to_the_peak_member_1@dummy.local,柴山晋太郎(1974-12-14),3,柴山晋太郎(1974-12-14); 後藤克弘(1968-04-07); 二村修(1967-06-22),完了,549,2025-09-05 07:46:40
|
||||||
|
きみこうじ,3,一般-3時間,3.0時間,きみこうじ_member_1@dummy.local,齋藤貴美子(1980-07-06),2,齋藤貴美子(1980-07-06); 江口浩次(1968-04-19),完了,550,2025-09-05 07:46:40
|
||||||
|
ウエストサイド,4,一般-5時間,5.0時間,ウエストサイド_member_1@dummy.local,後藤睦子(1961-05-01),8,睦子(2000-01-01); マサトシ(2000-01-01); ショウコ(2000-01-01); ヨシミ(2000-01-01); 後藤睦子(1961-05-01); 後藤正寿(1959-07-23); 大坪照子(1958-11-11); 松村芳美(1964-04-28),完了,551,2025-09-05 07:46:40
|
||||||
|
ベル,5,一般-3時間,3.0時間,ベル_member_1@dummy.local,川村健一(1969-10-08),7,健一(2000-01-01); ショウジ(2000-01-01); チナミ(2000-01-01); 川村健一(1969-10-08); 曽我部知奈美(1973-12-17); 伊藤徳幸(1975-02-06); 筒井勝児(1976-05-31),完了,552,2025-09-05 07:46:40
|
||||||
|
ぐりと愉快な仲間たち,6,一般-3時間,3.0時間,ぐりと愉快な仲間たち_member_1@dummy.local,長屋香代子(1961-10-27),4,(1961-10-27); (1961-05-26); 長屋香代子(1961-10-27); 長屋宣宏(1961-05-26),完了,553,2025-09-05 07:46:40
|
||||||
|
坂本555,7,一般-5時間,5.0時間,坂本555_member_1@dummy.local,坂本正憲(1972-05-30),3,坂本正憲(1972-05-30); 坂本彩子(1976-03-29); 坂本瑠璃子(2003-08-23),完了,554,2025-09-05 07:46:40
|
||||||
|
M sisters with D,8,一般-5時間,5.0時間,m__sisters_with__d_member_1@dummy.local,前田貴代美(1973-01-15),2,前田貴代美(1973-01-15); 中濱智恵美(1969-06-16),完了,555,2025-09-05 07:46:41
|
||||||
|
さなっく,9,一般-5時間,5.0時間,さなっく_member_1@dummy.local,山田朋博(1971-04-23),2,山田朋博(1971-04-23); 眞田尚亮(1982-11-30),完了,556,2025-09-05 07:46:41
|
||||||
|
煮込みラーメン,10,一般-3時間,3.0時間,煮込みラーメン_member_1@dummy.local,西岡嵩倫(1999-01-05),4,(1999-01-05); (1971-02-02); 西岡嵩倫(1999-01-05); 西岡影忠(1971-02-02),完了,557,2025-09-05 07:46:41
|
||||||
|
サウナとビリヤニ,11,一般-3時間,3.0時間,サウナとビリヤニ_member_1@dummy.local,坂口祐生(1992-01-07),3,坂口祐生(1992-01-07); 近藤準(1987-01-25); 圓山大貴(1993-05-10),完了,558,2025-09-05 07:46:41
|
||||||
|
ひろ君と愉快な仲間たち,12,お試し・一般-3時間,3.0時間,ひろ君と愉快な仲間たち_member_1@dummy.local,山脇裕子(1984-01-26),5,山脇裕子(1984-01-26); 高橋美智子(1975-04-21); 樋口博久(1964-01-08); 雨宮功治(1962-05-25); 広瀬貴士(1978-08-17),完了,559,2025-09-05 07:46:41
|
||||||
|
山下和乃,13,女性ソロ-3時間,3.0時間,山下和乃_member_1@dummy.local,山下和乃(2004-04-26),1,山下和乃(2004-04-26),完了,560,2025-09-05 07:46:42
|
||||||
|
Best Wishes,14,女性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,美貴(1973-05-06); 長谷川美貴(1973-05-06),完了,561,2025-09-05 07:46:42
|
||||||
|
しーくん,15,男性ソロ-3時間,3.0時間,しーくん_member_1@dummy.local,水門茂(1962-12-24),2,茂(1962-12-24); 水門茂(1962-12-24),完了,562,2025-09-05 07:46:42
|
||||||
|
風呂の会,16,男性ソロ-5時間,5.0時間,風呂の会_member_1@dummy.local,浅井貴弘(1984-07-11),2,貴弘(2000-01-01); 浅井貴弘(1984-07-11),完了,563,2025-09-05 07:46:42
|
||||||
|
近藤隆,17,男性ソロ-5時間,5.0時間,近藤隆_member_1@dummy.local,近藤隆(1962-06-28),1,近藤隆(1962-06-28),完了,564,2025-09-05 07:46:42
|
||||||
|
日吉将大,18,男性ソロ-3時間,3.0時間,日吉将大_member_1@dummy.local,日吉将大(1995-09-14),2,(1995-09-14); 日吉将大(1995-09-14),完了,565,2025-09-05 07:46:42
|
||||||
|
東京OLクラブ,19,男性ソロ-3時間,3.0時間,東京olクラブ_member_1@dummy.local,阿部昌隆(1956-04-20),1,阿部昌隆(1956-04-20),完了,566,2025-09-05 07:46:42
|
||||||
|
Best Wishes,20,男性ソロ-5時間,5.0時間,best_wishes_member_1@dummy.local,長谷川美貴(1973-05-06),2,寿郎(1973-10-26); 長谷川美貴(1973-05-06),完了,567,2025-09-05 07:46:42
|
||||||
|
脇屋貴司,21,男性ソロ-5時間,5.0時間,脇屋貴司_member_1@dummy.local,脇屋貴司(1983-10-26),1,脇屋貴司(1983-10-26),完了,568,2025-09-05 07:46:42
|
||||||
|
うぱうぱアイランド,22,ファミリー-3時間,3.0時間,うぱうぱアイランド_member_1@dummy.local,伊藤由美子(1992-03-28),3,伊藤由美子(1992-03-28); 伊藤嘉仁(1993-08-25); 伊藤嘉利(2022-09-13),完了,569,2025-09-05 07:46:42
|
||||||
|
Team117,23,ファミリー-3時間,3.0時間,team117_member_1@dummy.local,佐々木孝好(1970-12-20),4,佐々木孝好(1970-12-20); 佐々木享子(1977-08-25); 佐々木実希(2012-01-21); 佐々木麻妃(2016-07-01),完了,570,2025-09-05 07:46:43
|
||||||
|
チームしぇいや,24,ファミリー-3時間,3.0時間,チームしぇいや_member_1@dummy.local,山本龍也(1976-03-14),6,聖也(2009-09-09); 輝也(2015-06-03); 龍也(1976-03-14); 山本龍也(1976-03-14); 山本聖也(2009-09-09); 山本輝也(2015-06-03),完了,571,2025-09-05 07:46:43
|
||||||
|
まゆちー,25,お試し-3時間,3.0時間,まゆちー_member_1@dummy.local,浅田舞子(1986-02-22),4,浅田舞子(1986-02-22); 浅田真結菜(2014-03-30); 森美紀(1988-03-06); 森千晴(2017-08-04),完了,572,2025-09-05 07:56:33
|
||||||
|
ガンバルゾー,26,お試し-3時間,3.0時間,ガンバルゾー_member_1@dummy.local,森祐貴(1985-09-26),4,森祐貴(1985-09-26); 浅田直之(1987-12-12); 浅田晃汰(2014-01-06); 森光喜(2015-04-22),完了,573,2025-09-05 07:56:33
|
||||||
|
ランエンジョン!,27,お試し-5時間,5.0時間,ランエンジョン!_member_1@dummy.local,河合賢次(1972-12-14),2,河合賢次(1972-12-14); 中野真樹(1973-01-23),完了,574,2025-09-05 07:56:33
|
||||||
|
fun!fun!うごchan,28,お試し-5時間,5.0時間,funfunうごchan_member_1@dummy.local,早川宏美(1975-06-15),1,早川宏美(1975-06-15),完了,575,2025-09-05 08:36:17
|
||||||
|
ポエドリ,29,お試し-5時間,5.0時間,ポエドリ_member_1@dummy.local,高木俊裕(1984-03-09),1,高木俊裕(1984-03-09),完了,576,2025-09-05 08:36:18
|
||||||
|
前川一彦,30,男性ソロ-5時間,5.0時間,前川一彦_member_1@dummy.local,前川一彦(1990-01-01),1,前川一彦(1990-01-01),完了,577,2025-09-05 08:36:18
|
||||||
|
53
CPLIST/input/team2025.csv
Normal file
53
CPLIST/input/team2025.csv
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
部門別数,時間,部門,チーム名,メール,password,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
1,3,一般,いなりずし,takuyuna1123@icloud.com,ko1703,09014701703,児玉優美,1976/12/13,児玉豊久,1973/11/23,田中広美,1975/10/31,,,,,,,,,,
|
||||||
|
1,5,一般,Go to the peak!,shibashintan@c.vodafone.ne.jp,shi0145,090-8499-0145,柴山晋太郎,1974/12/14,後藤克弘,1968/04/07,二村修,1967/06/22,,,,,,,,,,
|
||||||
|
2,3,一般,きみこうじ,chibi-kimi.706@ezweb.ne.jp,sa8309,09062518309,齋藤貴美子,1980/07/06,江口浩次,1968/04/19,,,,,,,,,,,,
|
||||||
|
2,5,一般,ウエストサイド,chikachan-5101414@i.softbank.jp,go7471,09047997471,後藤睦子,1961/5/1,後藤正寿,1959/7/23,大坪照子,1958/11/11,松村芳美,1964/4/28,,,,,,,,
|
||||||
|
3,3,一般,ベル,kekomura1008@yahoo.co.jp,ka3001,090-3564-3001,川村健一,1969/10/08,曽我部知奈美,1973/12/17,伊藤徳幸,1975/02/06 ,筒井勝児,1976/05/31,,,,,,,,
|
||||||
|
3,5,一般,ランエンジョン!,baycools16@gmail.com,ka9749,090÷4790÷9749,河合賢次,1972/12/14,中野真樹,1973/01/23,,,,,,,,,,,,
|
||||||
|
4,3,一般,ぐりと愉快な仲間たち,kayochu.v.mame.526@icloud.com,na6547,090-1564-6547,長屋香代子,1961/10/27,長屋宣宏,1961/5/26,,,,,,,,,,,,
|
||||||
|
4,5,一般,坂本555,sakamoto180909@yahoo.co.jp,sa4396,090-8480-4396,坂本正憲,1972/5/30,坂本彩子,1976/3/29,坂本瑠璃子,2003/8/23,,,,,,,,,,
|
||||||
|
5,3,一般,リキとりんごてぃー,apple1977tea@yahoo.co.jp,te1499,08051241499,鄭寛子,1977/6/13,鄭昌彦,1971/5/26,,,,,,,,,,,,
|
||||||
|
5,5,一般,East Field,ryo1hi@outlook.com,hi0504,070-8564-0504,東野遼一,1983/09/27,東野智子,1977/03/16,,,,,,,,,,,,
|
||||||
|
6,3,一般,としちんかずちん,kazu-chin1998@docomo.ne.jp,shi9127,080-2616-9127,渋谷和広,1970/8/1,渋谷敏江,1956/6/16,,,,,,,,,,,,
|
||||||
|
6,5,一般,M sisters with D,m.kiyomi.115@gmail.com,ma3731,090-4869-3731,前田貴代美,1973/01/15,中濱智恵美,1969/06/16,,,,,,,,,,,,
|
||||||
|
7,3,一般,シマエナガ,c6d6.lpbm5-s@ezweb.ne.jp,shi1925,090-6336-1925,神谷孫斗,1997/03/02,小栗彩瑚,2001/9/21,,,,,,,,,,,,
|
||||||
|
7,5,一般,さなっく,santa04230722@icloud.com,ya7192,070-5640-7192,山田朋博,1971/04/23,眞田尚亮,1982/11/30,,,,,,,,,,,,
|
||||||
|
8,3,一般,煮込みラーメン,t.nishioka1575tt@gmail.com,ni9354,080-8523-9354,西岡嵩倫,1999/1/5,西岡影忠,1971/2/2,,,,,,,,,,,,
|
||||||
|
9,3,一般,そうたとなゆ,hmt.sota@gmail.com,ho6594,090-1109-6594,甫本創太,1991/06/07,後藤菜友,1994/02/22,,,,,,,,,,,,
|
||||||
|
10,3,一般,KOJ,balccitomatochop@gmail.com,to5670,090-2181-5670,轟原功樹,1978/08/10,田中美樹,1978/09/07,,,,,,,,,,,,
|
||||||
|
11,3,一般,サウナとビリヤニ,bitter_smile107@yahoo.co.jp,sa9007,090-4760-9007,坂口祐生,1992/1/7,近藤準,1987/1/25,圓山大貴,1993/5/10,,,,,,,,,,
|
||||||
|
1,3,お試し・一般,ひろ君と愉快な仲間たち,y0126k@yahoo.co.jp,ya7467,090-9902-7467,山脇裕子,1984/1/26,高橋美智子,1975/04/21,樋口博久,1964/01/08,雨宮功治,1962/05/25,広瀬貴士,1978/08/17,,,,,,
|
||||||
|
2,3,お試し・一般,フクニシ,appleorange100pct@yahoo.co.jp,fu2792,080-6954-2792,福西直之,1986/2/5,福西愛,1986/3/2,,,,,,,,,,,,
|
||||||
|
3,3,お試し・一般,あやみち,h613-y5m9t-mich@ezweb.ne.jp,ya3144,090-4447-3144,谷許文音,2006/07/26,谷許美千代,1976/03/27,,,,,,,,,,,,
|
||||||
|
1,3,お試し・男性ソロ,松村覚司,happy.dreams.come.true923@gmail.com,ma3625,090-8186-3625,松村覚司,1967/9/23,,,,,,,,,,,,,,
|
||||||
|
2,3,お試し・男性ソロ,高野清司,wakano_528@yahoo.co.jp,ta5865,090-5603-5865,高野清司,71歳,,,,,,,,,,,,,,
|
||||||
|
1,3,お試し・ファミリー,まゆちー,takoyaki_sena@icloud.com,a1246,090-6090-1246,浅田舞子,1986/02/22,浅田真結菜,2014/03/30,森美紀,1988/03/06,森千晴,2017/8/4,,,,,,,,
|
||||||
|
1,5,お試し・ファミリー,ポエドリ,takagitoshihiro8@yahoo.co.jp,ta4245,090-5866-4245,高木俊裕,1984/03/09,,,,,,,,,,,,,,
|
||||||
|
2,3,お試し・ファミリー,ガンバルゾー,youkeymr.01@gmail.com,mo6605,090-6080-6605,森祐貴,1985/9/26,浅田直之,1987/12/12,浅田晃汰,2014/01/06,森光喜,2015/4/22,,,,,,,,
|
||||||
|
2,5,お試し・ファミリー,fun!fun!うごchan,fulayota333@gmail.com,ha7384,090-6599-7384,早川宏美,1975/6/15,,,,,,,,,,,,,,
|
||||||
|
3,3,お試し・ファミリー,チームT,sphin28420@aim.com,te1882,080-6709-1882,寺田剛,1979/06/04,寺田恭子,1985/01/10,寺田向希,2023/11/08,,,,,,,,,,
|
||||||
|
1,3,女性ソロ,山下和乃,kazjamster@gmail.com,ya2450,090-4229-2450,山下和乃,2004/4/26,,,,,,,,,,,,,,
|
||||||
|
1,5,女性ソロ,Best Wishes,thunderhead_56@yahoo.co.jp,ha7226,090-5652-7226,長谷川美貴,1973/5/6,,,,,,,,,,,,,,
|
||||||
|
1,3,男性ソロ,しーくん,redleif57917913@ezweb.ne.jp,mi6827,090-2946-6827,水門茂,1962/12/24,,,,,,,,,,,,,,
|
||||||
|
1,5,男性ソロ,風呂の会,1845dondon@gmail.com,a9050,09096369050,浅井貴弘,1984/07/11,,,,,,,,,,,,,,
|
||||||
|
2,3,男性ソロ,野田達男,tatchi.sat111@docomo.ne.jp,no0873,0901417-0873,野田達男,1950/9/14,,,,,,,,,,,,,,
|
||||||
|
2,5,男性ソロ,近藤隆,kondo2000gt@yahoo.ne.jp,ko0666,09018300666,近藤隆,1962/6/28,,,,,,,,,,,,,,
|
||||||
|
3,3,男性ソロ,日吉将大,hiyomasa0034@gmail.com,hi6343,080-2733-6343,日吉将大,1995/09/14,,,,,,,,,,,,,,
|
||||||
|
3,5,男性ソロ,松野昌紀,matsubottkuri11994730@gmail.com,ma2606,090-1272-2606,松野昌紀,1972/9/30,,,,,,,,,,,,,,
|
||||||
|
4,3,男性ソロ,東京OLクラブ,abe_1755_31@yahoo.co.jp,a7102,090-2203-7102,阿部昌隆,1956/4/20,,,,,,,,,,,,,,
|
||||||
|
4,5,男性ソロ,白木稔人,amida48gan@icloud.com,shi6048,090-7302-6048,白木稔人,1972/5/17,,,,,,,,,,,,,,
|
||||||
|
5,3,男性ソロ,大阪OLC,t.okiura1961@gmail.com,o1141,090-7888-1141,沖浦徹二,1961/4/29,,,,,,,,,,,,,,
|
||||||
|
5,5,男性ソロ,Best Wishes,jovi_bounce14@yahoo.co.jp,ko0716,090−3284−0716,小林寿郎,1973/10/26,,,,,,,,,,,,,,
|
||||||
|
6,3,男性ソロ,つるまいOLC,junhagi68@gmail.com,ha1001,080-3159-1001,萩原淳,1968/3/17,,,,,,,,,,,,,,
|
||||||
|
6,5,男性ソロ,脇屋貴司,takarinkuririn@gmail.com,wa2659,080-3508-2659,脇屋貴司,1983/10/26,,,,,,,,,,,,,,
|
||||||
|
7,3,男性ソロ,㈱大垣ケーブルテレビ,so-kishida@ogaki-tv.co.jp,ki1207,0584-82-1207,岸田爽,2001/8/12,,,,,,,,,,,,,,
|
||||||
|
7,5,男性ソロ,前川一彦,yoshino-chuo@docomo.ne.jp,ma2351,090-1074-2351,前川一彦,不明,,,,,,,,,,,,,,
|
||||||
|
8,3,男性ソロ,㈱大垣ケーブルテレビ,ta-shiba@ogaki-tv.co.jp,shi1207,,芝建,1998/11/9,,,,,,,,,,,,,,
|
||||||
|
1,3,ファミリー,うぱうぱアイランド,serukasu@gmail.com,i4200,09084584200,伊藤由美子,19920328,伊藤嘉仁,19930825,伊藤嘉利,20220913,,,,,,,,,,
|
||||||
|
1,5,ファミリー,ながれぼし,h2798723ddwyus@i.softbank.jp,ta8317,090-1782-8317,高田めぐみ,1982/4/28,高田志穂,2013/12/5,,,,,,,,,,,,
|
||||||
|
2,3,ファミリー,Team117,miki.maki0107@gmail.com,sa3915,090-7678-3915,佐々木孝好,1970/12/20,佐々木享子,1977/8/25,佐々木実希,2012/1/21,佐々木麻妃,2016/7/1,,,,,,,,
|
||||||
|
2,5,ファミリー,500えん,roumnet@yahoo.co.jp,go6814,090-9890-6814,五百木弘道,1972/4/29,五百木芽彩,2015/3/13,,,,,,,,,,,,
|
||||||
|
3,3,ファミリー,チームしぇいや,rayrain3000@docomo.ne.jp,ya2905,090-3056-2905,山本龍也,1976/3/14,山本聖也,2009/9/9,山本輝也,2015/6/3,,,,,,,,,,
|
||||||
|
3,5,ファミリー,チームユズ,livertish_v.g.35@docomo.ne.jp,ko7822,090-7311-7822,小出龍,1983/2/27,小出柚希,2019/1/7,,,,,,,,,,,,
|
||||||
|
4,3,ファミリー,Y'sファミリー,inukisen@gmail.com,ya1285,09042581285,安田千穂,1984/3/7,安田尚広,1978/1/18,安田雫,2014/9/2,安田葵,2018/5/13,,,,,,,,
|
||||||
|
2
CPLIST/input/team_mail.csv
Normal file
2
CPLIST/input/team_mail.csv
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
部門別数,時間,部門,チーム名,メール,password,電話番号,氏名1
|
||||||
|
2,5,一般,ウエストサイド,hannivalscipio@gmail.com,ka9749,090-4790-9749,宮田 明
|
||||||
|
4
CPLIST/input/teams2025-errorfix.csv
Normal file
4
CPLIST/input/teams2025-errorfix.csv
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
2,5,お試し・ファミリー,fun!fun!うごchan,fulayota333@gmail.com,ha7384,090-6599-7384,早川宏美,1975/6/15,,,,,,,,,,,,,,
|
||||||
|
1,5,お試し・ファミリー,ポエドリ,takagitoshihiro8@yahoo.co.jp,ta4245,090-5866-4245,高木俊裕,1984/03/09,,,,,,,,,,,,,,
|
||||||
|
7,5,男性ソロ,前川一彦,yoshino-chuo@docomo.ne.jp,ma2351,090-1074-2351,前川一彦,1990/1/1,,,,,,,,,,,,,,
|
||||||
|
53
CPLIST/input/teams2025.csv
Normal file
53
CPLIST/input/teams2025.csv
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
部門別数,時間,部門,チーム名,メール,password,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
1,3,一般,いなりずし,takuyuna1123@icloud.com,ko1703,09014701703,児玉優美,1976/12/13,児玉豊久,1973/11/23,田中広美,1975/10/31,,,,,,,,,,
|
||||||
|
1,5,一般,Go to the peak!,shibashintan@c.vodafone.ne.jp,shi0145,090-8499-0145,柴山晋太郎,1974/12/14,後藤克弘,1968/04/07,二村修,1967/06/22,,,,,,,,,,
|
||||||
|
2,3,一般,きみこうじ,chibi-kimi.706@ezweb.ne.jp,sa8309,09062518309,齋藤貴美子,1980/07/06,江口浩次,1968/04/19,,,,,,,,,,,,
|
||||||
|
2,5,一般,ウエストサイド,chikachan-5101414@i.softbank.jp,go7471,09047997471,後藤睦子,1961/5/1,後藤正寿,1959/7/23,大坪照子,1958/11/11,松村芳美,1964/4/28,,,,,,,,
|
||||||
|
3,3,一般,ベル,kekomura1008@yahoo.co.jp,ka3001,090-3564-3001,川村健一,1969/10/08,曽我部知奈美,1973/12/17,伊藤徳幸,1975/02/06 ,筒井勝児,1976/05/31,,,,,,,,
|
||||||
|
3,5,一般,ランエンジョン!,baycools16@gmail.com,ka9749,090÷4790÷9749,河合賢次,1972/12/14,中野真樹,1973/01/23,,,,,,,,,,,,
|
||||||
|
4,3,一般,ぐりと愉快な仲間たち,kayochu.v.mame.526@icloud.com,na6547,090-1564-6547,長屋香代子,1961/10/27,長屋宣宏,1961/5/26,,,,,,,,,,,,
|
||||||
|
4,5,一般,坂本555,sakamoto180909@yahoo.co.jp,sa4396,090-8480-4396,坂本正憲,1972/5/30,坂本彩子,1976/3/29,坂本瑠璃子,2003/8/23,,,,,,,,,,
|
||||||
|
5,3,一般,リキとりんごてぃー,apple1977tea@yahoo.co.jp,te1499,08051241499,鄭寛子,1977/6/13,鄭昌彦,1971/5/26,,,,,,,,,,,,
|
||||||
|
5,5,一般,East Field,ryo1hi@outlook.com,hi0504,070-8564-0504,東野遼一,1983/09/27,東野智子,1977/03/16,,,,,,,,,,,,
|
||||||
|
6,3,一般,としちんかずちん,kazu-chin1998@docomo.ne.jp,shi9127,080-2616-9127,渋谷和広,1970/8/1,渋谷敏江,1956/6/16,,,,,,,,,,,,
|
||||||
|
6,5,一般,M sisters with D,m.kiyomi.115@gmail.com,ma3731,090-4869-3731,前田貴代美,1973/01/15,中濱智恵美,1969/06/16,,,,,,,,,,,,
|
||||||
|
7,3,一般,シマエナガ,c6d6.lpbm5-s@ezweb.ne.jp,shi1925,090-6336-1925,神谷孫斗,1997/03/02,小栗彩瑚,2001/9/21,,,,,,,,,,,,
|
||||||
|
7,5,一般,さなっく,santa04230722@icloud.com,ya7192,070-5640-7192,山田朋博,1971/04/23,眞田尚亮,1982/11/30,,,,,,,,,,,,
|
||||||
|
8,3,一般,煮込みラーメン,t.nishioka1575tt@gmail.com,ni9354,080-8523-9354,西岡嵩倫,1999/1/5,西岡影忠,1971/2/2,,,,,,,,,,,,
|
||||||
|
9,3,一般,そうたとなゆ,hmt.sota@gmail.com,ho6594,090-1109-6594,甫本創太,1991/06/07,後藤菜友,1994/02/22,,,,,,,,,,,,
|
||||||
|
10,3,一般,KOJ,balccitomatochop@gmail.com,to5670,090-2181-5670,轟原功樹,1978/08/10,田中美樹,1978/09/07,,,,,,,,,,,,
|
||||||
|
11,3,一般,サウナとビリヤニ,bitter_smile107@yahoo.co.jp,sa9007,090-4760-9007,坂口祐生,1992/1/7,近藤準,1987/1/25,圓山大貴,1993/5/10,,,,,,,,,,
|
||||||
|
1,3,お試し・一般,ひろ君と愉快な仲間たち,y0126k@yahoo.co.jp,ya7467,090-9902-7467,山脇裕子,1984/1/26,高橋美智子,1975/04/21,樋口博久,1964/01/08,雨宮功治,1962/05/25,広瀬貴士,1978/08/17,,,,,,
|
||||||
|
2,3,お試し・一般,フクニシ,appleorange100pct@yahoo.co.jp,fu2792,080-6954-2792,福西直之,1986/2/5,福西愛,1986/3/2,,,,,,,,,,,,
|
||||||
|
3,3,お試し・一般,あやみち,h613-y5m9t-mich@ezweb.ne.jp,ya3144,090-4447-3144,谷許文音,2006/07/26,谷許美千代,1976/03/27,,,,,,,,,,,,
|
||||||
|
1,3,お試し・男性ソロ,松村覚司,happy.dreams.come.true923@gmail.com,ma3625,090-8186-3625,松村覚司,1967/9/23,,,,,,,,,,,,,,
|
||||||
|
2,3,お試し・男性ソロ,高野清司,wakano_528@yahoo.co.jp,ta5865,090-5603-5865,高野清司,71歳,,,,,,,,,,,,,,
|
||||||
|
1,3,お試し・ファミリー,まゆちー,takoyaki_sena@icloud.com,a1246,090-6090-1246,浅田舞子,1986/02/22,浅田真結菜,2014/03/30,森美紀,1988/03/06,森千晴,2017/8/4,,,,,,,,
|
||||||
|
1,5,お試し・ファミリー,ポエドリ,takagitoshihiro8@yahoo.co.jp,ta4245,090-5866-4245,高木俊裕,1984/03/09,,,,,,,,,,,,,,
|
||||||
|
2,3,お試し・ファミリー,ガンバルゾー,youkeymr.01@gmail.com,mo6605,090-6080-6605,森祐貴,1985/9/26,浅田直之,1987/12/12,浅田晃汰,2014/01/06,森光喜,2015/4/22,,,,,,,,
|
||||||
|
2,5,お試し・ファミリー,fun!fun!うごchan,fulayota333@gmail.com,ha7384,090-6599-7384,早川宏美,1975/6/15,,,,,,,,,,,,,,
|
||||||
|
3,3,お試し・ファミリー,チームT,sphin28420@aim.com,te1882,080-6709-1882,寺田剛,1979/06/04,寺田恭子,1985/01/10,寺田向希,2023/11/08,,,,,,,,,,
|
||||||
|
1,3,女性ソロ,山下和乃,kazjamster@gmail.com,ya2450,090-4229-2450,山下和乃,2004/4/26,,,,,,,,,,,,,,
|
||||||
|
1,5,女性ソロ,Best Wishes,thunderhead_56@yahoo.co.jp,ha7226,090-5652-7226,長谷川美貴,1973/5/6,,,,,,,,,,,,,,
|
||||||
|
1,3,男性ソロ,しーくん,redleif57917913@ezweb.ne.jp,mi6827,090-2946-6827,水門茂,1962/12/24,,,,,,,,,,,,,,
|
||||||
|
1,5,男性ソロ,風呂の会,1845dondon@gmail.com,a9050,09096369050,浅井貴弘,1984/07/11,,,,,,,,,,,,,,
|
||||||
|
2,3,男性ソロ,野田達男,tatchi.sat111@docomo.ne.jp,no0873,0901417-0873,野田達男,1950/9/14,,,,,,,,,,,,,,
|
||||||
|
2,5,男性ソロ,近藤隆,kondo2000gt@yahoo.ne.jp,ko0666,09018300666,近藤隆,1962/6/28,,,,,,,,,,,,,,
|
||||||
|
3,3,男性ソロ,日吉将大,hiyomasa0034@gmail.com,hi6343,080-2733-6343,日吉将大,1995/09/14,,,,,,,,,,,,,,
|
||||||
|
3,5,男性ソロ,松野昌紀,matsubottkuri11994730@gmail.com,ma2606,090-1272-2606,松野昌紀,1972/9/30,,,,,,,,,,,,,,
|
||||||
|
4,3,男性ソロ,東京OLクラブ,abe_1755_31@yahoo.co.jp,a7102,090-2203-7102,阿部昌隆,1956/4/20,,,,,,,,,,,,,,
|
||||||
|
4,5,男性ソロ,白木稔人,amida48gan@icloud.com,shi6048,090-7302-6048,白木稔人,1972/5/17,,,,,,,,,,,,,,
|
||||||
|
5,3,男性ソロ,大阪OLC,t.okiura1961@gmail.com,o1141,090-7888-1141,沖浦徹二,1961/4/29,,,,,,,,,,,,,,
|
||||||
|
5,5,男性ソロ,Best Wishes,jovi_bounce14@yahoo.co.jp,ko0716,090−3284−0716,小林寿郎,1973/10/26,,,,,,,,,,,,,,
|
||||||
|
6,3,男性ソロ,つるまいOLC,junhagi68@gmail.com,ha1001,080-3159-1001,萩原淳,1968/3/17,,,,,,,,,,,,,,
|
||||||
|
6,5,男性ソロ,脇屋貴司,takarinkuririn@gmail.com,wa2659,080-3508-2659,脇屋貴司,1983/10/26,,,,,,,,,,,,,,
|
||||||
|
7,3,男性ソロ,㈱大垣ケーブルテレビ,so-kishida@ogaki-tv.co.jp,ki1207,0584-82-1207,岸田爽,2001/8/12,,,,,,,,,,,,,,
|
||||||
|
7,5,男性ソロ,前川一彦,yoshino-chuo@docomo.ne.jp,ma2351,090-1074-2351,前川一彦,不明,,,,,,,,,,,,,,
|
||||||
|
8,3,男性ソロ,㈱大垣ケーブルテレビ,ta-shiba@ogaki-tv.co.jp,shi1207,,芝建,1998/11/9,,,,,,,,,,,,,,
|
||||||
|
1,3,ファミリー,うぱうぱアイランド,serukasu@gmail.com,i4200,09084584200,伊藤由美子,19920328,伊藤嘉仁,19930825,伊藤嘉利,20220913,,,,,,,,,,
|
||||||
|
1,5,ファミリー,ながれぼし,h2798723ddwyus@i.softbank.jp,ta8317,090-1782-8317,高田めぐみ,1982/4/28,高田志穂,2013/12/5,,,,,,,,,,,,
|
||||||
|
2,3,ファミリー,Team117,miki.maki0107@gmail.com,sa3915,090-7678-3915,佐々木孝好,1970/12/20,佐々木享子,1977/8/25,佐々木実希,2012/1/21,佐々木麻妃,2016/7/1,,,,,,,,
|
||||||
|
2,5,ファミリー,500えん,roumnet@yahoo.co.jp,go6814,090-9890-6814,五百木弘道,1972/4/29,五百木芽彩,2015/3/13,,,,,,,,,,,,
|
||||||
|
3,3,ファミリー,チームしぇいや,rayrain3000@docomo.ne.jp,ya2905,090-3056-2905,山本龍也,1976/3/14,山本聖也,2009/9/9,山本輝也,2015/6/3,,,,,,,,,,
|
||||||
|
3,5,ファミリー,チームユズ,livertish_v.g.35@docomo.ne.jp,ko7822,090-7311-7822,小出龍,1983/2/27,小出柚希,2019/1/7,,,,,,,,,,,,
|
||||||
|
4,3,ファミリー,Y'sファミリー,inukisen@gmail.com,ya1285,09042581285,安田千穂,1984/3/7,安田尚広,1978/1/18,安田雫,2014/9/2,安田葵,2018/5/13,,,,,,,,
|
||||||
|
4
CPLIST/input/teams2025_test.csv
Normal file
4
CPLIST/input/teams2025_test.csv
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
1,3,一般,いなりずし,takuyuna1123@icloud.com,ko1703,09014701703,児玉優美,1976/12/13,児玉豊久,1973/11/23,田中広美,1975/10/31,,,,,,,,,,
|
||||||
|
1,5,一般,Go to the peak!,shibashintan@c.vodafone.ne.jp,shi0145,090-8499-0145,柴山晋太郎,1974/12/14,後藤克弘,1968/04/07,二村修,1967/06/22,,,,,,,,,,
|
||||||
|
2,3,一般,きみこうじ,chibi-kimi.706@ezweb.ne.jp,sa8309,09062518309,齋藤貴美子,1980/07/06,江口浩次,1968/04/19,,,,,,,,,,,,
|
||||||
|
3
CPLIST/input/test_solo_trial.csv
Normal file
3
CPLIST/input/test_solo_trial.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
1,3,お試し,テスト一人お試し,test_solo_trial@example.com,test123,090-1234-5678,山田太郎,1990/4/15,,,,,,,,,,,,
|
||||||
|
2,5,お試し,テスト一人お試し2,test_solo_trial2@example.com,test456,090-1234-5679,佐藤花子,1985/8/20,,,,,,,,,,,,
|
||||||
|
4
CPLIST/input/test_trial.csv
Normal file
4
CPLIST/input/test_trial.csv
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
3,3,お試し・ファミリー,まゆちー,takoyaki_sena@icloud.com,ma0222,090-3309-0222,浅田舞子,1986/02/22,浅田真結菜,2014/03/30,森美紀,1988/03/06,森千晴,2017/8/4,,,,,,,,
|
||||||
|
4,3,お試し・ファミリー,ガンバルゾー,youkeymr.01@gmail.com,mo3540,090-8962-3540,森祐貴,1985/9/26,浅田直之,1987/12/12,浅田晃汰,2014/01/06,森光喜,2015/4/22,,,,,,,,
|
||||||
|
7,5,お試し,ランエンジョン!,baycools16@gmail.com,ka9749,090÷4790÷9749,河合賢次,1972/12/14,中野真樹,1973/01/23,,,,,,,,,,,,
|
||||||
|
187
DEPLOYMENT_MIGRATION_GUIDE.md
Normal file
187
DEPLOYMENT_MIGRATION_GUIDE.md
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# Deploy先でのMigration手順ガイド
|
||||||
|
|
||||||
|
## 推奨手順(安全なアプローチ)
|
||||||
|
|
||||||
|
### パターンA: 新規クリーンDeployment(推奨)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 旧DBのバックアップ作成
|
||||||
|
pg_dump rogaining_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 2. Git pullで最新コード取得
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 3. migration_simple_reset.pyで一括リセット(推奨)
|
||||||
|
docker compose exec app python migration_simple_reset.py --full
|
||||||
|
|
||||||
|
# 4. 必要に応じてデータ復元スクリプト実行
|
||||||
|
# (既存データがある場合)
|
||||||
|
```
|
||||||
|
|
||||||
|
### パターンB: 段階的Migration修正
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 旧DBのバックアップ作成
|
||||||
|
pg_dump rogaining_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 2. Git pullで最新コード取得
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 3. 問題のあるmigrationファイルを一時的に削除
|
||||||
|
rm rog/migrations/0011_auto_20250827_1459.py
|
||||||
|
|
||||||
|
# 4. 正常なmigrationまで適用
|
||||||
|
docker compose exec app python manage.py migrate
|
||||||
|
|
||||||
|
# 5. migration_simple_reset.pyでクリーンアップ
|
||||||
|
docker compose exec app python migration_simple_reset.py --reset-only
|
||||||
|
docker compose exec app python migration_simple_reset.py --apply-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 元の提案手順の問題点
|
||||||
|
|
||||||
|
```bash
|
||||||
|
1)旧DBをリストア ✅ OK
|
||||||
|
2)Git pull で最新コード設置 ✅ OK
|
||||||
|
3)migrate してDB更新 ❌ 問題: 依存関係エラーで失敗する
|
||||||
|
4)migration_simple_reset.py実行 ✅ OK
|
||||||
|
```
|
||||||
|
|
||||||
|
**問題**: ステップ3で`NodeNotFoundError`が発生し、migrationが失敗します。
|
||||||
|
|
||||||
|
## 具体的なDeployment手順(本番推奨)
|
||||||
|
|
||||||
|
### 事前準備
|
||||||
|
```bash
|
||||||
|
# 本番環境への接続確認
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# 現在のmigration状態確認
|
||||||
|
docker compose exec app python manage.py showmigrations
|
||||||
|
```
|
||||||
|
|
||||||
|
### 実行手順
|
||||||
|
|
||||||
|
#### Step 1: バックアップ作成
|
||||||
|
```bash
|
||||||
|
# データベースバックアップ
|
||||||
|
docker compose exec postgres-db pg_dump -U admin rogaining_db > deploy_backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 現在のmigrationファイルバックアップ
|
||||||
|
cp -r rog/migrations rog/migrations_backup_deploy_$(date +%Y%m%d_%H%M%S)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: コード更新
|
||||||
|
```bash
|
||||||
|
# 最新コード取得
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# migration_simple_reset.pyが存在することを確認
|
||||||
|
ls -la migration_simple_reset.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Migration リセット実行
|
||||||
|
```bash
|
||||||
|
# 全体的なリセット(推奨)
|
||||||
|
docker compose exec app python migration_simple_reset.py --full
|
||||||
|
```
|
||||||
|
|
||||||
|
または段階的実行:
|
||||||
|
```bash
|
||||||
|
# バックアップのみ
|
||||||
|
docker compose exec app python migration_simple_reset.py --backup-only
|
||||||
|
|
||||||
|
# リセットのみ
|
||||||
|
docker compose exec app python migration_simple_reset.py --reset-only
|
||||||
|
|
||||||
|
# 適用のみ
|
||||||
|
docker compose exec app python migration_simple_reset.py --apply-only
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: 結果確認
|
||||||
|
```bash
|
||||||
|
# Migration状態確認
|
||||||
|
docker compose exec app python manage.py showmigrations
|
||||||
|
|
||||||
|
# アプリケーション動作確認
|
||||||
|
docker compose exec app python manage.py check
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 5: サービス再起動
|
||||||
|
```bash
|
||||||
|
# アプリケーション再起動
|
||||||
|
docker compose restart app
|
||||||
|
|
||||||
|
# 全サービス再起動(必要に応じて)
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### Migration失敗時の対処
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. migration_simple_reset.pyでクリーンアップ
|
||||||
|
docker compose exec app python migration_simple_reset.py --reset-only
|
||||||
|
|
||||||
|
# 2. 手動でmigration状態確認
|
||||||
|
docker compose exec app python manage.py showmigrations
|
||||||
|
|
||||||
|
# 3. 必要に応じて個別migration適用
|
||||||
|
docker compose exec app python manage.py migrate rog 0001 --fake
|
||||||
|
```
|
||||||
|
|
||||||
|
### バックアップからの復元
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# データベース復元
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogaining_db < backup_file.sql
|
||||||
|
|
||||||
|
# migrationファイル復元
|
||||||
|
rm -rf rog/migrations
|
||||||
|
cp -r rog/migrations_backup_deploy_YYYYMMDD_HHMMSS rog/migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
## 重要な注意事項
|
||||||
|
|
||||||
|
### ✅ 実行前チェックリスト
|
||||||
|
- [ ] データベースバックアップ作成済み
|
||||||
|
- [ ] migrationファイルバックアップ作成済み
|
||||||
|
- [ ] migration_simple_reset.pyが最新版
|
||||||
|
- [ ] Docker環境が正常動作中
|
||||||
|
- [ ] 十分なディスク容量確保
|
||||||
|
|
||||||
|
### ⚠️ 避けるべき操作
|
||||||
|
- `python manage.py migrate`を最初に実行(依存関係エラーの原因)
|
||||||
|
- バックアップなしでの作業
|
||||||
|
- 本番環境での実験的操作
|
||||||
|
|
||||||
|
### 🔄 ロールバック計画
|
||||||
|
```bash
|
||||||
|
# 問題発生時の緊急復元
|
||||||
|
docker compose down
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogaining_db < backup_file.sql
|
||||||
|
cp -r rog/migrations_backup_deploy_YYYYMMDD_HHMMSS rog/migrations
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
**推奨される最終手順:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. バックアップ作成
|
||||||
|
pg_dump rogaining_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 2. 最新コード取得
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 3. Migration一括リセット(問題を回避)
|
||||||
|
docker compose exec app python migration_simple_reset.py --full
|
||||||
|
|
||||||
|
# 4. 動作確認
|
||||||
|
docker compose exec app python manage.py check
|
||||||
|
docker compose restart app
|
||||||
|
```
|
||||||
|
|
||||||
|
この手順により、Migration依存関係の問題を回避し、安全にデプロイが可能になります。
|
||||||
321
DEPLOYMENT_MIGRATION_GUIDE_en.md
Normal file
321
DEPLOYMENT_MIGRATION_GUIDE_en.md
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
# Deployment Migration Guide for Production Environment
|
||||||
|
|
||||||
|
## Recommended Procedure (Safe Approach)
|
||||||
|
|
||||||
|
### Pattern A: Fresh Clean Deployment (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create backup of old database
|
||||||
|
pg_dump rogaining_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 2. Get latest code with Git pull
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 3. Perform batch reset with migration_simple_reset.py (Recommended)
|
||||||
|
docker compose exec app python migration_simple_reset.py --full
|
||||||
|
|
||||||
|
# 4. Execute data restoration scripts if needed
|
||||||
|
# (When existing data is present)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern B: Gradual Migration Fix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create backup of old database
|
||||||
|
pg_dump rogaining_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 2. Get latest code with Git pull
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 3. Temporarily remove problematic migration file
|
||||||
|
rm rog/migrations/0011_auto_20250827_1459.py
|
||||||
|
|
||||||
|
# 4. Apply migrations up to the last working one
|
||||||
|
docker compose exec app python manage.py migrate
|
||||||
|
|
||||||
|
# 5. Clean up with migration_simple_reset.py
|
||||||
|
docker compose exec app python migration_simple_reset.py --reset-only
|
||||||
|
docker compose exec app python migration_simple_reset.py --apply-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Issues with Original Proposed Procedure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
1) Restore old DB ✅ OK
|
||||||
|
2) Git pull to deploy latest code ✅ OK
|
||||||
|
3) Run migrate to update DB ❌ Problem: Will fail with dependency error
|
||||||
|
4) Execute migration_simple_reset.py ✅ OK
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue**: Step 3 will encounter `NodeNotFoundError` and migration will fail.
|
||||||
|
|
||||||
|
## Specific Deployment Procedure (Production Recommended)
|
||||||
|
|
||||||
|
### Pre-deployment Preparation
|
||||||
|
```bash
|
||||||
|
# Verify connection to production environment
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Check current migration status
|
||||||
|
docker compose exec app python manage.py showmigrations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution Steps
|
||||||
|
|
||||||
|
#### Step 1: Create Backups
|
||||||
|
```bash
|
||||||
|
# Database backup
|
||||||
|
docker compose exec postgres-db pg_dump -U admin rogaining_db > deploy_backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# Current migration files backup
|
||||||
|
cp -r rog/migrations rog/migrations_backup_deploy_$(date +%Y%m%d_%H%M%S)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Code Update
|
||||||
|
```bash
|
||||||
|
# Get latest code
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Verify migration_simple_reset.py exists
|
||||||
|
ls -la migration_simple_reset.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Execute Migration Reset
|
||||||
|
```bash
|
||||||
|
# Complete reset (recommended)
|
||||||
|
docker compose exec app python migration_simple_reset.py --full
|
||||||
|
```
|
||||||
|
|
||||||
|
Or step-by-step execution:
|
||||||
|
```bash
|
||||||
|
# Backup only
|
||||||
|
docker compose exec app python migration_simple_reset.py --backup-only
|
||||||
|
|
||||||
|
# Reset only
|
||||||
|
docker compose exec app python migration_simple_reset.py --reset-only
|
||||||
|
|
||||||
|
# Apply only
|
||||||
|
docker compose exec app python migration_simple_reset.py --apply-only
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Verify Results
|
||||||
|
```bash
|
||||||
|
# Check migration status
|
||||||
|
docker compose exec app python manage.py showmigrations
|
||||||
|
|
||||||
|
# Verify application functionality
|
||||||
|
docker compose exec app python manage.py check
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 5: Restart Services
|
||||||
|
```bash
|
||||||
|
# Restart application
|
||||||
|
docker compose restart app
|
||||||
|
|
||||||
|
# Restart all services (if needed)
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Migration Failure Recovery
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clean up with migration_simple_reset.py
|
||||||
|
docker compose exec app python migration_simple_reset.py --reset-only
|
||||||
|
|
||||||
|
# 2. Manually check migration status
|
||||||
|
docker compose exec app python manage.py showmigrations
|
||||||
|
|
||||||
|
# 3. Apply individual migrations if needed
|
||||||
|
docker compose exec app python manage.py migrate rog 0001 --fake
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore from Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database restoration
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogaining_db < backup_file.sql
|
||||||
|
|
||||||
|
# Migration files restoration
|
||||||
|
rm -rf rog/migrations
|
||||||
|
cp -r rog/migrations_backup_deploy_YYYYMMDD_HHMMSS rog/migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Considerations
|
||||||
|
|
||||||
|
### ✅ Pre-execution Checklist
|
||||||
|
- [ ] Database backup created
|
||||||
|
- [ ] Migration files backup created
|
||||||
|
- [ ] migration_simple_reset.py is latest version
|
||||||
|
- [ ] Docker environment running normally
|
||||||
|
- [ ] Sufficient disk space available
|
||||||
|
|
||||||
|
### ⚠️ Operations to Avoid
|
||||||
|
- Running `python manage.py migrate` first (causes dependency errors)
|
||||||
|
- Working without backups
|
||||||
|
- Experimental operations in production environment
|
||||||
|
|
||||||
|
### 🔄 Rollback Plan
|
||||||
|
```bash
|
||||||
|
# Emergency restoration when issues occur
|
||||||
|
docker compose down
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogaining_db < backup_file.sql
|
||||||
|
cp -r rog/migrations_backup_deploy_YYYYMMDD_HHMMSS rog/migrations
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Recommended Final Procedure:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create backup
|
||||||
|
pg_dump rogaining_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 2. Get latest code
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 3. Batch migration reset (avoids issues)
|
||||||
|
docker compose exec app python migration_simple_reset.py --full
|
||||||
|
|
||||||
|
# 4. Verify functionality
|
||||||
|
docker compose exec app python manage.py check
|
||||||
|
docker compose restart app
|
||||||
|
```
|
||||||
|
|
||||||
|
This procedure avoids migration dependency issues and enables safe deployment.
|
||||||
|
|
||||||
|
## Command Reference
|
||||||
|
|
||||||
|
### migration_simple_reset.py Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Complete workflow
|
||||||
|
python migration_simple_reset.py --full
|
||||||
|
|
||||||
|
# Backup only
|
||||||
|
python migration_simple_reset.py --backup-only
|
||||||
|
|
||||||
|
# Reset only (requires existing backup)
|
||||||
|
python migration_simple_reset.py --reset-only
|
||||||
|
|
||||||
|
# Apply only (requires simple migration to exist)
|
||||||
|
python migration_simple_reset.py --apply-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check service status
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Execute commands in app container
|
||||||
|
docker compose exec app [command]
|
||||||
|
|
||||||
|
# Execute commands in database container
|
||||||
|
docker compose exec postgres-db [command]
|
||||||
|
|
||||||
|
# Restart specific service
|
||||||
|
docker compose restart [service_name]
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs [service_name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create database backup
|
||||||
|
docker compose exec postgres-db pg_dump -U admin rogaining_db > backup.sql
|
||||||
|
|
||||||
|
# Restore database
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogaining_db < backup.sql
|
||||||
|
|
||||||
|
# Connect to database shell
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogaining_db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Scenarios and Solutions
|
||||||
|
|
||||||
|
### Scenario 1: Migration Dependency Error
|
||||||
|
**Error**: `NodeNotFoundError: Migration rog.0010_auto_20250827_1510 dependencies reference nonexistent parent node`
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
docker compose exec app python migration_simple_reset.py --full
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: Database Connection Error
|
||||||
|
**Error**: Database connection issues during migration
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Check database status
|
||||||
|
docker compose ps postgres-db
|
||||||
|
|
||||||
|
# Restart database if needed
|
||||||
|
docker compose restart postgres-db
|
||||||
|
|
||||||
|
# Wait for database to be ready
|
||||||
|
docker compose exec postgres-db pg_isready -U admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: Disk Space Issues
|
||||||
|
**Error**: Insufficient disk space during backup or migration
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Check disk usage
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Clean up Docker resources
|
||||||
|
docker system prune
|
||||||
|
|
||||||
|
# Remove old backups if safe
|
||||||
|
rm old_backup_files.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 4: Permission Issues
|
||||||
|
**Error**: Permission denied when executing scripts
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Make script executable
|
||||||
|
chmod +x migration_simple_reset.py
|
||||||
|
|
||||||
|
# Check file ownership
|
||||||
|
ls -la migration_simple_reset.py
|
||||||
|
|
||||||
|
# Fix ownership if needed
|
||||||
|
chown user:group migration_simple_reset.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Always Create Backups
|
||||||
|
- Database backup before any migration operation
|
||||||
|
- Migration files backup for rollback capability
|
||||||
|
- Configuration files backup
|
||||||
|
|
||||||
|
### 2. Test in Staging Environment
|
||||||
|
- Verify the migration procedure in staging first
|
||||||
|
- Test with production-like data volume
|
||||||
|
- Validate application functionality after migration
|
||||||
|
|
||||||
|
### 3. Monitor During Deployment
|
||||||
|
- Watch container logs during migration
|
||||||
|
- Monitor database performance
|
||||||
|
- Check application health endpoints
|
||||||
|
|
||||||
|
### 4. Document Changes
|
||||||
|
- Record migration procedure execution
|
||||||
|
- Note any deviations from standard procedure
|
||||||
|
- Update deployment documentation
|
||||||
|
|
||||||
|
### 5. Plan for Rollback
|
||||||
|
- Have clear rollback procedures ready
|
||||||
|
- Test rollback in staging environment
|
||||||
|
- Ensure backups are valid and accessible
|
||||||
|
|
||||||
|
This guide ensures safe and reliable deployment of the rogaining_srv application with proper migration handling.
|
||||||
21
Dockerfile.event_registration
Normal file
21
Dockerfile.event_registration
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 必要なパッケージをインストール
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Python依存関係をインストール
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# アプリケーションコードをコピー
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# スクリプトに実行権限を付与
|
||||||
|
RUN chmod +x register_event_users.py
|
||||||
|
|
||||||
|
# デフォルトコマンド
|
||||||
|
CMD ["python", "register_event_users.py", "--help"]
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# FROM python:3.9.9-slim-buster
|
# FROM python:3.9.9-slim-buster
|
||||||
FROM osgeo/gdal:ubuntu-small-3.4.0
|
FROM osgeo/gdal:ubuntu-small-3.4.0
|
||||||
|
# Install GDAL dependencies
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ ENV CPLUS_INCLUDE_PATH=/usr/include/gdal
|
|||||||
ENV C_INCLUDE_PATH=/usr/include/gdal
|
ENV C_INCLUDE_PATH=/usr/include/gdal
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install netcat gcc postgresql \
|
&& apt-get -y install netcat gcc postgresql curl \
|
||||||
&& apt-get clean
|
&& apt-get clean
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
|||||||
243
EMAIL_SENDING_MANUAL.md
Normal file
243
EMAIL_SENDING_MANUAL.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# チームメール送信システム操作マニュアル
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
このシステムは、ロゲイニング大会の参加チームに対して、パスワードやイベント情報を含むメールを一括送信するためのDjango管理コマンドです。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
### 必要な環境
|
||||||
|
- Docker Compose環境が稼働していること
|
||||||
|
- PostgreSQLデータベースが接続されていること
|
||||||
|
- SMTPサーバー設定が完了していること(Outlook: smtp.outlook.com:587)
|
||||||
|
|
||||||
|
### 必要なファイル
|
||||||
|
1. **CSVファイル**: チーム情報を含むデータファイル
|
||||||
|
2. **メールテンプレートファイル**:
|
||||||
|
- `/templates/emails/team_registration_subject.txt` (件名テンプレート)
|
||||||
|
- `/templates/emails/team_registration_body.txt` (本文テンプレート)
|
||||||
|
|
||||||
|
## CSVファイル形式
|
||||||
|
|
||||||
|
### ファイル配置場所
|
||||||
|
```
|
||||||
|
CPLIST/input/team_mail.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSVファイルの形式
|
||||||
|
```csv
|
||||||
|
team_name,email,password,category,duration,leader_name,phone_number,member_names
|
||||||
|
チーム名,メールアドレス,パスワード,部門,時間,代表者名,電話番号,メンバー名
|
||||||
|
```
|
||||||
|
|
||||||
|
### 例
|
||||||
|
```csv
|
||||||
|
team_name,email,password,category,duration,leader_name,phone_number,member_names
|
||||||
|
ウエストサイド,hannivalscipio@gmail.com,west123,一般,3,田中太郎,090-1234-5678,田中太郎・佐藤花子
|
||||||
|
```
|
||||||
|
|
||||||
|
## メールテンプレート
|
||||||
|
|
||||||
|
### 件名テンプレート (`/templates/emails/team_registration_subject.txt`)
|
||||||
|
```
|
||||||
|
【岐阜ロゲ in 大垣】チーム「{{ team_name }}」パスワードのご連絡
|
||||||
|
```
|
||||||
|
|
||||||
|
### 本文テンプレート (`/templates/emails/team_registration_body.txt`)
|
||||||
|
```
|
||||||
|
{{ team_name }} 代表者 {{leader_name}} 様
|
||||||
|
|
||||||
|
岐阜ロゲ in 大垣 へのご参加ありがとうございます。
|
||||||
|
|
||||||
|
ご連絡が大変遅くなり、申し訳ございません。
|
||||||
|
|
||||||
|
以下の内容でパスワードをお送りいたしますので、よろしくお願い申し上げます。
|
||||||
|
|
||||||
|
■ チーム情報
|
||||||
|
|
||||||
|
チーム名: {{ team_name }}
|
||||||
|
部門: {{ category }}({{ duration }}時間)
|
||||||
|
|
||||||
|
ユーザー名: {{ email }}
|
||||||
|
パスワード: {{ password }}
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
岐阜ロゲ in 大垣
|
||||||
|
運営:NPO 岐阜aiネットワーク
|
||||||
|
```
|
||||||
|
|
||||||
|
### 利用可能な変数
|
||||||
|
- `{{ team_name }}` - チーム名
|
||||||
|
- `{{ email }}` - メールアドレス
|
||||||
|
- `{{ password }}` - パスワード
|
||||||
|
- `{{ category }}` - 部門
|
||||||
|
- `{{ duration }}` - 時間
|
||||||
|
- `{{ leader_name }}` - 代表者名
|
||||||
|
- `{{ phone_number }}` - 電話番号
|
||||||
|
- `{{ member_names }}` - メンバー名
|
||||||
|
|
||||||
|
## 操作手順
|
||||||
|
|
||||||
|
### 1. 事前準備
|
||||||
|
1. CSVファイルを `CPLIST/input/team_mail.csv` に配置
|
||||||
|
2. メールテンプレートファイルを確認・編集
|
||||||
|
3. Docker環境が起動していることを確認
|
||||||
|
|
||||||
|
### 2. ドライラン(テスト実行)
|
||||||
|
実際にメールを送信する前に、テスト実行を行います:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/rogaining_srv
|
||||||
|
docker compose exec app python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --dry_run
|
||||||
|
```
|
||||||
|
|
||||||
|
**ドライランの確認項目:**
|
||||||
|
- CSVファイルが正常に読み込まれるか
|
||||||
|
- テンプレートが正しく適用されるか
|
||||||
|
- 送信対象の件数が正しいか
|
||||||
|
- メールの件名・本文のプレビューが正しいか
|
||||||
|
|
||||||
|
### 3. 実際のメール送信
|
||||||
|
ドライランで問題がないことを確認後、実際の送信を行います:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/rogaining_srv
|
||||||
|
docker compose exec app python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 送信結果の確認
|
||||||
|
コマンド実行後、以下の情報が表示されます:
|
||||||
|
- 処理行数
|
||||||
|
- メール送信数
|
||||||
|
- エラーがあった場合のエラー内容
|
||||||
|
|
||||||
|
## コマンドオプション
|
||||||
|
|
||||||
|
### 基本コマンド
|
||||||
|
```bash
|
||||||
|
python manage.py send_team_emails --csv_file='<CSVファイルパス>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### オプション一覧
|
||||||
|
- `--csv_file`: CSVファイルのパス(必須)
|
||||||
|
- `--dry_run`: ドライラン(テスト実行)モード
|
||||||
|
- `--delay`: メール送信間隔(秒)デフォルト: 1秒
|
||||||
|
|
||||||
|
### 使用例
|
||||||
|
```bash
|
||||||
|
# ドライランモード
|
||||||
|
python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --dry_run
|
||||||
|
|
||||||
|
# 実際の送信
|
||||||
|
python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv'
|
||||||
|
|
||||||
|
# 送信間隔を3秒に設定
|
||||||
|
python manage.py send_team_emails --csv_file='CPLIST/input/team_mail.csv' --delay=3
|
||||||
|
```
|
||||||
|
|
||||||
|
## エラー対処法
|
||||||
|
|
||||||
|
### よくあるエラーと対処法
|
||||||
|
|
||||||
|
#### 1. CSVファイルが見つからない
|
||||||
|
```
|
||||||
|
CommandError: CSVファイルが見つかりません: CPLIST/input/team_mail.csv
|
||||||
|
```
|
||||||
|
**対処法:**
|
||||||
|
- ファイルパスを確認
|
||||||
|
- ファイル名のスペルミスをチェック
|
||||||
|
- ファイルが存在することを `ls -la CPLIST/input/` で確認
|
||||||
|
|
||||||
|
#### 2. テンプレートファイルが見つからない
|
||||||
|
```
|
||||||
|
TemplateDoesNotExist: emails/team_registration_subject.txt
|
||||||
|
```
|
||||||
|
**対処法:**
|
||||||
|
- テンプレートファイルが正しい場所に配置されているか確認
|
||||||
|
- ファイル名が正しいかチェック
|
||||||
|
- Docker容器を再起動: `docker compose restart app`
|
||||||
|
|
||||||
|
#### 3. SMTP接続エラー
|
||||||
|
```
|
||||||
|
SMTPException: SMTP Auth failure
|
||||||
|
```
|
||||||
|
**対処法:**
|
||||||
|
- メールサーバー設定を確認
|
||||||
|
- 認証情報(ユーザー名・パスワード)を確認
|
||||||
|
- ネットワーク接続を確認
|
||||||
|
|
||||||
|
#### 4. CSV読み込みエラー
|
||||||
|
**対処法:**
|
||||||
|
- CSVファイルの文字エンコーディング(UTF-8 BOM)を確認
|
||||||
|
- CSVヘッダーが正しいか確認
|
||||||
|
- 必須フィールドが欠けていないかチェック
|
||||||
|
|
||||||
|
## セキュリティ注意事項
|
||||||
|
|
||||||
|
1. **パスワード情報の取り扱い**
|
||||||
|
- CSVファイルには機密情報が含まれるため、適切なアクセス権限を設定
|
||||||
|
- 送信完了後はCSVファイルを安全な場所に移動またはバックアップ
|
||||||
|
|
||||||
|
2. **メール送信記録**
|
||||||
|
- 送信ログを保存し、送信状況を記録
|
||||||
|
- 重複送信を避けるため、送信済みチームを管理
|
||||||
|
|
||||||
|
3. **レート制限**
|
||||||
|
- 大量送信時はレート制限を考慮し、適切な間隔を設定
|
||||||
|
- SMTPサーバーの制限を確認
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### Docker関連
|
||||||
|
```bash
|
||||||
|
# コンテナの状態確認
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# コンテナの再起動
|
||||||
|
docker compose restart app
|
||||||
|
|
||||||
|
# ログの確認
|
||||||
|
docker compose logs app
|
||||||
|
```
|
||||||
|
|
||||||
|
### データベース接続確認
|
||||||
|
```bash
|
||||||
|
# データベース接続テスト
|
||||||
|
docker compose exec app python manage.py dbshell
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSVファイル確認
|
||||||
|
```bash
|
||||||
|
# ファイル存在確認
|
||||||
|
ls -la CPLIST/input/
|
||||||
|
|
||||||
|
# ファイル内容確認
|
||||||
|
head -5 CPLIST/input/team_mail.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
## 運用Tips
|
||||||
|
|
||||||
|
1. **バッチ送信**
|
||||||
|
- 大量のメール送信時は、CSVファイルを分割して複数回に分けて送信
|
||||||
|
- 送信間隔を適切に設定してサーバー負荷を軽減
|
||||||
|
|
||||||
|
2. **テスト環境での確認**
|
||||||
|
- 本番送信前に、テスト用メールアドレスでの動作確認を推奨
|
||||||
|
- ドライランを必ず実行
|
||||||
|
|
||||||
|
3. **バックアップ**
|
||||||
|
- 送信前にCSVファイルとテンプレートファイルをバックアップ
|
||||||
|
- 送信ログを保存
|
||||||
|
|
||||||
|
## 更新履歴
|
||||||
|
|
||||||
|
- 2025年9月5日: 初版作成
|
||||||
|
- 基本的なメール送信機能
|
||||||
|
- Django テンプレートシステム統合
|
||||||
|
- Outlook SMTP設定対応
|
||||||
|
|
||||||
|
## 連絡先
|
||||||
|
|
||||||
|
システムに関する問い合わせ:
|
||||||
|
- 運営:NPO 岐阜aiネットワーク
|
||||||
|
- メール:rogaining@gifuai.net
|
||||||
204
EVENT_REGISTRATION_README.md
Normal file
204
EVENT_REGISTRATION_README.md
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# イベントユーザー登録システム
|
||||||
|
|
||||||
|
外部システムAPI仕様書.mdを前提に、ユーザーデータCSVから各ユーザーごとにユーザー登録、チーム登録、エントリー登録、イベント参加を行うPythonスクリプトです。
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
このシステムは以下の処理を自動化します:
|
||||||
|
|
||||||
|
1. **カスタムユーザー登録 API**
|
||||||
|
- メールアドレスをキーに既存ユーザーを取得
|
||||||
|
- 検索がヒットしなければ、ユーザー登録
|
||||||
|
- 検索がヒットすれば、パスワードを更新
|
||||||
|
- event_codeに指定event_codeを設定
|
||||||
|
- zekken_number にゼッケン番号を入力
|
||||||
|
- team_name にチーム名を入力
|
||||||
|
|
||||||
|
2. **チーム登録、メンバー登録**
|
||||||
|
- 部門・時間・チーム名でチーム登録
|
||||||
|
- メンバーを1名ずつ7名まで登録
|
||||||
|
- それぞれダミーメールアドレスと名前と生年月日でメンバー登録
|
||||||
|
|
||||||
|
3. **エントリー登録**
|
||||||
|
- 指定されたイベントにチームを登録
|
||||||
|
|
||||||
|
4. **イベント参加**
|
||||||
|
- 登録したエントリーでイベント参加
|
||||||
|
|
||||||
|
## CSVファイル形式
|
||||||
|
|
||||||
|
CSVファイル(`CPLIST/input/team2025.csv`)は以下の項目を持ちます:
|
||||||
|
|
||||||
|
```
|
||||||
|
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 項目説明
|
||||||
|
|
||||||
|
- **部門別数**: 部門の番号
|
||||||
|
- **時間**: 競技時間
|
||||||
|
- **部門**: 競技部門名
|
||||||
|
- **チーム名**: チーム名
|
||||||
|
- **メール**: 代表者メールアドレス
|
||||||
|
- **パスワード**: パスワード
|
||||||
|
- **電話番号**: 代表者電話番号
|
||||||
|
- **氏名1〜7**: チームメンバーの氏名(最大7名)
|
||||||
|
- **誕生日1〜7**: チームメンバーの生年月日(YYYY/MM/DD形式)
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 基本的な実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# デフォルトイベントコード(大垣2509)で実行
|
||||||
|
./run_event_registration.sh
|
||||||
|
|
||||||
|
# 指定したイベントコードで実行
|
||||||
|
./run_event_registration.sh "大垣2509"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. テスト実行(DRY RUN)
|
||||||
|
|
||||||
|
実際のAPI呼び出しを行わずに処理の流れを確認:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run_event_registration.sh "大垣2509" --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. カスタムCSVファイルを使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run_event_registration.sh "大垣2509" --csv-file CPLIST/input/custom_teams.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. カスタムAPI URLを指定
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run_event_registration.sh "大垣2509" --base-url http://production-server:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Pythonスクリプトを直接実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python register_event_users.py --event_code "大垣2509" --csv_file CPLIST/input/team2025.csv --dry_run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Composeでの実行
|
||||||
|
|
||||||
|
### 環境変数設定
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export EVENT_CODE="大垣2509"
|
||||||
|
export CSV_FILE="CPLIST/input/team2025.csv"
|
||||||
|
export BASE_URL="http://web:8000"
|
||||||
|
export DRY_RUN="true" # テスト実行の場合
|
||||||
|
```
|
||||||
|
|
||||||
|
### 実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.event-registration.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## オプション
|
||||||
|
|
||||||
|
| オプション | 説明 | デフォルト値 |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `--event_code` | イベントコード | 必須 |
|
||||||
|
| `--csv_file` | CSVファイルパス | `CPLIST/input/team2025.csv` |
|
||||||
|
| `--base_url` | APIベースURL | `http://localhost:8000` |
|
||||||
|
| `--dry_run` | テスト実行フラグ | False |
|
||||||
|
|
||||||
|
## ログ
|
||||||
|
|
||||||
|
- 実行ログは `logs/register_event_users.log` に出力されます
|
||||||
|
- コンソールにも同時出力されます
|
||||||
|
|
||||||
|
## 処理統計
|
||||||
|
|
||||||
|
処理完了後、以下の統計情報が表示されます:
|
||||||
|
|
||||||
|
- 処理完了チーム数
|
||||||
|
- 作成ユーザー数
|
||||||
|
- 更新ユーザー数
|
||||||
|
- 登録チーム数
|
||||||
|
- 作成エントリー数
|
||||||
|
- 参加登録数
|
||||||
|
- エラー数とその詳細
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
1. **API認証**: システムが稼働していることを確認してください
|
||||||
|
2. **CSVファイル**: 必要な項目が正しく入力されていることを確認してください
|
||||||
|
3. **重複処理**: 同じデータを複数回実行すると重複エラーが発生する可能性があります
|
||||||
|
4. **メール認証**: 新規ユーザー登録時はメール認証が必要な場合があります
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### よくあるエラー
|
||||||
|
|
||||||
|
1. **CSVファイルが見つからない**
|
||||||
|
```
|
||||||
|
エラー: CSVファイルが見つかりません: CPLIST/input/team2025.csv
|
||||||
|
```
|
||||||
|
→ CSVファイルのパスを確認してください
|
||||||
|
|
||||||
|
2. **API接続エラー**
|
||||||
|
```
|
||||||
|
エラー: APIサーバーに接続できません
|
||||||
|
```
|
||||||
|
→ BASE_URLが正しいか、サーバーが稼働しているか確認してください
|
||||||
|
|
||||||
|
3. **重複ゼッケン番号エラー**
|
||||||
|
```
|
||||||
|
チーム登録エラー: このゼッケン番号は既に使用されています
|
||||||
|
```
|
||||||
|
→ 既に登録済みのデータを再実行しようとしています
|
||||||
|
|
||||||
|
### ログの確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# リアルタイムでログを確認
|
||||||
|
tail -f logs/register_event_users.log
|
||||||
|
|
||||||
|
# エラーのみを確認
|
||||||
|
grep ERROR logs/register_event_users.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 開発者向け情報
|
||||||
|
|
||||||
|
### ファイル構成
|
||||||
|
|
||||||
|
```
|
||||||
|
rogaining_srv/
|
||||||
|
├── register_event_users.py # メインスクリプト
|
||||||
|
├── run_event_registration.sh # 実行スクリプト
|
||||||
|
├── docker-compose.event-registration.yml # Docker Compose設定
|
||||||
|
├── Dockerfile.event_registration # Dockerfile
|
||||||
|
├── CPLIST/input/team2025.csv # CSVデータファイル
|
||||||
|
└── logs/register_event_users.log # ログファイル
|
||||||
|
```
|
||||||
|
|
||||||
|
### API エンドポイント
|
||||||
|
|
||||||
|
使用するAPIエンドポイント:
|
||||||
|
|
||||||
|
- `POST /api/register/` - ユーザー仮登録
|
||||||
|
- `POST /api/login/` - ログイン
|
||||||
|
- `POST /api/register_team` - チーム登録
|
||||||
|
- `POST /api/teams/{team_id}/members/` - メンバー追加
|
||||||
|
- `POST /api/entry/` - エントリー登録
|
||||||
|
- `POST /api/start_from_rogapp` - イベント参加
|
||||||
|
|
||||||
|
### カスタマイズ
|
||||||
|
|
||||||
|
処理をカスタマイズする場合は、`register_event_users.py`の以下のメソッドを編集してください:
|
||||||
|
|
||||||
|
- `get_or_create_user()` - ユーザー登録ロジック
|
||||||
|
- `register_team_and_members()` - チーム登録ロジック
|
||||||
|
- `create_event_entry()` - エントリー登録ロジック
|
||||||
|
- `participate_in_event()` - イベント参加ロジック
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
このプロジェクトはロゲイニングシステムの一部です。
|
||||||
202
LOCATION_INTERACTION_SYSTEM_README.md
Normal file
202
LOCATION_INTERACTION_SYSTEM_README.md
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
# Location Interaction System - evaluation_value Based Implementation
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
LocationモデルのDestinationにuse_qr_codeフラグとevaluation_valueフィールドを使用した、拡張されたロケーションインタラクションシステムを実装しました。
|
||||||
|
|
||||||
|
## システム構成
|
||||||
|
|
||||||
|
### 1. Locationモデル拡張
|
||||||
|
|
||||||
|
**ファイル**: `rog/models.py`
|
||||||
|
|
||||||
|
- `evaluation_value` フィールドを使用してインタラクションタイプを決定
|
||||||
|
- 値の意味:
|
||||||
|
- `"0"` または `null`: 通常ポイント
|
||||||
|
- `"1"`: 写真撮影 + 買い物ポイント
|
||||||
|
- `"2"`: QRコードスキャン + クイズ回答
|
||||||
|
|
||||||
|
### 2. ビジネスロジック
|
||||||
|
|
||||||
|
**ファイル**: `rog/location_interaction.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
# インタラクションタイプ定数
|
||||||
|
INTERACTION_TYPE_NORMAL = "0" # 通常ポイント
|
||||||
|
INTERACTION_TYPE_PHOTO = "1" # 写真撮影ポイント
|
||||||
|
INTERACTION_TYPE_QR_QUIZ = "2" # QRコード + クイズポイント
|
||||||
|
|
||||||
|
# 主要関数
|
||||||
|
- get_interaction_type(location): ロケーションのインタラクションタイプを判定
|
||||||
|
- validate_interaction_requirements(location, request_data): 必要なデータの検証
|
||||||
|
- get_point_calculation(location, interaction_result): ポイント計算
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. チェックインAPI
|
||||||
|
|
||||||
|
**ファイル**: `rog/location_checkin_view.py`
|
||||||
|
|
||||||
|
**エンドポイント**: `POST /api/location-checkin/`
|
||||||
|
|
||||||
|
**リクエスト形式**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"location_id": 123,
|
||||||
|
"latitude": 35.1234,
|
||||||
|
"longitude": 136.5678,
|
||||||
|
"photo": "base64_encoded_image_data", // evaluation_value="1"の場合必須
|
||||||
|
"qr_code_data": "{\"quiz_id\": 1, \"correct_answer\": \"答え\"}", // evaluation_value="2"の場合必須
|
||||||
|
"quiz_answer": "ユーザーの回答" // evaluation_value="2"の場合必須
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**レスポンス形式**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"checkin_id": 456,
|
||||||
|
"points_awarded": 10,
|
||||||
|
"point_type": "photo_shopping",
|
||||||
|
"message": "写真撮影が完了しました。買い物ポイントを獲得!",
|
||||||
|
"location_name": "ロケーション名",
|
||||||
|
"interaction_type": "1",
|
||||||
|
"interaction_result": {
|
||||||
|
"photo_saved": true,
|
||||||
|
"photo_filename": "checkin_123_20250103_143022.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. APIデータ拡張
|
||||||
|
|
||||||
|
**ファイル**: `rog/serializers.py`
|
||||||
|
|
||||||
|
LocationSerializerを拡張して、以下の情報を追加:
|
||||||
|
- `interaction_type`: インタラクションタイプ ("0", "1", "2")
|
||||||
|
- `requires_photo`: 写真撮影が必要かどうか
|
||||||
|
- `requires_qr_code`: QRコードスキャンが必要かどうか
|
||||||
|
- `interaction_instructions`: ユーザー向け指示メッセージ
|
||||||
|
|
||||||
|
### 5. テスト用Webインターフェース
|
||||||
|
|
||||||
|
**ファイル**: `templates/location_checkin_test.html`
|
||||||
|
|
||||||
|
**アクセス**: `/api/location-checkin-test/`
|
||||||
|
|
||||||
|
機能:
|
||||||
|
- ロケーション一覧の表示
|
||||||
|
- evaluation_valueに基づく要件の表示
|
||||||
|
- 写真アップロード (evaluation_value="1")
|
||||||
|
- QRデータ・クイズ入力 (evaluation_value="2")
|
||||||
|
- チェックイン実行とテスト
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 通常ポイント (evaluation_value="0")
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const data = {
|
||||||
|
location_id: 123,
|
||||||
|
latitude: 35.1234,
|
||||||
|
longitude: 136.5678
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/api/location-checkin/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 写真撮影ポイント (evaluation_value="1")
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const data = {
|
||||||
|
location_id: 123,
|
||||||
|
latitude: 35.1234,
|
||||||
|
longitude: 136.5678,
|
||||||
|
photo: "base64_encoded_image_data" // 写真必須
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. QRコード + クイズポイント (evaluation_value="2")
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const data = {
|
||||||
|
location_id: 123,
|
||||||
|
latitude: 35.1234,
|
||||||
|
longitude: 136.5678,
|
||||||
|
qr_code_data: '{"quiz_id": 1, "correct_answer": "岐阜城"}', // QRコードデータ
|
||||||
|
quiz_answer: "岐阜城" // ユーザーの回答
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## ポイント計算システム
|
||||||
|
|
||||||
|
### 基本ポイント
|
||||||
|
- 通常ポイント: 10ポイント
|
||||||
|
- 写真撮影ポイント: 15ポイント
|
||||||
|
- QRコード + クイズポイント: 20ポイント (正解時)
|
||||||
|
|
||||||
|
### ボーナスポイント
|
||||||
|
- クイズ正解ボーナス: +5ポイント
|
||||||
|
- 写真保存成功ボーナス: +2ポイント
|
||||||
|
|
||||||
|
## エラーハンドリング
|
||||||
|
|
||||||
|
### 検証エラー
|
||||||
|
- 必須フィールド不足
|
||||||
|
- 距離制限外
|
||||||
|
- 写真データ不正
|
||||||
|
- QRコードデータ不正
|
||||||
|
|
||||||
|
### 処理エラー
|
||||||
|
- 写真保存失敗
|
||||||
|
- データベースエラー
|
||||||
|
- ネットワークエラー
|
||||||
|
|
||||||
|
## セキュリティ考慮事項
|
||||||
|
|
||||||
|
1. **認証**: `@login_required`デコレータでユーザー認証必須
|
||||||
|
2. **CSRF**: `@csrf_exempt`だが、トークン検証推奨
|
||||||
|
3. **距離検証**: Haversine公式による正確な距離計算
|
||||||
|
4. **データ検証**: 入力データの厳密な検証
|
||||||
|
|
||||||
|
## データベース影響
|
||||||
|
|
||||||
|
### 新規追加なし
|
||||||
|
- 既存の`evaluation_value`フィールドを活用
|
||||||
|
- `Useractions`テーブルでチェックイン記録
|
||||||
|
|
||||||
|
### 推奨される追加フィールド (今後の拡張)
|
||||||
|
- `Location.checkin_radius`: チェックイン許可範囲
|
||||||
|
- `Location.use_qr_code`: QRコード使用フラグ
|
||||||
|
- `Location.quiz_data`: クイズデータ
|
||||||
|
|
||||||
|
## 今後の拡張予定
|
||||||
|
|
||||||
|
1. **写真検証**: AI による撮影内容検証
|
||||||
|
2. **QRコード生成**: 動的QRコード生成システム
|
||||||
|
3. **ゲーミフィケーション**: バッジ・称号システム
|
||||||
|
4. **リアルタイム**: WebSocket による即座反映
|
||||||
|
5. **統計**: インタラクション統計・分析
|
||||||
|
|
||||||
|
## テスト手順
|
||||||
|
|
||||||
|
1. テストページにアクセス: `/api/location-checkin-test/`
|
||||||
|
2. evaluation_valueが異なるロケーションを選択
|
||||||
|
3. 各インタラクションタイプでチェックイン実行
|
||||||
|
4. レスポンスの確認
|
||||||
|
|
||||||
|
## 関連ファイル
|
||||||
|
|
||||||
|
- `rog/models.py`: Locationモデル定義
|
||||||
|
- `rog/serializers.py`: LocationSerializer拡張
|
||||||
|
- `rog/location_interaction.py`: ビジネスロジック
|
||||||
|
- `rog/location_checkin_view.py`: チェックインAPI
|
||||||
|
- `rog/urls.py`: URL設定
|
||||||
|
- `templates/location_checkin_test.html`: テストインターフェース
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
この実装により、evaluation_valueに基づく柔軟なロケーションインタラクションシステムが完成しました。各ロケーションで異なるユーザー体験を提供し、ゲーミフィケーション要素を追加することで、より魅力的なロゲイニング体験を実現します。
|
||||||
293
MIGRATE_ENHANCED_README.md
Normal file
293
MIGRATE_ENHANCED_README.md
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
# Old RogDB → RogDB 移行手順書
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
old_rogdb から rogdb へのデータ移行を行います。テーブル構造の違いにより、一部テーブルは専用スクリプトで処理します。
|
||||||
|
|
||||||
|
## 移行対象テーブル
|
||||||
|
|
||||||
|
### 通常移行(migrate_old_rogdb_to_rogdb.py)
|
||||||
|
- rog_customuser
|
||||||
|
- rog_newcategory
|
||||||
|
- rog_newevent2
|
||||||
|
- rog_member
|
||||||
|
- rog_useractions
|
||||||
|
- その他 rog_* テーブル
|
||||||
|
|
||||||
|
### 専用移行スクリプト
|
||||||
|
|
||||||
|
#### 1. rog_team (migrate_rog_team_enhanced.py)
|
||||||
|
**理由**: 新DBで追加フィールドあり
|
||||||
|
- `class_name` (character varying(100))
|
||||||
|
- `event_id` (bigint) - rog_newevent2への外部キー
|
||||||
|
- `location` (geometry(Point,4326)) - PostGIS座標
|
||||||
|
- `password` (character varying(100))
|
||||||
|
- `trial` (boolean)
|
||||||
|
- `zekken_number` (character varying(50))
|
||||||
|
- `created_at` (timestamp with time zone)
|
||||||
|
- `updated_at` (timestamp with time zone)
|
||||||
|
|
||||||
|
#### 2. rog_entry (migrate_rog_entry_enhanced.py)
|
||||||
|
**理由**: camelCaseカラム名の予約語問題
|
||||||
|
- `hasGoaled` (boolean)
|
||||||
|
- `hasParticipated` (boolean)
|
||||||
|
|
||||||
|
#### 3. rog_goalimages (migrate_rog_goalimages_enhanced.py)
|
||||||
|
**理由**: team_name → zekken_number 変換ロジック
|
||||||
|
- 旧DBで`zekken_number`がブランク/NULLの場合
|
||||||
|
- `team_name`を使用してrog_entryから対応する`zekken_number`を検索・取得
|
||||||
|
- team_name → zekken_numberマッピングキャッシュを事前構築
|
||||||
|
|
||||||
|
## 移行手順
|
||||||
|
|
||||||
|
### 事前チェック
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# NULL値チェック
|
||||||
|
make null-check
|
||||||
|
|
||||||
|
# カラム名チェック
|
||||||
|
make column-check
|
||||||
|
|
||||||
|
# Docker コンテナ状況確認
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 段階的移行
|
||||||
|
|
||||||
|
#### ステップ1: 基本テーブル移行
|
||||||
|
```bash
|
||||||
|
# 通常テーブル移行(rog_team, rog_entry除く)
|
||||||
|
make migrate-old-rogdb
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ステップ2: rog_team構造変換移行
|
||||||
|
```bash
|
||||||
|
# rog_team専用移行
|
||||||
|
make migrate-rog-team
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ステップ3: rog_entry camelCase対応移行
|
||||||
|
```bash
|
||||||
|
# rog_entry専用移行
|
||||||
|
make migrate-rog-entry
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ステップ4: rog_goalimages team_name変換移行
|
||||||
|
```bash
|
||||||
|
# rog_goalimages専用移行(team_name→zekken_number変換)
|
||||||
|
make migrate-rog-goalimages
|
||||||
|
```
|
||||||
|
|
||||||
|
### 一括移行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 全テーブル一括移行
|
||||||
|
make migrate-full
|
||||||
|
```
|
||||||
|
|
||||||
|
## 外部キー依存関係
|
||||||
|
|
||||||
|
移行順序に注意が必要な依存関係:
|
||||||
|
|
||||||
|
1. **rog_customuser** → 他テーブルのowner_id, user_id参照
|
||||||
|
2. **rog_newcategory** → rog_team, rog_entryのcategory_id参照
|
||||||
|
3. **rog_newevent2** → rog_team, rog_entryのevent_id参照
|
||||||
|
4. **rog_team** → rog_entryのteam_id参照
|
||||||
|
5. **rog_entry** → rog_entrymemberのentry_id参照、rog_goalimadesのzekken_number解決
|
||||||
|
6. **rog_goalimages** → rog_customuserのuser_id参照、team_name→zekken_number変換
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### エラー対応
|
||||||
|
|
||||||
|
#### NULL値制約違反
|
||||||
|
```bash
|
||||||
|
# NULL値の詳細チェック
|
||||||
|
docker compose exec app python check_null_values.py
|
||||||
|
|
||||||
|
# 個別テーブルのNULL値確認
|
||||||
|
docker compose exec postgres-db psql -U admin -d old_rogdb -c "
|
||||||
|
SELECT column_name, COUNT(*)
|
||||||
|
FROM rog_team t, information_schema.columns c
|
||||||
|
WHERE c.table_name = 'rog_team' AND t.column_name IS NULL
|
||||||
|
GROUP BY column_name;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 外部キー制約違反
|
||||||
|
```bash
|
||||||
|
# 参照整合性チェック
|
||||||
|
docker compose exec postgres-db psql -U admin -d old_rogdb -c "
|
||||||
|
SELECT t.team_id, COUNT(*)
|
||||||
|
FROM rog_entry t
|
||||||
|
LEFT JOIN rog_team tt ON t.team_id = tt.id
|
||||||
|
WHERE tt.id IS NULL
|
||||||
|
GROUP BY t.team_id;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### team_name → zekken_number変換失敗
|
||||||
|
```bash
|
||||||
|
# rog_goalimagesのteam_name一覧確認
|
||||||
|
docker compose exec postgres-db psql -U admin -d old_rogdb -c "
|
||||||
|
SELECT DISTINCT team_name, zekken_number
|
||||||
|
FROM rog_goalimages
|
||||||
|
WHERE zekken_number IS NULL OR zekken_number = ''
|
||||||
|
ORDER BY team_name;
|
||||||
|
"
|
||||||
|
|
||||||
|
# 新DBでのteam_name → zekken_numberマッピング確認
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb -c "
|
||||||
|
SELECT t.team_name, e.zekken_number
|
||||||
|
FROM rog_team t
|
||||||
|
JOIN rog_entry e ON t.id = e.team_id
|
||||||
|
ORDER BY t.team_name;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PostgreSQL予約語エラー
|
||||||
|
- camelCaseカラムや予約語は自動でダブルクォートで囲まれます
|
||||||
|
- エラーが発生した場合は該当スクリプトで quote_column_if_needed() を確認
|
||||||
|
|
||||||
|
### ログ確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 移行ログのリアルタイム確認
|
||||||
|
docker compose logs -f app
|
||||||
|
|
||||||
|
# 特定期間のログ確認
|
||||||
|
docker compose logs --since="2025-08-25T08:00:00" app
|
||||||
|
```
|
||||||
|
|
||||||
|
## 設定値
|
||||||
|
|
||||||
|
### 環境変数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# データベース接続設定
|
||||||
|
OLD_ROGDB_HOST=postgres-db
|
||||||
|
OLD_ROGDB_NAME=old_rogdb
|
||||||
|
OLD_ROGDB_USER=admin
|
||||||
|
OLD_ROGDB_PASSWORD=admin123456
|
||||||
|
|
||||||
|
ROGDB_HOST=postgres-db
|
||||||
|
ROGDB_NAME=rogdb
|
||||||
|
ROGDB_USER=admin
|
||||||
|
ROGDB_PASSWORD=admin123456
|
||||||
|
|
||||||
|
# 除外テーブル設定(カンマ区切り)
|
||||||
|
EXCLUDE_TABLES=rog_session,django_migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
### デフォルト値設定
|
||||||
|
|
||||||
|
#### rog_team
|
||||||
|
- `trial`: False
|
||||||
|
- `event_id`: 最初のイベントID
|
||||||
|
- `location`: NULL
|
||||||
|
- `password`: ''
|
||||||
|
- `class_name`: ''
|
||||||
|
- `zekken_number`: ''
|
||||||
|
|
||||||
|
#### rog_entry
|
||||||
|
- `hasGoaled`: False
|
||||||
|
- `hasParticipated`: False
|
||||||
|
- `is_active`: True
|
||||||
|
- `is_trial`: False
|
||||||
|
- `zekken_label`: ''
|
||||||
|
|
||||||
|
## 移行後確認
|
||||||
|
|
||||||
|
### データ件数確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# テーブル別レコード数比較
|
||||||
|
docker compose exec postgres-db psql -U admin -d old_rogdb -c "
|
||||||
|
SELECT 'rog_team' as table_name, COUNT(*) as old_count FROM rog_team
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'rog_entry', COUNT(*) FROM rog_entry
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'rog_goalimages', COUNT(*) FROM rog_goalimages;
|
||||||
|
"
|
||||||
|
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb -c "
|
||||||
|
SELECT 'rog_team' as table_name, COUNT(*) as new_count FROM rog_team
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'rog_entry', COUNT(*) FROM rog_entry
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'rog_goalimages', COUNT(*) FROM rog_goalimages;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 制約確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 外部キー制約確認
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb -c "
|
||||||
|
SELECT conname, contype
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conrelid IN (
|
||||||
|
SELECT oid FROM pg_class WHERE relname IN ('rog_team', 'rog_entry', 'rog_goalimages')
|
||||||
|
);
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### team_name → zekken_number 変換確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# rog_goalimadesでzekken_number変換結果確認
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb -c "
|
||||||
|
SELECT team_name, zekken_number, COUNT(*) as count
|
||||||
|
FROM rog_goalimages
|
||||||
|
GROUP BY team_name, zekken_number
|
||||||
|
ORDER BY team_name;
|
||||||
|
"
|
||||||
|
|
||||||
|
# 変換できなかったレコード確認
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb -c "
|
||||||
|
SELECT team_name, COUNT(*) as blank_zekken_count
|
||||||
|
FROM rog_goalimages
|
||||||
|
WHERE zekken_number IS NULL OR zekken_number = ''
|
||||||
|
GROUP BY team_name
|
||||||
|
ORDER BY blank_zekken_count DESC;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## バックアップ・ロールバック
|
||||||
|
|
||||||
|
### 移行前バックアップ
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# rogdbのバックアップ
|
||||||
|
docker compose exec postgres-db pg_dump -U admin rogdb > rogdb_backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### ロールバック
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 移行テーブルのクリア
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb -c "
|
||||||
|
TRUNCATE rog_team, rog_entry, rog_goalimages CASCADE;
|
||||||
|
"
|
||||||
|
|
||||||
|
# バックアップからの復元
|
||||||
|
docker compose exec -T postgres-db psql -U admin -d rogdb < rogdb_backup_YYYYMMDD_HHMMSS.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## よくある問題
|
||||||
|
|
||||||
|
1. **メモリ不足**: docker-compose.ymlでPostgreSQLのメモリ制限を確認
|
||||||
|
2. **コンテナ再起動**: 移行中にコンテナが再起動する場合はresources設定を調整
|
||||||
|
3. **文字化け**: PostgreSQLの文字エンコーディング設定確認
|
||||||
|
4. **タイムアウト**: 大量データの場合はバッチサイズを調整
|
||||||
|
|
||||||
|
## 参考ファイル
|
||||||
|
|
||||||
|
- `docker-compose.yml`: データベース設定
|
||||||
|
- `migrate_old_rogdb_to_rogdb.py`: 通常テーブル移行
|
||||||
|
- `migrate_rog_team_enhanced.py`: rog_team専用移行
|
||||||
|
- `migrate_rog_entry_enhanced.py`: rog_entry専用移行
|
||||||
|
- `migrate_rog_goalimages_enhanced.py`: rog_goalimages専用移行(team_name→zekken変換)
|
||||||
|
- `check_null_values.py`: NULL値事前チェック
|
||||||
|
- `Makefile`: 移行タスク定義
|
||||||
205
MIGRATE_OLD_ROGDB_README.md
Normal file
205
MIGRATE_OLD_ROGDB_README.md
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
# Old RogDB → RogDB データ移行ガイド (エラー修正版)
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
old_rogdb データベースの `rog_*` テーブルから rogdb データベースの `rog_*` テーブルへデータを移行するスクリプトです。
|
||||||
|
|
||||||
|
## 修正点 (v3)
|
||||||
|
- PostgreSQL予約語(`like`など)のカラム名をクォートで囲む対応
|
||||||
|
- **キャメルケースカラム名**(`hasGoaled`, `deadlineDateTime`など)の自動クォート対応
|
||||||
|
- **NULL値の自動処理**:NOT NULL制約違反を防ぐデフォルト値設定
|
||||||
|
- トランザクションエラー時の自動ロールバック機能強化
|
||||||
|
- データベース接続のautocommit設定でトランザクション問題を回避
|
||||||
|
- より堅牢なエラーハンドリング
|
||||||
|
- カラム名事前チェック機能の追加
|
||||||
|
- NULL値事前チェック機能の追加
|
||||||
|
|
||||||
|
### NULL値デフォルト設定
|
||||||
|
以下のカラムで自動的にデフォルト値を設定:
|
||||||
|
- `trial`, `is_trial`: `False`
|
||||||
|
- `is_active`: `True`
|
||||||
|
- `hasGoaled`, `hasParticipated`: `False`
|
||||||
|
- `public`, `class_*`: `True`
|
||||||
|
- その他のBoolean型: 一般的なデフォルト値
|
||||||
|
|
||||||
|
## 機能
|
||||||
|
- 自動テーブル構造比較
|
||||||
|
- UPSERT操作(存在する場合は更新、しない場合は挿入)
|
||||||
|
- 主キーベースの重複チェック
|
||||||
|
- 詳細な移行統計レポート
|
||||||
|
- 予約語カラムの自動クォート処理
|
||||||
|
- エラーハンドリングとロールバック
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. Docker Compose での実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本実行
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# 環境変数を使用した実行
|
||||||
|
docker compose exec -e OLD_ROGDB_HOST=old-postgres app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# 特定テーブルを除外
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Makefileタスクの使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本移行
|
||||||
|
make migrate-old-rogdb
|
||||||
|
|
||||||
|
# カラム名チェックのみ
|
||||||
|
make check-columns
|
||||||
|
|
||||||
|
# NULL値チェックのみ
|
||||||
|
make check-null-values
|
||||||
|
|
||||||
|
# 完全な移行前チェック(カラム名 + NULL値)
|
||||||
|
make pre-migration-check
|
||||||
|
|
||||||
|
# 安全な移行(カラム名チェック + 移行実行)
|
||||||
|
make migrate-old-rogdb-safe
|
||||||
|
|
||||||
|
# 統計情報のみ表示
|
||||||
|
make migrate-rogdb-stats
|
||||||
|
|
||||||
|
# ドライラン(テーブル一覧のみ表示)
|
||||||
|
make migrate-rogdb-dryrun
|
||||||
|
```
|
||||||
|
|
||||||
|
## 環境変数
|
||||||
|
|
||||||
|
### Old RogDB 接続設定
|
||||||
|
```bash
|
||||||
|
OLD_ROGDB_HOST=postgres-db # デフォルト: postgres-db
|
||||||
|
OLD_ROGDB_NAME=old_rogdb # デフォルト: old_rogdb
|
||||||
|
OLD_ROGDB_USER=admin # デフォルト: admin
|
||||||
|
OLD_ROGDB_PASSWORD=admin123456 # デフォルト: admin123456
|
||||||
|
OLD_ROGDB_PORT=5432 # デフォルト: 5432
|
||||||
|
```
|
||||||
|
|
||||||
|
### RogDB 接続設定
|
||||||
|
```bash
|
||||||
|
ROGDB_HOST=postgres-db # デフォルト: postgres-db
|
||||||
|
ROGDB_NAME=rogdb # デフォルト: rogdb
|
||||||
|
ROGDB_USER=admin # デフォルト: admin
|
||||||
|
ROGDB_PASSWORD=admin123456 # デフォルト: admin123456
|
||||||
|
ROGDB_PORT=5432 # デフォルト: 5432
|
||||||
|
```
|
||||||
|
|
||||||
|
### その他の設定
|
||||||
|
```bash
|
||||||
|
EXCLUDE_TABLES=table1,table2 # 除外するテーブル(カンマ区切り)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 移行対象テーブル
|
||||||
|
|
||||||
|
スクリプトは `rog_` で始まる全てのテーブルを自動検出し、以下の処理を行います:
|
||||||
|
|
||||||
|
### 主要テーブル(例)
|
||||||
|
- `rog_customuser` - ユーザー情報
|
||||||
|
- `rog_newevent2` - イベント情報
|
||||||
|
- `rog_team` - チーム情報
|
||||||
|
- `rog_member` - メンバー情報
|
||||||
|
- `rog_entry` - エントリー情報
|
||||||
|
- `rog_location2025` - チェックポイント情報
|
||||||
|
- `rog_checkpoint` - チェックポイント記録
|
||||||
|
- その他 `rog_*` テーブル
|
||||||
|
|
||||||
|
### 移行ロジック
|
||||||
|
1. **テーブル構造比較**: 共通カラムのみを移行対象とする
|
||||||
|
2. **主キーチェック**: 既存レコードの有無を確認
|
||||||
|
3. **UPSERT操作**:
|
||||||
|
- 存在する場合: UPDATE(主キー以外のカラムを更新)
|
||||||
|
- 存在しない場合: INSERT(新規追加)
|
||||||
|
|
||||||
|
## 出力例
|
||||||
|
|
||||||
|
```
|
||||||
|
================================================================================
|
||||||
|
Old RogDB → RogDB データ移行開始
|
||||||
|
================================================================================
|
||||||
|
データベースに接続中...
|
||||||
|
✅ データベース接続成功
|
||||||
|
old_rogdb rog_テーブル: 15個
|
||||||
|
rogdb rog_テーブル: 15個
|
||||||
|
共通 rog_テーブル: 15個
|
||||||
|
移行対象テーブル (15個): ['rog_customuser', 'rog_newevent2', ...]
|
||||||
|
|
||||||
|
=== rog_customuser データ移行開始 ===
|
||||||
|
共通カラム (12個): ['date_joined', 'email', 'first_name', ...]
|
||||||
|
主キー: ['id']
|
||||||
|
移行対象レコード数: 50件
|
||||||
|
進捗: 50/50 件処理完了
|
||||||
|
✅ rog_customuser 移行完了:
|
||||||
|
挿入: 25件
|
||||||
|
更新: 25件
|
||||||
|
エラー: 0件
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
移行完了サマリー
|
||||||
|
================================================================================
|
||||||
|
処理対象テーブル: 15個
|
||||||
|
総挿入件数: 1250件
|
||||||
|
総更新件数: 750件
|
||||||
|
総エラー件数: 0件
|
||||||
|
|
||||||
|
--- テーブル別詳細 ---
|
||||||
|
rog_customuser: 挿入25, 更新25, エラー0
|
||||||
|
rog_newevent2: 挿入10, 更新5, エラー0
|
||||||
|
...
|
||||||
|
✅ 全ての移行が正常に完了しました!
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
1. **バックアップ推奨**: 移行前にrogdbのバックアップを取得してください
|
||||||
|
2. **権限確認**: 両データベースへの読み書き権限が必要です
|
||||||
|
3. **外部キー制約**: 移行順序によっては外部キー制約エラーが発生する可能性があります
|
||||||
|
4. **大量データ**: 大量データの場合は時間がかかる場合があります
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### よくあるエラー
|
||||||
|
|
||||||
|
#### 1. 接続エラー
|
||||||
|
```
|
||||||
|
❌ データベース接続エラー: connection refused
|
||||||
|
```
|
||||||
|
**対処法**: データベースサービスが起動していることを確認
|
||||||
|
|
||||||
|
#### 2. 権限エラー
|
||||||
|
```
|
||||||
|
❌ テーブル移行エラー: permission denied
|
||||||
|
```
|
||||||
|
**対処法**: データベースユーザーの権限を確認
|
||||||
|
|
||||||
|
#### 3. 外部キー制約エラー
|
||||||
|
```
|
||||||
|
❌ レコード処理エラー: foreign key constraint
|
||||||
|
```
|
||||||
|
**対処法**: 依存関係のあるテーブルから先に移行
|
||||||
|
|
||||||
|
### デバッグ方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ログレベルを上げて詳細情報を表示
|
||||||
|
docker compose exec app python -c "
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
exec(open('migrate_old_rogdb_to_rogdb.py').read())
|
||||||
|
"
|
||||||
|
|
||||||
|
# 特定テーブルのみテスト
|
||||||
|
docker compose exec app python -c "
|
||||||
|
from migrate_old_rogdb_to_rogdb import RogTableMigrator
|
||||||
|
migrator = RogTableMigrator()
|
||||||
|
migrator.connect_databases()
|
||||||
|
migrator.migrate_table_data('rog_customuser')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
このスクリプトはMITライセンスの下で公開されています。
|
||||||
156
MIGRATION_RESET_REPORT.md
Normal file
156
MIGRATION_RESET_REPORT.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# Migration Reset - 完了報告書
|
||||||
|
|
||||||
|
## 実行日時
|
||||||
|
2025年8月28日 13:33:05 - 13:43:58
|
||||||
|
|
||||||
|
## 実行された作業内容
|
||||||
|
|
||||||
|
### 1. 問題の特定
|
||||||
|
- **問題**: Migration 0011_auto_20250827_1459.py が存在しない依存関係 0010_auto_20250827_1510 を参照していた
|
||||||
|
- **エラー内容**: `NodeNotFoundError: Migration rog.0010_auto_20250827_1510 dependencies reference nonexistent parent node`
|
||||||
|
|
||||||
|
### 2. Migrationリセット作業
|
||||||
|
|
||||||
|
#### バックアップ作成
|
||||||
|
- **バックアップディレクトリ**: `rog/migrations_backup_20250828_042950`
|
||||||
|
- **内容**: 既存の11個のmigrationファイルをバックアップ
|
||||||
|
|
||||||
|
#### データベース履歴クリア
|
||||||
|
- **削除レコード数**: 72件の`django_migrations`レコード
|
||||||
|
- **対象**: `rog`アプリの全migration履歴
|
||||||
|
|
||||||
|
#### 新しいシンプルなMigration作成
|
||||||
|
- **ファイル**: `rog/migrations/0001_simple_initial.py`
|
||||||
|
- **内容**: Core modelsのみ (managed=True models)
|
||||||
|
- `CustomUser`
|
||||||
|
- `Category`
|
||||||
|
- `NewEvent`
|
||||||
|
- `Team`
|
||||||
|
- `Location`
|
||||||
|
- `Entry`
|
||||||
|
|
||||||
|
### 3. Migration適用結果
|
||||||
|
|
||||||
|
```
|
||||||
|
Operations to perform:
|
||||||
|
Apply all migrations: admin, auth, contenttypes, knox, rog, sessions
|
||||||
|
Running migrations:
|
||||||
|
Applying rog.0001_simple_initial... FAKED
|
||||||
|
Applying admin.0001_initial... FAKED
|
||||||
|
Applying admin.0002_logentry_remove_auto_add... OK
|
||||||
|
Applying admin.0003_logentry_add_action_flag_choices... OK
|
||||||
|
Applying knox.0001_initial... FAKED
|
||||||
|
Applying knox.0002_auto_20150916_1425... OK
|
||||||
|
Applying knox.0003_auto_20150916_1526... OK
|
||||||
|
Applying knox.0004_authtoken_expires... OK
|
||||||
|
Applying knox.0005_authtoken_token_key... OK
|
||||||
|
Applying knox.0006_auto_20160818_0932... OK
|
||||||
|
Applying knox.0007_auto_20190111_0542... OK
|
||||||
|
Applying knox.0008_remove_authtoken_salt... OK
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 最終状態確認
|
||||||
|
|
||||||
|
#### Migration状態
|
||||||
|
```
|
||||||
|
admin
|
||||||
|
[X] 0001_initial
|
||||||
|
[X] 0002_logentry_remove_auto_add
|
||||||
|
[X] 0003_logentry_add_action_flag_choices
|
||||||
|
auth
|
||||||
|
[X] 0001_initial
|
||||||
|
[X] 0002_alter_permission_name_max_length
|
||||||
|
[X] 0003_alter_user_email_max_length
|
||||||
|
[X] 0004_alter_user_username_opts
|
||||||
|
[X] 0005_alter_user_last_login_null
|
||||||
|
[X] 0006_require_contenttypes_0002
|
||||||
|
[X] 0007_alter_validators_add_error_messages
|
||||||
|
[X] 0008_alter_user_username_max_length
|
||||||
|
[X] 0009_alter_user_last_name_max_length
|
||||||
|
[X] 0010_alter_group_name_max_length
|
||||||
|
[X] 0011_update_proxy_permissions
|
||||||
|
[X] 0012_alter_user_first_name_max_length
|
||||||
|
contenttypes
|
||||||
|
[X] 0001_initial
|
||||||
|
[X] 0002_remove_content_type_name
|
||||||
|
knox
|
||||||
|
[X] 0001_initial
|
||||||
|
[X] 0002_auto_20150916_1425
|
||||||
|
[X] 0003_auto_20150916_1526
|
||||||
|
[X] 0004_authtoken_expires
|
||||||
|
[X] 0005_authtoken_token_key
|
||||||
|
[X] 0006_auto_20160818_0932
|
||||||
|
[X] 0007_auto_20190111_0542
|
||||||
|
[X] 0008_remove_authtoken_salt
|
||||||
|
rog
|
||||||
|
[X] 0001_simple_initial
|
||||||
|
sessions
|
||||||
|
[X] 0001_initial
|
||||||
|
```
|
||||||
|
|
||||||
|
## 解決されたポイント
|
||||||
|
|
||||||
|
### 1. managed=False modelの除外
|
||||||
|
- **問題**: 初期migrationに`managed=False`のモデル(GifuAreas、JpnAdminMainPerf、JpnSubPerf)が含まれていた
|
||||||
|
- **解決**: これらのモデルを除外したクリーンなmigrationを作成
|
||||||
|
|
||||||
|
### 2. 依存関係の修正
|
||||||
|
- **問題**: 存在しないmigrationへの参照
|
||||||
|
- **解決**: 正しい依存関係でmigrationを再構築
|
||||||
|
|
||||||
|
### 3. コアモデルの確立
|
||||||
|
- **成果**: 最低限必要なモデル構造を確立
|
||||||
|
- **含有モデル**: ユーザー、イベント、チーム、ロケーション、エントリー
|
||||||
|
|
||||||
|
## 作成されたツール
|
||||||
|
|
||||||
|
### migration_simple_reset.py
|
||||||
|
- **目的**: Migration reset作業の自動化
|
||||||
|
- **機能**:
|
||||||
|
- バックアップ作成
|
||||||
|
- Migration履歴クリア
|
||||||
|
- シンプルなmigration作成
|
||||||
|
- Migration適用
|
||||||
|
- 状態確認
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
```bash
|
||||||
|
# 完全なリセット workflow
|
||||||
|
python migration_simple_reset.py --full
|
||||||
|
|
||||||
|
# バックアップのみ
|
||||||
|
python migration_simple_reset.py --backup-only
|
||||||
|
|
||||||
|
# リセットのみ
|
||||||
|
python migration_simple_reset.py --reset-only
|
||||||
|
|
||||||
|
# 適用のみ
|
||||||
|
python migration_simple_reset.py --apply-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## 今後の展開
|
||||||
|
|
||||||
|
### 1. 追加モデルの段階的追加
|
||||||
|
- Geographic models(managed=Falseとして適切に)
|
||||||
|
- 追加機能用のモデル
|
||||||
|
- 関連テーブル
|
||||||
|
|
||||||
|
### 2. データ移行
|
||||||
|
- 既存データの段階的移行
|
||||||
|
- 写真データの整合性確保
|
||||||
|
- GPS記録の移行
|
||||||
|
|
||||||
|
### 3. デプロイメント準備
|
||||||
|
- 本番環境での同様作業
|
||||||
|
- データベースバックアップ確保
|
||||||
|
- ロールバック計画
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
**✅ Migration混乱の解決に成功**
|
||||||
|
- 複雑な依存関係問題を解決
|
||||||
|
- クリーンなMigration状態を確立
|
||||||
|
- 今後の追加開発に向けた基盤を整備
|
||||||
|
- デプロイメント時の混乱要因を除去
|
||||||
|
|
||||||
|
**次のステップ**: 必要に応じて追加モデルを段階的に追加し、データ移行を実行
|
||||||
148
MIGRATION_STATISTICS_README.md
Normal file
148
MIGRATION_STATISTICS_README.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# 移行結果統計情報表示スクリプト
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
移行処理の結果を詳細な統計情報として表示するスクリプトです。Docker Compose環境で実行可能で、移行データの品質チェックや分析に役立ちます。
|
||||||
|
|
||||||
|
## 実行方法
|
||||||
|
|
||||||
|
### 1. Docker Composeで実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 統計情報を表示
|
||||||
|
docker compose exec app python migration_statistics.py
|
||||||
|
|
||||||
|
# または Makeタスクを使用
|
||||||
|
make migration-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 他の移行関連コマンド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 移行実行
|
||||||
|
make migration-run
|
||||||
|
|
||||||
|
# Location2025移行
|
||||||
|
make migration-location2025
|
||||||
|
|
||||||
|
# データ保護移行
|
||||||
|
make migration-data-protection
|
||||||
|
|
||||||
|
# データベースシェル
|
||||||
|
make db-shell
|
||||||
|
|
||||||
|
# アプリケーションログ確認
|
||||||
|
make app-logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 表示される統計情報
|
||||||
|
|
||||||
|
### 📊 基本統計情報
|
||||||
|
- 各テーブルのレコード数
|
||||||
|
- 全体のデータ量概要
|
||||||
|
|
||||||
|
### 🎯 イベント別統計
|
||||||
|
- 登録イベント一覧
|
||||||
|
- イベント別参加チーム数、メンバー数、エントリー数
|
||||||
|
|
||||||
|
### 📍 GPSチェックイン統計
|
||||||
|
- 総チェックイン数、参加チーム数
|
||||||
|
- 時間帯別チェックイン分布
|
||||||
|
- CP利用ランキング(上位10位)
|
||||||
|
|
||||||
|
### 👥 チーム統計
|
||||||
|
- 総チーム数、クラス数
|
||||||
|
- クラス別チーム分布
|
||||||
|
- 平均メンバー数
|
||||||
|
|
||||||
|
### 🔍 データ品質チェック
|
||||||
|
- 重複データチェック
|
||||||
|
- 異常時刻データチェック
|
||||||
|
- データ整合性チェック
|
||||||
|
|
||||||
|
### 📄 JSON出力
|
||||||
|
- 統計情報をJSONファイルで出力
|
||||||
|
- 外部システムでの利用や保存に便利
|
||||||
|
|
||||||
|
## 出力例
|
||||||
|
|
||||||
|
```
|
||||||
|
================================================================================
|
||||||
|
📊 移行データ基本統計情報
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
📋 テーブル別レコード数:
|
||||||
|
テーブル名 日本語名 レコード数
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
rog_newevent2 イベント 12件
|
||||||
|
rog_team チーム 450件
|
||||||
|
rog_member メンバー 1,200件
|
||||||
|
rog_entry エントリー 450件
|
||||||
|
rog_gpscheckin GPSチェックイン 8,500件
|
||||||
|
rog_checkpoint チェックポイント 800件
|
||||||
|
rog_location2025 ロケーション2025 50件
|
||||||
|
rog_customuser ユーザー 25件
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
合計 11,487件
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
🎯 イベント別統計情報
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
📅 登録イベント数: 12件
|
||||||
|
|
||||||
|
イベント詳細:
|
||||||
|
ID イベント名 開催日 登録日時
|
||||||
|
------------------------------------------------------------
|
||||||
|
1 美濃加茂 2024-05-19 2024-08-25 10:30
|
||||||
|
2 岐阜市 2024-04-28 2024-08-25 10:30
|
||||||
|
3 大垣2 2024-04-20 2024-08-25 10:30
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### データベース接続エラー
|
||||||
|
```bash
|
||||||
|
# コンテナの状態確認
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# PostgreSQLログ確認
|
||||||
|
make db-logs
|
||||||
|
|
||||||
|
# アプリケーションログ確認
|
||||||
|
make app-logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 環境変数の確認
|
||||||
|
```bash
|
||||||
|
# 環境変数が正しく設定されているか確認
|
||||||
|
docker compose exec app env | grep POSTGRES
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手動でのデータベース接続テスト
|
||||||
|
```bash
|
||||||
|
# PostgreSQLコンテナに直接接続
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb
|
||||||
|
|
||||||
|
# テーブル確認
|
||||||
|
\dt
|
||||||
|
|
||||||
|
# 基本的なクエリテスト
|
||||||
|
SELECT COUNT(*) FROM rog_gpscheckin;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 関連ファイル
|
||||||
|
|
||||||
|
- `migration_statistics.py` - 統計表示メインスクリプト
|
||||||
|
- `migration_final_simple.py` - GPS記録移行スクリプト
|
||||||
|
- `migration_location2025_support.py` - Location2025移行スクリプト
|
||||||
|
- `migration_data_protection.py` - データ保護移行スクリプト
|
||||||
|
- `Makefile` - 実行用タスク定義
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- Docker Composeが正常に起動していることを確認してください
|
||||||
|
- PostgreSQLコンテナが稼働していることを確認してください
|
||||||
|
- 統計情報は実行時点のデータベース状態を反映します
|
||||||
|
- JSON出力ファイルは `/app/` ディレクトリに保存されます
|
||||||
86
Makefile
86
Makefile
@ -31,3 +31,89 @@ volume:
|
|||||||
shell:
|
shell:
|
||||||
docker-compose exec api python3 manage.py shell
|
docker-compose exec api python3 manage.py shell
|
||||||
|
|
||||||
|
# 移行関連タスク
|
||||||
|
migration-stats:
|
||||||
|
docker compose exec app python migration_statistics.py
|
||||||
|
|
||||||
|
migration-run:
|
||||||
|
docker compose exec app python migration_final_simple.py
|
||||||
|
|
||||||
|
migration-location2025:
|
||||||
|
docker compose exec app python migration_location2025_support.py
|
||||||
|
|
||||||
|
migration-data-protection:
|
||||||
|
docker compose exec app python migration_data_protection.py
|
||||||
|
|
||||||
|
# Old RogDB → RogDB 移行
|
||||||
|
migrate-old-rogdb:
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# rog_team 専用移行 (構造変換)
|
||||||
|
migrate-rog-team:
|
||||||
|
docker compose exec app python migrate_rog_team_enhanced.py
|
||||||
|
|
||||||
|
# rog_entry 専用移行 (camelCase対応)
|
||||||
|
migrate-rog-entry:
|
||||||
|
docker compose exec app python migrate_rog_entry_enhanced.py
|
||||||
|
|
||||||
|
# rog_goalimages 専用移行 (team_name→zekken_number変換)
|
||||||
|
migrate-rog-goalimages:
|
||||||
|
docker compose exec app python migrate_rog_goalimages_enhanced.py
|
||||||
|
|
||||||
|
# 完全移行(通常テーブル + 特殊テーブル)
|
||||||
|
migrate-full:
|
||||||
|
@echo "=== 1. 通常テーブル移行 (特殊テーブル除く) ==="
|
||||||
|
$(MAKE) migrate-old-rogdb
|
||||||
|
@echo "=== 2. rog_team構造変換移行 ==="
|
||||||
|
$(MAKE) migrate-rog-team
|
||||||
|
@echo "=== 3. rog_entry camelCase対応移行 ==="
|
||||||
|
$(MAKE) migrate-rog-entry
|
||||||
|
@echo "=== 4. rog_goalimages team_name→zekken変換移行 ==="
|
||||||
|
$(MAKE) migrate-rog-goalimages
|
||||||
|
@echo "=== 移行完了 ==="
|
||||||
|
|
||||||
|
# カラム名チェック
|
||||||
|
check-columns:
|
||||||
|
docker compose exec app python check_column_names.py
|
||||||
|
|
||||||
|
# NULL値チェック
|
||||||
|
check-null-values:
|
||||||
|
docker compose exec app python check_null_values.py
|
||||||
|
|
||||||
|
# 完全な移行前チェック
|
||||||
|
pre-migration-check:
|
||||||
|
@echo "=== カラム名チェック ==="
|
||||||
|
docker compose exec app python check_column_names.py
|
||||||
|
@echo "=== NULL値チェック ==="
|
||||||
|
docker compose exec app python check_null_values.py
|
||||||
|
|
||||||
|
# 移行前準備(カラム名チェック + 移行実行)
|
||||||
|
migrate-old-rogdb-safe:
|
||||||
|
@echo "=== カラム名チェック実行 ==="
|
||||||
|
docker compose exec app python check_column_names.py
|
||||||
|
@echo "=== 移行実行 ==="
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
migrate-old-rogdb-stats:
|
||||||
|
docker compose exec app python -c "from migrate_old_rogdb_to_rogdb import RogTableMigrator; m = RogTableMigrator(); m.connect_databases(); m.get_rog_tables()"
|
||||||
|
|
||||||
|
migrate-old-rogdb-dryrun:
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=all app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
migrate-old-rogdb-exclude-users:
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# データベース関連
|
||||||
|
db-shell:
|
||||||
|
docker compose exec postgres-db psql -U $(POSTGRES_USER) -d $(POSTGRES_DBNAME)
|
||||||
|
|
||||||
|
db-backup:
|
||||||
|
docker compose exec postgres-db pg_dump -U $(POSTGRES_USER) $(POSTGRES_DBNAME) > backup_$(shell date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# ログ確認
|
||||||
|
app-logs:
|
||||||
|
docker compose logs app --tail=100 -f
|
||||||
|
|
||||||
|
db-logs:
|
||||||
|
docker compose logs postgres-db --tail=50 -f
|
||||||
|
|
||||||
|
|||||||
245
TEAM_CSV_IMPORT_MANUAL.md
Normal file
245
TEAM_CSV_IMPORT_MANUAL.md
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
# チームCSVインポート機能 操作マニュアル
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
このマニュアルは、CSVファイルからチーム登録データを一括インポートする機能の使用方法を説明します。
|
||||||
|
|
||||||
|
## 機能概要
|
||||||
|
- CSVファイルからチーム情報を一括読み込み
|
||||||
|
- ユーザー、チーム、メンバー、エントリーの自動作成
|
||||||
|
- リーダー(氏名1)の自動設定
|
||||||
|
- イベント参加登録の自動処理
|
||||||
|
- カテゴリー自動選択(NewCategoryデータベース参照)
|
||||||
|
- インポート結果のCSV出力
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
### 1. Docker環境
|
||||||
|
```bash
|
||||||
|
# Dockerコンテナが起動していることを確認
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# 起動していない場合
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. イベント作成
|
||||||
|
インポート前に対象イベントがデータベースに存在している必要があります。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# イベント存在確認
|
||||||
|
docker compose exec app python manage.py shell -c "
|
||||||
|
from rog.models import NewEvent2
|
||||||
|
events = NewEvent2.objects.all()
|
||||||
|
for event in events:
|
||||||
|
print(f'イベントコード: {event.event_code}, 名前: {event.event_name}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSVファイル形式
|
||||||
|
|
||||||
|
### 必須列
|
||||||
|
| 列名 | 説明 | 例 |
|
||||||
|
|------|------|-----|
|
||||||
|
| 部門別数 | 部門番号 | 1 |
|
||||||
|
| 時間 | 競技時間 | 3, 5 |
|
||||||
|
| 部門 | 部門名 | 一般, ファミリー |
|
||||||
|
| チーム名 | チーム名 | いなりずし |
|
||||||
|
| メール | 代表者メールアドレス | test@example.com |
|
||||||
|
| パスワード | ログインパスワード | password123 |
|
||||||
|
| 電話番号 | 代表者電話番号 | 090-1234-5678 |
|
||||||
|
| 氏名1 | 代表者氏名(リーダー) | 山田太郎 |
|
||||||
|
| 誕生日1 | 代表者誕生日 | 1990/4/15 |
|
||||||
|
|
||||||
|
### オプション列
|
||||||
|
| 列名 | 説明 |
|
||||||
|
|------|------|
|
||||||
|
| 氏名2〜氏名7 | 追加メンバー氏名 |
|
||||||
|
| 誕生日2〜誕生日7 | 追加メンバー誕生日 |
|
||||||
|
|
||||||
|
### CSVファイル例
|
||||||
|
```csv
|
||||||
|
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3
|
||||||
|
1,3,一般,いなりずし,test@example.com,pass123,090-1234-5678,山田太郎,1990/4/15,山田花子,1992/8/20,田中次郎,1988/12/3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 操作手順
|
||||||
|
|
||||||
|
### 1. ドライラン実行(推奨)
|
||||||
|
実際のデータ変更前に、処理内容を確認します。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app python manage.py import_teams \
|
||||||
|
--event_code="岐阜ロゲイニング2025" \
|
||||||
|
--csv_file="CPLIST/input/teams2025.csv" \
|
||||||
|
--dry_run
|
||||||
|
```
|
||||||
|
|
||||||
|
**出力例:**
|
||||||
|
```
|
||||||
|
[DRY RUN] 行 2: チーム=いなりずし
|
||||||
|
ユーザー既存: test@example.com パスワード:既存
|
||||||
|
エントリー: ゼッケン1, カテゴリー:一般, 時間:3時間
|
||||||
|
参加登録: 新規作成予定
|
||||||
|
メンバー: 3名 [山田太郎(1990/4/15), 山田花子(1992/8/20), 田中次郎(1988/12/3)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 本実行
|
||||||
|
ドライランで問題がないことを確認後、実際のインポートを実行します。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app python manage.py import_teams \
|
||||||
|
--event_code="岐阜ロゲイニング2025" \
|
||||||
|
--csv_file="CPLIST/input/teams2025.csv"
|
||||||
|
```
|
||||||
|
|
||||||
|
## コマンドパラメータ
|
||||||
|
|
||||||
|
| パラメータ | 必須 | 説明 | 例 |
|
||||||
|
|-----------|------|------|-----|
|
||||||
|
| --event_code | ✓ | 対象イベントコード | "岐阜ロゲイニング2025" |
|
||||||
|
| --csv_file | ✓ | CSVファイルパス | "CPLIST/input/teams2025.csv" |
|
||||||
|
| --dry_run | - | ドライラン実行 | (パラメータのみ) |
|
||||||
|
|
||||||
|
## 処理内容詳細
|
||||||
|
|
||||||
|
### 1. ユーザー登録
|
||||||
|
- **既存ユーザー**: メールアドレスで検索し、既存の場合は再利用
|
||||||
|
- **新規ユーザー**: メール、パスワード、電話番号で新規作成
|
||||||
|
|
||||||
|
### 2. チーム登録
|
||||||
|
- **既存チーム**: 同一オーナー・同一チーム名の場合は再利用
|
||||||
|
- **新規チーム**: チーム名、オーナー、イベント情報で新規作成
|
||||||
|
|
||||||
|
### 3. メンバー登録
|
||||||
|
- **リーダー設定**: 氏名1の人を自動的にチームオーナー(リーダー)に設定
|
||||||
|
- **追加メンバー**: 氏名2〜氏名7の人をメンバーとして登録
|
||||||
|
- **ダミーユーザー**: メンバー用に自動生成されるダミーアカウント
|
||||||
|
|
||||||
|
### 4. エントリー登録
|
||||||
|
- **カテゴリー選択**: NewCategoryデータベースから最適なカテゴリーを自動選択
|
||||||
|
- **ゼッケン番号**: 自動採番(既存の最大番号+1)
|
||||||
|
- **重複チェック**: 同一チーム・同一イベントの重複登録を防止
|
||||||
|
|
||||||
|
## カテゴリー自動選択ロジック
|
||||||
|
|
||||||
|
1. **完全一致**: `部門名-時間時間`(例:一般-3時間)
|
||||||
|
2. **部分一致**: 部門名と時間が一致し、メンバー数条件を満たすもの
|
||||||
|
3. **新規作成**: 該当なしの場合は新規カテゴリー作成
|
||||||
|
|
||||||
|
**既存カテゴリー例:**
|
||||||
|
- 一般-3時間(最大7名)
|
||||||
|
- 一般-5時間(最大7名)
|
||||||
|
- ファミリー-3時間(最大7名)
|
||||||
|
- ファミリー-5時間(最大7名)
|
||||||
|
- 男子ソロ-3時間(最大1名)
|
||||||
|
- 女子ソロ-5時間(最大1名)
|
||||||
|
|
||||||
|
## 出力ファイル
|
||||||
|
|
||||||
|
### CSV結果ファイル
|
||||||
|
実行完了後、以下の形式でCSVファイルが出力されます:
|
||||||
|
|
||||||
|
**ファイル名:** `import_results_{イベントコード}_{タイムスタンプ}.csv`
|
||||||
|
**場所:** CSVファイルと同じディレクトリ
|
||||||
|
|
||||||
|
**出力項目:**
|
||||||
|
- チーム名
|
||||||
|
- ゼッケン番号
|
||||||
|
- カテゴリー
|
||||||
|
- 時間
|
||||||
|
- オーナーメール
|
||||||
|
- リーダー(氏名と誕生日)
|
||||||
|
- メンバー数
|
||||||
|
- メンバー一覧
|
||||||
|
- 参加登録状況
|
||||||
|
- エントリーID
|
||||||
|
- 作成日時
|
||||||
|
|
||||||
|
## エラー処理
|
||||||
|
|
||||||
|
### よくあるエラー
|
||||||
|
|
||||||
|
#### 1. イベントが見つからない
|
||||||
|
```
|
||||||
|
エラー: イベントコード '存在しないイベント' が見つかりません
|
||||||
|
```
|
||||||
|
**対処法:** 正しいイベントコードを確認してください。
|
||||||
|
|
||||||
|
#### 2. CSVファイルが見つからない
|
||||||
|
```
|
||||||
|
エラー: CSVファイル 'ファイルパス' が見つかりません
|
||||||
|
```
|
||||||
|
**対処法:** ファイルパスを確認してください。
|
||||||
|
|
||||||
|
#### 3. カテゴリー制約エラー
|
||||||
|
```
|
||||||
|
エラー: このカテゴリーはソロ参加のみ可能です
|
||||||
|
```
|
||||||
|
**対処法:** メンバー数とカテゴリーの制約を確認してください。
|
||||||
|
|
||||||
|
### エラー出力例
|
||||||
|
```
|
||||||
|
エラー数: 3
|
||||||
|
行 2: メールアドレスが必要です
|
||||||
|
行 5: チーム名が必要です
|
||||||
|
行 8: このカテゴリーはソロ参加のみ可能です
|
||||||
|
```
|
||||||
|
|
||||||
|
## データ確認方法
|
||||||
|
|
||||||
|
### インポート結果確認
|
||||||
|
```bash
|
||||||
|
# エントリー確認
|
||||||
|
docker compose exec app python manage.py shell -c "
|
||||||
|
from rog.models import Entry, NewEvent2
|
||||||
|
event = NewEvent2.objects.get(event_code='岐阜ロゲイニング2025')
|
||||||
|
entries = Entry.objects.filter(event=event)
|
||||||
|
print(f'総エントリー数: {entries.count()}')
|
||||||
|
for entry in entries[:5]: # 最初の5件
|
||||||
|
print(f'ゼッケン{entry.zekken_number}: {entry.team.team_name} ({entry.category.category_name})')
|
||||||
|
"
|
||||||
|
|
||||||
|
# チーム・メンバー確認
|
||||||
|
docker compose exec app python manage.py shell -c "
|
||||||
|
from rog.models import Team, Member
|
||||||
|
teams = Team.objects.filter(event__event_code='岐阜ロゲイニング2025')
|
||||||
|
print(f'総チーム数: {teams.count()}')
|
||||||
|
for team in teams[:3]: # 最初の3チーム
|
||||||
|
members = team.members.all()
|
||||||
|
print(f'チーム: {team.team_name} (リーダー: {team.owner.firstname})')
|
||||||
|
print(f' メンバー数: {members.count()}')
|
||||||
|
for member in members:
|
||||||
|
print(f' - {member.firstname}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
1. **バックアップ**: 本実行前に必ずデータベースのバックアップを取得してください
|
||||||
|
2. **重複実行**: 同じCSVファイルを複数回実行すると重複データが作成される可能性があります
|
||||||
|
3. **文字エンコーディング**: CSVファイルはUTF-8で保存してください
|
||||||
|
4. **メールアドレス**: 重複不可のため、既存ユーザーと重複しないよう注意してください
|
||||||
|
5. **カテゴリー制約**: NewCategoryの設定(メンバー数制限等)に従います
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### Q: インポートが途中で止まる
|
||||||
|
A: エラーメッセージを確認し、該当行のデータを修正してください。
|
||||||
|
|
||||||
|
### Q: ゼッケン番号が重複する
|
||||||
|
A: 既存エントリーを削除してから再実行してください。
|
||||||
|
|
||||||
|
### Q: カテゴリーが正しく選択されない
|
||||||
|
A: NewCategoryデータベースの設定を確認してください。
|
||||||
|
|
||||||
|
### Q: メンバーが登録されない
|
||||||
|
A: CSVの列名が正しいか(氏名1、氏名2等)確認してください。
|
||||||
|
|
||||||
|
## サポート
|
||||||
|
|
||||||
|
技術的な問題や質問がある場合は、システム開発チームまでお問い合わせください。
|
||||||
|
|
||||||
|
---
|
||||||
|
**作成日:** 2025年9月5日
|
||||||
|
**バージョン:** 1.0
|
||||||
|
**対象システム:** 岐阜ロゲイニングサーバー
|
||||||
22
add_use_qr_code_migration.py
Normal file
22
add_use_qr_code_migration.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated migration for adding use_qr_code field to Location model
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rog', '0001_initial'), # 最新のマイグレーションファイル名に合わせてください
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='location',
|
||||||
|
name='use_qr_code',
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='QRコードを使用したインタラクションを有効にする',
|
||||||
|
verbose_name='Use QR Code for interaction'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
150
analyze_event_data_raw.py
Normal file
150
analyze_event_data_raw.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
# プロジェクト設定
|
||||||
|
sys.path.append('/app')
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def analyze_event_data_raw():
|
||||||
|
"""生のSQLを使ってイベント・チーム・エントリーデータを分析"""
|
||||||
|
|
||||||
|
print("=== 生SQLによるイベント・データ分析 ===")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# 1. NewEvent2テーブルの構造確認
|
||||||
|
print("\n1. rog_newevent2テーブル構造:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_newevent2'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
for col in columns:
|
||||||
|
print(f" - {col[0]}: {col[1]} ({'NULL' if col[2] == 'YES' else 'NOT NULL'})")
|
||||||
|
|
||||||
|
# 2. 全イベント一覧
|
||||||
|
print("\n2. 全イベント一覧:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, event_name, event_day, venue_address
|
||||||
|
FROM rog_newevent2
|
||||||
|
ORDER BY id;
|
||||||
|
""")
|
||||||
|
events = cursor.fetchall()
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
print(f" - ID:{event[0]}, Name:{event[1]}, Date:{event[2]}, Venue:{event[3]}")
|
||||||
|
|
||||||
|
# 各イベントのエントリー数とチーム数
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_entry WHERE event_id = %s", [event[0]])
|
||||||
|
entry_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_team WHERE event_id = %s", [event[0]])
|
||||||
|
team_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
print(f" Entry:{entry_count}, Team:{team_count}")
|
||||||
|
|
||||||
|
# 3. FC岐阜関連イベント検索
|
||||||
|
print("\n3. FC岐阜関連イベント検索:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, event_name, event_day, venue_address
|
||||||
|
FROM rog_newevent2
|
||||||
|
WHERE event_name ILIKE %s OR event_name ILIKE %s OR event_name ILIKE %s
|
||||||
|
ORDER BY id;
|
||||||
|
""", ['%FC岐阜%', '%fc岐阜%', '%岐阜%'])
|
||||||
|
|
||||||
|
fc_events = cursor.fetchall()
|
||||||
|
if fc_events:
|
||||||
|
for event in fc_events:
|
||||||
|
print(f" - ID:{event[0]}, Name:{event[1]}, Date:{event[2]}")
|
||||||
|
|
||||||
|
# 関連エントリー
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT e.id, t.id as team_id, t.name as team_name, t.zekken_number
|
||||||
|
FROM rog_entry e
|
||||||
|
JOIN rog_team t ON e.team_id = t.id
|
||||||
|
WHERE e.event_id = %s
|
||||||
|
LIMIT 10;
|
||||||
|
""", [event[0]])
|
||||||
|
|
||||||
|
entries = cursor.fetchall()
|
||||||
|
if entries:
|
||||||
|
print(" エントリー詳細:")
|
||||||
|
for entry in entries:
|
||||||
|
print(f" Entry ID:{entry[0]}, Team ID:{entry[1]}, Team:{entry[2]}, Zekken:{entry[3]}")
|
||||||
|
|
||||||
|
# 関連チーム(ゼッケン番号付き)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, name, zekken_number
|
||||||
|
FROM rog_team
|
||||||
|
WHERE event_id = %s AND zekken_number IS NOT NULL AND zekken_number != ''
|
||||||
|
LIMIT 10;
|
||||||
|
""", [event[0]])
|
||||||
|
|
||||||
|
teams_with_zekken = cursor.fetchall()
|
||||||
|
if teams_with_zekken:
|
||||||
|
print(" ゼッケン番号付きチーム:")
|
||||||
|
for team in teams_with_zekken:
|
||||||
|
print(f" Team ID:{team[0]}, Name:{team[1]}, Zekken:{team[2]}")
|
||||||
|
else:
|
||||||
|
print(" ゼッケン番号付きチームが見つかりません")
|
||||||
|
else:
|
||||||
|
print(" FC岐阜関連イベントが見つかりません")
|
||||||
|
|
||||||
|
# 4. 全体のゼッケン番号付きチーム確認
|
||||||
|
print("\n4. 全体のゼッケン番号付きチーム状況:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM rog_team
|
||||||
|
WHERE zekken_number IS NOT NULL AND zekken_number != '';
|
||||||
|
""")
|
||||||
|
zekken_team_count = cursor.fetchone()[0]
|
||||||
|
print(f" ゼッケン番号付きチーム総数: {zekken_team_count}")
|
||||||
|
|
||||||
|
if zekken_team_count > 0:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT t.id, t.name, t.zekken_number, e.event_name
|
||||||
|
FROM rog_team t
|
||||||
|
LEFT JOIN rog_newevent2 e ON t.event_id = e.id
|
||||||
|
WHERE t.zekken_number IS NOT NULL AND t.zekken_number != ''
|
||||||
|
LIMIT 10;
|
||||||
|
""")
|
||||||
|
|
||||||
|
sample_teams = cursor.fetchall()
|
||||||
|
print(" サンプル:")
|
||||||
|
for team in sample_teams:
|
||||||
|
print(f" ID:{team[0]}, Name:{team[1]}, Zekken:{team[2]}, Event:{team[3]}")
|
||||||
|
|
||||||
|
# 5. 通過審査管理画面で使われる可能性のあるクエリの確認
|
||||||
|
print("\n5. 通過審査管理用データ確認:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT e.id as event_id, e.event_name, COUNT(t.id) as team_count,
|
||||||
|
COUNT(CASE WHEN t.zekken_number IS NOT NULL AND t.zekken_number != '' THEN 1 END) as zekken_teams
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_team t ON e.id = t.event_id
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
ORDER BY e.id;
|
||||||
|
""")
|
||||||
|
|
||||||
|
event_stats = cursor.fetchall()
|
||||||
|
print(" イベント別チーム・ゼッケン統計:")
|
||||||
|
for stat in event_stats:
|
||||||
|
print(f" イベントID:{stat[0]}, Name:{stat[1]}, 総チーム:{stat[2]}, ゼッケン付き:{stat[3]}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
analyze_event_data_raw()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
125
analyze_fc_gifu_data.py
Normal file
125
analyze_fc_gifu_data.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
# プロジェクト設定
|
||||||
|
sys.path.append('/app')
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from rog.models import Entry, Team, NewEvent2, Member
|
||||||
|
from django.db.models import Q
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def analyze_fc_gifu_data():
|
||||||
|
"""FC岐阜関連のイベント・チーム・エントリーデータを詳細分析"""
|
||||||
|
|
||||||
|
print("=== FC岐阜イベント・データ詳細分析 ===")
|
||||||
|
|
||||||
|
# 1. FC岐阜関連イベントを検索
|
||||||
|
print("\n1. FC岐阜関連イベント検索:")
|
||||||
|
fc_events = NewEvent2.objects.filter(
|
||||||
|
Q(event_name__icontains='FC岐阜') |
|
||||||
|
Q(event_name__icontains='fc岐阜') |
|
||||||
|
Q(event_name__icontains='岐阜')
|
||||||
|
)
|
||||||
|
|
||||||
|
if fc_events.exists():
|
||||||
|
for event in fc_events:
|
||||||
|
print(f" - ID:{event.id}, Name:{event.event_name}, Date:{event.event_day}")
|
||||||
|
|
||||||
|
# イベントに関連するエントリーを確認
|
||||||
|
entries = Entry.objects.filter(event=event)
|
||||||
|
print(f" 関連エントリー数: {entries.count()}")
|
||||||
|
|
||||||
|
# エントリーのチーム情報を表示
|
||||||
|
if entries.exists():
|
||||||
|
print(" エントリー詳細:")
|
||||||
|
for entry in entries[:10]: # 最初の10件のみ表示
|
||||||
|
team = entry.team
|
||||||
|
print(f" Entry ID:{entry.id}, Team ID:{team.id}, Team Name:{team.name}, Zekken:{team.zekken_number}")
|
||||||
|
|
||||||
|
# イベントに関連するチームを直接検索
|
||||||
|
teams = Team.objects.filter(event=event)
|
||||||
|
print(f" 関連チーム数: {teams.count()}")
|
||||||
|
|
||||||
|
if teams.exists():
|
||||||
|
print(" チーム詳細:")
|
||||||
|
for team in teams[:10]: # 最初の10件のみ表示
|
||||||
|
print(f" Team ID:{team.id}, Name:{team.name}, Zekken:{team.zekken_number}")
|
||||||
|
else:
|
||||||
|
print(" FC岐阜関連イベントが見つかりません")
|
||||||
|
|
||||||
|
# 2. 全イベント一覧を確認
|
||||||
|
print("\n2. 全イベント一覧:")
|
||||||
|
all_events = NewEvent2.objects.all()
|
||||||
|
for event in all_events:
|
||||||
|
entry_count = Entry.objects.filter(event=event).count()
|
||||||
|
team_count = Team.objects.filter(event=event).count()
|
||||||
|
print(f" - ID:{event.id}, Name:{event.event_name}, Date:{event.event_day}, Entry:{entry_count}, Team:{team_count}")
|
||||||
|
|
||||||
|
# 3. ゼッケン番号が設定されているチームを確認
|
||||||
|
print("\n3. ゼッケン番号付きチーム:")
|
||||||
|
teams_with_zekken = Team.objects.exclude(zekken_number__isnull=True).exclude(zekken_number='')
|
||||||
|
print(f" ゼッケン番号付きチーム数: {teams_with_zekken.count()}")
|
||||||
|
|
||||||
|
if teams_with_zekken.exists():
|
||||||
|
print(" サンプル:")
|
||||||
|
for team in teams_with_zekken[:10]:
|
||||||
|
print(f" ID:{team.id}, Name:{team.name}, Zekken:{team.zekken_number}, Event:{team.event.event_name if team.event else 'None'}")
|
||||||
|
|
||||||
|
# 4. 特定のイベントID(仮に100とする)を詳細調査
|
||||||
|
print("\n4. イベントID 100 詳細調査:")
|
||||||
|
try:
|
||||||
|
event_100 = NewEvent2.objects.get(id=100)
|
||||||
|
print(f" イベント: {event_100.event_name} ({event_100.event_day})")
|
||||||
|
|
||||||
|
# エントリー確認
|
||||||
|
entries_100 = Entry.objects.filter(event=event_100)
|
||||||
|
print(f" エントリー数: {entries_100.count()}")
|
||||||
|
|
||||||
|
# チーム確認
|
||||||
|
teams_100 = Team.objects.filter(event=event_100)
|
||||||
|
print(f" チーム数: {teams_100.count()}")
|
||||||
|
|
||||||
|
# ゼッケン番号付きチーム確認
|
||||||
|
teams_100_with_zekken = teams_100.exclude(zekken_number__isnull=True).exclude(zekken_number='')
|
||||||
|
print(f" ゼッケン番号付きチーム数: {teams_100_with_zekken.count()}")
|
||||||
|
|
||||||
|
if teams_100_with_zekken.exists():
|
||||||
|
print(" ゼッケン番号付きチーム:")
|
||||||
|
for team in teams_100_with_zekken:
|
||||||
|
print(f" ID:{team.id}, Name:{team.name}, Zekken:{team.zekken_number}")
|
||||||
|
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
print(" イベントID 100は存在しません")
|
||||||
|
|
||||||
|
# 5. Entryテーブルとチームの関係確認
|
||||||
|
print("\n5. Entry-Team関係確認:")
|
||||||
|
total_entries = Entry.objects.all().count()
|
||||||
|
entries_with_teams = Entry.objects.exclude(team__isnull=True).count()
|
||||||
|
print(f" 総エントリー数: {total_entries}")
|
||||||
|
print(f" チーム関連付けありエントリー数: {entries_with_teams}")
|
||||||
|
|
||||||
|
# サンプルエントリーの詳細
|
||||||
|
print(" サンプルエントリー詳細:")
|
||||||
|
sample_entries = Entry.objects.all()[:5]
|
||||||
|
for entry in sample_entries:
|
||||||
|
team = entry.team
|
||||||
|
event = entry.event
|
||||||
|
print(f" Entry ID:{entry.id}, Team:{team.name if team else 'None'}({team.id if team else 'None'}), Event:{event.event_name if event else 'None'}({event.id if event else 'None'})")
|
||||||
|
if team:
|
||||||
|
print(f" Team Zekken:{team.zekken_number}, Team Event:{team.event.event_name if team.event else 'None'}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
analyze_fc_gifu_data()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
392
analyze_nginx_logs.py
Normal file
392
analyze_nginx_logs.py
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
nginxログ分析: チェックイン・画像アップロード機能の使用状況を確認
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
from collections import defaultdict, Counter
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def analyze_provided_logs():
|
||||||
|
"""
|
||||||
|
ユーザーが提供したログデータを分析
|
||||||
|
"""
|
||||||
|
print("🔍 提供されたログデータの分析")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# ユーザーが提供したログデータ
|
||||||
|
log_data = """
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:15 +0000] "GET /api/new-events/ HTTP/1.0" 200 22641 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:15 +0000] "GET /api/entry/ HTTP/1.0" 200 11524 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:15 +0000] "GET /api/teams/ HTTP/1.0" 200 674 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:15 +0000] "GET /api/categories/ HTTP/1.0" 200 2824 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:18 +0000] "GET /api/user/current-entry-info/ HTTP/1.0" 200 512 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:19 +0000] "PATCH /api/entries/897/update-status/ HTTP/1.0" 200 1281 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:31 +0000] "GET /api/entry/ HTTP/1.0" 200 11523 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:31 +0000] "GET /api/teams/ HTTP/1.0" 200 674 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:31 +0000] "GET /api/new-events/ HTTP/1.0" 200 22641 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
nginx-1 | 172.18.0.1 - - [05/Sep/2025:17:25:31 +0000] "GET /api/categories/ HTTP/1.0" 200 2824 "-" "Dart/3.9 (dart:io)" "202.215.43.20"
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
analyze_log_content(log_data.split('\n'))
|
||||||
|
|
||||||
|
def analyze_log_content(lines):
|
||||||
|
"""
|
||||||
|
ログ内容を分析する共通関数
|
||||||
|
"""
|
||||||
|
print(f"📊 ログ行数: {len(lines)}")
|
||||||
|
|
||||||
|
# チェックイン・画像関連のエンドポイント
|
||||||
|
checkin_endpoints = {
|
||||||
|
'/api/checkinimage/': '画像アップロード',
|
||||||
|
'/gifuroge/checkin_from_rogapp': 'チェックイン登録',
|
||||||
|
'/api/bulk_upload_checkin_photos/': '一括写真アップロード',
|
||||||
|
'/api/user/current-entry-info/': 'ユーザー参加情報',
|
||||||
|
'/api/entries/': 'エントリー操作',
|
||||||
|
'/api/new-events/': 'イベント情報',
|
||||||
|
'/api/teams/': 'チーム情報',
|
||||||
|
'/api/categories/': 'カテゴリ情報'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 分析結果
|
||||||
|
endpoint_counts = defaultdict(int)
|
||||||
|
methods = Counter()
|
||||||
|
status_codes = Counter()
|
||||||
|
user_agents = Counter()
|
||||||
|
client_ips = Counter()
|
||||||
|
dart_requests = 0
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ログパターンマッチング
|
||||||
|
# nginx-1 | IP - - [timestamp] "METHOD path HTTP/1.0" status size "-" "user-agent" "real-ip"
|
||||||
|
match = re.search(r'"(\w+)\s+([^"]+)\s+HTTP/[\d\.]+"\s+(\d+)\s+(\d+)\s+"[^"]*"\s+"([^"]*)"\s+"([^"]*)"', line)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
method = match.group(1)
|
||||||
|
path = match.group(2)
|
||||||
|
status = match.group(3)
|
||||||
|
size = match.group(4)
|
||||||
|
user_agent = match.group(5)
|
||||||
|
real_ip = match.group(6)
|
||||||
|
|
||||||
|
methods[method] += 1
|
||||||
|
status_codes[status] += 1
|
||||||
|
user_agents[user_agent] += 1
|
||||||
|
client_ips[real_ip] += 1
|
||||||
|
|
||||||
|
# Dartクライアント(スマホアプリ)の検出
|
||||||
|
if 'Dart/' in user_agent:
|
||||||
|
dart_requests += 1
|
||||||
|
|
||||||
|
# エンドポイント別カウント
|
||||||
|
for endpoint in checkin_endpoints:
|
||||||
|
if endpoint in path:
|
||||||
|
endpoint_counts[endpoint] += 1
|
||||||
|
|
||||||
|
# 結果表示
|
||||||
|
print(f"\n📱 Dartクライアント(スマホアプリ)リクエスト: {dart_requests}件")
|
||||||
|
|
||||||
|
print(f"\n🎯 関連APIエンドポイントの使用状況:")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
found_activity = False
|
||||||
|
for endpoint, description in checkin_endpoints.items():
|
||||||
|
count = endpoint_counts[endpoint]
|
||||||
|
if count > 0:
|
||||||
|
print(f"✅ {description:20s} ({endpoint}): {count}回")
|
||||||
|
found_activity = True
|
||||||
|
else:
|
||||||
|
print(f"❌ {description:20s} ({endpoint}): アクセスなし")
|
||||||
|
|
||||||
|
print(f"\n📊 HTTPメソッド別:")
|
||||||
|
for method, count in methods.most_common():
|
||||||
|
print(f" {method}: {count}回")
|
||||||
|
|
||||||
|
print(f"\n📊 ステータスコード別:")
|
||||||
|
for status, count in status_codes.most_common():
|
||||||
|
print(f" HTTP {status}: {count}回")
|
||||||
|
|
||||||
|
print(f"\n📊 User Agent:")
|
||||||
|
for ua, count in user_agents.most_common():
|
||||||
|
ua_short = ua[:50] + "..." if len(ua) > 50 else ua
|
||||||
|
print(f" {ua_short}: {count}回")
|
||||||
|
|
||||||
|
print(f"\n📊 クライアントIP:")
|
||||||
|
for ip, count in client_ips.most_common():
|
||||||
|
print(f" {ip}: {count}回")
|
||||||
|
|
||||||
|
# チェックイン・画像機能の判定
|
||||||
|
print(f"\n🎯 機能使用状況の判定:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
checkin_active = endpoint_counts['/gifuroge/checkin_from_rogapp'] > 0
|
||||||
|
image_upload_active = endpoint_counts['/api/checkinimage/'] > 0
|
||||||
|
bulk_upload_active = endpoint_counts['/api/bulk_upload_checkin_photos/'] > 0
|
||||||
|
|
||||||
|
print(f"チェックイン登録機能: {'✅ 使用中' if checkin_active else '❌ 未使用'}")
|
||||||
|
print(f"画像アップロード機能: {'✅ 使用中' if image_upload_active else '❌ 未使用'}")
|
||||||
|
print(f"一括写真アップロード機能: {'✅ 使用中' if bulk_upload_active else '❌ 未使用'}")
|
||||||
|
print(f"スマホアプリ(Dartクライアント): {'✅ アクティブ' if dart_requests > 0 else '❌ 非アクティブ'}")
|
||||||
|
|
||||||
|
if dart_requests > 0:
|
||||||
|
print(f"\n📱 スマホアプリの動作状況:")
|
||||||
|
print(f" • アプリは正常に動作している")
|
||||||
|
print(f" • イベント情報、エントリー情報、チーム情報を取得中")
|
||||||
|
print(f" • エントリーステータスの更新も実行中")
|
||||||
|
print(f" • ただし、チェックインや画像アップロードは確認されていない")
|
||||||
|
|
||||||
|
return found_activity
|
||||||
|
"""
|
||||||
|
nginxログを分析してチェックイン・画像関連の活動を確認
|
||||||
|
"""
|
||||||
|
print("🔍 nginx ログ分析: チェックイン・画像アップロード機能")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# nginxログを取得
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker-compose', 'logs', '--tail=500', 'nginx'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ ログ取得エラー: {result.stderr}")
|
||||||
|
return
|
||||||
|
|
||||||
|
log_lines = result.stdout.split('\n')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 実行エラー: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 分析用パターン
|
||||||
|
patterns = {
|
||||||
|
'checkin_api': re.compile(r'(POST|GET).*/(checkin_from_rogapp|checkin|addCheckin)', re.I),
|
||||||
|
'image_api': re.compile(r'(POST|GET).*/checkinimage', re.I),
|
||||||
|
'bulk_upload': re.compile(r'(POST|GET).*/bulk_upload', re.I),
|
||||||
|
'dart_client': re.compile(r'"Dart/[\d\.]+ \(dart:io\)"'),
|
||||||
|
'api_access': re.compile(r'"(GET|POST|PUT|PATCH|DELETE) (/api/[^"]+)'),
|
||||||
|
'status_codes': re.compile(r'" (\d{3}) \d+')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 分析結果
|
||||||
|
results = {
|
||||||
|
'checkin_requests': [],
|
||||||
|
'image_requests': [],
|
||||||
|
'bulk_upload_requests': [],
|
||||||
|
'dart_requests': [],
|
||||||
|
'api_endpoints': Counter(),
|
||||||
|
'status_codes': Counter(),
|
||||||
|
'client_ips': Counter()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ログ行の解析
|
||||||
|
for line in log_lines:
|
||||||
|
if not line.strip() or 'nginx-1' not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 各パターンをチェック
|
||||||
|
if patterns['checkin_api'].search(line):
|
||||||
|
results['checkin_requests'].append(line)
|
||||||
|
|
||||||
|
if patterns['image_api'].search(line):
|
||||||
|
results['image_requests'].append(line)
|
||||||
|
|
||||||
|
if patterns['bulk_upload'].search(line):
|
||||||
|
results['bulk_upload_requests'].append(line)
|
||||||
|
|
||||||
|
if patterns['dart_client'].search(line):
|
||||||
|
results['dart_requests'].append(line)
|
||||||
|
|
||||||
|
# APIエンドポイント集計
|
||||||
|
api_match = patterns['api_access'].search(line)
|
||||||
|
if api_match:
|
||||||
|
method, endpoint = api_match.groups()
|
||||||
|
results['api_endpoints'][f"{method} {endpoint}"] += 1
|
||||||
|
|
||||||
|
# ステータスコード集計
|
||||||
|
status_match = patterns['status_codes'].search(line)
|
||||||
|
if status_match:
|
||||||
|
results['status_codes'][status_match.group(1)] += 1
|
||||||
|
|
||||||
|
# クライアントIP集計
|
||||||
|
ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+) - -', line)
|
||||||
|
if ip_match:
|
||||||
|
results['client_ips'][ip_match.group(1)] += 1
|
||||||
|
|
||||||
|
# 結果表示
|
||||||
|
print_analysis_results(results)
|
||||||
|
|
||||||
|
def print_analysis_results(results):
|
||||||
|
"""
|
||||||
|
分析結果を表示
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. チェックインAPI使用状況
|
||||||
|
print(f"\n📍 チェックインAPI アクセス状況")
|
||||||
|
print("-" * 50)
|
||||||
|
if results['checkin_requests']:
|
||||||
|
print(f"件数: {len(results['checkin_requests'])}件")
|
||||||
|
for req in results['checkin_requests'][-5:]: # 最新5件
|
||||||
|
print(f" {extract_log_info(req)}")
|
||||||
|
else:
|
||||||
|
print("❌ チェックインAPIへのアクセスなし")
|
||||||
|
|
||||||
|
# 2. 画像アップロードAPI使用状況
|
||||||
|
print(f"\n🖼️ 画像アップロードAPI アクセス状況")
|
||||||
|
print("-" * 50)
|
||||||
|
if results['image_requests']:
|
||||||
|
print(f"件数: {len(results['image_requests'])}件")
|
||||||
|
for req in results['image_requests'][-5:]: # 最新5件
|
||||||
|
print(f" {extract_log_info(req)}")
|
||||||
|
else:
|
||||||
|
print("❌ 画像アップロードAPIへのアクセスなし")
|
||||||
|
|
||||||
|
# 3. 一括アップロードAPI使用状況
|
||||||
|
print(f"\n📤 一括アップロードAPI アクセス状況")
|
||||||
|
print("-" * 50)
|
||||||
|
if results['bulk_upload_requests']:
|
||||||
|
print(f"件数: {len(results['bulk_upload_requests'])}件")
|
||||||
|
for req in results['bulk_upload_requests'][-5:]: # 最新5件
|
||||||
|
print(f" {extract_log_info(req)}")
|
||||||
|
else:
|
||||||
|
print("❌ 一括アップロードAPIへのアクセスなし")
|
||||||
|
|
||||||
|
# 4. Dartクライアント(スマホアプリ)の活動
|
||||||
|
print(f"\n📱 スマホアプリ(Dart)アクセス状況")
|
||||||
|
print("-" * 50)
|
||||||
|
if results['dart_requests']:
|
||||||
|
print(f"件数: {len(results['dart_requests'])}件")
|
||||||
|
|
||||||
|
# Dartクライアントが使用しているAPIエンドポイント
|
||||||
|
dart_endpoints = Counter()
|
||||||
|
for req in results['dart_requests']:
|
||||||
|
api_match = re.search(r'"(GET|POST|PUT|PATCH|DELETE) (/api/[^"]+)', req)
|
||||||
|
if api_match:
|
||||||
|
method, endpoint = api_match.groups()
|
||||||
|
dart_endpoints[f"{method} {endpoint}"] += 1
|
||||||
|
|
||||||
|
print("主要なAPIエンドポイント:")
|
||||||
|
for endpoint, count in dart_endpoints.most_common(10):
|
||||||
|
print(f" {endpoint}: {count}回")
|
||||||
|
else:
|
||||||
|
print("❌ スマホアプリからのアクセスなし")
|
||||||
|
|
||||||
|
# 5. 全体のAPI使用状況(Top 10)
|
||||||
|
print(f"\n🌐 API使用状況 (Top 10)")
|
||||||
|
print("-" * 50)
|
||||||
|
for endpoint, count in results['api_endpoints'].most_common(10):
|
||||||
|
print(f" {endpoint}: {count}回")
|
||||||
|
|
||||||
|
# 6. HTTPステータスコード分布
|
||||||
|
print(f"\n📊 HTTPステータスコード分布")
|
||||||
|
print("-" * 50)
|
||||||
|
for status, count in results['status_codes'].most_common():
|
||||||
|
status_emoji = get_status_emoji(status)
|
||||||
|
print(f" {status_emoji} {status}: {count}回")
|
||||||
|
|
||||||
|
# 7. クライアントIP分布
|
||||||
|
print(f"\n🌍 アクセス元IP分布")
|
||||||
|
print("-" * 50)
|
||||||
|
for ip, count in results['client_ips'].most_common(5):
|
||||||
|
ip_type = "🏠 ローカル" if ip.startswith(('192.168', '172.', '127.')) else "🌐 外部"
|
||||||
|
print(f" {ip_type} {ip}: {count}回")
|
||||||
|
|
||||||
|
def extract_log_info(log_line):
|
||||||
|
"""
|
||||||
|
ログ行から重要な情報を抽出
|
||||||
|
"""
|
||||||
|
# 時刻を抽出
|
||||||
|
time_match = re.search(r'\[([^\]]+)\]', log_line)
|
||||||
|
time_str = time_match.group(1) if time_match else "Unknown"
|
||||||
|
|
||||||
|
# メソッドとパスを抽出
|
||||||
|
method_match = re.search(r'"(GET|POST|PUT|PATCH|DELETE) ([^"]+)', log_line)
|
||||||
|
method_path = method_match.groups() if method_match else ("Unknown", "Unknown")
|
||||||
|
|
||||||
|
# ステータスコードを抽出
|
||||||
|
status_match = re.search(r'" (\d{3}) \d+', log_line)
|
||||||
|
status = status_match.group(1) if status_match else "Unknown"
|
||||||
|
|
||||||
|
# User Agentを抽出
|
||||||
|
ua_match = re.search(r'"([^"]+)" "[^"]*"$', log_line)
|
||||||
|
user_agent = ua_match.group(1) if ua_match else "Unknown"
|
||||||
|
|
||||||
|
return f"{time_str} | {method_path[0]} {method_path[1][:50]}... | {status} | {user_agent[:20]}..."
|
||||||
|
|
||||||
|
def get_status_emoji(status_code):
|
||||||
|
"""
|
||||||
|
HTTPステータスコードに対応する絵文字を返す
|
||||||
|
"""
|
||||||
|
if status_code.startswith('2'):
|
||||||
|
return '✅'
|
||||||
|
elif status_code.startswith('3'):
|
||||||
|
return '🔀'
|
||||||
|
elif status_code.startswith('4'):
|
||||||
|
return '❌'
|
||||||
|
elif status_code.startswith('5'):
|
||||||
|
return '💥'
|
||||||
|
else:
|
||||||
|
return '❓'
|
||||||
|
|
||||||
|
def analyze_nginx_logs():
|
||||||
|
"""
|
||||||
|
Dockerコンテナからnginxログを取得・分析
|
||||||
|
"""
|
||||||
|
print("🔍 Dockerからnginxログを取得中...")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# docker-compose logsでnginxのログを取得
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker-compose', 'logs', '--tail=100', 'nginx'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = result.stdout.split('\n')
|
||||||
|
analyze_log_content(lines)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ ログ取得エラー: {e}")
|
||||||
|
print(f"stderr: {e.stderr}")
|
||||||
|
print("\n💡 Dockerコンテナが起動していることを確認してください")
|
||||||
|
print(" docker-compose ps")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("❌ docker-composeコマンドが見つかりません")
|
||||||
|
print("💡 Dockerがインストールされていることを確認してください")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🚀 スマホアプリのnginxログ解析ツール")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
choice = input("\n分析方法を選択してください:\n1. Dockerからログを取得\n2. 提供されたログデータを分析\n選択 (1/2): ")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if choice == "1":
|
||||||
|
analyze_nginx_logs()
|
||||||
|
elif choice == "2":
|
||||||
|
analyze_provided_logs()
|
||||||
|
else:
|
||||||
|
print("無効な選択です。提供されたログデータを分析します。")
|
||||||
|
analyze_provided_logs()
|
||||||
|
|
||||||
|
print(f"\n✅ 分析完了")
|
||||||
|
print(f"\n💡 結論:")
|
||||||
|
print(f" ログから、チェックイン・画像アップロード機能の実際の使用状況を確認できます")
|
||||||
|
print(f" スマホアプリ(Dart)の活動状況も把握可能です")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラー: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
158
analyze_old_rogdb.py
Normal file
158
analyze_old_rogdb.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb構造分析&データ移行準備スクリプト
|
||||||
|
old_rogdbの構造を詳細に分析し、rogdbへの移行計画を立てる
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
print("=== old_rogdb構造分析 ===")
|
||||||
|
|
||||||
|
# old_rogdb直接接続設定
|
||||||
|
old_db_config = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'database': 'old_rogdb',
|
||||||
|
'user': 'admin',
|
||||||
|
'password': 'admin123456',
|
||||||
|
'port': 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# old_rogdbに直接接続
|
||||||
|
old_conn = psycopg2.connect(**old_db_config)
|
||||||
|
old_cursor = old_conn.cursor()
|
||||||
|
|
||||||
|
print("✅ old_rogdb接続成功")
|
||||||
|
|
||||||
|
print("\\n=== 1. old_rogdb rog_entry構造分析 ===")
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_entry' AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
old_entry_columns = old_cursor.fetchall()
|
||||||
|
|
||||||
|
print("old_rogdb.rog_entry 構造:")
|
||||||
|
for col_name, data_type, nullable, default in old_entry_columns:
|
||||||
|
print(f" - {col_name}: {data_type} {'(NULL可)' if nullable == 'YES' else '(NOT NULL)'} {f'[default: {default}]' if default else ''}")
|
||||||
|
|
||||||
|
# old_rogdb rog_entry データ確認
|
||||||
|
old_cursor.execute("SELECT COUNT(*) FROM rog_entry;")
|
||||||
|
old_entry_count = old_cursor.fetchone()[0]
|
||||||
|
print(f"\\nold_rogdb.rog_entry データ件数: {old_entry_count}件")
|
||||||
|
|
||||||
|
# サンプルデータ確認
|
||||||
|
old_cursor.execute("SELECT * FROM rog_entry LIMIT 3;")
|
||||||
|
old_entry_samples = old_cursor.fetchall()
|
||||||
|
print("\\nサンプルデータ(最初の3件):")
|
||||||
|
for i, row in enumerate(old_entry_samples):
|
||||||
|
print(f" Row {i+1}: {row}")
|
||||||
|
|
||||||
|
print("\\n=== 2. old_rogdb rog_team構造分析 ===")
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_team' AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
old_team_columns = old_cursor.fetchall()
|
||||||
|
|
||||||
|
print("old_rogdb.rog_team 構造:")
|
||||||
|
for col_name, data_type, nullable, default in old_team_columns:
|
||||||
|
print(f" - {col_name}: {data_type} {'(NULL可)' if nullable == 'YES' else '(NOT NULL)'} {f'[default: {default}]' if default else ''}")
|
||||||
|
|
||||||
|
old_cursor.execute("SELECT COUNT(*) FROM rog_team;")
|
||||||
|
old_team_count = old_cursor.fetchone()[0]
|
||||||
|
print(f"\\nold_rogdb.rog_team データ件数: {old_team_count}件")
|
||||||
|
|
||||||
|
print("\\n=== 3. old_rogdb rog_member構造分析 ===")
|
||||||
|
try:
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_member' AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
old_member_columns = old_cursor.fetchall()
|
||||||
|
|
||||||
|
if old_member_columns:
|
||||||
|
print("old_rogdb.rog_member 構造:")
|
||||||
|
for col_name, data_type, nullable, default in old_member_columns:
|
||||||
|
print(f" - {col_name}: {data_type} {'(NULL可)' if nullable == 'YES' else '(NOT NULL)'} {f'[default: {default}]' if default else ''}")
|
||||||
|
|
||||||
|
old_cursor.execute("SELECT COUNT(*) FROM rog_member;")
|
||||||
|
old_member_count = old_cursor.fetchone()[0]
|
||||||
|
print(f"\\nold_rogdb.rog_member データ件数: {old_member_count}件")
|
||||||
|
else:
|
||||||
|
print("old_rogdb.rog_member テーブルが存在しません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"old_rogdb.rog_member 確認エラー: {e}")
|
||||||
|
|
||||||
|
print("\\n=== 4. FC岐阜関連データ詳細分析 ===")
|
||||||
|
|
||||||
|
# FC岐阜イベント確認
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT id, event_name, start_datetime, end_datetime
|
||||||
|
FROM rog_newevent2
|
||||||
|
WHERE event_name LIKE '%FC岐阜%' OR event_name LIKE '%fc岐阜%'
|
||||||
|
ORDER BY id;
|
||||||
|
""")
|
||||||
|
fc_events = old_cursor.fetchall()
|
||||||
|
|
||||||
|
print("FC岐阜関連イベント:")
|
||||||
|
for event_id, name, start, end in fc_events:
|
||||||
|
print(f" Event {event_id}: '{name}' ({start} - {end})")
|
||||||
|
|
||||||
|
# このイベントのエントリー数確認
|
||||||
|
old_cursor.execute("SELECT COUNT(*) FROM rog_entry WHERE event_id = %s;", (event_id,))
|
||||||
|
entry_count = old_cursor.fetchone()[0]
|
||||||
|
print(f" エントリー数: {entry_count}件")
|
||||||
|
|
||||||
|
# FC岐阜イベントのエントリー詳細
|
||||||
|
if fc_events:
|
||||||
|
fc_event_id = fc_events[0][0] # 最初のFC岐阜イベント
|
||||||
|
print(f"\\nFC岐阜イベント(ID:{fc_event_id})のエントリー詳細:")
|
||||||
|
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.category_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, rc.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON re.category_id = rc.id
|
||||||
|
WHERE re.event_id = %s
|
||||||
|
ORDER BY re.zekken_number
|
||||||
|
LIMIT 10;
|
||||||
|
""", (fc_event_id,))
|
||||||
|
|
||||||
|
fc_entry_details = old_cursor.fetchall()
|
||||||
|
for entry_id, team_id, cat_id, zekken, label, team_name, cat_name in fc_entry_details:
|
||||||
|
print(f" Entry {entry_id}: Team {team_id}({team_name}) - ゼッケン{zekken} - {cat_name}")
|
||||||
|
|
||||||
|
print("\\n=== 5. 移行計画 ===")
|
||||||
|
print("移行が必要なテーブル:")
|
||||||
|
print(" 1. old_rogdb.rog_team → rogdb.rog_team")
|
||||||
|
print(" 2. old_rogdb.rog_entry → rogdb.rog_entry")
|
||||||
|
print(" 3. old_rogdb.rog_member → rogdb.rog_member (存在する場合)")
|
||||||
|
print("\\n注意点:")
|
||||||
|
print(" - イベントはrog_newevent2を使用")
|
||||||
|
print(" - 外部キー制約の整合性確保")
|
||||||
|
print(" - データ型の変換(必要に応じて)")
|
||||||
|
print(" - 重複データの回避")
|
||||||
|
|
||||||
|
old_cursor.close()
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
117
api_requirements_migration.sql
Normal file
117
api_requirements_migration.sql
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
-- サーバーAPI変更要求書対応データベース移行スクリプト
|
||||||
|
-- 2025年8月27日
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1. NewEvent2テーブルにstatusフィールド追加
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_newevent2' AND column_name = 'status'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_newevent2 ADD COLUMN status VARCHAR(20) DEFAULT 'draft'
|
||||||
|
CHECK (status IN ('public', 'private', 'draft', 'closed'));
|
||||||
|
|
||||||
|
-- 既存のpublicフィールドからstatusフィールドへの移行
|
||||||
|
UPDATE rog_newevent2 SET status = CASE
|
||||||
|
WHEN public = true THEN 'public'
|
||||||
|
ELSE 'draft'
|
||||||
|
END;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rog_newevent2.status IS 'イベントステータス (public/private/draft/closed)';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 2. Entryテーブルにスタッフ権限フィールド追加
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_entry' AND column_name = 'staff_privileges'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_entry ADD COLUMN staff_privileges BOOLEAN DEFAULT FALSE;
|
||||||
|
COMMENT ON COLUMN rog_entry.staff_privileges IS 'スタッフ権限フラグ';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_entry' AND column_name = 'can_access_private_events'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_entry ADD COLUMN can_access_private_events BOOLEAN DEFAULT FALSE;
|
||||||
|
COMMENT ON COLUMN rog_entry.can_access_private_events IS '非公開イベント参加権限';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_entry' AND column_name = 'team_validation_status'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_entry ADD COLUMN team_validation_status VARCHAR(20) DEFAULT 'approved'
|
||||||
|
CHECK (team_validation_status IN ('approved', 'pending', 'rejected'));
|
||||||
|
COMMENT ON COLUMN rog_entry.team_validation_status IS 'チーム承認状況';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 3. インデックス追加
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_newevent2_status ON rog_newevent2(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_entry_staff_privileges ON rog_entry(staff_privileges) WHERE staff_privileges = TRUE;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_entry_validation_status ON rog_entry(team_validation_status);
|
||||||
|
|
||||||
|
-- 4. データ整合性チェック
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
rec RECORD;
|
||||||
|
inconsistent_count INTEGER := 0;
|
||||||
|
BEGIN
|
||||||
|
-- publicフィールドとstatusフィールドの整合性チェック
|
||||||
|
FOR rec IN (
|
||||||
|
SELECT id, event_name, public, status
|
||||||
|
FROM rog_newevent2
|
||||||
|
WHERE (public = TRUE AND status != 'public')
|
||||||
|
OR (public = FALSE AND status = 'public')
|
||||||
|
) LOOP
|
||||||
|
RAISE NOTICE 'Inconsistent status for event %: public=%, status=%',
|
||||||
|
rec.event_name, rec.public, rec.status;
|
||||||
|
inconsistent_count := inconsistent_count + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF inconsistent_count > 0 THEN
|
||||||
|
RAISE NOTICE 'Found % events with inconsistent public/status values', inconsistent_count;
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'All events have consistent public/status values';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 5. 統計情報更新
|
||||||
|
ANALYZE rog_newevent2;
|
||||||
|
ANALYZE rog_entry;
|
||||||
|
|
||||||
|
-- 6. 移行結果サマリー
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
event_count INTEGER;
|
||||||
|
entry_count INTEGER;
|
||||||
|
public_events INTEGER;
|
||||||
|
private_events INTEGER;
|
||||||
|
draft_events INTEGER;
|
||||||
|
staff_entries INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO event_count FROM rog_newevent2;
|
||||||
|
SELECT COUNT(*) INTO entry_count FROM rog_entry;
|
||||||
|
SELECT COUNT(*) INTO public_events FROM rog_newevent2 WHERE status = 'public';
|
||||||
|
SELECT COUNT(*) INTO private_events FROM rog_newevent2 WHERE status = 'private';
|
||||||
|
SELECT COUNT(*) INTO draft_events FROM rog_newevent2 WHERE status = 'draft';
|
||||||
|
SELECT COUNT(*) INTO staff_entries FROM rog_entry WHERE staff_privileges = TRUE;
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '=== 移行完了サマリー ===';
|
||||||
|
RAISE NOTICE 'イベント総数: %', event_count;
|
||||||
|
RAISE NOTICE ' - Public: %', public_events;
|
||||||
|
RAISE NOTICE ' - Private: %', private_events;
|
||||||
|
RAISE NOTICE ' - Draft: %', draft_events;
|
||||||
|
RAISE NOTICE 'エントリー総数: %', entry_count;
|
||||||
|
RAISE NOTICE ' - スタッフ権限付与: %', staff_entries;
|
||||||
|
RAISE NOTICE '';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
319
check_checkin_status.py
Normal file
319
check_checkin_status.py
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
チェックイン機能確認ツール: 総合的にチェックイン機能の状態を調査
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
def check_checkin_api_status():
|
||||||
|
"""
|
||||||
|
チェックインAPIの基本動作確認
|
||||||
|
"""
|
||||||
|
print("🔍 チェックインAPI動作確認")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 基本的な接続確認
|
||||||
|
test_urls = [
|
||||||
|
"http://localhost:8100/gifuroge/checkin_from_rogapp",
|
||||||
|
"http://localhost:8100/api/checkin_from_rogapp"
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in test_urls:
|
||||||
|
try:
|
||||||
|
# GETリクエストでエンドポイントの存在確認
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
print(f"✅ {url} → HTTP {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 405:
|
||||||
|
print(f" 💡 405 Method Not Allowed は正常(POSTのみ許可)")
|
||||||
|
elif response.status_code == 404:
|
||||||
|
print(f" ❌ 404 Not Found - エンドポイントが見つからない")
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print(f"❌ {url} → 接続エラー")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {url} → エラー: {e}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
def test_checkin_with_real_data():
|
||||||
|
"""
|
||||||
|
実際のデータでチェックインテスト
|
||||||
|
"""
|
||||||
|
print("🎯 実際のデータでチェックインテスト")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
# 実際のイベントとチームを取得
|
||||||
|
try:
|
||||||
|
result = subprocess.run([
|
||||||
|
'docker', 'compose', 'exec', 'app', 'python', 'manage.py', 'shell', '-c',
|
||||||
|
"""
|
||||||
|
from rog.models import NewEvent2, Entry, Team
|
||||||
|
# 最新のイベント取得
|
||||||
|
event = NewEvent2.objects.first()
|
||||||
|
if event:
|
||||||
|
print(f"EVENT:{event.event_name}")
|
||||||
|
# そのイベントのエントリー取得
|
||||||
|
entry = Entry.objects.filter(event=event).first()
|
||||||
|
if entry and entry.team:
|
||||||
|
print(f"TEAM:{entry.team.team_name}")
|
||||||
|
print(f"ZEKKEN:{entry.zekken_number}")
|
||||||
|
# スタート済みかチェック
|
||||||
|
from rog.models import GpsLog
|
||||||
|
start_log = GpsLog.objects.filter(
|
||||||
|
zekken_number=entry.zekken_number,
|
||||||
|
event_code=event.event_name,
|
||||||
|
cp_number='START'
|
||||||
|
).first()
|
||||||
|
print(f"STARTED:{bool(start_log)}")
|
||||||
|
else:
|
||||||
|
print("TEAM:None")
|
||||||
|
else:
|
||||||
|
print("EVENT:None")
|
||||||
|
"""
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
lines = result.stdout.strip().split('\n')
|
||||||
|
event_name = None
|
||||||
|
team_name = None
|
||||||
|
zekken_number = None
|
||||||
|
is_started = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('EVENT:'):
|
||||||
|
event_name = line.split(':', 1)[1]
|
||||||
|
elif line.startswith('TEAM:'):
|
||||||
|
team_name = line.split(':', 1)[1]
|
||||||
|
elif line.startswith('ZEKKEN:'):
|
||||||
|
zekken_number = line.split(':', 1)[1]
|
||||||
|
elif line.startswith('STARTED:'):
|
||||||
|
is_started = line.split(':', 1)[1] == 'True'
|
||||||
|
|
||||||
|
print(f"📊 取得したテストデータ:")
|
||||||
|
print(f" イベント: {event_name}")
|
||||||
|
print(f" チーム: {team_name}")
|
||||||
|
print(f" ゼッケン: {zekken_number}")
|
||||||
|
print(f" スタート済み: {is_started}")
|
||||||
|
|
||||||
|
if event_name and team_name and event_name != 'None' and team_name != 'None':
|
||||||
|
# チェックインテスト実行
|
||||||
|
test_data = {
|
||||||
|
"event_code": event_name,
|
||||||
|
"team_name": team_name,
|
||||||
|
"cp_number": "1",
|
||||||
|
"image": "data:image/jpeg;base64,test",
|
||||||
|
"buy_flag": False
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"\n🚀 チェックインテスト実行:")
|
||||||
|
print(f" URL: http://localhost:8100/api/checkin_from_rogapp")
|
||||||
|
print(f" データ: {json.dumps(test_data, ensure_ascii=False, indent=2)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8100/api/checkin_from_rogapp",
|
||||||
|
json=test_data,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n📥 レスポンス:")
|
||||||
|
print(f" ステータス: HTTP {response.status_code}")
|
||||||
|
print(f" 内容: {response.text}")
|
||||||
|
|
||||||
|
if response.status_code == 400:
|
||||||
|
response_data = response.json()
|
||||||
|
if "スタートしていません" in response_data.get('message', ''):
|
||||||
|
print(f"\n💡 スタート処理が必要です。start_from_rogapp APIを先に実行してください。")
|
||||||
|
return test_start_api(event_name, team_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ チェックインテストエラー: {e}")
|
||||||
|
else:
|
||||||
|
print(f"❌ テストデータが不足しています")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ データ取得エラー: {e}")
|
||||||
|
|
||||||
|
def test_start_api(event_name, team_name):
|
||||||
|
"""
|
||||||
|
スタートAPIのテスト
|
||||||
|
"""
|
||||||
|
print(f"\n🏁 スタートAPIテスト")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
start_data = {
|
||||||
|
"event_code": event_name,
|
||||||
|
"team_name": team_name,
|
||||||
|
"image": "data:image/jpeg;base64,start_test"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8100/gifuroge/start_from_rogapp",
|
||||||
|
json=start_data,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📥 スタートAPIレスポンス:")
|
||||||
|
print(f" ステータス: HTTP {response.status_code}")
|
||||||
|
print(f" 内容: {response.text}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"✅ スタート成功!チェックインを再試行します...")
|
||||||
|
time.sleep(1)
|
||||||
|
# チェックインを再試行
|
||||||
|
test_checkin_after_start(event_name, team_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ スタートAPIエラー: {e}")
|
||||||
|
|
||||||
|
def test_checkin_after_start(event_name, team_name):
|
||||||
|
"""
|
||||||
|
スタート後のチェックインテスト
|
||||||
|
"""
|
||||||
|
print(f"\n🎯 スタート後チェックインテスト")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
checkin_data = {
|
||||||
|
"event_code": event_name,
|
||||||
|
"team_name": team_name,
|
||||||
|
"cp_number": "1",
|
||||||
|
"image": "data:image/jpeg;base64,checkin_test",
|
||||||
|
"buy_flag": False
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8100/api/checkin_from_rogapp",
|
||||||
|
json=checkin_data,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📥 チェックインレスポンス:")
|
||||||
|
print(f" ステータス: HTTP {response.status_code}")
|
||||||
|
print(f" 内容: {response.text}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"🎉 チェックイン成功!")
|
||||||
|
elif response.status_code == 400:
|
||||||
|
print(f"⚠️ チェックイン失敗(400)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ チェックインエラー: {e}")
|
||||||
|
|
||||||
|
def check_recent_logs():
|
||||||
|
"""
|
||||||
|
最近のログを確認
|
||||||
|
"""
|
||||||
|
print(f"\n📋 最近のチェックイン関連ログ")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run([
|
||||||
|
'docker', 'compose', 'logs', '--tail=30', 'app'
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
lines = result.stdout.split('\n')
|
||||||
|
checkin_logs = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if any(keyword in line.lower() for keyword in ['checkin', 'start', 'gpslog', '502', '400', '405']):
|
||||||
|
checkin_logs.append(line)
|
||||||
|
|
||||||
|
if checkin_logs:
|
||||||
|
print("🔍 関連ログ:")
|
||||||
|
for log in checkin_logs[-10:]: # 最新10件
|
||||||
|
print(f" {log}")
|
||||||
|
else:
|
||||||
|
print(" 📝 チェックイン関連のログが見つかりませんでした")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ ログ確認エラー: {e}")
|
||||||
|
|
||||||
|
def check_database_status():
|
||||||
|
"""
|
||||||
|
データベースの状態確認
|
||||||
|
"""
|
||||||
|
print(f"\n💾 データベース状態確認")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run([
|
||||||
|
'docker', 'compose', 'exec', 'app', 'python', 'manage.py', 'shell', '-c',
|
||||||
|
"""
|
||||||
|
from rog.models import GpsLog, NewEvent2, Entry
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# 最近のGpsLogエントリー
|
||||||
|
recent_logs = GpsLog.objects.order_by('-id')[:5]
|
||||||
|
print(f"RECENT_LOGS:{len(recent_logs)}")
|
||||||
|
for log in recent_logs:
|
||||||
|
print(f"LOG:{log.id}|{log.event_code}|{log.zekken_number}|{log.cp_number}|{log.checkin_time}")
|
||||||
|
|
||||||
|
# イベント数
|
||||||
|
event_count = NewEvent2.objects.count()
|
||||||
|
print(f"EVENTS:{event_count}")
|
||||||
|
|
||||||
|
# エントリー数
|
||||||
|
entry_count = Entry.objects.count()
|
||||||
|
print(f"ENTRIES:{entry_count}")
|
||||||
|
"""
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
lines = result.stdout.strip().split('\n')
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('RECENT_LOGS:'):
|
||||||
|
count = line.split(':', 1)[1]
|
||||||
|
print(f" 最近のGpsLogエントリー: {count}件")
|
||||||
|
elif line.startswith('LOG:'):
|
||||||
|
parts = line.split(':', 1)[1].split('|')
|
||||||
|
if len(parts) >= 5:
|
||||||
|
print(f" ID:{parts[0]} イベント:{parts[1]} ゼッケン:{parts[2]} CP:{parts[3]} 時刻:{parts[4]}")
|
||||||
|
elif line.startswith('EVENTS:'):
|
||||||
|
count = line.split(':', 1)[1]
|
||||||
|
print(f" 総イベント数: {count}")
|
||||||
|
elif line.startswith('ENTRIES:'):
|
||||||
|
count = line.split(':', 1)[1]
|
||||||
|
print(f" 総エントリー数: {count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ データベース確認エラー: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
メイン実行関数
|
||||||
|
"""
|
||||||
|
print("🚀 チェックイン機能 総合確認ツール")
|
||||||
|
print(f"実行時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. API基本動作確認
|
||||||
|
check_checkin_api_status()
|
||||||
|
|
||||||
|
# 2. データベース状態確認
|
||||||
|
check_database_status()
|
||||||
|
|
||||||
|
# 3. 実際のデータでテスト
|
||||||
|
test_checkin_with_real_data()
|
||||||
|
|
||||||
|
# 4. 最近のログ確認
|
||||||
|
check_recent_logs()
|
||||||
|
|
||||||
|
print(f"\n📊 確認完了")
|
||||||
|
print("=" * 60)
|
||||||
|
print("💡 次のステップ:")
|
||||||
|
print(" 1. 502エラーが出る場合 → nginx設定確認")
|
||||||
|
print(" 2. 405エラーが出る場合 → URLパス確認")
|
||||||
|
print(" 3. 400エラーが出る場合 → データ確認")
|
||||||
|
print(" 4. スタート前エラー → start_from_rogapp API実行")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
175
check_column_names.py
Normal file
175
check_column_names.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
カラム名検証スクリプト
|
||||||
|
PostgreSQLで問題となるカラム名を事前にチェック
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL予約語
|
||||||
|
RESERVED_KEYWORDS = {
|
||||||
|
'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert',
|
||||||
|
'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint',
|
||||||
|
'default', 'check', 'unique', 'primary', 'foreign', 'key', 'references'
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_column_names():
|
||||||
|
"""全rog_テーブルのカラム名をチェック"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# rog_テーブル一覧取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
tables = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
logger.info(f"チェック対象テーブル: {len(tables)}個")
|
||||||
|
|
||||||
|
problematic_columns = {}
|
||||||
|
|
||||||
|
for table_name in tables:
|
||||||
|
# テーブルのカラム一覧取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
problem_cols = []
|
||||||
|
|
||||||
|
for col in columns:
|
||||||
|
# 予約語チェック
|
||||||
|
if col.lower() in RESERVED_KEYWORDS:
|
||||||
|
problem_cols.append((col, '予約語'))
|
||||||
|
|
||||||
|
# キャメルケース/大文字チェック
|
||||||
|
elif any(c.isupper() for c in col) or col != col.lower():
|
||||||
|
problem_cols.append((col, 'キャメルケース/大文字'))
|
||||||
|
|
||||||
|
if problem_cols:
|
||||||
|
problematic_columns[table_name] = problem_cols
|
||||||
|
|
||||||
|
# 結果出力
|
||||||
|
if problematic_columns:
|
||||||
|
logger.warning("⚠️ 問題のあるカラム名が見つかりました:")
|
||||||
|
for table, cols in problematic_columns.items():
|
||||||
|
logger.warning(f" {table}:")
|
||||||
|
for col, reason in cols:
|
||||||
|
logger.warning(f" - {col} ({reason})")
|
||||||
|
else:
|
||||||
|
logger.info("✅ 全てのカラム名は問題ありません")
|
||||||
|
|
||||||
|
# クォートが必要なカラムのリスト生成
|
||||||
|
need_quotes = set()
|
||||||
|
for table, cols in problematic_columns.items():
|
||||||
|
for col, reason in cols:
|
||||||
|
need_quotes.add(col)
|
||||||
|
|
||||||
|
if need_quotes:
|
||||||
|
logger.info("📋 クォートが必要なカラム一覧:")
|
||||||
|
for col in sorted(need_quotes):
|
||||||
|
logger.info(f" '{col}' -> '\"{col}\"'")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return problematic_columns
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ カラム名チェックエラー: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def test_quoted_query():
|
||||||
|
"""クォート付きクエリのテスト"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 問題のあるテーブルでテストクエリ実行
|
||||||
|
test_tables = ['rog_entry', 'rog_newevent2']
|
||||||
|
|
||||||
|
for table_name in test_tables:
|
||||||
|
logger.info(f"=== {table_name} クエリテスト ===")
|
||||||
|
|
||||||
|
# カラム一覧取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# クォート付きカラム名生成
|
||||||
|
def quote_column_if_needed(column_name):
|
||||||
|
if column_name.lower() in RESERVED_KEYWORDS:
|
||||||
|
return f'"{column_name}"'
|
||||||
|
if any(c.isupper() for c in column_name) or column_name != column_name.lower():
|
||||||
|
return f'"{column_name}"'
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
quoted_columns = [quote_column_if_needed(col) for col in columns]
|
||||||
|
columns_str = ', '.join(quoted_columns)
|
||||||
|
|
||||||
|
# テストクエリ実行
|
||||||
|
try:
|
||||||
|
test_query = f"SELECT {columns_str} FROM {table_name} LIMIT 1"
|
||||||
|
logger.info(f"テストクエリ: {test_query[:100]}...")
|
||||||
|
cursor.execute(test_query)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
logger.info(f"✅ {table_name}: クエリ成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name}: クエリエラー: {e}")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ クエリテストエラー: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("PostgreSQL カラム名検証スクリプト")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
# カラム名チェック
|
||||||
|
problematic_columns = check_column_names()
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# クエリテスト
|
||||||
|
test_quoted_query()
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("検証完了")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
136
check_database_connection.py
Normal file
136
check_database_connection.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
データベース接続状況とold_rogdbデータ確認スクリプト
|
||||||
|
現在のDB接続状況を確認し、old_rogdbの実際のデータを調査
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connection, connections
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
print("=== データベース接続状況確認 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 現在のデータベース設定を確認
|
||||||
|
print("\\n1. Django設定確認:")
|
||||||
|
databases = settings.DATABASES
|
||||||
|
for db_name, config in databases.items():
|
||||||
|
print(f" {db_name}: {config.get('NAME', 'Unknown')} @ {config.get('HOST', 'localhost')}")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# 現在接続しているデータベース名を確認
|
||||||
|
cursor.execute("SELECT current_database();")
|
||||||
|
current_db = cursor.fetchone()[0]
|
||||||
|
print(f"\\n2. 現在接続中のDB: {current_db}")
|
||||||
|
|
||||||
|
# データベース内のテーブル一覧確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE '%rog%'
|
||||||
|
ORDER BY table_name;
|
||||||
|
""")
|
||||||
|
tables = cursor.fetchall()
|
||||||
|
print(f"\\n3. rogaine関連テーブル:")
|
||||||
|
for table in tables:
|
||||||
|
print(f" - {table[0]}")
|
||||||
|
|
||||||
|
# old_rogdbスキーマまたはテーブルの存在確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT schemaname, tablename, hasindexes, hasrules, hastriggers
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE tablename LIKE '%rog%'
|
||||||
|
ORDER BY schemaname, tablename;
|
||||||
|
""")
|
||||||
|
all_rog_tables = cursor.fetchall()
|
||||||
|
print(f"\\n4. 全スキーマのrog関連テーブル:")
|
||||||
|
for schema, table, idx, rules, triggers in all_rog_tables:
|
||||||
|
print(f" {schema}.{table}")
|
||||||
|
|
||||||
|
# データ存在確認
|
||||||
|
print(f"\\n5. 現在のデータ状況:")
|
||||||
|
|
||||||
|
# rog_entry データ確認
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_entry;")
|
||||||
|
entry_count = cursor.fetchone()[0]
|
||||||
|
print(f" rog_entry: {entry_count}件")
|
||||||
|
|
||||||
|
if entry_count > 0:
|
||||||
|
cursor.execute("SELECT * FROM rog_entry LIMIT 3;")
|
||||||
|
sample_entries = cursor.fetchall()
|
||||||
|
print(" サンプルエントリー:")
|
||||||
|
for entry in sample_entries:
|
||||||
|
print(f" ID:{entry[0]}, Team:{entry[5]}, Event:{entry[3]}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" rog_entry エラー: {e}")
|
||||||
|
|
||||||
|
# rog_team データ確認
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_team;")
|
||||||
|
team_count = cursor.fetchone()[0]
|
||||||
|
print(f" rog_team: {team_count}件")
|
||||||
|
|
||||||
|
if team_count > 0:
|
||||||
|
cursor.execute("SELECT id, team_name, zekken_number FROM rog_team WHERE zekken_number IS NOT NULL AND zekken_number != '' LIMIT 5;")
|
||||||
|
sample_teams = cursor.fetchall()
|
||||||
|
print(" ゼッケン付きチーム:")
|
||||||
|
for team in sample_teams:
|
||||||
|
print(f" ID:{team[0]}, Name:{team[1]}, Zekken:{team[2]}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" rog_team エラー: {e}")
|
||||||
|
|
||||||
|
# もしold_rogdbが別のスキーマにある場合
|
||||||
|
print(f"\\n6. 別スキーマのold_rogdbデータ確認:")
|
||||||
|
try:
|
||||||
|
# old_rogdbスキーマが存在するかチェック
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT schema_name
|
||||||
|
FROM information_schema.schemata
|
||||||
|
WHERE schema_name LIKE '%old%' OR schema_name LIKE '%rog%';
|
||||||
|
""")
|
||||||
|
schemas = cursor.fetchall()
|
||||||
|
print(" 利用可能なスキーマ:")
|
||||||
|
for schema in schemas:
|
||||||
|
print(f" - {schema[0]}")
|
||||||
|
|
||||||
|
# old_rogdbスキーマがある場合、そのデータを確認
|
||||||
|
for schema in schemas:
|
||||||
|
schema_name = schema[0]
|
||||||
|
if 'old' in schema_name.lower():
|
||||||
|
try:
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {schema_name}.rog_entry;")
|
||||||
|
old_entry_count = cursor.fetchone()[0]
|
||||||
|
print(f" {schema_name}.rog_entry: {old_entry_count}件")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {schema_name}.rog_entry: アクセスエラー - {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" スキーマ確認エラー: {e}")
|
||||||
|
|
||||||
|
# old_rogdbが別のデータベースの場合の確認
|
||||||
|
print(f"\\n7. 利用可能なデータベース一覧:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT datname
|
||||||
|
FROM pg_database
|
||||||
|
WHERE datistemplate = false
|
||||||
|
ORDER BY datname;
|
||||||
|
""")
|
||||||
|
databases = cursor.fetchall()
|
||||||
|
for db in databases:
|
||||||
|
print(f" - {db[0]}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
0
check_event_codes.py
Normal file
0
check_event_codes.py
Normal file
179
check_null_values.py
Normal file
179
check_null_values.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
NULL値チェック・デフォルト値テストスクリプト
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_null_values():
|
||||||
|
"""NULL値の問題を事前チェック"""
|
||||||
|
try:
|
||||||
|
old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
old_conn.autocommit = True
|
||||||
|
new_conn.autocommit = True
|
||||||
|
|
||||||
|
old_cursor = old_conn.cursor()
|
||||||
|
new_cursor = new_conn.cursor()
|
||||||
|
|
||||||
|
# 共通テーブル取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT table_name FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name LIKE 'rog_%'
|
||||||
|
""")
|
||||||
|
old_tables = [row[0] for row in old_cursor.fetchall()]
|
||||||
|
|
||||||
|
new_cursor.execute("""
|
||||||
|
SELECT table_name FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name LIKE 'rog_%'
|
||||||
|
""")
|
||||||
|
new_tables = [row[0] for row in new_cursor.fetchall()]
|
||||||
|
|
||||||
|
common_tables = list(set(old_tables) & set(new_tables))
|
||||||
|
|
||||||
|
logger.info(f"チェック対象テーブル: {len(common_tables)}個")
|
||||||
|
|
||||||
|
null_issues = {}
|
||||||
|
|
||||||
|
for table_name in common_tables:
|
||||||
|
logger.info(f"=== {table_name} NULL値チェック ===")
|
||||||
|
|
||||||
|
# 新しいDBのNOT NULL制約確認
|
||||||
|
new_cursor.execute("""
|
||||||
|
SELECT column_name, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s AND table_schema = 'public'
|
||||||
|
AND is_nullable = 'NO'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
not_null_columns = new_cursor.fetchall()
|
||||||
|
|
||||||
|
if not not_null_columns:
|
||||||
|
logger.info(f" NOT NULL制約なし")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f" NOT NULL制約カラム: {[col[0] for col in not_null_columns]}")
|
||||||
|
|
||||||
|
# 古いDBのNULL値チェック
|
||||||
|
for col_name, is_nullable, default_val in not_null_columns:
|
||||||
|
try:
|
||||||
|
# PostgreSQL予約語とcamelCaseカラムのクォート処理
|
||||||
|
reserved_words = ['group', 'like', 'order', 'user', 'table', 'index', 'where', 'from', 'select']
|
||||||
|
quoted_col = f'"{col_name}"' if (col_name.lower() in reserved_words or any(c.isupper() for c in col_name)) else col_name
|
||||||
|
|
||||||
|
# カラム存在チェック
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = %s AND column_name = %s AND table_schema = 'public'
|
||||||
|
""", (table_name, col_name))
|
||||||
|
|
||||||
|
if old_cursor.fetchone()[0] == 0:
|
||||||
|
logger.warning(f" ⚠️ {col_name}: 古いDBに存在しないカラム")
|
||||||
|
continue
|
||||||
|
|
||||||
|
old_cursor.execute(f"""
|
||||||
|
SELECT COUNT(*) FROM {table_name}
|
||||||
|
WHERE {quoted_col} IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
|
null_count = old_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if null_count > 0:
|
||||||
|
logger.warning(f" ⚠️ {col_name}: {null_count}件のNULL値あり (デフォルト: {default_val})")
|
||||||
|
|
||||||
|
if table_name not in null_issues:
|
||||||
|
null_issues[table_name] = []
|
||||||
|
null_issues[table_name].append((col_name, null_count, default_val))
|
||||||
|
else:
|
||||||
|
logger.info(f" ✅ {col_name}: NULL値なし")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" ❌ {col_name}: チェックエラー: {e}")
|
||||||
|
|
||||||
|
# サマリー
|
||||||
|
if null_issues:
|
||||||
|
logger.warning("=" * 60)
|
||||||
|
logger.warning("NULL値問題のあるテーブル:")
|
||||||
|
for table, issues in null_issues.items():
|
||||||
|
logger.warning(f" {table}:")
|
||||||
|
for col, count, default in issues:
|
||||||
|
logger.warning(f" - {col}: {count}件 (デフォルト: {default})")
|
||||||
|
else:
|
||||||
|
logger.info("✅ NULL値の問題はありません")
|
||||||
|
|
||||||
|
old_cursor.close()
|
||||||
|
new_cursor.close()
|
||||||
|
old_conn.close()
|
||||||
|
new_conn.close()
|
||||||
|
|
||||||
|
return null_issues
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ NULL値チェックエラー: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def suggest_default_values(null_issues):
|
||||||
|
"""デフォルト値の提案"""
|
||||||
|
if not null_issues:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("推奨デフォルト値設定:")
|
||||||
|
|
||||||
|
for table_name, issues in null_issues.items():
|
||||||
|
logger.info(f" '{table_name}': {{")
|
||||||
|
for col_name, count, default in issues:
|
||||||
|
# データ型に基づくデフォルト値推測
|
||||||
|
if 'trial' in col_name.lower() or 'is_' in col_name.lower():
|
||||||
|
suggested = 'False'
|
||||||
|
elif 'public' in col_name.lower():
|
||||||
|
suggested = 'True'
|
||||||
|
elif 'name' in col_name.lower() or 'description' in col_name.lower():
|
||||||
|
suggested = "''"
|
||||||
|
elif 'order' in col_name.lower() or 'sort' in col_name.lower():
|
||||||
|
suggested = '0'
|
||||||
|
else:
|
||||||
|
suggested = 'None # 要確認'
|
||||||
|
|
||||||
|
logger.info(f" '{col_name}': {suggested}, # {count}件のNULL")
|
||||||
|
logger.info(" },")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("NULL値チェック・デフォルト値提案スクリプト")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
null_issues = check_null_values()
|
||||||
|
suggest_default_values(null_issues)
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("チェック完了")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
93
check_old_entries.py
Normal file
93
check_old_entries.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から新しいデータベースへのエントリーデータ移行スクリプト
|
||||||
|
rog_entry テーブルのデータを NewEvent2 システムに移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from rog.models import NewEvent2, Entry, Team, NewCategory, CustomUser
|
||||||
|
|
||||||
|
print("=== old_rogdb エントリーデータ移行 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# old_rogdb の rog_entry データを確認
|
||||||
|
print("old_rogdb の rog_entry データを確認中...")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# rog_entry テーブルの構造とデータを確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_entry'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
|
||||||
|
print("✅ rog_entry テーブル構造:")
|
||||||
|
for col_name, data_type in columns:
|
||||||
|
print(f" - {col_name}: {data_type}")
|
||||||
|
|
||||||
|
# データ件数確認
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_entry;")
|
||||||
|
entry_count = cursor.fetchone()[0]
|
||||||
|
print(f"✅ rog_entry データ件数: {entry_count}件")
|
||||||
|
|
||||||
|
# サンプルデータ確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, team_id, event_id, category_id, date,
|
||||||
|
zekken_number, zekken_label, is_active
|
||||||
|
FROM rog_entry
|
||||||
|
LIMIT 5;
|
||||||
|
""")
|
||||||
|
sample_data = cursor.fetchall()
|
||||||
|
|
||||||
|
print("\\n✅ サンプルデータ:")
|
||||||
|
for row in sample_data:
|
||||||
|
print(f" ID:{row[0]}, Team:{row[1]}, Event:{row[2]}, Category:{row[3]}, Zekken:{row[5]}")
|
||||||
|
|
||||||
|
# イベント情報の確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT e.id, e.event_name, COUNT(re.id) as entry_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_entry re ON e.id = re.event_id
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
HAVING COUNT(re.id) > 0
|
||||||
|
ORDER BY entry_count DESC;
|
||||||
|
""")
|
||||||
|
event_data = cursor.fetchall()
|
||||||
|
|
||||||
|
print("\\n✅ エントリーがあるイベント:")
|
||||||
|
for event_id, event_name, count in event_data:
|
||||||
|
print(f" Event ID:{event_id} '{event_name}': {count}件")
|
||||||
|
|
||||||
|
# FC岐阜イベントのエントリー確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT re.id, re.zekken_number, re.zekken_label,
|
||||||
|
t.team_name, c.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_newevent2 e ON re.event_id = e.id
|
||||||
|
JOIN rog_team t ON re.team_id = t.id
|
||||||
|
JOIN rog_newcategory c ON re.category_id = c.id
|
||||||
|
WHERE e.event_name LIKE '%FC岐阜%'
|
||||||
|
ORDER BY re.zekken_number
|
||||||
|
LIMIT 10;
|
||||||
|
""")
|
||||||
|
fc_entries = cursor.fetchall()
|
||||||
|
|
||||||
|
print("\\n✅ FC岐阜イベントのエントリー(最初の10件):")
|
||||||
|
for entry_id, zekken, label, team_name, category in fc_entries:
|
||||||
|
print(f" ゼッケン{zekken}: {team_name} ({category})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
66
clear_rog_migrations.py
Normal file
66
clear_rog_migrations.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
マイグレーション履歴リセットスクリプト
|
||||||
|
rogアプリのマイグレーション履歴をクリアして、新しいシンプルマイグレーションを適用
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.core.management.color import no_style
|
||||||
|
|
||||||
|
print("=== マイグレーション履歴のクリア ===")
|
||||||
|
|
||||||
|
# データベース接続を取得
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# rogアプリのマイグレーション履歴をクリア
|
||||||
|
print("rogアプリのマイグレーション履歴を削除中...")
|
||||||
|
cursor.execute("DELETE FROM django_migrations WHERE app = 'rog';")
|
||||||
|
|
||||||
|
print("✅ rogアプリのマイグレーション履歴を削除しました")
|
||||||
|
|
||||||
|
# コミット
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
print("\n=== マイグレーション状態確認 ===")
|
||||||
|
# マイグレーション状態を確認
|
||||||
|
execute_from_command_line(['manage.py', 'showmigrations', 'rog'])
|
||||||
|
|
||||||
|
print("\n=== 新しいマイグレーションを偽装適用 ===")
|
||||||
|
# 依存関係チェックを無視してマイグレーションを偽装適用
|
||||||
|
try:
|
||||||
|
# まず --run-syncdb で既存のテーブル構造を認識させる
|
||||||
|
execute_from_command_line(['manage.py', 'migrate', '--run-syncdb'])
|
||||||
|
except Exception as sync_error:
|
||||||
|
print(f"syncdb エラー(継続): {sync_error}")
|
||||||
|
|
||||||
|
# マイグレーション履歴に直接レコードを挿入
|
||||||
|
print("マイグレーション履歴を直接挿入中...")
|
||||||
|
# 新しいカーソルを作成
|
||||||
|
with connection.cursor() as new_cursor:
|
||||||
|
new_cursor.execute("""
|
||||||
|
INSERT INTO django_migrations (app, name, applied)
|
||||||
|
VALUES ('rog', '0001_simple_initial', NOW())
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
""")
|
||||||
|
connection.commit()
|
||||||
|
print("✅ マイグレーション履歴を挿入しました")
|
||||||
|
|
||||||
|
print("\n=== 最終確認 ===")
|
||||||
|
# 最終確認
|
||||||
|
execute_from_command_line(['manage.py', 'showmigrations'])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
connection.rollback()
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
83
complete_migration_reset.py
Normal file
83
complete_migration_reset.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
チーム・エントリーデータ完全リセット&再移行スクリプト
|
||||||
|
既存のTeam/Entryデータをクリアして、old_rogdbから完全に移行し直す
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from rog.models import Team, Entry, Member
|
||||||
|
from django.db import transaction
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("=== チーム・エントリーデータ完全リセット&再移行 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
print("1. 既存データをクリア中...")
|
||||||
|
|
||||||
|
# 関連データを順番にクリア
|
||||||
|
entry_count = Entry.objects.count()
|
||||||
|
member_count = Member.objects.count()
|
||||||
|
team_count = Team.objects.count()
|
||||||
|
|
||||||
|
print(f" 削除対象: Entry({entry_count}件), Member({member_count}件), Team({team_count}件)")
|
||||||
|
|
||||||
|
Entry.objects.all().delete()
|
||||||
|
Member.objects.all().delete()
|
||||||
|
Team.objects.all().delete()
|
||||||
|
|
||||||
|
print(" ✅ 既存データクリア完了")
|
||||||
|
|
||||||
|
print("\\n2. チームデータ移行を実行中...")
|
||||||
|
result = subprocess.run([
|
||||||
|
'python', 'migrate_rog_team_enhanced.py'
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(" ✅ チーム移行完了")
|
||||||
|
else:
|
||||||
|
print(f" ❌ チーム移行エラー: {result.stderr}")
|
||||||
|
|
||||||
|
print("\\n3. エントリーデータ移行を実行中...")
|
||||||
|
result = subprocess.run([
|
||||||
|
'python', 'migrate_rog_entry_enhanced.py'
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(" ✅ エントリー移行完了")
|
||||||
|
else:
|
||||||
|
print(f" ❌ エントリー移行エラー: {result.stderr}")
|
||||||
|
|
||||||
|
print("\\n4. 移行結果確認...")
|
||||||
|
from rog.models import NewEvent2
|
||||||
|
|
||||||
|
team_count = Team.objects.count()
|
||||||
|
entry_count = Entry.objects.count()
|
||||||
|
|
||||||
|
print(f" Team: {team_count}件")
|
||||||
|
print(f" Entry: {entry_count}件")
|
||||||
|
|
||||||
|
# FC岐阜イベントのエントリー確認
|
||||||
|
fc_event = NewEvent2.objects.filter(event_name__icontains='FC岐阜').first()
|
||||||
|
if fc_event:
|
||||||
|
fc_entries = Entry.objects.filter(event=fc_event)
|
||||||
|
print(f" FC岐阜イベントエントリー: {fc_entries.count()}件")
|
||||||
|
|
||||||
|
if fc_entries.exists():
|
||||||
|
print(" ✅ ゼッケン番号表示問題が解決されました!")
|
||||||
|
for entry in fc_entries[:3]:
|
||||||
|
print(f" ゼッケン{entry.zekken_number}: {entry.team.team_name}")
|
||||||
|
else:
|
||||||
|
print(" ⚠️ FC岐阜にエントリーがありません")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
@ -1,7 +1,30 @@
|
|||||||
"""
|
"""
|
||||||
Django settings for config project.
|
Django settings for config project.
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 3.2.9.
|
Generated by 'django-adminMIDDLEWARE = MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware', # できるだけ上部に
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]aders.middleware.CorsMiddleware', # できるだけ上部に
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
# 'rog.middleware.DetailedRequestLoggingMiddleware', # 一時的に無効化
|
||||||
|
# 'rog.middleware.APIResponseEnhancementMiddleware', # 一時的に無効化
|
||||||
|
] using Django 3.2.9.
|
||||||
|
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||||
@ -14,6 +37,17 @@ from pathlib import Path
|
|||||||
import environ
|
import environ
|
||||||
import os
|
import os
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
|
import warnings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Suppress matplotlib and other library debug logs
|
||||||
|
os.environ['MPLBACKEND'] = 'Agg'
|
||||||
|
warnings.filterwarnings('ignore')
|
||||||
|
|
||||||
|
# Disable specific library debug logging
|
||||||
|
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('matplotlib.font_manager').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('PIL').setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
@ -68,6 +102,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
# 'rog.middleware.APIResponseEnhancementMiddleware', # 一時的にコメントアウト
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
ROOT_URLCONF = 'config.urls'
|
||||||
@ -157,6 +192,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
'OPTIONS': {
|
||||||
|
'min_length': 4,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
@ -224,6 +262,9 @@ LEAFLET_CONFIG = {
|
|||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication', ),
|
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication', ),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'rest_framework.permissions.AllowAny', # デフォルトは認証不要に変更
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -237,7 +278,7 @@ EMAIL_HOST = 'smtp.outlook.com'
|
|||||||
EMAIL_PORT = 587
|
EMAIL_PORT = 587
|
||||||
EMAIL_USE_TLS = True
|
EMAIL_USE_TLS = True
|
||||||
EMAIL_HOST_USER = 'rogaining@gifuai.net'
|
EMAIL_HOST_USER = 'rogaining@gifuai.net'
|
||||||
EMAIL_HOST_PASSWORD = 'ctcpy9823"x~'
|
EMAIL_HOST_PASSWORD = 'gifuainetwork@123'
|
||||||
DEFAULT_FROM_EMAIL = 'rogaining@gifuai.net'
|
DEFAULT_FROM_EMAIL = 'rogaining@gifuai.net'
|
||||||
|
|
||||||
APP_DOWNLOAD_LINK = 'https://apps.apple.com/jp/app/%E5%B2%90%E9%98%9C%E3%83%8A%E3%83%93/id6444221792'
|
APP_DOWNLOAD_LINK = 'https://apps.apple.com/jp/app/%E5%B2%90%E9%98%9C%E3%83%8A%E3%83%93/id6444221792'
|
||||||
@ -272,14 +313,14 @@ LOGGING = {
|
|||||||
# 'formatter': 'verbose',
|
# 'formatter': 'verbose',
|
||||||
#},
|
#},
|
||||||
'console': {
|
'console': {
|
||||||
'level': 'DEBUG',
|
'level': 'INFO',
|
||||||
'class': 'logging.StreamHandler',
|
'class': 'logging.StreamHandler',
|
||||||
'formatter': 'verbose',
|
'formatter': 'verbose',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'root': {
|
'root': {
|
||||||
'handlers': ['console'],
|
'handlers': ['console'],
|
||||||
'level': 'DEBUG',
|
'level': 'INFO',
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
'django': {
|
'django': {
|
||||||
@ -297,6 +338,37 @@ LOGGING = {
|
|||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': True,
|
'propagate': True,
|
||||||
},
|
},
|
||||||
|
# Suppress verbose debug logs from various libraries
|
||||||
|
'matplotlib': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'geos': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'env': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'pyplot': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'font_manager': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'environ': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'WARNING',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,3 +395,9 @@ def get_s3_url(file_path):
|
|||||||
return f"https://{AWS_S3_CUSTOM_DOMAIN}/{file_path}"
|
return f"https://{AWS_S3_CUSTOM_DOMAIN}/{file_path}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Bulk Upload Settings
|
||||||
|
BULK_UPLOAD_MAX_FILES = 50 # 一度にアップロードできる最大ファイル数
|
||||||
|
BULK_UPLOAD_MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB per file
|
||||||
|
BULK_UPLOAD_ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.heic']
|
||||||
|
BULK_UPLOAD_UPLOAD_DIR = 'bulk_checkin_photos/'
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,8 @@ urlpatterns = [
|
|||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('auth/', include('knox.urls')),
|
path('auth/', include('knox.urls')),
|
||||||
path('api/', include("rog.urls")),
|
path('api/', include("rog.urls")),
|
||||||
|
# 🔧 ろげイニングアプリ互換性対応: gifurogeパスをAPIルートにマッピング
|
||||||
|
path('gifuroge/', include("rog.urls", namespace='gifuroge')),
|
||||||
]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
admin.site.site_header = "ROGANING"
|
admin.site.site_header = "ROGANING"
|
||||||
|
|||||||
37
create_app_versions_table.sql
Normal file
37
create_app_versions_table.sql
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
-- アプリバージョン管理テーブル作成
|
||||||
|
-- 2025年8月27日 - サーバーAPI変更要求書対応
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS app_versions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
version VARCHAR(20) NOT NULL,
|
||||||
|
platform VARCHAR(10) NOT NULL CHECK (platform IN ('android', 'ios')),
|
||||||
|
build_number VARCHAR(20),
|
||||||
|
is_latest BOOLEAN DEFAULT FALSE,
|
||||||
|
is_required BOOLEAN DEFAULT FALSE,
|
||||||
|
update_message TEXT,
|
||||||
|
download_url TEXT,
|
||||||
|
release_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE(version, platform)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- インデックス作成
|
||||||
|
CREATE INDEX idx_app_versions_platform ON app_versions(platform);
|
||||||
|
CREATE INDEX idx_app_versions_latest ON app_versions(is_latest) WHERE is_latest = TRUE;
|
||||||
|
|
||||||
|
-- 初期データ挿入(例)
|
||||||
|
INSERT INTO app_versions (version, platform, build_number, is_latest, is_required, update_message, download_url)
|
||||||
|
VALUES
|
||||||
|
('1.3.0', 'android', '130', TRUE, FALSE, '新機能が追加されました。更新を必ずしてください。', 'https://play.google.com/store/apps/details?id=com.gifurogeining.app'),
|
||||||
|
('1.3.0', 'ios', '130', TRUE, FALSE, '新機能が追加されました。更新を必ずしてください。', 'https://apps.apple.com/jp/app/id123456789'),
|
||||||
|
('1.2.0', 'android', '120', FALSE, FALSE, '前バージョン', 'https://play.google.com/store/apps/details?id=com.gifurogeining.app'),
|
||||||
|
('1.2.0', 'ios', '120', FALSE, FALSE, '前バージョン', 'https://apps.apple.com/jp/app/id123456789');
|
||||||
|
|
||||||
|
COMMENT ON TABLE app_versions IS 'アプリバージョン管理テーブル';
|
||||||
|
COMMENT ON COLUMN app_versions.version IS 'セマンティックバージョン (1.2.3)';
|
||||||
|
COMMENT ON COLUMN app_versions.platform IS 'プラットフォーム (android/ios)';
|
||||||
|
COMMENT ON COLUMN app_versions.build_number IS 'ビルド番号';
|
||||||
|
COMMENT ON COLUMN app_versions.is_latest IS '最新版フラグ';
|
||||||
|
COMMENT ON COLUMN app_versions.is_required IS '強制更新フラグ';
|
||||||
|
COMMENT ON COLUMN app_versions.update_message IS 'ユーザー向け更新メッセージ';
|
||||||
|
COMMENT ON COLUMN app_versions.download_url IS 'アプリストアURL';
|
||||||
80
create_checkin_extended_table.sql
Normal file
80
create_checkin_extended_table.sql
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
-- チェックイン拡張情報テーブル作成
|
||||||
|
-- 2025年8月27日 - サーバーAPI変更要求書対応
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rog_checkin_extended (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
gpslog_id INTEGER REFERENCES rog_gpslog(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- GPS拡張情報
|
||||||
|
gps_latitude DECIMAL(10, 8),
|
||||||
|
gps_longitude DECIMAL(11, 8),
|
||||||
|
gps_accuracy DECIMAL(6, 2),
|
||||||
|
gps_timestamp TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- カメラメタデータ
|
||||||
|
camera_capture_time TIMESTAMP WITH TIME ZONE,
|
||||||
|
device_info TEXT,
|
||||||
|
|
||||||
|
-- 審査・検証情報
|
||||||
|
validation_status VARCHAR(20) DEFAULT 'pending'
|
||||||
|
CHECK (validation_status IN ('pending', 'approved', 'rejected', 'requires_review')),
|
||||||
|
validation_comment TEXT,
|
||||||
|
validated_by INTEGER REFERENCES rog_customuser(id),
|
||||||
|
validated_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- スコア情報
|
||||||
|
bonus_points INTEGER DEFAULT 0,
|
||||||
|
scoring_breakdown JSONB,
|
||||||
|
|
||||||
|
-- システム情報
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- インデックス作成
|
||||||
|
CREATE INDEX idx_checkin_extended_gpslog ON rog_checkin_extended(gpslog_id);
|
||||||
|
CREATE INDEX idx_checkin_extended_validation_status ON rog_checkin_extended(validation_status);
|
||||||
|
CREATE INDEX idx_checkin_extended_validated_by ON rog_checkin_extended(validated_by);
|
||||||
|
CREATE INDEX idx_checkin_extended_created_at ON rog_checkin_extended(created_at);
|
||||||
|
|
||||||
|
-- トリガー関数:updated_at自動更新
|
||||||
|
CREATE OR REPLACE FUNCTION update_checkin_extended_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- トリガー作成
|
||||||
|
CREATE TRIGGER trigger_update_checkin_extended_updated_at
|
||||||
|
BEFORE UPDATE ON rog_checkin_extended
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_checkin_extended_updated_at();
|
||||||
|
|
||||||
|
-- コメント追加
|
||||||
|
COMMENT ON TABLE rog_checkin_extended IS 'チェックイン拡張情報テーブル - GPS精度、カメラメタデータ、審査情報';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.gpslog_id IS '関連するGPSログID';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.gps_latitude IS 'GPS緯度';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.gps_longitude IS 'GPS経度';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.gps_accuracy IS 'GPS精度(メートル)';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.gps_timestamp IS 'GPS取得時刻';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.camera_capture_time IS 'カメラ撮影時刻';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.device_info IS 'デバイス情報';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.validation_status IS '審査ステータス';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.validation_comment IS '審査コメント';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.validated_by IS '審査者ID';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.validated_at IS '審査日時';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.bonus_points IS 'ボーナスポイント';
|
||||||
|
COMMENT ON COLUMN rog_checkin_extended.scoring_breakdown IS 'スコア詳細(JSON)';
|
||||||
|
|
||||||
|
-- 初期データ例
|
||||||
|
INSERT INTO rog_checkin_extended (
|
||||||
|
gpslog_id, gps_latitude, gps_longitude, gps_accuracy, gps_timestamp,
|
||||||
|
camera_capture_time, device_info, validation_status, bonus_points,
|
||||||
|
scoring_breakdown
|
||||||
|
) VALUES
|
||||||
|
(1, 35.4091, 136.7581, 5.2, '2025-09-15 11:30:00+09:00',
|
||||||
|
'2025-09-15 11:30:00+09:00', 'iPhone 12', 'pending', 5,
|
||||||
|
'{"base_points": 10, "camera_bonus": 5, "total_points": 15}'::jsonb)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
91
create_fc_gifu_entries.py
Normal file
91
create_fc_gifu_entries.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
FC岐阜イベント用のエントリーデータ作成スクリプト
|
||||||
|
既存のチームをFC岐阜イベントにエントリーして、ゼッケン番号表示を可能にする
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from rog.models import NewEvent2, Entry, Team, NewCategory, CustomUser
|
||||||
|
|
||||||
|
print("=== FC岐阜イベント用エントリーデータ作成 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# FC岐阜イベントを取得
|
||||||
|
fc_gifu_event = NewEvent2.objects.filter(event_name__icontains='FC岐阜').first()
|
||||||
|
if not fc_gifu_event:
|
||||||
|
print("❌ FC岐阜イベントが見つかりません")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"✅ FC岐阜イベント確認: {fc_gifu_event.event_name} (ID: {fc_gifu_event.id})")
|
||||||
|
|
||||||
|
# カテゴリを取得または作成
|
||||||
|
category, created = NewCategory.objects.get_or_create(
|
||||||
|
category_name="一般",
|
||||||
|
defaults={'category_number': 1}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
print(f"✅ カテゴリ作成: {category.category_name}")
|
||||||
|
else:
|
||||||
|
print(f"✅ 既存カテゴリ使用: {category.category_name}")
|
||||||
|
|
||||||
|
# 既存のチームを取得
|
||||||
|
teams = Team.objects.all()[:10] # 最初の10チームを使用
|
||||||
|
print(f"✅ 対象チーム数: {teams.count()}件")
|
||||||
|
|
||||||
|
# エントリーを作成
|
||||||
|
created_entries = 0
|
||||||
|
zekken_number = 1
|
||||||
|
|
||||||
|
for team in teams:
|
||||||
|
# 既にエントリーが存在するかチェック
|
||||||
|
existing_entry = Entry.objects.filter(
|
||||||
|
team=team,
|
||||||
|
event=fc_gifu_event
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not existing_entry:
|
||||||
|
# エントリーを作成
|
||||||
|
entry = Entry.objects.create(
|
||||||
|
team=team,
|
||||||
|
event=fc_gifu_event,
|
||||||
|
category=category,
|
||||||
|
date=fc_gifu_event.start_datetime,
|
||||||
|
owner=team.owner,
|
||||||
|
zekken_number=zekken_number,
|
||||||
|
zekken_label=f"FC岐阜-{zekken_number:03d}",
|
||||||
|
is_active=True,
|
||||||
|
hasParticipated=False,
|
||||||
|
hasGoaled=False
|
||||||
|
)
|
||||||
|
print(f" ✅ エントリー作成: {team.team_name} -> ゼッケン{zekken_number}")
|
||||||
|
created_entries += 1
|
||||||
|
zekken_number += 1
|
||||||
|
else:
|
||||||
|
print(f" ⏭️ 既存エントリー: {team.team_name}")
|
||||||
|
|
||||||
|
print(f"\n=== 作成完了 ===")
|
||||||
|
print(f"新規エントリー数: {created_entries}件")
|
||||||
|
|
||||||
|
# 確認
|
||||||
|
fc_entries = Entry.objects.filter(event=fc_gifu_event)
|
||||||
|
print(f"FC岐阜イベントの総エントリー数: {fc_entries.count()}件")
|
||||||
|
|
||||||
|
print("\n=== ゼッケン番号一覧 ===")
|
||||||
|
for entry in fc_entries.order_by('zekken_number')[:5]:
|
||||||
|
print(f"ゼッケン{entry.zekken_number}: {entry.team.team_name}")
|
||||||
|
|
||||||
|
if fc_entries.count() > 5:
|
||||||
|
print(f"... 他 {fc_entries.count() - 5}件")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
174
create_location2025_table.sql
Normal file
174
create_location2025_table.sql
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
-- rog_location2025テーブル手動作成SQL (デプロイ先用)
|
||||||
|
-- 実行前に必要な拡張機能が有効になっていることを確認してください
|
||||||
|
-- CREATE EXTENSION IF NOT EXISTS postgis;
|
||||||
|
|
||||||
|
-- 既存テーブルが存在する場合は削除 (必要に応じてコメントアウト)
|
||||||
|
-- DROP TABLE IF EXISTS rog_location2025;
|
||||||
|
|
||||||
|
-- rog_location2025テーブル作成
|
||||||
|
CREATE TABLE IF NOT EXISTS rog_location2025 (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
cp_number INTEGER NOT NULL,
|
||||||
|
event_id INTEGER NOT NULL,
|
||||||
|
cp_name VARCHAR(255) NOT NULL,
|
||||||
|
latitude DOUBLE PRECISION,
|
||||||
|
longitude DOUBLE PRECISION,
|
||||||
|
location GEOMETRY(POINT, 4326),
|
||||||
|
cp_point INTEGER NOT NULL DEFAULT 10,
|
||||||
|
photo_point INTEGER NOT NULL DEFAULT 0,
|
||||||
|
buy_point INTEGER NOT NULL DEFAULT 0,
|
||||||
|
checkin_radius DOUBLE PRECISION NOT NULL DEFAULT 15.0,
|
||||||
|
auto_checkin BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
shop_closed BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
shop_shutdown BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
opening_hours TEXT,
|
||||||
|
address VARCHAR(512),
|
||||||
|
phone VARCHAR(32),
|
||||||
|
website VARCHAR(200),
|
||||||
|
description TEXT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
csv_source_file VARCHAR(255),
|
||||||
|
csv_upload_date TIMESTAMP WITH TIME ZONE,
|
||||||
|
csv_upload_user_id BIGINT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by_id BIGINT,
|
||||||
|
updated_by_id BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- インデックス作成
|
||||||
|
CREATE INDEX IF NOT EXISTS rog_location2025_cp_number_idx ON rog_location2025 (cp_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS rog_location2025_event_id_idx ON rog_location2025 (event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS rog_location2025_is_active_idx ON rog_location2025 (is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS location2025_event_cp_idx ON rog_location2025 (event_id, cp_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS location2025_event_active_idx ON rog_location2025 (event_id, is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS location2025_csv_date_idx ON rog_location2025 (csv_upload_date);
|
||||||
|
|
||||||
|
-- 空間インデックス (PostGIS必須)
|
||||||
|
CREATE INDEX IF NOT EXISTS location2025_location_gist_idx ON rog_location2025 USING GIST (location);
|
||||||
|
|
||||||
|
-- 外部キー制約追加 (テーブルが存在する場合)
|
||||||
|
-- rog_newevent2テーブルが存在することを前提
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- event_idの外部キー制約
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'rog_newevent2') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'rog_location2025_event_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_location2025
|
||||||
|
ADD CONSTRAINT rog_location2025_event_id_fkey
|
||||||
|
FOREIGN KEY (event_id) REFERENCES rog_newevent2(id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- csv_upload_user_idの外部キー制約
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'rog_customuser') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'rog_location2025_csv_upload_user_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_location2025
|
||||||
|
ADD CONSTRAINT rog_location2025_csv_upload_user_id_fkey
|
||||||
|
FOREIGN KEY (csv_upload_user_id) REFERENCES rog_customuser(id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- created_by_idの外部キー制約
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'rog_customuser') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'rog_location2025_created_by_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_location2025
|
||||||
|
ADD CONSTRAINT rog_location2025_created_by_id_fkey
|
||||||
|
FOREIGN KEY (created_by_id) REFERENCES rog_customuser(id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- updated_by_idの外部キー制約
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'rog_customuser') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'rog_location2025_updated_by_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_location2025
|
||||||
|
ADD CONSTRAINT rog_location2025_updated_by_id_fkey
|
||||||
|
FOREIGN KEY (updated_by_id) REFERENCES rog_customuser(id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ユニーク制約
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_name = 'rog_location2025_cp_number_event_id_unique'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE rog_location2025
|
||||||
|
ADD CONSTRAINT rog_location2025_cp_number_event_id_unique
|
||||||
|
UNIQUE (cp_number, event_id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- updated_atの自動更新トリガー作成
|
||||||
|
CREATE OR REPLACE FUNCTION update_rog_location2025_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS rog_location2025_updated_at_trigger ON rog_location2025;
|
||||||
|
CREATE TRIGGER rog_location2025_updated_at_trigger
|
||||||
|
BEFORE UPDATE ON rog_location2025
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_rog_location2025_updated_at();
|
||||||
|
|
||||||
|
-- 作成確認
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
tablename,
|
||||||
|
tableowner
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE tablename = 'rog_location2025';
|
||||||
|
|
||||||
|
-- カラム確認
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
is_nullable,
|
||||||
|
column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_location2025'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
|
||||||
|
COMMENT ON TABLE rog_location2025 IS '2025年版チェックポイント管理テーブル';
|
||||||
|
COMMENT ON COLUMN rog_location2025.cp_number IS 'CP番号';
|
||||||
|
COMMENT ON COLUMN rog_location2025.event_id IS 'イベントID';
|
||||||
|
COMMENT ON COLUMN rog_location2025.cp_name IS 'CP名';
|
||||||
|
COMMENT ON COLUMN rog_location2025.latitude IS '緯度';
|
||||||
|
COMMENT ON COLUMN rog_location2025.longitude IS '経度';
|
||||||
|
COMMENT ON COLUMN rog_location2025.location IS '位置(PostGIS Point)';
|
||||||
|
COMMENT ON COLUMN rog_location2025.cp_point IS 'チェックポイント得点';
|
||||||
|
COMMENT ON COLUMN rog_location2025.photo_point IS '写真ポイント';
|
||||||
|
COMMENT ON COLUMN rog_location2025.buy_point IS '買い物ポイント';
|
||||||
|
COMMENT ON COLUMN rog_location2025.checkin_radius IS 'チェックイン範囲(m)';
|
||||||
|
COMMENT ON COLUMN rog_location2025.auto_checkin IS '自動チェックイン';
|
||||||
|
COMMENT ON COLUMN rog_location2025.shop_closed IS '休業中';
|
||||||
|
COMMENT ON COLUMN rog_location2025.shop_shutdown IS '閉業';
|
||||||
|
COMMENT ON COLUMN rog_location2025.opening_hours IS '営業時間';
|
||||||
|
COMMENT ON COLUMN rog_location2025.address IS '住所';
|
||||||
|
COMMENT ON COLUMN rog_location2025.phone IS '電話番号';
|
||||||
|
COMMENT ON COLUMN rog_location2025.website IS 'ウェブサイト';
|
||||||
|
COMMENT ON COLUMN rog_location2025.description IS '説明';
|
||||||
|
COMMENT ON COLUMN rog_location2025.is_active IS '有効';
|
||||||
|
COMMENT ON COLUMN rog_location2025.sort_order IS '表示順';
|
||||||
|
COMMENT ON COLUMN rog_location2025.csv_source_file IS 'CSVファイル名';
|
||||||
|
COMMENT ON COLUMN rog_location2025.csv_upload_date IS 'CSVアップロード日時';
|
||||||
|
COMMENT ON COLUMN rog_location2025.csv_upload_user_id IS 'CSVアップロードユーザーID';
|
||||||
|
COMMENT ON COLUMN rog_location2025.created_at IS '作成日時';
|
||||||
|
COMMENT ON COLUMN rog_location2025.updated_at IS '更新日時';
|
||||||
|
COMMENT ON COLUMN rog_location2025.created_by_id IS '作成者ID';
|
||||||
|
COMMENT ON COLUMN rog_location2025.updated_by_id IS '更新者ID';
|
||||||
87
create_uploaded_images_table.sql
Normal file
87
create_uploaded_images_table.sql
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
-- 画像管理テーブル作成
|
||||||
|
-- サーバーAPI変更要求書対応 - 最優先項目
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rog_uploaded_images (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- 基本情報
|
||||||
|
original_filename VARCHAR(255) NOT NULL,
|
||||||
|
server_filename VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
file_url TEXT NOT NULL,
|
||||||
|
file_size BIGINT NOT NULL,
|
||||||
|
mime_type VARCHAR(50) NOT NULL,
|
||||||
|
|
||||||
|
-- 関連情報
|
||||||
|
event_code VARCHAR(50),
|
||||||
|
team_name VARCHAR(255),
|
||||||
|
cp_number INTEGER,
|
||||||
|
|
||||||
|
-- アップロード情報
|
||||||
|
upload_source VARCHAR(50) DEFAULT 'direct', -- 'direct', 'sharing_intent', 'bulk_upload'
|
||||||
|
device_platform VARCHAR(20), -- 'ios', 'android'
|
||||||
|
|
||||||
|
-- メタデータ
|
||||||
|
capture_timestamp TIMESTAMP WITH TIME ZONE,
|
||||||
|
upload_timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
device_info TEXT,
|
||||||
|
|
||||||
|
-- 処理状況
|
||||||
|
processing_status VARCHAR(20) DEFAULT 'uploaded', -- 'uploaded', 'processing', 'processed', 'failed'
|
||||||
|
thumbnail_url TEXT,
|
||||||
|
|
||||||
|
-- 外部キー
|
||||||
|
gpslog_id INTEGER REFERENCES rog_gpslog(id) ON DELETE SET NULL,
|
||||||
|
entry_id INTEGER REFERENCES rog_entry(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- システム情報
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- インデックス作成
|
||||||
|
CREATE INDEX idx_uploaded_images_event_team ON rog_uploaded_images(event_code, team_name);
|
||||||
|
CREATE INDEX idx_uploaded_images_cp_number ON rog_uploaded_images(cp_number);
|
||||||
|
CREATE INDEX idx_uploaded_images_upload_timestamp ON rog_uploaded_images(upload_timestamp);
|
||||||
|
CREATE INDEX idx_uploaded_images_processing_status ON rog_uploaded_images(processing_status);
|
||||||
|
CREATE INDEX idx_uploaded_images_gpslog ON rog_uploaded_images(gpslog_id);
|
||||||
|
|
||||||
|
-- コメント追加
|
||||||
|
COMMENT ON TABLE rog_uploaded_images IS '画像アップロード管理テーブル - マルチアップロード対応';
|
||||||
|
COMMENT ON COLUMN rog_uploaded_images.original_filename IS '元のファイル名';
|
||||||
|
COMMENT ON COLUMN rog_uploaded_images.server_filename IS 'サーバー上のファイル名';
|
||||||
|
COMMENT ON COLUMN rog_uploaded_images.file_url IS '画像URL';
|
||||||
|
COMMENT ON COLUMN rog_uploaded_images.file_size IS 'ファイルサイズ(バイト)';
|
||||||
|
COMMENT ON COLUMN rog_uploaded_images.upload_source IS 'アップロード方法';
|
||||||
|
COMMENT ON COLUMN rog_uploaded_images.device_platform IS 'デバイスプラットフォーム';
|
||||||
|
COMMENT ON COLUMN rog_uploaded_images.processing_status IS '処理状況';
|
||||||
|
|
||||||
|
-- 制約追加
|
||||||
|
ALTER TABLE rog_uploaded_images ADD CONSTRAINT chk_file_size
|
||||||
|
CHECK (file_size > 0 AND file_size <= 10485760); -- 最大10MB
|
||||||
|
|
||||||
|
ALTER TABLE rog_uploaded_images ADD CONSTRAINT chk_mime_type
|
||||||
|
CHECK (mime_type IN ('image/jpeg', 'image/png', 'image/heic', 'image/webp'));
|
||||||
|
|
||||||
|
ALTER TABLE rog_uploaded_images ADD CONSTRAINT chk_upload_source
|
||||||
|
CHECK (upload_source IN ('direct', 'sharing_intent', 'bulk_upload'));
|
||||||
|
|
||||||
|
ALTER TABLE rog_uploaded_images ADD CONSTRAINT chk_device_platform
|
||||||
|
CHECK (device_platform IN ('ios', 'android', 'web'));
|
||||||
|
|
||||||
|
ALTER TABLE rog_uploaded_images ADD CONSTRAINT chk_processing_status
|
||||||
|
CHECK (processing_status IN ('uploaded', 'processing', 'processed', 'failed'));
|
||||||
|
|
||||||
|
-- トリガー関数:updated_at自動更新
|
||||||
|
CREATE OR REPLACE FUNCTION update_uploaded_images_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- トリガー作成
|
||||||
|
CREATE TRIGGER trigger_update_uploaded_images_updated_at
|
||||||
|
BEFORE UPDATE ON rog_uploaded_images
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_uploaded_images_updated_at();
|
||||||
103
custom-pg_hba.conf
Normal file
103
custom-pg_hba.conf
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# PostgreSQL Client Authentication Configuration File
|
||||||
|
# ===================================================
|
||||||
|
#
|
||||||
|
# Refer to the "Client Authentication" section in the PostgreSQL
|
||||||
|
# documentation for a complete description of this file. A short
|
||||||
|
# synopsis follows.
|
||||||
|
#
|
||||||
|
# This file controls: which hosts are allowed to connect, how clients
|
||||||
|
# are authenticated, which PostgreSQL user names they can use, which
|
||||||
|
# databases they can access. Records take one of these forms:
|
||||||
|
#
|
||||||
|
# local DATABASE USER METHOD [OPTIONS]
|
||||||
|
# host DATABASE USER ADDRESS METHOD [OPTIONS]
|
||||||
|
# hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
|
||||||
|
# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
|
||||||
|
#
|
||||||
|
# (The uppercase items must be replaced by actual values.)
|
||||||
|
#
|
||||||
|
# The first field is the connection type: "local" is a Unix-domain
|
||||||
|
# socket, "host" is either a plain or SSL-encrypted TCP/IP socket,
|
||||||
|
# "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a
|
||||||
|
# plain TCP/IP socket.
|
||||||
|
#
|
||||||
|
# DATABASE can be "all", "sameuser", "samerole", "replication", a
|
||||||
|
# database name, or a comma-separated list thereof. The "all"
|
||||||
|
# keyword does not match "replication". Access to replication
|
||||||
|
# must be enabled in a separate record (see example below).
|
||||||
|
#
|
||||||
|
# USER can be "all", a user name, a group name prefixed with "+", or a
|
||||||
|
# comma-separated list thereof. In both the DATABASE and USER fields
|
||||||
|
# you can also write a file name prefixed with "@" to include names
|
||||||
|
# from a separate file.
|
||||||
|
#
|
||||||
|
# ADDRESS specifies the set of hosts the record matches. It can be a
|
||||||
|
# host name, or it is made up of an IP address and a CIDR mask that is
|
||||||
|
# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that
|
||||||
|
# specifies the number of significant bits in the mask. A host name
|
||||||
|
# that starts with a dot (.) matches a suffix of the actual host name.
|
||||||
|
# Alternatively, you can write an IP address and netmask in separate
|
||||||
|
# columns to specify the set of hosts. Instead of a CIDR-address, you
|
||||||
|
# can write "samehost" to match any of the server's own IP addresses,
|
||||||
|
# or "samenet" to match any address in any subnet that the server is
|
||||||
|
# directly connected to.
|
||||||
|
#
|
||||||
|
# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256",
|
||||||
|
# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert".
|
||||||
|
# Note that "password" sends passwords in clear text; "md5" or
|
||||||
|
# "scram-sha-256" are preferred since they send encrypted passwords.
|
||||||
|
#
|
||||||
|
# OPTIONS are a set of options for the authentication in the format
|
||||||
|
# NAME=VALUE. The available options depend on the different
|
||||||
|
# authentication methods -- refer to the "Client Authentication"
|
||||||
|
# section in the documentation for a list of which options are
|
||||||
|
# available for which authentication methods.
|
||||||
|
#
|
||||||
|
# Database and user names containing spaces, commas, quotes and other
|
||||||
|
# special characters must be quoted. Quoting one of the keywords
|
||||||
|
# "all", "sameuser", "samerole" or "replication" makes the name lose
|
||||||
|
# its special character, and just match a database or username with
|
||||||
|
# that name.
|
||||||
|
#
|
||||||
|
# This file is read on server startup and when the server receives a
|
||||||
|
# SIGHUP signal. If you edit the file on a running system, you have to
|
||||||
|
# SIGHUP the server for the changes to take effect, run "pg_ctl reload",
|
||||||
|
# or execute "SELECT pg_reload_conf()".
|
||||||
|
#
|
||||||
|
# Put your actual configuration here
|
||||||
|
# ----------------------------------
|
||||||
|
#
|
||||||
|
# If you want to allow non-local connections, you need to add more
|
||||||
|
# "host" records. In that case you will also need to make PostgreSQL
|
||||||
|
# listen on a non-local interface via the listen_addresses
|
||||||
|
# configuration parameter, or via the -i or -h command line switches.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# DO NOT DISABLE!
|
||||||
|
# If you change this first entry you will need to make sure that the
|
||||||
|
# database superuser can access the database using some other method.
|
||||||
|
# Noninteractive access to all databases is required during automatic
|
||||||
|
# maintenance (custom daily cronjobs, replication, and similar tasks).
|
||||||
|
#
|
||||||
|
# Database administrative login by Unix domain socket
|
||||||
|
local all postgres peer
|
||||||
|
|
||||||
|
# TYPE DATABASE USER ADDRESS METHOD
|
||||||
|
|
||||||
|
# "local" is for Unix domain socket connections only
|
||||||
|
local all all peer
|
||||||
|
# IPv4 local connections:
|
||||||
|
host all all 127.0.0.1/32 md5
|
||||||
|
# IPv6 local connections:
|
||||||
|
host all all ::1/128 md5
|
||||||
|
# Allow replication connections from localhost, by a user with the
|
||||||
|
# replication privilege.
|
||||||
|
local replication all peer
|
||||||
|
host replication all 127.0.0.1/32 md5
|
||||||
|
host replication all ::1/128 md5
|
||||||
|
host all all 172.0.0.0/8 md5
|
||||||
|
host all all 192.168.0.0/16 md5
|
||||||
|
host all all 0.0.0.0/0 md5
|
||||||
|
host replication replicator 0.0.0.0/0 md5
|
||||||
318
debug_502_error.py
Normal file
318
debug_502_error.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
502エラー調査スクリプト: checkin_from_rogappエンドポイントのデバッグ
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def test_checkin_endpoint():
|
||||||
|
"""
|
||||||
|
checkin_from_rogappエンドポイントをテストして502エラーを再現
|
||||||
|
"""
|
||||||
|
print("🔍 502エラー調査: checkin_from_rogappエンドポイント")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# テストデータ
|
||||||
|
test_data = {
|
||||||
|
"event_code": "fc_gifu_2025",
|
||||||
|
"team_name": "テストチーム",
|
||||||
|
"cp_number": "1",
|
||||||
|
"image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABA...", # 短縮版
|
||||||
|
"buy_flag": False,
|
||||||
|
"gps_coordinates": {
|
||||||
|
"latitude": 35.6762,
|
||||||
|
"longitude": 139.6503,
|
||||||
|
"accuracy": 5.0,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
},
|
||||||
|
"camera_metadata": {
|
||||||
|
"capture_time": datetime.now().isoformat(),
|
||||||
|
"device_info": "debug_script"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# URLを構築
|
||||||
|
base_url = "http://localhost:8100"
|
||||||
|
checkin_url = f"{base_url}/gifuroge/checkin_from_rogapp"
|
||||||
|
|
||||||
|
print(f"🎯 テスト対象URL: {checkin_url}")
|
||||||
|
print(f"📊 テストデータ: {json.dumps({k: v if k != 'image' else '[BASE64_DATA]' for k, v in test_data.items()}, indent=2, ensure_ascii=False)}")
|
||||||
|
|
||||||
|
# まず、GETリクエストでエンドポイントの存在確認
|
||||||
|
print(f"\n🔍 エンドポイント存在確認(GETリクエスト)")
|
||||||
|
try:
|
||||||
|
get_response = requests.get(checkin_url, timeout=10)
|
||||||
|
print(f"GET レスポンス: {get_response.status_code}")
|
||||||
|
if get_response.status_code == 405:
|
||||||
|
print(f"✅ エンドポイントは存在するが、GETメソッドは許可されていない(正常)")
|
||||||
|
elif get_response.status_code == 404:
|
||||||
|
print(f"❌ エンドポイントが見つからない")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ GET テストエラー: {e}")
|
||||||
|
|
||||||
|
# 正しいURLパターンでもテスト
|
||||||
|
alternative_urls = [
|
||||||
|
f"{base_url}/rog/checkin_from_rogapp",
|
||||||
|
f"{base_url}/api/checkin_from_rogapp",
|
||||||
|
f"{base_url}/checkin_from_rogapp"
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"\n🔍 代替URLパターンテスト")
|
||||||
|
for url in alternative_urls:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=5)
|
||||||
|
print(f" {url}: {resp.status_code}")
|
||||||
|
if resp.status_code in [200, 405]:
|
||||||
|
print(f" ✅ このURLが正しい可能性があります")
|
||||||
|
except:
|
||||||
|
print(f" {url}: 接続エラー")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"\n🚀 POSTリクエスト送信中...")
|
||||||
|
|
||||||
|
# リクエスト送信
|
||||||
|
response = requests.post(
|
||||||
|
checkin_url,
|
||||||
|
json=test_data,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'Debug-Script/1.0'
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📥 レスポンス受信:")
|
||||||
|
print(f" ステータスコード: {response.status_code}")
|
||||||
|
print(f" ヘッダー: {dict(response.headers)}")
|
||||||
|
|
||||||
|
if response.status_code == 405:
|
||||||
|
print(f"❌ 405 Method Not Allowed エラー")
|
||||||
|
print(f" 💡 原因: エンドポイントが存在するが、POSTメソッドが許可されていない")
|
||||||
|
print(f" 📋 許可されているメソッドを確認が必要")
|
||||||
|
print(f" レスポンステキスト: {response.text}")
|
||||||
|
return False
|
||||||
|
elif response.status_code == 502:
|
||||||
|
print(f"❌ 502 Bad Gateway エラーを確認しました")
|
||||||
|
print(f" レスポンステキスト: {response.text}")
|
||||||
|
return False
|
||||||
|
elif response.status_code == 200:
|
||||||
|
print(f"✅ 正常レスポンス")
|
||||||
|
print(f" レスポンスデータ: {response.json()}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 予期しないステータスコード: {response.status_code}")
|
||||||
|
print(f" レスポンステキスト: {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
print(f"❌ 接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.Timeout as e:
|
||||||
|
print(f"❌ タイムアウトエラー: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ その他のエラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def monitor_logs_during_test():
|
||||||
|
"""
|
||||||
|
テスト実行中のログを監視
|
||||||
|
"""
|
||||||
|
print(f"\n🔍 ログ監視開始")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# アプリケーションログを監視
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker', 'compose', 'logs', '--tail=20', '--follow', 'app'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📋 アプリケーションログ:")
|
||||||
|
print(result.stdout)
|
||||||
|
|
||||||
|
if result.stderr:
|
||||||
|
print(f"⚠️ エラー出力:")
|
||||||
|
print(result.stderr)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print(f"⏰ ログ監視タイムアウト(正常)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ ログ監視エラー: {e}")
|
||||||
|
|
||||||
|
def check_docker_services():
|
||||||
|
"""
|
||||||
|
Dockerサービスの状態確認
|
||||||
|
"""
|
||||||
|
print(f"\n🐳 Dockerサービス状態確認")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# サービス状態確認
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker', 'compose', 'ps'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📊 サービス状態:")
|
||||||
|
print(result.stdout)
|
||||||
|
|
||||||
|
# ヘルスチェック
|
||||||
|
health_result = subprocess.run(
|
||||||
|
['docker', 'compose', 'exec', 'app', 'python', 'manage.py', 'check'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if health_result.returncode == 0:
|
||||||
|
print(f"✅ Djangoアプリケーション: 正常")
|
||||||
|
print(health_result.stdout)
|
||||||
|
else:
|
||||||
|
print(f"❌ Djangoアプリケーション: エラー")
|
||||||
|
print(health_result.stderr)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Dockerコマンドエラー: {e}")
|
||||||
|
print(f"stderr: {e.stderr}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ その他のエラー: {e}")
|
||||||
|
|
||||||
|
def analyze_nginx_config():
|
||||||
|
"""
|
||||||
|
nginx設定の確認
|
||||||
|
"""
|
||||||
|
print(f"\n🌐 nginx設定確認")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# nginx設定テスト
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker', 'compose', 'exec', 'nginx', 'nginx', '-t'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"✅ nginx設定: 正常")
|
||||||
|
print(result.stdout)
|
||||||
|
else:
|
||||||
|
print(f"❌ nginx設定: エラー")
|
||||||
|
print(result.stderr)
|
||||||
|
|
||||||
|
# 最近のnginxエラーログ
|
||||||
|
error_log_result = subprocess.run(
|
||||||
|
['docker', 'compose', 'logs', '--tail=10', 'nginx'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n📋 nginx最近のログ:")
|
||||||
|
print(error_log_result.stdout)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ nginx確認エラー: {e}")
|
||||||
|
|
||||||
|
def check_django_configuration():
|
||||||
|
"""
|
||||||
|
Django設定の確認
|
||||||
|
"""
|
||||||
|
print(f"\n⚙️ Django設定確認")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Django URL設定の確認
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker', 'compose', 'exec', 'app', 'python', 'manage.py', 'show_urls'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"✅ URL確認コマンド実行成功")
|
||||||
|
# checkin_from_rogappが含まれているかチェック
|
||||||
|
if 'checkin_from_rogapp' in result.stdout:
|
||||||
|
print(f"✅ checkin_from_rogappエンドポイントがURL設定に存在")
|
||||||
|
else:
|
||||||
|
print(f"❌ checkin_from_rogappエンドポイントがURL設定に見つからない")
|
||||||
|
print(f"URL一覧(抜粋):")
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'checkin' in line.lower() or 'rogapp' in line.lower():
|
||||||
|
print(f" {line}")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ show_urlsコマンドが利用できません")
|
||||||
|
|
||||||
|
# CSRF設定確認
|
||||||
|
csrf_result = subprocess.run(
|
||||||
|
['docker', 'compose', 'exec', 'app', 'python', '-c',
|
||||||
|
"import django; django.setup(); from django.conf import settings; print(f'CSRF_COOKIE_SECURE: {settings.CSRF_COOKIE_SECURE}'); print(f'CSRF_TRUSTED_ORIGINS: {getattr(settings, \"CSRF_TRUSTED_ORIGINS\", \"Not set\")}')"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if csrf_result.returncode == 0:
|
||||||
|
print(f"\n🔒 CSRF設定:")
|
||||||
|
print(csrf_result.stdout)
|
||||||
|
else:
|
||||||
|
print(f"⚠️ CSRF設定確認エラー: {csrf_result.stderr}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Django設定確認エラー: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
メイン実行関数
|
||||||
|
"""
|
||||||
|
print(f"🚨 405 Method Not Allowed エラー調査ツール")
|
||||||
|
print(f"時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. Dockerサービス状態確認
|
||||||
|
check_docker_services()
|
||||||
|
|
||||||
|
# 2. nginx設定確認
|
||||||
|
analyze_nginx_config()
|
||||||
|
|
||||||
|
# 3. Django設定確認
|
||||||
|
check_django_configuration()
|
||||||
|
|
||||||
|
# 4. エンドポイントテスト
|
||||||
|
print(f"\n🎯 エンドポイントテスト実行")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
success = test_checkin_endpoint()
|
||||||
|
|
||||||
|
# 5. ログ確認
|
||||||
|
monitor_logs_during_test()
|
||||||
|
|
||||||
|
# 結果まとめ
|
||||||
|
print(f"\n📊 調査結果まとめ")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✅ checkin_from_rogappエンドポイントは正常に動作しています")
|
||||||
|
print(f"💡 502エラーは一時的な問題だった可能性があります")
|
||||||
|
else:
|
||||||
|
print(f"❌ 405 Method Not Allowed エラーを確認しました")
|
||||||
|
print(f"💡 問題の原因と対策:")
|
||||||
|
print(f" 🔧 考えられる原因:")
|
||||||
|
print(f" 1. CSRFトークンの問題")
|
||||||
|
print(f" 2. @api_viewデコレータの設定問題")
|
||||||
|
print(f" 3. URLパターンの不一致")
|
||||||
|
print(f" 4. nginx設定でPOSTメソッドがブロックされている")
|
||||||
|
print(f" 🛠️ 推奨対策:")
|
||||||
|
print(f" 1. views.pyで@api_view(['POST'])の設定確認")
|
||||||
|
print(f" 2. urls.pyでのルーティング確認")
|
||||||
|
print(f" 3. nginx設定でPOSTメソッド許可確認")
|
||||||
|
print(f" 4. CSRF設定の確認")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
49
debug_events.py
Normal file
49
debug_events.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Location2025とイベントの関係を調査するスクリプト
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rog.models import Location2025, NewEvent2
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print('=== Location2025とイベントの関係調査 ===')
|
||||||
|
|
||||||
|
# Location2025のeventフィールドの外部キー先を確認
|
||||||
|
event_field = Location2025._meta.get_field('event')
|
||||||
|
print(f'Location2025.event field references: {event_field.related_model}')
|
||||||
|
|
||||||
|
# 現在のLocation2025データのイベント分布
|
||||||
|
print('\n=== Location2025のイベント分布 ===')
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT l.event_id, ne.event_name, COUNT(*) as count
|
||||||
|
FROM rog_location2025 l
|
||||||
|
LEFT JOIN rog_newevent2 ne ON l.event_id = ne.id
|
||||||
|
GROUP BY l.event_id, ne.event_name
|
||||||
|
ORDER BY count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
event_id, event_name, count = row
|
||||||
|
print(f' Event ID {event_id}: {event_name} ({count}件)')
|
||||||
|
|
||||||
|
# NewEvent2の一覧
|
||||||
|
print('\n=== NewEvent2テーブルの全イベント ===')
|
||||||
|
for event in NewEvent2.objects.all()[:10]:
|
||||||
|
print(f' ID {event.id}: {event.event_name} (status: {event.status})')
|
||||||
|
|
||||||
|
# CSVアップロード画面のイベント選択肢を確認
|
||||||
|
print('\n=== CSVアップロード画面のイベント選択肢 ===')
|
||||||
|
events = NewEvent2.objects.filter(status='public').order_by('-start_datetime')
|
||||||
|
for event in events[:5]:
|
||||||
|
print(f' ID {event.id}: {event.event_name} (status: {event.status}, start: {event.start_datetime})')
|
||||||
|
|
||||||
|
# 実際のLocation2025サンプルデータ
|
||||||
|
print('\n=== Location2025サンプルデータ ===')
|
||||||
|
sample_locations = Location2025.objects.all()[:3]
|
||||||
|
for loc in sample_locations:
|
||||||
|
print(f' CP{loc.cp_number}: {loc.cp_name} -> Event ID {loc.event_id} ({loc.event.event_name if loc.event else "None"})')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
85
debug_test_event.py
Normal file
85
debug_test_event.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
TestEventが検索でヒットしない問題のデバッグスクリプト
|
||||||
|
Deploy先でこのスクリプトを実行してください
|
||||||
|
|
||||||
|
実行方法:
|
||||||
|
docker compose exec app python debug_test_event.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
# Django設定
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from rog.models import NewEvent2, Location2025
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
def debug_test_event():
|
||||||
|
print("=== TestEvent検索問題デバッグ ===")
|
||||||
|
|
||||||
|
# 1. 全イベント数
|
||||||
|
total_events = NewEvent2.objects.count()
|
||||||
|
print(f"総イベント数: {total_events}")
|
||||||
|
|
||||||
|
# 2. TestEventを含むイベントの検索(大文字小文字区別なし)
|
||||||
|
test_events = NewEvent2.objects.filter(event_name__icontains='testevent')
|
||||||
|
print(f"TestEventを含むイベント(大小文字無視): {test_events.count()}件")
|
||||||
|
|
||||||
|
for event in test_events:
|
||||||
|
print(f" - ID {event.id}: '{event.event_name}' (status: {event.status})")
|
||||||
|
|
||||||
|
# 3. Testを含むイベントの検索
|
||||||
|
test_partial = NewEvent2.objects.filter(event_name__icontains='test')
|
||||||
|
print(f"Testを含むイベント: {test_partial.count()}件")
|
||||||
|
|
||||||
|
for event in test_partial:
|
||||||
|
print(f" - ID {event.id}: '{event.event_name}' (status: {event.status})")
|
||||||
|
|
||||||
|
# 4. 最近作成されたイベント(上位10件)
|
||||||
|
print("\n=== 最近作成されたイベント(上位10件) ===")
|
||||||
|
recent_events = NewEvent2.objects.order_by('-id')[:10]
|
||||||
|
for event in recent_events:
|
||||||
|
print(f" - ID {event.id}: '{event.event_name}' (status: {event.status})")
|
||||||
|
|
||||||
|
# 5. 各種検索パターンテスト
|
||||||
|
print("\n=== 各種検索パターンテスト ===")
|
||||||
|
search_patterns = [
|
||||||
|
'TestEvent',
|
||||||
|
'testevent',
|
||||||
|
'Test',
|
||||||
|
'test',
|
||||||
|
'EVENT',
|
||||||
|
'event'
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in search_patterns:
|
||||||
|
results = NewEvent2.objects.filter(event_name__icontains=pattern)
|
||||||
|
print(f"'{pattern}' を含むイベント: {results.count()}件")
|
||||||
|
if results.count() > 0 and results.count() <= 3:
|
||||||
|
for event in results:
|
||||||
|
print(f" - '{event.event_name}'")
|
||||||
|
|
||||||
|
# 6. ステータス別イベント数
|
||||||
|
print("\n=== ステータス別イベント数 ===")
|
||||||
|
from django.db.models import Count
|
||||||
|
status_counts = NewEvent2.objects.values('status').annotate(count=Count('id')).order_by('status')
|
||||||
|
for item in status_counts:
|
||||||
|
print(f" {item['status']}: {item['count']}件")
|
||||||
|
|
||||||
|
# 7. 特定の文字列での完全一致検索
|
||||||
|
print("\n=== 完全一致検索テスト ===")
|
||||||
|
exact_match = NewEvent2.objects.filter(event_name='TestEvent')
|
||||||
|
print(f"'TestEvent'完全一致: {exact_match.count()}件")
|
||||||
|
|
||||||
|
if exact_match.exists():
|
||||||
|
for event in exact_match:
|
||||||
|
print(f" - ID {event.id}: '{event.event_name}' (status: {event.status})")
|
||||||
|
# 関連するLocation2025も確認
|
||||||
|
cp_count = Location2025.objects.filter(event=event).count()
|
||||||
|
print(f" 関連チェックポイント: {cp_count}件")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
debug_test_event()
|
||||||
@ -1,5 +1,3 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres-db:
|
postgres-db:
|
||||||
image: kartoza/postgis:12.0
|
image: kartoza/postgis:12.0
|
||||||
@ -8,12 +6,27 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql
|
- postgres_data:/var/lib/postgresql
|
||||||
- ./custom-postgresql.conf:/etc/postgresql/12/main/postgresql.conf
|
- ./custom-postgresql.conf:/etc/postgresql/12/main/postgresql.conf
|
||||||
|
- ./custom-pg_hba.conf:/etc/postgresql/12/main/pg_hba.conf
|
||||||
- ./rogaining.sql:/sql/rogaining.sql
|
- ./rogaining.sql:/sql/rogaining.sql
|
||||||
|
- ./sqls:/sqls
|
||||||
|
- ./create_location2025_table.sql:/sql/create_location2025_table.sql
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=${POSTGRES_USER}
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
- POSTGRES_PASS=${POSTGRES_PASS}
|
- POSTGRES_PASS=${POSTGRES_PASS}
|
||||||
- POSTGRES_DBNAME=${POSTGRES_DBNAME}
|
- POSTGRES_DBNAME=${POSTGRES_DBNAME}
|
||||||
- POSTGRES_MAX_CONNECTIONS=600
|
- POSTGRES_MAX_CONNECTIONS=600
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
memory: 1G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DBNAME}"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
restart: "on-failure"
|
restart: "on-failure"
|
||||||
networks:
|
networks:
|
||||||
- rog-api
|
- rog-api
|
||||||
@ -22,16 +35,28 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.gdal
|
dockerfile: Dockerfile.gdal
|
||||||
command: bash -c "./wait-for-postgres.sh postgres-db && python manage.py migrate && gunicorn config.wsgi:application --bind 0.0.0.0:8000"
|
command: bash -c "./wait-for-postgres.sh postgres-db && gunicorn config.wsgi:application --bind 0.0.0.0:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- static_volume:/app/static
|
- static_volume:/app/static
|
||||||
- media_volume:/app/media
|
- media_volume:/app/media
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
- MPLBACKEND=Agg
|
||||||
|
- MATPLOTLIB_BACKEND=Agg
|
||||||
|
- PYTHONWARNINGS=ignore
|
||||||
|
- GDAL_DISABLE_READDIR_ON_OPEN=YES
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000')\" || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
restart: "on-failure"
|
restart: "on-failure"
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres-db
|
postgres-db:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- rog-api
|
- rog-api
|
||||||
|
|
||||||
|
|||||||
60
docker-compose-simple.yml
Normal file
60
docker-compose-simple.yml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
services:
|
||||||
|
postgres-db:
|
||||||
|
image: kartoza/postgis:12.0
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql
|
||||||
|
- ./custom-postgresql.conf:/etc/postgresql/12/main/postgresql.conf
|
||||||
|
- ./rogaining.sql:/sql/rogaining.sql
|
||||||
|
- ./sqls:/sqls
|
||||||
|
- ./create_location2025_table.sql:/sql/create_location2025_table.sql
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
- POSTGRES_PASS=${POSTGRES_PASS}
|
||||||
|
- POSTGRES_DBNAME=${POSTGRES_DBNAME}
|
||||||
|
- POSTGRES_MAX_CONNECTIONS=600
|
||||||
|
restart: "no"
|
||||||
|
networks:
|
||||||
|
- rog-api
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.gdal
|
||||||
|
command: bash -c "./wait-for-postgres.sh postgres-db && python manage.py migrate && gunicorn config.wsgi:application --bind 0.0.0.0:8000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- static_volume:/app/static
|
||||||
|
- media_volume:/app/media
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- postgres-db
|
||||||
|
networks:
|
||||||
|
- rog-api
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.19
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- static_volume:/app/static
|
||||||
|
- media_volume:/app/media
|
||||||
|
- ./supervisor/html:/usr/share/nginx/html
|
||||||
|
ports:
|
||||||
|
- 8100:80
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
networks:
|
||||||
|
- rog-api
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rog-api:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
static_volume:
|
||||||
|
media_volume:
|
||||||
34
docker-compose.event-registration.yml
Normal file
34
docker-compose.event-registration.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
event-registration:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.event_registration
|
||||||
|
container_name: rogaining_event_registration
|
||||||
|
volumes:
|
||||||
|
- ./CPLIST/input:/app/CPLIST/input:ro
|
||||||
|
- ./logs:/app/logs
|
||||||
|
environment:
|
||||||
|
- EVENT_CODE=${EVENT_CODE:-大垣2509}
|
||||||
|
- CSV_FILE=${CSV_FILE:-CPLIST/input/team2025.csv}
|
||||||
|
- BASE_URL=${BASE_URL:-http://web:8000}
|
||||||
|
- DRY_RUN=${DRY_RUN:-false}
|
||||||
|
networks:
|
||||||
|
- rogaining_network
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
echo 'イベントユーザー登録処理を開始します...' &&
|
||||||
|
python register_event_users.py
|
||||||
|
--event_code $${EVENT_CODE}
|
||||||
|
--csv_file $${CSV_FILE}
|
||||||
|
--base_url $${BASE_URL}
|
||||||
|
$${DRY_RUN:+--dry_run}
|
||||||
|
"
|
||||||
|
|
||||||
|
# 既存のサービス(webなど)を参照するためのネットワーク定義
|
||||||
|
networks:
|
||||||
|
rogaining_network:
|
||||||
|
external: true
|
||||||
59
external_db_connection_test.py
Normal file
59
external_db_connection_test.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
外部スクリプトからDBコンテナに接続するサンプル
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import DictCursor
|
||||||
|
|
||||||
|
# 環境変数から接続情報を取得
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': os.getenv('PG_HOST', 'localhost'),
|
||||||
|
'port': os.getenv('PG_PORT', '5432'),
|
||||||
|
'database': os.getenv('POSTGRES_DBNAME', 'rogdb'),
|
||||||
|
'user': os.getenv('POSTGRES_USER', 'admin'),
|
||||||
|
'password': os.getenv('POSTGRES_PASS', 'admin123456')
|
||||||
|
}
|
||||||
|
|
||||||
|
def connect_to_db():
|
||||||
|
"""データベースに接続"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(**DB_CONFIG)
|
||||||
|
print(f"✅ データベースに接続成功: {DB_CONFIG['host']}:{DB_CONFIG['port']}")
|
||||||
|
return conn
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
print(f"❌ データベース接続エラー: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_connection():
|
||||||
|
"""接続テスト"""
|
||||||
|
conn = connect_to_db()
|
||||||
|
if conn:
|
||||||
|
try:
|
||||||
|
with conn.cursor(cursor_factory=DictCursor) as cur:
|
||||||
|
cur.execute("SELECT version();")
|
||||||
|
version = cur.fetchone()
|
||||||
|
print(f"PostgreSQL バージョン: {version[0]}")
|
||||||
|
|
||||||
|
# テーブル一覧を取得
|
||||||
|
cur.execute("""
|
||||||
|
SELECT tablename FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
ORDER BY tablename;
|
||||||
|
""")
|
||||||
|
tables = cur.fetchall()
|
||||||
|
print(f"テーブル数: {len(tables)}")
|
||||||
|
for table in tables[:5]: # 最初の5個を表示
|
||||||
|
print(f" - {table[0]}")
|
||||||
|
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
print(f"❌ クエリ実行エラー: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== データベース接続テスト ===")
|
||||||
|
print(f"接続先: {DB_CONFIG['host']}:{DB_CONFIG['port']}")
|
||||||
|
print(f"データベース: {DB_CONFIG['database']}")
|
||||||
|
print(f"ユーザー: {DB_CONFIG['user']}")
|
||||||
|
test_connection()
|
||||||
146
fix_fc_gifu_zekken_numbers.py
Normal file
146
fix_fc_gifu_zekken_numbers.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
# プロジェクト設定
|
||||||
|
sys.path.append('/app')
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connection, transaction
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def assign_zekken_numbers_to_fc_gifu():
|
||||||
|
"""FC岐阜イベント(ID:10)のチームにゼッケン番号を割り当て"""
|
||||||
|
|
||||||
|
print("=== FC岐阜イベントチームゼッケン番号割り当て ===")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# 1. FC岐阜イベントの現状確認
|
||||||
|
print("\n1. FC岐阜イベント(ID:10)現状確認:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT t.id, t.team_name, t.zekken_number, t.event_id
|
||||||
|
FROM rog_team t
|
||||||
|
JOIN rog_entry e ON t.id = e.team_id
|
||||||
|
WHERE e.event_id = 10
|
||||||
|
ORDER BY t.id;
|
||||||
|
""")
|
||||||
|
fc_teams = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f" FC岐阜関連チーム数: {len(fc_teams)}")
|
||||||
|
print(" 現在の状況:")
|
||||||
|
for team in fc_teams[:5]: # 最初の5件のみ表示
|
||||||
|
print(f" Team ID:{team[0]}, Name:{team[1]}, Zekken:{team[2]}, Event:{team[3]}")
|
||||||
|
|
||||||
|
# 2. ゼッケン番号が未設定のチームを特定
|
||||||
|
teams_without_zekken = [team for team in fc_teams if not team[2]]
|
||||||
|
print(f"\n ゼッケン番号未設定チーム数: {len(teams_without_zekken)}")
|
||||||
|
|
||||||
|
if not teams_without_zekken:
|
||||||
|
print(" 🎉 すべてのチームにゼッケン番号が設定済み")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. 既存のゼッケン番号を確認(競合回避)
|
||||||
|
print("\n2. 既存ゼッケン番号確認:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT zekken_number
|
||||||
|
FROM rog_team
|
||||||
|
WHERE zekken_number IS NOT NULL AND zekken_number != ''
|
||||||
|
ORDER BY zekken_number;
|
||||||
|
""")
|
||||||
|
existing_zekkens = [row[0] for row in cursor.fetchall()]
|
||||||
|
print(f" 既存ゼッケン番号: {existing_zekkens}")
|
||||||
|
|
||||||
|
# 4. ユーザー確認
|
||||||
|
print(f"\n3. ゼッケン番号割り当て準備:")
|
||||||
|
print(f" 対象チーム数: {len(teams_without_zekken)}")
|
||||||
|
print(f" 割り当て予定ゼッケン番号: FC001-FC{len(teams_without_zekken):03d}")
|
||||||
|
|
||||||
|
confirm = input("\n ゼッケン番号を割り当てますか? (y/N): ")
|
||||||
|
if confirm.lower() != 'y':
|
||||||
|
print(" 処理をキャンセルしました")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. ゼッケン番号割り当て実行
|
||||||
|
print("\n4. ゼッケン番号割り当て実行:")
|
||||||
|
with transaction.atomic():
|
||||||
|
for i, team in enumerate(teams_without_zekken, 1):
|
||||||
|
team_id = team[0]
|
||||||
|
team_name = team[1]
|
||||||
|
zekken_number = f"FC{i:03d}"
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE rog_team
|
||||||
|
SET zekken_number = %s, updated_at = NOW()
|
||||||
|
WHERE id = %s;
|
||||||
|
""", [zekken_number, team_id])
|
||||||
|
|
||||||
|
print(f" Team ID:{team_id} ({team_name}) → ゼッケン番号: {zekken_number}")
|
||||||
|
|
||||||
|
print(f"\n ✅ {len(teams_without_zekken)}チームにゼッケン番号を割り当てました")
|
||||||
|
|
||||||
|
# 6. 結果確認
|
||||||
|
print("\n5. 割り当て結果確認:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT t.id, t.team_name, t.zekken_number
|
||||||
|
FROM rog_team t
|
||||||
|
JOIN rog_entry e ON t.id = e.team_id
|
||||||
|
WHERE e.event_id = 10 AND t.zekken_number IS NOT NULL
|
||||||
|
ORDER BY t.zekken_number;
|
||||||
|
""")
|
||||||
|
updated_teams = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f" ゼッケン番号付きチーム数: {len(updated_teams)}")
|
||||||
|
print(" 割り当て結果(サンプル):")
|
||||||
|
for team in updated_teams[:10]:
|
||||||
|
print(f" {team[2]}: {team[1]} (ID:{team[0]})")
|
||||||
|
|
||||||
|
# 7. 通過審査管理画面での影響確認
|
||||||
|
print("\n6. 通過審査管理画面への影響:")
|
||||||
|
print(" これで通過審査管理画面で以下が表示されるはずです:")
|
||||||
|
print(" - ALL(全参加者)")
|
||||||
|
for team in updated_teams[:5]:
|
||||||
|
print(f" - {team[2]}({team[1]})")
|
||||||
|
print(" - ...")
|
||||||
|
|
||||||
|
def reset_zekken_numbers():
|
||||||
|
"""FC岐阜イベントのゼッケン番号をリセット(テスト用)"""
|
||||||
|
print("\n=== ゼッケン番号リセット(テスト用) ===")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
confirm = input("FC岐阜イベントのゼッケン番号をリセットしますか? (y/N): ")
|
||||||
|
if confirm.lower() != 'y':
|
||||||
|
print("リセットをキャンセルしました")
|
||||||
|
return
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE rog_team
|
||||||
|
SET zekken_number = NULL, updated_at = NOW()
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT DISTINCT t.id
|
||||||
|
FROM rog_team t
|
||||||
|
JOIN rog_entry e ON t.id = e.team_id
|
||||||
|
WHERE e.event_id = 10
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
affected_rows = cursor.rowcount
|
||||||
|
print(f"✅ {affected_rows}チームのゼッケン番号をリセットしました")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == '--reset':
|
||||||
|
reset_zekken_numbers()
|
||||||
|
else:
|
||||||
|
assign_zekken_numbers_to_fc_gifu()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
140
investigate_team_structure.py
Normal file
140
investigate_team_structure.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
# プロジェクト設定
|
||||||
|
sys.path.append('/app')
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def investigate_team_table_structure():
|
||||||
|
"""チームテーブルの構造とFC岐阜問題を調査"""
|
||||||
|
|
||||||
|
print("=== Team テーブル構造とFC岐阜問題調査 ===")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# 1. rog_teamテーブルの構造確認
|
||||||
|
print("\n1. rog_teamテーブル構造:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_team'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
for col in columns:
|
||||||
|
print(f" - {col[0]}: {col[1]} ({'NULL' if col[2] == 'YES' else 'NOT NULL'})")
|
||||||
|
|
||||||
|
# 2. rog_teamテーブルの総件数
|
||||||
|
print("\n2. rog_teamテーブルの状況:")
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_team;")
|
||||||
|
total_teams = cursor.fetchone()[0]
|
||||||
|
print(f" 総チーム数: {total_teams}")
|
||||||
|
|
||||||
|
# 3. FC岐阜イベント(ID:10)の詳細調査
|
||||||
|
print("\n3. FC岐阜イベント(ID:10)詳細調査:")
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_entry WHERE event_id = 10;")
|
||||||
|
fc_entries = cursor.fetchone()[0]
|
||||||
|
print(f" FC岐阜イベントエントリー数: {fc_entries}")
|
||||||
|
|
||||||
|
# 4. FC岐阜エントリーのサンプル表示
|
||||||
|
print("\n4. FC岐阜エントリーサンプル:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, team_id, event_id, date
|
||||||
|
FROM rog_entry
|
||||||
|
WHERE event_id = 10
|
||||||
|
LIMIT 10;
|
||||||
|
""")
|
||||||
|
fc_entry_samples = cursor.fetchall()
|
||||||
|
for entry in fc_entry_samples:
|
||||||
|
print(f" Entry ID:{entry[0]}, Team ID:{entry[1]}, Event ID:{entry[2]}, Date:{entry[3]}")
|
||||||
|
|
||||||
|
# 5. FC岐阜エントリーのteam_idを調べる
|
||||||
|
print("\n5. FC岐阜エントリーのteam_id分析:")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT team_id, COUNT(*) as count
|
||||||
|
FROM rog_entry
|
||||||
|
WHERE event_id = 10
|
||||||
|
GROUP BY team_id
|
||||||
|
ORDER BY count DESC;
|
||||||
|
""")
|
||||||
|
team_id_stats = cursor.fetchall()
|
||||||
|
for stat in team_id_stats:
|
||||||
|
print(f" Team ID:{stat[0]}, エントリー数:{stat[1]}")
|
||||||
|
|
||||||
|
# 6. 実際のteam_idでチーム情報を確認
|
||||||
|
print("\n6. 実際のチーム情報確認:")
|
||||||
|
if team_id_stats:
|
||||||
|
sample_team_ids = [stat[0] for stat in team_id_stats[:5]]
|
||||||
|
for team_id in sample_team_ids:
|
||||||
|
cursor.execute("SELECT * FROM rog_team WHERE id = %s;", [team_id])
|
||||||
|
team_info = cursor.fetchone()
|
||||||
|
if team_info:
|
||||||
|
print(f" Team ID:{team_id} 存在する: {team_info}")
|
||||||
|
else:
|
||||||
|
print(f" Team ID:{team_id} 存在しない")
|
||||||
|
|
||||||
|
# 7. ゼッケン番号付きチームの確認(実際のカラム名を使用)
|
||||||
|
print("\n7. ゼッケン番号関連調査:")
|
||||||
|
if 'zekken_number' in [col[0] for col in columns]:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM rog_team
|
||||||
|
WHERE zekken_number IS NOT NULL AND zekken_number != '';
|
||||||
|
""")
|
||||||
|
zekken_count = cursor.fetchone()[0]
|
||||||
|
print(f" ゼッケン番号付きチーム数: {zekken_count}")
|
||||||
|
|
||||||
|
if zekken_count > 0:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, zekken_number, event_id
|
||||||
|
FROM rog_team
|
||||||
|
WHERE zekken_number IS NOT NULL AND zekken_number != ''
|
||||||
|
LIMIT 10;
|
||||||
|
""")
|
||||||
|
zekken_teams = cursor.fetchall()
|
||||||
|
print(" ゼッケン番号付きチームサンプル:")
|
||||||
|
for team in zekken_teams:
|
||||||
|
print(f" Team ID:{team[0]}, Zekken:{team[1]}, Event ID:{team[2]}")
|
||||||
|
|
||||||
|
# 8. 通過審査管理画面の問題の原因を特定
|
||||||
|
print("\n8. 通過審査管理画面問題の分析:")
|
||||||
|
print(" FC岐阜イベント(ID:10)について:")
|
||||||
|
print(f" - エントリー数: {fc_entries}")
|
||||||
|
print(f" - 関連チーム情報の確認が必要")
|
||||||
|
|
||||||
|
# 実際に存在するチームを探す
|
||||||
|
if team_id_stats:
|
||||||
|
existing_teams = []
|
||||||
|
missing_teams = []
|
||||||
|
for team_id, count in team_id_stats:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_team WHERE id = %s;", [team_id])
|
||||||
|
exists = cursor.fetchone()[0] > 0
|
||||||
|
if exists:
|
||||||
|
existing_teams.append((team_id, count))
|
||||||
|
else:
|
||||||
|
missing_teams.append((team_id, count))
|
||||||
|
|
||||||
|
print(f" - 存在するチーム: {len(existing_teams)}")
|
||||||
|
print(f" - 存在しないチーム: {len(missing_teams)}")
|
||||||
|
|
||||||
|
if missing_teams:
|
||||||
|
print(" 🔴 問題発見: エントリーが参照するチームが存在しない!")
|
||||||
|
for team_id, count in missing_teams[:3]:
|
||||||
|
print(f" Missing Team ID:{team_id} ({count}エントリー)")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
investigate_team_table_structure()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
377
migrate_all_events_complete.py
Normal file
377
migrate_all_events_complete.py
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への全イベントデータ移行スクリプト
|
||||||
|
FC岐阜の成功事例をベースに全てのイベントのteam/member/entryを移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rog.models import NewEvent2, Team, Entry, NewCategory, CustomUser, Member
|
||||||
|
import psycopg2
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
print("=== old_rogdb から 全イベントデータ移行 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# old_rogdbに直接接続
|
||||||
|
old_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='old_rogdb',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ old_rogdbに接続成功")
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
# === STEP 0: 移行対象イベントの確認 ===
|
||||||
|
print("\\n=== STEP 0: 移行対象イベントの確認 ===")
|
||||||
|
|
||||||
|
# 新DBのイベント一覧を取得
|
||||||
|
existing_events = list(NewEvent2.objects.values_list('id', 'event_name'))
|
||||||
|
existing_event_ids = [event_id for event_id, _ in existing_events]
|
||||||
|
|
||||||
|
print(f"新DB既存イベント: {len(existing_events)}件")
|
||||||
|
for event_id, event_name in existing_events[:10]:
|
||||||
|
print(f" Event {event_id}: {event_name}")
|
||||||
|
|
||||||
|
# old_rogdbでエントリーがあるイベントを確認
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT e.id, e.event_name, COUNT(re.id) as entry_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_entry re ON e.id = re.event_id
|
||||||
|
WHERE e.id IN ({})
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
HAVING COUNT(re.id) > 0
|
||||||
|
ORDER BY COUNT(re.id) DESC;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
events_with_entries = old_cursor.fetchall()
|
||||||
|
print(f"\\n移行対象イベント(エントリーあり): {len(events_with_entries)}件")
|
||||||
|
for event_id, event_name, entry_count in events_with_entries:
|
||||||
|
print(f" Event {event_id}: '{event_name}' - {entry_count}件のエントリー")
|
||||||
|
|
||||||
|
# === STEP 1: 全イベントのTeam & Member データ取得 ===
|
||||||
|
print("\\n=== STEP 1: 全イベントの Team & Member データ取得 ===")
|
||||||
|
|
||||||
|
# 全イベントのチーム情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT DISTINCT rt.id, rt.team_name, rt.owner_id, rt.category_id,
|
||||||
|
rc.category_name, cu.email, cu.firstname, cu.lastname, re.event_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON rt.category_id = rc.id
|
||||||
|
LEFT JOIN rog_customuser cu ON rt.owner_id = cu.id
|
||||||
|
WHERE re.event_id IN ({})
|
||||||
|
ORDER BY re.event_id, rt.id;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
all_team_data = old_cursor.fetchall()
|
||||||
|
print(f"全イベント関連チーム: {len(all_team_data)}件")
|
||||||
|
|
||||||
|
# イベント別チーム数統計
|
||||||
|
teams_by_event = defaultdict(int)
|
||||||
|
teams_by_event = defaultdict(int)
|
||||||
|
for _, _, _, _, _, _, _, _, event_id in all_team_data:
|
||||||
|
teams_by_event[event_id] += 1
|
||||||
|
|
||||||
|
print("\\nイベント別チーム数:")
|
||||||
|
for event_id, count in sorted(teams_by_event.items()):
|
||||||
|
event_name = next((name for eid, name in existing_events if eid == event_id), "不明")
|
||||||
|
print(f" Event {event_id} ({event_name}): {count}チーム")
|
||||||
|
|
||||||
|
# 全イベントのメンバー情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT rm.team_id, rm.user_id, cu.email, cu.firstname, cu.lastname, re.event_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_member rm ON re.team_id = rm.team_id
|
||||||
|
JOIN rog_customuser cu ON rm.user_id = cu.id
|
||||||
|
WHERE re.event_id IN ({})
|
||||||
|
ORDER BY re.event_id, rm.team_id, rm.user_id;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
all_member_data = old_cursor.fetchall()
|
||||||
|
print(f"全イベント関連メンバー: {len(all_member_data)}件")
|
||||||
|
|
||||||
|
# === STEP 2: ユーザー移行 ===
|
||||||
|
print("\\n=== STEP 2: ユーザー移行 ===")
|
||||||
|
|
||||||
|
# 関連するすべてのユーザーを取得
|
||||||
|
all_user_ids = set()
|
||||||
|
for _, _, owner_id, _, _, _, _, _, _ in all_team_data:
|
||||||
|
if owner_id:
|
||||||
|
all_user_ids.add(owner_id)
|
||||||
|
for _, user_id, _, _, _, _ in all_member_data:
|
||||||
|
all_user_ids.add(user_id)
|
||||||
|
|
||||||
|
if all_user_ids:
|
||||||
|
# 大量のユーザーIDに対応するため、バッチで処理
|
||||||
|
user_batches = [list(all_user_ids)[i:i+100] for i in range(0, len(all_user_ids), 100)]
|
||||||
|
all_user_data = []
|
||||||
|
user_batches = [list(all_user_ids)[i:i+100] for i in range(0, len(all_user_ids), 100)]
|
||||||
|
all_user_data = []
|
||||||
|
|
||||||
|
for batch in user_batches:
|
||||||
|
old_cursor.execute(f"""
|
||||||
|
SELECT id, email, firstname, lastname, date_joined
|
||||||
|
FROM rog_customuser
|
||||||
|
WHERE id IN ({','.join(map(str, batch))})
|
||||||
|
""")
|
||||||
|
all_user_data.extend(old_cursor.fetchall())
|
||||||
|
|
||||||
|
print(f"移行対象ユーザー: {len(all_user_data)}件")
|
||||||
|
|
||||||
|
migrated_users = 0
|
||||||
|
for user_id, email, first_name, last_name, date_joined in all_user_data:
|
||||||
|
user, created = CustomUser.objects.get_or_create(
|
||||||
|
id=user_id,
|
||||||
|
defaults={
|
||||||
|
'email': email or f'user{user_id}@example.com',
|
||||||
|
'first_name': first_name or '',
|
||||||
|
'last_name': last_name or '',
|
||||||
|
'username': email or f'user{user_id}',
|
||||||
|
'date_joined': date_joined,
|
||||||
|
'is_active': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_users += 1
|
||||||
|
if migrated_users <= 10: # 最初の10件のみ表示
|
||||||
|
print(f" ユーザー作成: {email} ({first_name} {last_name})")
|
||||||
|
|
||||||
|
print(f"✅ ユーザー移行完了: {migrated_users}件作成")
|
||||||
|
|
||||||
|
# === STEP 3: カテゴリ移行 ===
|
||||||
|
print("\\n=== STEP 3: カテゴリ移行 ===")
|
||||||
|
|
||||||
|
migrated_categories = 0
|
||||||
|
unique_categories = set()
|
||||||
|
unique_categories = set()
|
||||||
|
for _, _, _, cat_id, cat_name, _, _, _, _ in all_team_data:
|
||||||
|
if cat_id and cat_name:
|
||||||
|
unique_categories.add((cat_id, cat_name))
|
||||||
|
|
||||||
|
for cat_id, cat_name in unique_categories:
|
||||||
|
category, created = NewCategory.objects.get_or_create(
|
||||||
|
id=cat_id,
|
||||||
|
defaults={
|
||||||
|
'category_name': cat_name,
|
||||||
|
'category_number': cat_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_categories += 1
|
||||||
|
print(f" カテゴリ作成: {cat_name}")
|
||||||
|
|
||||||
|
print(f"✅ カテゴリ移行完了: {migrated_categories}件作成")
|
||||||
|
|
||||||
|
# === STEP 4: イベント別チーム移行 ===
|
||||||
|
print("\\n=== STEP 4: イベント別チーム移行 ===")
|
||||||
|
|
||||||
|
total_migrated_teams = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\\n--- Event {event_id}: {event_name} ---")
|
||||||
|
event_teams = [data for data in all_team_data if data[8] == event_id]
|
||||||
|
event_migrated_teams = 0
|
||||||
|
|
||||||
|
for team_id, team_name, owner_id, cat_id, cat_name, email, first_name, last_name, _ in event_teams:
|
||||||
|
try:
|
||||||
|
# カテゴリを取得
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
|
||||||
|
# チームを作成
|
||||||
|
team, created = Team.objects.get_or_create(
|
||||||
|
id=team_id,
|
||||||
|
defaults={
|
||||||
|
'team_name': team_name,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'category': category,
|
||||||
|
'event_id': event_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
event_migrated_teams += 1
|
||||||
|
total_migrated_teams += 1
|
||||||
|
if event_migrated_teams <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" チーム作成: {team_name} (ID: {team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ チーム作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_teams}件のチームを移行")
|
||||||
|
|
||||||
|
print(f"\\n✅ 全チーム移行完了: {total_migrated_teams}件作成")
|
||||||
|
|
||||||
|
# === STEP 5: メンバー移行 ===
|
||||||
|
print("\\n=== STEP 5: メンバー移行 ===")
|
||||||
|
|
||||||
|
total_migrated_members = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_members = [data for data in all_member_data if data[5] == event_id]
|
||||||
|
if not event_members:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\\n--- Event {event_id}: {event_name} ---")
|
||||||
|
event_migrated_members = 0
|
||||||
|
|
||||||
|
for team_id, user_id, email, first_name, last_name, _ in event_members:
|
||||||
|
try:
|
||||||
|
# チームとユーザーを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
user = CustomUser.objects.get(id=user_id)
|
||||||
|
|
||||||
|
# メンバーを作成
|
||||||
|
member, created = Member.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
event_migrated_members += 1
|
||||||
|
total_migrated_members += 1
|
||||||
|
if event_migrated_members <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" メンバー追加: {email} → {team.team_name}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が見つかりません")
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
print(f" ⚠️ ユーザー{user_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ メンバー追加エラー: {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_members}件のメンバーを移行")
|
||||||
|
|
||||||
|
print(f"\\n✅ 全メンバー移行完了: {total_migrated_members}件作成")
|
||||||
|
|
||||||
|
# === STEP 6: エントリー移行 ===
|
||||||
|
print("\\n=== STEP 6: エントリー移行 ===")
|
||||||
|
|
||||||
|
# データベースのis_trialフィールドにデフォルト値を設定
|
||||||
|
print("データベーステーブルのis_trialフィールドを修正中...")
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
try:
|
||||||
|
django_cursor.execute("""
|
||||||
|
ALTER TABLE rog_entry
|
||||||
|
ALTER COLUMN is_trial SET DEFAULT FALSE;
|
||||||
|
""")
|
||||||
|
print(" ✅ is_trialフィールドにデフォルト値を設定")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ is_trial修正エラー: {e}")
|
||||||
|
|
||||||
|
total_migrated_entries = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\\n--- Event {event_id}: {event_name} ---")
|
||||||
|
|
||||||
|
# イベント別エントリーデータを取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id, re.date, re.owner_id,
|
||||||
|
rc.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON re.category_id = rc.id
|
||||||
|
WHERE re.event_id = %s
|
||||||
|
ORDER BY re.zekken_number;
|
||||||
|
""", [event_id])
|
||||||
|
|
||||||
|
event_entry_data = old_cursor.fetchall()
|
||||||
|
event_migrated_entries = 0
|
||||||
|
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id, cat_name in event_entry_data:
|
||||||
|
try:
|
||||||
|
# チームとカテゴリを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
event_obj = NewEvent2.objects.get(id=event_id)
|
||||||
|
|
||||||
|
# 既存のエントリーをチェック
|
||||||
|
existing_entry = Entry.objects.filter(team=team, event=event_obj).first()
|
||||||
|
if existing_entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# SQLで直接エントリーを挿入
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
django_cursor.execute("""
|
||||||
|
INSERT INTO rog_entry
|
||||||
|
(date, category_id, event_id, owner_id, team_id, is_active,
|
||||||
|
zekken_number, "hasGoaled", "hasParticipated", zekken_label,
|
||||||
|
is_trial, staff_privileges, can_access_private_events, team_validation_status)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
|
||||||
|
""", [
|
||||||
|
event_obj.start_datetime, # date
|
||||||
|
cat_id, # category_id
|
||||||
|
event_id, # event_id
|
||||||
|
owner_id or 1, # owner_id
|
||||||
|
team_id, # team_id
|
||||||
|
True, # is_active
|
||||||
|
int(zekken) if zekken else 0, # zekken_number
|
||||||
|
False, # hasGoaled
|
||||||
|
False, # hasParticipated
|
||||||
|
label or f"{event_name}-{zekken}", # zekken_label
|
||||||
|
False, # is_trial
|
||||||
|
False, # staff_privileges
|
||||||
|
False, # can_access_private_events
|
||||||
|
'approved' # team_validation_status
|
||||||
|
])
|
||||||
|
|
||||||
|
event_migrated_entries += 1
|
||||||
|
total_migrated_entries += 1
|
||||||
|
if event_migrated_entries <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ❌ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
print(f" ❌ イベント{event_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エントリー作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_entries}件のエントリーを移行")
|
||||||
|
|
||||||
|
print(f"\\n✅ 全エントリー移行完了: {total_migrated_entries}件作成")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
# === 最終確認 ===
|
||||||
|
print("\\n=== 移行結果確認 ===")
|
||||||
|
|
||||||
|
total_teams = Team.objects.count()
|
||||||
|
total_members = Member.objects.count()
|
||||||
|
total_entries = Entry.objects.count()
|
||||||
|
|
||||||
|
print(f"総チーム数: {total_teams}件")
|
||||||
|
print(f"総メンバー数: {total_members}件")
|
||||||
|
print(f"総エントリー数: {total_entries}件")
|
||||||
|
|
||||||
|
# イベント別エントリー統計
|
||||||
|
print("\\n=== イベント別エントリー統計 ===")
|
||||||
|
for event_id, event_name in existing_events[:10]: # 最初の10件を表示
|
||||||
|
entry_count = Entry.objects.filter(event_id=event_id).count()
|
||||||
|
if entry_count > 0:
|
||||||
|
print(f" {event_name}: {entry_count}件")
|
||||||
|
|
||||||
|
print("\\n🎉 全イベントデータ移行が完了しました!")
|
||||||
|
print("🎯 通過審査管理画面で全てのイベントのゼッケン番号が表示されるようになります。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
828
migrate_all_events_complete_with_gps.py
Normal file
828
migrate_all_events_complete_with_gps.py
Normal file
@ -0,0 +1,828 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への全イベントデータ移行スクリプト(GPS情報移行機能付き)
|
||||||
|
FC岐阜の成功事例をベースに全てのイベントのteam/member/entryを移行
|
||||||
|
さらに、gifurogeのgps_informationをrogdbのrog_checkinsに移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rog.models import NewEvent2, Team, Entry, NewCategory, CustomUser, Member
|
||||||
|
import psycopg2
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
print("=== old_rogdb から 全イベントデータ移行(GPS情報付き) ===")
|
||||||
|
|
||||||
|
# GPS情報移行用のヘルパー関数
|
||||||
|
def load_event_dates_from_db():
|
||||||
|
"""gifurogeのevent_tableからイベントコードと日付のマッピングを取得"""
|
||||||
|
event_dates = {}
|
||||||
|
try:
|
||||||
|
# gifuroge データベースに接続
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='gifuroge',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# event_tableからイベントコードと開始日・終了日を取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT event_code, event_day, end_day
|
||||||
|
FROM event_table
|
||||||
|
WHERE event_code IS NOT NULL AND event_day IS NOT NULL
|
||||||
|
ORDER BY event_day
|
||||||
|
""")
|
||||||
|
|
||||||
|
events = cursor.fetchall()
|
||||||
|
for event_code, event_day, end_day in events:
|
||||||
|
# デバッグ用:読み込まれた生データを表示
|
||||||
|
print(f"🔍 生データ: {event_code} | event_day={event_day}({type(event_day)}) | end_day={end_day}({type(end_day)})")
|
||||||
|
|
||||||
|
# event_dayの日付フォーマットを統一(yyyy-mm-dd形式に変換)
|
||||||
|
start_date = None
|
||||||
|
end_date = None
|
||||||
|
|
||||||
|
# event_day(開始日)の処理
|
||||||
|
if isinstance(event_day, str):
|
||||||
|
if '/' in event_day:
|
||||||
|
start_date = normalize_date_format(event_day.replace('/', '-'))
|
||||||
|
elif '-' in event_day:
|
||||||
|
start_date = normalize_date_format(event_day)
|
||||||
|
else:
|
||||||
|
date_part = event_day.split(' ')[0] if ' ' in event_day else event_day
|
||||||
|
start_date = normalize_date_format(date_part.replace('/', '-'))
|
||||||
|
else:
|
||||||
|
start_date = normalize_date_format(event_day.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
# end_day(終了日)の処理
|
||||||
|
if end_day:
|
||||||
|
if isinstance(end_day, str):
|
||||||
|
if '/' in end_day:
|
||||||
|
end_date = normalize_date_format(end_day.replace('/', '-'))
|
||||||
|
elif '-' in end_day:
|
||||||
|
end_date = normalize_date_format(end_day)
|
||||||
|
else:
|
||||||
|
date_part = end_day.split(' ')[0] if ' ' in end_day else end_day
|
||||||
|
end_date = normalize_date_format(date_part.replace('/', '-'))
|
||||||
|
else:
|
||||||
|
end_date = normalize_date_format(end_day.strftime('%Y-%m-%d'))
|
||||||
|
else:
|
||||||
|
# end_dayが設定されていない場合は、event_dayと同じ日とする
|
||||||
|
end_date = start_date
|
||||||
|
|
||||||
|
# イベント期間情報を保存
|
||||||
|
event_dates[event_code] = {
|
||||||
|
'start_date': start_date,
|
||||||
|
'end_date': end_date,
|
||||||
|
'display_date': start_date # 主要な表示用日付
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print(f"📅 event_tableから{len(event_dates)}件のイベント情報を読み込みました:")
|
||||||
|
for code, date_info in event_dates.items():
|
||||||
|
if date_info['start_date'] == date_info['end_date']:
|
||||||
|
print(f" {code}: {date_info['start_date']}")
|
||||||
|
else:
|
||||||
|
print(f" {code}: {date_info['start_date']} - {date_info['end_date']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ event_table読み込みエラー: {e}")
|
||||||
|
# フォールバック用のデフォルト値
|
||||||
|
event_dates = {
|
||||||
|
'gifu2024': {'start_date': '2024-10-27', 'end_date': '2024-10-27', 'display_date': '2024-10-27'},
|
||||||
|
'gifu2023': {'start_date': '2023-11-12', 'end_date': '2023-11-12', 'display_date': '2023-11-12'},
|
||||||
|
'gifu2022': {'start_date': '2022-11-13', 'end_date': '2022-11-13', 'display_date': '2022-11-13'},
|
||||||
|
'test2024': {'start_date': '2024-12-15', 'end_date': '2024-12-15', 'display_date': '2024-12-15'},
|
||||||
|
'test2025': {'start_date': '2025-01-25', 'end_date': '2025-01-25', 'display_date': '2025-01-25'},
|
||||||
|
'郡上': {'start_date': '2024-06-15', 'end_date': '2024-06-15', 'display_date': '2024-06-15'}
|
||||||
|
}
|
||||||
|
print(f"デフォルトのイベント日付を使用します: {len(event_dates)}件")
|
||||||
|
|
||||||
|
return event_dates
|
||||||
|
|
||||||
|
def get_event_date(event_code, event_dates_cache):
|
||||||
|
"""イベントコードから日付を取得(キャッシュ使用)"""
|
||||||
|
if event_code in event_dates_cache:
|
||||||
|
return event_dates_cache[event_code]['display_date']
|
||||||
|
|
||||||
|
# 未知のイベントコードの場合、警告を出してデフォルト日付を返す
|
||||||
|
print(f"⚠️ 未知のイベントコード '{event_code}' - デフォルト日付2024-01-01を使用")
|
||||||
|
return '2024-01-01' # デフォルト日付
|
||||||
|
|
||||||
|
def normalize_date_format(date_str):
|
||||||
|
"""日付文字列をyyyy-mm-dd形式に正規化"""
|
||||||
|
try:
|
||||||
|
# datetimeオブジェクトの場合
|
||||||
|
if hasattr(date_str, 'strftime'):
|
||||||
|
return date_str.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# 文字列の場合
|
||||||
|
if isinstance(date_str, str):
|
||||||
|
# スラッシュ区切りをハイフン区切りに変換
|
||||||
|
if '/' in date_str:
|
||||||
|
date_str = date_str.replace('/', '-')
|
||||||
|
|
||||||
|
# yyyy-m-d や yyyy-mm-d などを yyyy-mm-dd に正規化
|
||||||
|
parts = date_str.split('-')
|
||||||
|
if len(parts) == 3:
|
||||||
|
year, month, day = parts
|
||||||
|
return f"{year}-{month.zfill(2)}-{day.zfill(2)}"
|
||||||
|
|
||||||
|
return date_str
|
||||||
|
except:
|
||||||
|
return date_str
|
||||||
|
|
||||||
|
def is_within_event_period(gps_datetime, event_code, event_dates_cache):
|
||||||
|
"""GPS記録の日時がイベント期間内かチェック"""
|
||||||
|
if event_code not in event_dates_cache:
|
||||||
|
return True # 未知のイベントの場合は通す
|
||||||
|
|
||||||
|
event_info = event_dates_cache[event_code]
|
||||||
|
start_date = normalize_date_format(event_info['start_date'])
|
||||||
|
end_date = normalize_date_format(event_info['end_date'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# GPS記録の日付部分を取得して正規化
|
||||||
|
gps_date = normalize_date_format(gps_datetime.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
# イベント期間内かチェック
|
||||||
|
return start_date <= gps_date <= end_date
|
||||||
|
except Exception as e:
|
||||||
|
print(f"日付比較エラー: GPS={gps_datetime}, イベント={event_code}, エラー={e}")
|
||||||
|
return True # エラーの場合は通す
|
||||||
|
|
||||||
|
def parse_goal_time(goal_time_str, event_date):
|
||||||
|
"""ゴール時刻をパース"""
|
||||||
|
if not goal_time_str or not event_date:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# HH:MM形式からdatetimeに変換
|
||||||
|
time_parts = goal_time_str.split(':')
|
||||||
|
if len(time_parts) == 2:
|
||||||
|
hour, minute = int(time_parts[0]), int(time_parts[1])
|
||||||
|
event_datetime = datetime.strptime(event_date, '%Y-%m-%d')
|
||||||
|
goal_datetime = event_datetime.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||||
|
# JST timezone設定
|
||||||
|
jst = pytz.timezone('Asia/Tokyo')
|
||||||
|
return jst.localize(goal_datetime)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ゴール時刻パースエラー: {goal_time_str} - {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def convert_utc_to_jst(utc_datetime):
|
||||||
|
"""UTC時刻をJSTに変換"""
|
||||||
|
if not utc_datetime:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(utc_datetime, str):
|
||||||
|
utc_datetime = datetime.fromisoformat(utc_datetime.replace('Z', '+00:00'))
|
||||||
|
|
||||||
|
# UTCとして扱い、JSTに変換
|
||||||
|
if utc_datetime.tzinfo is None:
|
||||||
|
utc = pytz.UTC
|
||||||
|
utc_datetime = utc.localize(utc_datetime)
|
||||||
|
|
||||||
|
jst = pytz.timezone('Asia/Tokyo')
|
||||||
|
return utc_datetime.astimezone(jst)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"時刻変換エラー: {utc_datetime} - {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def migrate_gps_data():
|
||||||
|
"""GPS情報をgifurogeからrogdbに移行"""
|
||||||
|
print("\n=== GPS情報移行開始 ===")
|
||||||
|
|
||||||
|
# まず、イベント日付情報を読み込み
|
||||||
|
event_dates_cache = load_event_dates_from_db()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# gifuroge データベースに接続
|
||||||
|
gifuroge_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='gifuroge',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
# rogdb データベースに接続
|
||||||
|
rogdb_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='rogdb',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ GPS移行用データベース接続成功")
|
||||||
|
|
||||||
|
with gifuroge_conn.cursor() as source_cursor, rogdb_conn.cursor() as target_cursor:
|
||||||
|
|
||||||
|
# 既存のGPSチェックイン記録をクリア
|
||||||
|
target_cursor.execute("DELETE FROM rog_gpscheckin;")
|
||||||
|
print("既存のGPSチェックイン記録をクリアしました")
|
||||||
|
|
||||||
|
# GPS記録を取得(serial_number < 20000のみ、実際のGPS記録)
|
||||||
|
source_cursor.execute("""
|
||||||
|
SELECT serial_number, zekken_number, event_code, cp_number, create_at, goal_time
|
||||||
|
FROM gps_information
|
||||||
|
WHERE serial_number < 20000
|
||||||
|
ORDER BY serial_number
|
||||||
|
""")
|
||||||
|
|
||||||
|
gps_records = source_cursor.fetchall()
|
||||||
|
print(f"移行対象GPS記録数: {len(gps_records)}件")
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
skip_count = 0
|
||||||
|
error_count = 0
|
||||||
|
event_stats = defaultdict(set)
|
||||||
|
skip_stats = defaultdict(int) # スキップ統計
|
||||||
|
skip_reasons = defaultdict(int) # スキップ理由別統計
|
||||||
|
large_skip_events = set() # 大量スキップイベントの詳細分析用
|
||||||
|
skip_date_ranges = defaultdict(list) # スキップされたGPS日付の範囲集計用
|
||||||
|
|
||||||
|
for record in gps_records:
|
||||||
|
serial_number, zekken, event_code, cp_number, create_at, goal_time = record
|
||||||
|
|
||||||
|
try:
|
||||||
|
# イベント日付取得(キャッシュから)
|
||||||
|
event_date = get_event_date(event_code, event_dates_cache)
|
||||||
|
# event_dateはNoneを返さなくなったので、この条件は不要だが安全のため残す
|
||||||
|
if not event_date:
|
||||||
|
# 時刻変換してGPS日付を取得
|
||||||
|
jst_create_at = convert_utc_to_jst(create_at)
|
||||||
|
gps_date = jst_create_at.strftime('%Y-%m-%d') if jst_create_at else 'N/A'
|
||||||
|
print(f"⚠️ イベント日付取得失敗: {event_code} GPS日付:{gps_date}")
|
||||||
|
skip_count += 1
|
||||||
|
skip_stats[event_code] += 1
|
||||||
|
skip_reasons["イベント日付取得失敗"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 時刻変換
|
||||||
|
jst_create_at = convert_utc_to_jst(create_at)
|
||||||
|
jst_goal_time = parse_goal_time(goal_time, event_date) if goal_time else None
|
||||||
|
|
||||||
|
if not jst_create_at:
|
||||||
|
print(f"時刻変換失敗: {serial_number}")
|
||||||
|
error_count += 1
|
||||||
|
skip_stats[event_code] += 1
|
||||||
|
skip_reasons["時刻変換失敗"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 未知のイベントコードの場合はGPS日付も表示
|
||||||
|
if event_code not in event_dates_cache:
|
||||||
|
gps_date = jst_create_at.strftime('%Y-%m-%d')
|
||||||
|
print(f"⚠️ 未知のイベントコード '{event_code}' GPS日付:{gps_date} - デフォルト日付2024-01-01を使用")
|
||||||
|
|
||||||
|
# GPS記録がイベント期間内かチェック
|
||||||
|
if not is_within_event_period(jst_create_at, event_code, event_dates_cache):
|
||||||
|
# GPS日付を正規化(期間外スキップ用)
|
||||||
|
gps_date = normalize_date_format(jst_create_at.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
# 大量スキップイベントの詳細分析
|
||||||
|
should_show_detail = (skip_count < 10 or
|
||||||
|
(event_code in ['各務原', '岐阜市', '養老ロゲ', '郡上', '大垣2', 'test下呂'] and
|
||||||
|
skip_stats[event_code] < 5))
|
||||||
|
|
||||||
|
if should_show_detail:
|
||||||
|
event_info = event_dates_cache.get(event_code, {})
|
||||||
|
start_date = normalize_date_format(event_info.get('start_date', 'N/A'))
|
||||||
|
end_date = normalize_date_format(event_info.get('end_date', 'N/A'))
|
||||||
|
|
||||||
|
# 600件超のイベントは特別扱い
|
||||||
|
if event_code in ['各務原', '岐阜市', '養老ロゲ', '郡上', '大垣2', 'test下呂']:
|
||||||
|
large_skip_events.add(event_code)
|
||||||
|
print(f"🔍 大量スキップイベント詳細分析 - {event_code}:")
|
||||||
|
print(f" イベントコード: {event_code}")
|
||||||
|
print(f" GPS元時刻: {create_at}")
|
||||||
|
print(f" GPS JST時刻: {jst_create_at}")
|
||||||
|
print(f" GPS日付(正規化前): {jst_create_at.strftime('%Y-%m-%d')}")
|
||||||
|
print(f" GPS日付(正規化後): {gps_date}")
|
||||||
|
print(f" イベント開始日(正規化前): {event_info.get('start_date', 'N/A')}")
|
||||||
|
print(f" イベント開始日(正規化後): {start_date}")
|
||||||
|
print(f" イベント終了日(正規化前): {event_info.get('end_date', 'N/A')}")
|
||||||
|
print(f" イベント終了日(正規化後): {end_date}")
|
||||||
|
print(f" 比較結果: {start_date} <= {gps_date} <= {end_date}")
|
||||||
|
print(f" 文字列比較1: '{start_date}' <= '{gps_date}' = {start_date <= gps_date}")
|
||||||
|
print(f" 文字列比較2: '{gps_date}' <= '{end_date}' = {gps_date <= end_date}")
|
||||||
|
print(f" 年差: GPS年={gps_date[:4]}, イベント年={start_date[:4]}")
|
||||||
|
else:
|
||||||
|
# デバッグ情報を追加
|
||||||
|
print(f"🔍 デバッグ情報:")
|
||||||
|
print(f" イベントコード: {event_code}")
|
||||||
|
print(f" GPS元時刻: {create_at}")
|
||||||
|
print(f" GPS JST時刻: {jst_create_at}")
|
||||||
|
print(f" GPS日付(正規化前): {jst_create_at.strftime('%Y-%m-%d')}")
|
||||||
|
print(f" GPS日付(正規化後): {gps_date}")
|
||||||
|
print(f" イベント開始日(正規化前): {event_info.get('start_date', 'N/A')}")
|
||||||
|
print(f" イベント開始日(正規化後): {start_date}")
|
||||||
|
print(f" イベント終了日(正規化前): {event_info.get('end_date', 'N/A')}")
|
||||||
|
print(f" イベント終了日(正規化後): {end_date}")
|
||||||
|
print(f" 比較結果: {start_date} <= {gps_date} <= {end_date}")
|
||||||
|
print(f" 文字列比較1: '{start_date}' <= '{gps_date}' = {start_date <= gps_date}")
|
||||||
|
print(f" 文字列比較2: '{gps_date}' <= '{end_date}' = {gps_date <= end_date}")
|
||||||
|
|
||||||
|
print(f"期間外GPS記録スキップ: {event_code} GPS日付:{gps_date} イベント期間:{start_date}-{end_date}")
|
||||||
|
|
||||||
|
# 大量スキップイベントのGPS日付を記録
|
||||||
|
if event_code in ['各務原', '岐阜市', '養老ロゲ', '郡上', '大垣2', 'test下呂']:
|
||||||
|
skip_date_ranges[event_code].append(gps_date)
|
||||||
|
|
||||||
|
skip_count += 1
|
||||||
|
skip_stats[event_code] += 1
|
||||||
|
skip_reasons["期間外"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# チェックイン記録挿入
|
||||||
|
target_cursor.execute("""
|
||||||
|
INSERT INTO rog_gpscheckin (
|
||||||
|
zekken, event_code, cp_number, checkin_time, record_time, serial_number
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
|
""", (zekken, event_code, cp_number, jst_create_at, jst_create_at, str(serial_number)))
|
||||||
|
|
||||||
|
event_stats[event_code].add(zekken)
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
if success_count % 100 == 0:
|
||||||
|
print(f"GPS移行進捗: {success_count}件完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"GPS移行エラー (Serial: {serial_number}): {e}")
|
||||||
|
error_count += 1
|
||||||
|
skip_stats[event_code] += 1
|
||||||
|
skip_reasons["その他エラー"] += 1
|
||||||
|
|
||||||
|
# コミット
|
||||||
|
rogdb_conn.commit()
|
||||||
|
|
||||||
|
print(f"\n✅ GPS移行完了:")
|
||||||
|
print(f" 成功: {success_count}件")
|
||||||
|
print(f" スキップ: {skip_count}件")
|
||||||
|
print(f" エラー: {error_count}件")
|
||||||
|
|
||||||
|
# イベント別統計を表示
|
||||||
|
print("\n=== イベント別GPS統計 ===")
|
||||||
|
for event_code, zekken_set in event_stats.items():
|
||||||
|
print(f" {event_code}: {len(zekken_set)}チーム")
|
||||||
|
|
||||||
|
# スキップ統計を表示
|
||||||
|
print("\n=== スキップ統計(イベント別) ===")
|
||||||
|
for event_code, skip_count_by_event in skip_stats.items():
|
||||||
|
print(f" {event_code}: {skip_count_by_event}件スキップ")
|
||||||
|
|
||||||
|
# スキップ理由別統計を表示
|
||||||
|
print("\n=== スキップ理由別統計 ===")
|
||||||
|
for reason, count in skip_reasons.items():
|
||||||
|
print(f" {reason}: {count}件")
|
||||||
|
|
||||||
|
# 大量スキップイベントの詳細分析結果
|
||||||
|
if large_skip_events:
|
||||||
|
print("\n=== 600件超大量スキップイベント分析結果 ===")
|
||||||
|
for event_code in large_skip_events:
|
||||||
|
total_skipped = skip_stats[event_code]
|
||||||
|
event_info = event_dates_cache.get(event_code, {})
|
||||||
|
|
||||||
|
# スキップされたGPS日付の範囲を分析
|
||||||
|
skipped_dates = skip_date_ranges.get(event_code, [])
|
||||||
|
if skipped_dates:
|
||||||
|
# 日付を昇順にソートしてユニーク化
|
||||||
|
unique_dates = sorted(set(skipped_dates))
|
||||||
|
date_range_start = unique_dates[0] if unique_dates else 'N/A'
|
||||||
|
date_range_end = unique_dates[-1] if unique_dates else 'N/A'
|
||||||
|
|
||||||
|
# 年月日の分析
|
||||||
|
year_counts = defaultdict(int)
|
||||||
|
month_counts = defaultdict(int)
|
||||||
|
for date_str in unique_dates:
|
||||||
|
try:
|
||||||
|
year = date_str[:4]
|
||||||
|
month = date_str[:7] # YYYY-MM
|
||||||
|
year_counts[year] += 1
|
||||||
|
month_counts[month] += 1
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"📊 {event_code}:")
|
||||||
|
print(f" 総スキップ数: {total_skipped}件")
|
||||||
|
print(f" 設定イベント期間: {event_info.get('start_date', 'N/A')} - {event_info.get('end_date', 'N/A')}")
|
||||||
|
|
||||||
|
if skipped_dates:
|
||||||
|
print(f" スキップされたGPS記録の期間: {date_range_start} ~ {date_range_end}")
|
||||||
|
print(f" ユニークな日付数: {len(unique_dates)}日")
|
||||||
|
|
||||||
|
# 年別集計
|
||||||
|
if year_counts:
|
||||||
|
print(f" 年別GPS記録数:")
|
||||||
|
for year in sorted(year_counts.keys()):
|
||||||
|
print(f" {year}年: {year_counts[year]}日分の記録")
|
||||||
|
|
||||||
|
# 月別集計(上位5件)
|
||||||
|
if month_counts:
|
||||||
|
top_months = sorted(month_counts.items(), key=lambda x: x[1], reverse=True)[:5]
|
||||||
|
print(f" 月別GPS記録数(上位5件):")
|
||||||
|
for month, count in top_months:
|
||||||
|
print(f" {month}: {count}日分の記録")
|
||||||
|
|
||||||
|
print(f" 推測される問題: イベント期間設定が実際のGPS記録日付と大幅にずれている")
|
||||||
|
print(f" 解決策: event_tableのevent_day/end_dayを実際のイベント開催日に修正する必要があります")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 最終統計
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
||||||
|
total_gps_records = target_cursor.fetchone()[0]
|
||||||
|
print(f"\n最終GPS記録数: {total_gps_records}件")
|
||||||
|
|
||||||
|
gifuroge_conn.close()
|
||||||
|
rogdb_conn.close()
|
||||||
|
|
||||||
|
return success_count > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ GPS移行エラー: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# old_rogdbに直接接続
|
||||||
|
old_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='old_rogdb',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ old_rogdbに接続成功")
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
# === STEP 0: 移行対象イベントの確認 ===
|
||||||
|
print("\n=== STEP 0: 移行対象イベントの確認 ===")
|
||||||
|
|
||||||
|
# 新DBのイベント一覧を取得
|
||||||
|
existing_events = list(NewEvent2.objects.values_list('id', 'event_name'))
|
||||||
|
existing_event_ids = [event_id for event_id, _ in existing_events]
|
||||||
|
|
||||||
|
print(f"新DB既存イベント: {len(existing_events)}件")
|
||||||
|
for event_id, event_name in existing_events[:10]:
|
||||||
|
print(f" Event {event_id}: {event_name}")
|
||||||
|
|
||||||
|
# old_rogdbでエントリーがあるイベントを確認
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT e.id, e.event_name, COUNT(re.id) as entry_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_entry re ON e.id = re.event_id
|
||||||
|
WHERE e.id IN ({})
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
HAVING COUNT(re.id) > 0
|
||||||
|
ORDER BY COUNT(re.id) DESC;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
events_with_entries = old_cursor.fetchall()
|
||||||
|
print(f"\n移行対象イベント(エントリーあり): {len(events_with_entries)}件")
|
||||||
|
for event_id, event_name, entry_count in events_with_entries:
|
||||||
|
print(f" Event {event_id}: '{event_name}' - {entry_count}件のエントリー")
|
||||||
|
|
||||||
|
# === STEP 1: 全イベントのTeam & Member データ取得 ===
|
||||||
|
print("\n=== STEP 1: 全イベントの Team & Member データ取得 ===")
|
||||||
|
|
||||||
|
# 全イベントのチーム情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT DISTINCT rt.id, rt.team_name, rt.owner_id, rt.category_id,
|
||||||
|
rc.category_name, cu.email, cu.firstname, cu.lastname, re.event_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON rt.category_id = rc.id
|
||||||
|
LEFT JOIN rog_customuser cu ON rt.owner_id = cu.id
|
||||||
|
WHERE re.event_id IN ({})
|
||||||
|
ORDER BY re.event_id, rt.id;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
all_team_data = old_cursor.fetchall()
|
||||||
|
print(f"全イベント関連チーム: {len(all_team_data)}件")
|
||||||
|
|
||||||
|
# イベント別チーム数統計
|
||||||
|
teams_by_event = defaultdict(int)
|
||||||
|
for _, _, _, _, _, _, _, _, event_id in all_team_data:
|
||||||
|
teams_by_event[event_id] += 1
|
||||||
|
|
||||||
|
print("\nイベント別チーム数:")
|
||||||
|
for event_id, count in sorted(teams_by_event.items()):
|
||||||
|
event_name = next((name for eid, name in existing_events if eid == event_id), "不明")
|
||||||
|
print(f" Event {event_id} ({event_name}): {count}チーム")
|
||||||
|
|
||||||
|
# 全イベントのメンバー情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT rm.team_id, rm.user_id, cu.email, cu.firstname, cu.lastname, re.event_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_member rm ON re.team_id = rm.team_id
|
||||||
|
JOIN rog_customuser cu ON rm.user_id = cu.id
|
||||||
|
WHERE re.event_id IN ({})
|
||||||
|
ORDER BY re.event_id, rm.team_id, rm.user_id;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
all_member_data = old_cursor.fetchall()
|
||||||
|
print(f"全イベント関連メンバー: {len(all_member_data)}件")
|
||||||
|
|
||||||
|
# === STEP 2: ユーザー移行 ===
|
||||||
|
print("\n=== STEP 2: ユーザー移行 ===")
|
||||||
|
|
||||||
|
# 関連するすべてのユーザーを取得
|
||||||
|
all_user_ids = set()
|
||||||
|
for _, _, owner_id, _, _, _, _, _, _ in all_team_data:
|
||||||
|
if owner_id:
|
||||||
|
all_user_ids.add(owner_id)
|
||||||
|
for _, user_id, _, _, _, _ in all_member_data:
|
||||||
|
all_user_ids.add(user_id)
|
||||||
|
|
||||||
|
if all_user_ids:
|
||||||
|
# 大量のユーザーIDに対応するため、バッチで処理
|
||||||
|
user_batches = [list(all_user_ids)[i:i+100] for i in range(0, len(all_user_ids), 100)]
|
||||||
|
all_user_data = []
|
||||||
|
|
||||||
|
for batch in user_batches:
|
||||||
|
old_cursor.execute(f"""
|
||||||
|
SELECT id, email, firstname, lastname, date_joined
|
||||||
|
FROM rog_customuser
|
||||||
|
WHERE id IN ({','.join(map(str, batch))})
|
||||||
|
""")
|
||||||
|
all_user_data.extend(old_cursor.fetchall())
|
||||||
|
|
||||||
|
print(f"移行対象ユーザー: {len(all_user_data)}件")
|
||||||
|
|
||||||
|
migrated_users = 0
|
||||||
|
for user_id, email, first_name, last_name, date_joined in all_user_data:
|
||||||
|
user, created = CustomUser.objects.get_or_create(
|
||||||
|
id=user_id,
|
||||||
|
defaults={
|
||||||
|
'email': email or f'user{user_id}@example.com',
|
||||||
|
'first_name': first_name or '',
|
||||||
|
'last_name': last_name or '',
|
||||||
|
'username': email or f'user{user_id}',
|
||||||
|
'date_joined': date_joined,
|
||||||
|
'is_active': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_users += 1
|
||||||
|
if migrated_users <= 10: # 最初の10件のみ表示
|
||||||
|
print(f" ユーザー作成: {email} ({first_name} {last_name})")
|
||||||
|
|
||||||
|
print(f"✅ ユーザー移行完了: {migrated_users}件作成")
|
||||||
|
|
||||||
|
# === STEP 3: カテゴリ移行 ===
|
||||||
|
print("\n=== STEP 3: カテゴリ移行 ===")
|
||||||
|
|
||||||
|
migrated_categories = 0
|
||||||
|
unique_categories = set()
|
||||||
|
for _, _, _, cat_id, cat_name, _, _, _, _ in all_team_data:
|
||||||
|
if cat_id and cat_name:
|
||||||
|
unique_categories.add((cat_id, cat_name))
|
||||||
|
|
||||||
|
for cat_id, cat_name in unique_categories:
|
||||||
|
category, created = NewCategory.objects.get_or_create(
|
||||||
|
id=cat_id,
|
||||||
|
defaults={
|
||||||
|
'category_name': cat_name,
|
||||||
|
'category_number': cat_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_categories += 1
|
||||||
|
print(f" カテゴリ作成: {cat_name}")
|
||||||
|
|
||||||
|
print(f"✅ カテゴリ移行完了: {migrated_categories}件作成")
|
||||||
|
|
||||||
|
# === STEP 4: イベント別チーム移行 ===
|
||||||
|
print("\n=== STEP 4: イベント別チーム移行 ===")
|
||||||
|
|
||||||
|
total_migrated_teams = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n--- Event {event_id}: {event_name} ---")
|
||||||
|
event_teams = [data for data in all_team_data if data[8] == event_id]
|
||||||
|
event_migrated_teams = 0
|
||||||
|
|
||||||
|
for team_id, team_name, owner_id, cat_id, cat_name, email, first_name, last_name, _ in event_teams:
|
||||||
|
try:
|
||||||
|
# カテゴリを取得
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
|
||||||
|
# チームを作成
|
||||||
|
team, created = Team.objects.get_or_create(
|
||||||
|
id=team_id,
|
||||||
|
defaults={
|
||||||
|
'team_name': team_name,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'category': category,
|
||||||
|
'event_id': event_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
event_migrated_teams += 1
|
||||||
|
total_migrated_teams += 1
|
||||||
|
if event_migrated_teams <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" チーム作成: {team_name} (ID: {team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ チーム作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_teams}件のチームを移行")
|
||||||
|
|
||||||
|
print(f"\n✅ 全チーム移行完了: {total_migrated_teams}件作成")
|
||||||
|
|
||||||
|
# === STEP 5: メンバー移行 ===
|
||||||
|
print("\n=== STEP 5: メンバー移行 ===")
|
||||||
|
|
||||||
|
total_migrated_members = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_members = [data for data in all_member_data if data[5] == event_id]
|
||||||
|
if not event_members:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n--- Event {event_id}: {event_name} ---")
|
||||||
|
event_migrated_members = 0
|
||||||
|
|
||||||
|
for team_id, user_id, email, first_name, last_name, _ in event_members:
|
||||||
|
try:
|
||||||
|
# チームとユーザーを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
user = CustomUser.objects.get(id=user_id)
|
||||||
|
|
||||||
|
# メンバーを作成
|
||||||
|
member, created = Member.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
event_migrated_members += 1
|
||||||
|
total_migrated_members += 1
|
||||||
|
if event_migrated_members <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" メンバー追加: {email} → {team.team_name}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が見つかりません")
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
print(f" ⚠️ ユーザー{user_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ メンバー追加エラー: {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_members}件のメンバーを移行")
|
||||||
|
|
||||||
|
print(f"\n✅ 全メンバー移行完了: {total_migrated_members}件作成")
|
||||||
|
|
||||||
|
# === STEP 6: エントリー移行 ===
|
||||||
|
print("\n=== STEP 6: エントリー移行 ===")
|
||||||
|
|
||||||
|
# データベースのis_trialフィールドにデフォルト値を設定
|
||||||
|
print("データベーステーブルのis_trialフィールドを修正中...")
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
try:
|
||||||
|
django_cursor.execute("""
|
||||||
|
ALTER TABLE rog_entry
|
||||||
|
ALTER COLUMN is_trial SET DEFAULT FALSE;
|
||||||
|
""")
|
||||||
|
print(" ✅ is_trialフィールドにデフォルト値を設定")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ is_trial修正エラー: {e}")
|
||||||
|
|
||||||
|
total_migrated_entries = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n--- Event {event_id}: {event_name} ---")
|
||||||
|
|
||||||
|
# イベント別エントリーデータを取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id, re.date, re.owner_id,
|
||||||
|
rc.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON re.category_id = rc.id
|
||||||
|
WHERE re.event_id = %s
|
||||||
|
ORDER BY re.zekken_number;
|
||||||
|
""", [event_id])
|
||||||
|
|
||||||
|
event_entry_data = old_cursor.fetchall()
|
||||||
|
event_migrated_entries = 0
|
||||||
|
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id, cat_name in event_entry_data:
|
||||||
|
try:
|
||||||
|
# チームとカテゴリを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
event_obj = NewEvent2.objects.get(id=event_id)
|
||||||
|
|
||||||
|
# 既存のエントリーをチェック
|
||||||
|
existing_entry = Entry.objects.filter(team=team, event=event_obj).first()
|
||||||
|
if existing_entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# SQLで直接エントリーを挿入
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
django_cursor.execute("""
|
||||||
|
INSERT INTO rog_entry
|
||||||
|
(date, category_id, event_id, owner_id, team_id, is_active,
|
||||||
|
zekken_number, "hasGoaled", "hasParticipated", zekken_label,
|
||||||
|
is_trial, staff_privileges, can_access_private_events, team_validation_status)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
|
||||||
|
""", [
|
||||||
|
event_obj.start_datetime, # date
|
||||||
|
cat_id, # category_id
|
||||||
|
event_id, # event_id
|
||||||
|
owner_id or 1, # owner_id
|
||||||
|
team_id, # team_id
|
||||||
|
True, # is_active
|
||||||
|
int(zekken) if zekken else 0, # zekken_number
|
||||||
|
False, # hasGoaled
|
||||||
|
False, # hasParticipated
|
||||||
|
label or f"{event_name}-{zekken}", # zekken_label
|
||||||
|
False, # is_trial
|
||||||
|
False, # staff_privileges
|
||||||
|
False, # can_access_private_events
|
||||||
|
'approved' # team_validation_status
|
||||||
|
])
|
||||||
|
|
||||||
|
event_migrated_entries += 1
|
||||||
|
total_migrated_entries += 1
|
||||||
|
if event_migrated_entries <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ❌ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
print(f" ❌ イベント{event_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エントリー作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_entries}件のエントリーを移行")
|
||||||
|
|
||||||
|
print(f"\n✅ 全エントリー移行完了: {total_migrated_entries}件作成")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
# === STEP 7: GPS情報移行 ===
|
||||||
|
print("\n=== STEP 7: GPS情報移行 ===")
|
||||||
|
|
||||||
|
gps_migration_success = migrate_gps_data()
|
||||||
|
|
||||||
|
if gps_migration_success:
|
||||||
|
print("✅ GPS情報移行が正常に完了しました")
|
||||||
|
else:
|
||||||
|
print("⚠️ GPS情報移行中にエラーが発生しました")
|
||||||
|
|
||||||
|
# === 最終確認 ===
|
||||||
|
print("\n=== 移行結果確認 ===")
|
||||||
|
|
||||||
|
total_teams = Team.objects.count()
|
||||||
|
total_members = Member.objects.count()
|
||||||
|
total_entries = Entry.objects.count()
|
||||||
|
|
||||||
|
print(f"総チーム数: {total_teams}件")
|
||||||
|
print(f"総メンバー数: {total_members}件")
|
||||||
|
print(f"総エントリー数: {total_entries}件")
|
||||||
|
|
||||||
|
# GPS記録数も追加で確認
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
with django_conn.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
||||||
|
gps_count = cursor.fetchone()[0]
|
||||||
|
print(f"総GPS記録数: {gps_count}件")
|
||||||
|
|
||||||
|
# イベント別エントリー統計
|
||||||
|
print("\n=== イベント別エントリー統計 ===")
|
||||||
|
for event_id, event_name in existing_events[:10]: # 最初の10件を表示
|
||||||
|
entry_count = Entry.objects.filter(event_id=event_id).count()
|
||||||
|
if entry_count > 0:
|
||||||
|
print(f" {event_name}: {entry_count}件")
|
||||||
|
|
||||||
|
print("\n🎉 全イベントデータ移行(GPS情報付き)が完了しました!")
|
||||||
|
print("🎯 通過審査管理画面で全てのイベントのゼッケン番号が表示されるようになります。")
|
||||||
|
print("📍 GPS情報も移行され、チェックイン記録が利用可能になります。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
332
migrate_all_events_sql.py
Normal file
332
migrate_all_events_sql.py
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への全イベントデータ移行スクリプト(SQL生成方式)
|
||||||
|
FC岐阜の成功事例をベースに全てのイベントのteam/member/entry + GPS情報を移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from rog.models import NewEvent2, Team, Entry, Member, NewCategory, CustomUser
|
||||||
|
|
||||||
|
print("📋 全イベントデータ移行スクリプト(SQL生成方式)を開始します")
|
||||||
|
|
||||||
|
# SQLファイル名
|
||||||
|
sql_file = "migrate_all_events_with_gps.sql"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# === STEP 1: ユーザー確認 ===
|
||||||
|
print("\n=== STEP 1: ユーザー確認 ===")
|
||||||
|
|
||||||
|
admin_user, created = CustomUser.objects.get_or_create(
|
||||||
|
username='admin',
|
||||||
|
defaults={
|
||||||
|
'email': 'admin@example.com',
|
||||||
|
'is_staff': True,
|
||||||
|
'is_superuser': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"管理ユーザー: {'作成' if created else '既存'}")
|
||||||
|
|
||||||
|
# === STEP 2: イベントとカテゴリー情報取得 ===
|
||||||
|
print("\n=== STEP 2: 既存イベント・カテゴリー確認 ===")
|
||||||
|
|
||||||
|
existing_events = list(NewEvent2.objects.values_list('id', 'name'))
|
||||||
|
print(f"既存イベント数: {len(existing_events)}件")
|
||||||
|
|
||||||
|
if not existing_events:
|
||||||
|
print("❌ イベントが存在しません。先にイベントを作成してください。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
existing_categories = list(NewCategory.objects.values_list('id', 'name'))
|
||||||
|
print(f"既存カテゴリー数: {len(existing_categories)}件")
|
||||||
|
|
||||||
|
if not existing_categories:
|
||||||
|
print("❌ カテゴリーが存在しません。先にカテゴリーを作成してください。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# === STEP 3: SQLファイル生成 ===
|
||||||
|
print(f"\n=== STEP 3: SQLファイル生成 ({sql_file}) ===")
|
||||||
|
|
||||||
|
with open(sql_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write("-- 全イベントデータ移行SQL(GPS情報含む)\n")
|
||||||
|
f.write(f"-- 生成日時: {datetime.now()}\n\n")
|
||||||
|
|
||||||
|
# 1. チーム移行SQL
|
||||||
|
f.write("-- ========================================\n")
|
||||||
|
f.write("-- 1. チーム移行(old_rogdb → rogdb)\n")
|
||||||
|
f.write("-- ========================================\n\n")
|
||||||
|
|
||||||
|
f.write("""
|
||||||
|
-- old_rogdbからチーム情報を移行
|
||||||
|
INSERT INTO rog_team (
|
||||||
|
id, name, owner_id, event_id, reg_date,
|
||||||
|
representative_name, representative_phone,
|
||||||
|
representative_email, is_deleted
|
||||||
|
)
|
||||||
|
SELECT DISTINCT
|
||||||
|
t.id,
|
||||||
|
t.name,
|
||||||
|
COALESCE(t.owner_id, {admin_user_id}) as owner_id,
|
||||||
|
t.event_id,
|
||||||
|
t.reg_date,
|
||||||
|
COALESCE(t.representative_name, t.name) as representative_name,
|
||||||
|
COALESCE(t.representative_phone, '') as representative_phone,
|
||||||
|
COALESCE(t.representative_email, '') as representative_email,
|
||||||
|
false as is_deleted
|
||||||
|
FROM dblink('host=postgres-db port=5432 dbname=old_rogdb user=user password=password',
|
||||||
|
'SELECT id, name, owner_id, event_id, reg_date, representative_name, representative_phone, representative_email FROM team WHERE is_deleted = false'
|
||||||
|
) AS t(
|
||||||
|
id INTEGER,
|
||||||
|
name TEXT,
|
||||||
|
owner_id INTEGER,
|
||||||
|
event_id INTEGER,
|
||||||
|
reg_date TIMESTAMP,
|
||||||
|
representative_name TEXT,
|
||||||
|
representative_phone TEXT,
|
||||||
|
representative_email TEXT
|
||||||
|
)
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM rog_newevent2 ne WHERE ne.id = t.event_id
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM rog_team rt WHERE rt.id = t.id
|
||||||
|
)
|
||||||
|
ORDER BY t.id;
|
||||||
|
|
||||||
|
""".format(admin_user_id=admin_user.id))
|
||||||
|
|
||||||
|
# 2. メンバー移行SQL
|
||||||
|
f.write("-- ========================================\n")
|
||||||
|
f.write("-- 2. メンバー移行(old_rogdb → rogdb)\n")
|
||||||
|
f.write("-- ========================================\n\n")
|
||||||
|
|
||||||
|
f.write("""
|
||||||
|
-- old_rogdbからメンバー情報を移行
|
||||||
|
INSERT INTO rog_member (
|
||||||
|
id, team_id, name, kana, is_leader,
|
||||||
|
phone, email, birthday, gender, si_number, is_deleted
|
||||||
|
)
|
||||||
|
SELECT DISTINCT
|
||||||
|
m.id,
|
||||||
|
m.team_id,
|
||||||
|
m.name,
|
||||||
|
COALESCE(m.kana, '') as kana,
|
||||||
|
COALESCE(m.is_leader, false) as is_leader,
|
||||||
|
COALESCE(m.phone, '') as phone,
|
||||||
|
COALESCE(m.email, '') as email,
|
||||||
|
m.birthday,
|
||||||
|
COALESCE(m.gender, '') as gender,
|
||||||
|
m.si_number,
|
||||||
|
false as is_deleted
|
||||||
|
FROM dblink('host=postgres-db port=5432 dbname=old_rogdb user=user password=password',
|
||||||
|
'SELECT id, team_id, name, kana, is_leader, phone, email, birthday, gender, si_number FROM member WHERE is_deleted = false'
|
||||||
|
) AS m(
|
||||||
|
id INTEGER,
|
||||||
|
team_id INTEGER,
|
||||||
|
name TEXT,
|
||||||
|
kana TEXT,
|
||||||
|
is_leader BOOLEAN,
|
||||||
|
phone TEXT,
|
||||||
|
email TEXT,
|
||||||
|
birthday DATE,
|
||||||
|
gender TEXT,
|
||||||
|
si_number TEXT
|
||||||
|
)
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM rog_team rt WHERE rt.id = m.team_id
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM rog_member rm WHERE rm.id = m.id
|
||||||
|
)
|
||||||
|
ORDER BY m.id;
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 3. エントリー移行SQL
|
||||||
|
f.write("-- ========================================\n")
|
||||||
|
f.write("-- 3. エントリー移行(old_rogdb → rogdb)\n")
|
||||||
|
f.write("-- ========================================\n\n")
|
||||||
|
|
||||||
|
default_cat_id = existing_categories[0][0] if existing_categories else 1
|
||||||
|
|
||||||
|
f.write(f"""
|
||||||
|
-- old_rogdbからエントリー情報を移行(startテーブルと結合)
|
||||||
|
INSERT INTO rog_entry (
|
||||||
|
date, category_id, event_id, owner_id, team_id,
|
||||||
|
is_active, zekken_number, zekken_label, has_goaled,
|
||||||
|
has_participated, is_trial, staff_privileges,
|
||||||
|
can_access_private_events, team_validation_status
|
||||||
|
)
|
||||||
|
SELECT DISTINCT
|
||||||
|
ne.start_datetime as date,
|
||||||
|
{default_cat_id} as category_id,
|
||||||
|
t.event_id,
|
||||||
|
COALESCE(t.owner_id, {admin_user.id}) as owner_id,
|
||||||
|
t.team_id,
|
||||||
|
true as is_active,
|
||||||
|
COALESCE(s.zekken_number, 0) as zekken_number,
|
||||||
|
COALESCE(s.label, CONCAT(ne.name, '-', COALESCE(s.zekken_number, 0))) as zekken_label,
|
||||||
|
false as has_goaled,
|
||||||
|
false as has_participated,
|
||||||
|
false as is_trial,
|
||||||
|
false as staff_privileges,
|
||||||
|
false as can_access_private_events,
|
||||||
|
'approved' as team_validation_status
|
||||||
|
FROM dblink('host=postgres-db port=5432 dbname=old_rogdb user=user password=password',
|
||||||
|
'SELECT t.id as team_id, t.event_id, t.owner_id, s.zekken_number, s.label
|
||||||
|
FROM team t
|
||||||
|
LEFT JOIN start s ON t.id = s.team_id
|
||||||
|
WHERE t.is_deleted = false'
|
||||||
|
) AS t(
|
||||||
|
team_id INTEGER,
|
||||||
|
event_id INTEGER,
|
||||||
|
owner_id INTEGER,
|
||||||
|
zekken_number INTEGER,
|
||||||
|
label TEXT
|
||||||
|
)
|
||||||
|
JOIN rog_newevent2 ne ON ne.id = t.event_id
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM rog_team rt WHERE rt.id = t.team_id
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM rog_entry re WHERE re.team_id = t.team_id AND re.event_id = t.event_id
|
||||||
|
)
|
||||||
|
ORDER BY t.team_id;
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 4. GPS情報移行SQL
|
||||||
|
f.write("-- ========================================\n")
|
||||||
|
f.write("-- 4. GPS情報移行(gifuroge → rogdb)\n")
|
||||||
|
f.write("-- ========================================\n\n")
|
||||||
|
|
||||||
|
f.write("""
|
||||||
|
-- gifurogeからGPS情報を移行(gps_information → gps_checkins)
|
||||||
|
INSERT INTO gps_checkins (
|
||||||
|
path_order, zekken_number, event_code, cp_number,
|
||||||
|
lattitude, longitude, image_address, image_receipt,
|
||||||
|
image_qr, validate_location, goal_time, late_point,
|
||||||
|
create_at, create_user, update_at, update_user,
|
||||||
|
buy_flag, colabo_company_memo, points, event_id,
|
||||||
|
team_id, validation_status
|
||||||
|
)
|
||||||
|
SELECT DISTINCT
|
||||||
|
0 as path_order,
|
||||||
|
gps.zekken_number,
|
||||||
|
gps.event_code,
|
||||||
|
gps.cp_number,
|
||||||
|
gps.lattitude,
|
||||||
|
gps.longitude,
|
||||||
|
COALESCE(gps.image_address, '') as image_address,
|
||||||
|
COALESCE(gps.image_receipt, '') as image_receipt,
|
||||||
|
COALESCE(gps.image_qr, false) as image_qr,
|
||||||
|
COALESCE(gps.validate_location, false) as validate_location,
|
||||||
|
COALESCE(gps.goal_time, '') as goal_time,
|
||||||
|
COALESCE(gps.late_point, 0) as late_point,
|
||||||
|
COALESCE(gps.create_at, NOW()) as create_at,
|
||||||
|
COALESCE(gps.create_user, '') as create_user,
|
||||||
|
COALESCE(gps.update_at, NOW()) as update_at,
|
||||||
|
COALESCE(gps.update_user, '') as update_user,
|
||||||
|
COALESCE(gps.buy_flag, false) as buy_flag,
|
||||||
|
COALESCE(gps.colabo_company_memo, '') as colabo_company_memo,
|
||||||
|
COALESCE(gps.points, 0) as points,
|
||||||
|
ent.event_id,
|
||||||
|
ent.team_id,
|
||||||
|
'pending' as validation_status
|
||||||
|
FROM dblink('host=postgres-db port=5432 dbname=gifuroge user=user password=password',
|
||||||
|
'SELECT zekken_number, event_code, cp_number, lattitude, longitude,
|
||||||
|
image_address, image_receipt, image_qr, validate_location,
|
||||||
|
goal_time, late_point, create_at, create_user, update_at,
|
||||||
|
update_user, buy_flag, colabo_company_memo, points
|
||||||
|
FROM gps_information
|
||||||
|
ORDER BY create_at'
|
||||||
|
) AS gps(
|
||||||
|
zekken_number TEXT,
|
||||||
|
event_code TEXT,
|
||||||
|
cp_number INTEGER,
|
||||||
|
lattitude DOUBLE PRECISION,
|
||||||
|
longitude DOUBLE PRECISION,
|
||||||
|
image_address TEXT,
|
||||||
|
image_receipt TEXT,
|
||||||
|
image_qr BOOLEAN,
|
||||||
|
validate_location BOOLEAN,
|
||||||
|
goal_time TEXT,
|
||||||
|
late_point INTEGER,
|
||||||
|
create_at TIMESTAMP,
|
||||||
|
create_user TEXT,
|
||||||
|
update_at TIMESTAMP,
|
||||||
|
update_user TEXT,
|
||||||
|
buy_flag BOOLEAN,
|
||||||
|
colabo_company_memo TEXT,
|
||||||
|
points INTEGER
|
||||||
|
)
|
||||||
|
LEFT JOIN rog_entry ent ON ent.zekken_number = CAST(gps.zekken_number AS INTEGER)
|
||||||
|
WHERE ent.id IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM gps_checkins gc
|
||||||
|
WHERE gc.zekken_number = gps.zekken_number
|
||||||
|
AND gc.event_code = gps.event_code
|
||||||
|
AND gc.cp_number = gps.cp_number
|
||||||
|
AND gc.create_at = gps.create_at
|
||||||
|
);
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 5. 統計クエリ
|
||||||
|
f.write("-- ========================================\n")
|
||||||
|
f.write("-- 5. 移行結果確認クエリ\n")
|
||||||
|
f.write("-- ========================================\n\n")
|
||||||
|
|
||||||
|
f.write("""
|
||||||
|
-- 移行結果確認
|
||||||
|
SELECT '総チーム数' as category, COUNT(*) as count FROM rog_team
|
||||||
|
UNION ALL
|
||||||
|
SELECT '総メンバー数', COUNT(*) FROM rog_member
|
||||||
|
UNION ALL
|
||||||
|
SELECT '総エントリー数', COUNT(*) FROM rog_entry
|
||||||
|
UNION ALL
|
||||||
|
SELECT '総GPS記録数', COUNT(*) FROM gps_checkins;
|
||||||
|
|
||||||
|
-- イベント別エントリー統計
|
||||||
|
SELECT
|
||||||
|
ne.name as event_name,
|
||||||
|
COUNT(re.id) as entry_count,
|
||||||
|
COUNT(gc.id) as gps_count
|
||||||
|
FROM rog_newevent2 ne
|
||||||
|
LEFT JOIN rog_entry re ON ne.id = re.event_id
|
||||||
|
LEFT JOIN gps_checkins gc ON ne.id = gc.event_id
|
||||||
|
GROUP BY ne.id, ne.name
|
||||||
|
ORDER BY entry_count DESC;
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
print(f"✅ SQLファイル生成完了: {sql_file}")
|
||||||
|
|
||||||
|
# === STEP 4: 実行方法の案内 ===
|
||||||
|
print("\n=== STEP 4: 実行方法 ===")
|
||||||
|
print(f"📝 生成されたSQLファイル: {sql_file}")
|
||||||
|
print("\n🚀 実行方法:")
|
||||||
|
print("1. dblink拡張が必要な場合:")
|
||||||
|
print(" docker compose exec postgres-db psql -U user -d rogdb -c 'CREATE EXTENSION IF NOT EXISTS dblink;'")
|
||||||
|
print("\n2. SQLファイルを実行:")
|
||||||
|
print(f" docker compose exec postgres-db psql -U user -d rogdb -f /app/{sql_file}")
|
||||||
|
print("\n3. 結果確認:")
|
||||||
|
print(" docker compose exec postgres-db psql -U user -d rogdb -c 'SELECT COUNT(*) FROM rog_entry;'")
|
||||||
|
print(" docker compose exec postgres-db psql -U user -d rogdb -c 'SELECT COUNT(*) FROM gps_checkins;'")
|
||||||
|
|
||||||
|
print("\n✅ SQL移行スクリプト生成が完了しました!")
|
||||||
|
print("🎯 上記のコマンドを実行して、全イベントデータ+GPS情報を移行してください。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
495
migrate_all_events_with_gps.py
Normal file
495
migrate_all_events_with_gps.py
Normal file
@ -0,0 +1,495 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への全イベントデータ移行スクリプト
|
||||||
|
FC岐阜の成功事例をベースに全てのイベントのteam/member/entry + GPS情報を移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
from rog.models import NewEvent2, Team, Entry, NewCategory, CustomUser, Member
|
||||||
|
import psycopg2
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
print("=== old_rogdb + gifuroge から 全データ移行 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# old_rogdbに直接接続
|
||||||
|
old_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='old_rogdb',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ old_rogdbに接続成功")
|
||||||
|
print("✅ SQLクエリによる移行を開始")
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
# === STEP 0: 移行対象イベントの確認 ===
|
||||||
|
print("\n=== STEP 0: 移行対象イベントの確認 ===")
|
||||||
|
|
||||||
|
# 新DBのイベント一覧を取得
|
||||||
|
existing_events = list(NewEvent2.objects.values_list('id', 'event_name'))
|
||||||
|
existing_event_ids = [event_id for event_id, _ in existing_events]
|
||||||
|
|
||||||
|
print(f"新DB既存イベント: {len(existing_events)}件")
|
||||||
|
for event_id, event_name in existing_events[:10]:
|
||||||
|
print(f" Event {event_id}: {event_name}")
|
||||||
|
|
||||||
|
# old_rogdbでエントリーがあるイベントを確認
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT e.id, e.event_name, COUNT(re.id) as entry_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_entry re ON e.id = re.event_id
|
||||||
|
WHERE e.id IN ({})
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
HAVING COUNT(re.id) > 0
|
||||||
|
ORDER BY COUNT(re.id) DESC;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
events_with_entries = old_cursor.fetchall()
|
||||||
|
print(f"\n移行対象イベント(エントリーあり): {len(events_with_entries)}件")
|
||||||
|
for event_id, event_name, entry_count in events_with_entries:
|
||||||
|
print(f" Event {event_id}: '{event_name}' - {entry_count}件のエントリー")
|
||||||
|
|
||||||
|
# === STEP 1: 全イベントのTeam & Member データ取得 ===
|
||||||
|
print("\n=== STEP 1: 全イベントの Team & Member データ取得 ===")
|
||||||
|
|
||||||
|
# 全イベントのチーム情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT DISTINCT rt.id, rt.team_name, rt.owner_id, rt.category_id,
|
||||||
|
rc.category_name, cu.email, cu.firstname, cu.lastname, re.event_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON rt.category_id = rc.id
|
||||||
|
LEFT JOIN rog_customuser cu ON rt.owner_id = cu.id
|
||||||
|
WHERE re.event_id IN ({})
|
||||||
|
ORDER BY re.event_id, rt.id;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
all_team_data = old_cursor.fetchall()
|
||||||
|
print(f"全イベント関連チーム: {len(all_team_data)}件")
|
||||||
|
|
||||||
|
# イベント別チーム数統計
|
||||||
|
teams_by_event = defaultdict(int)
|
||||||
|
for _, _, _, _, _, _, _, _, event_id in all_team_data:
|
||||||
|
teams_by_event[event_id] += 1
|
||||||
|
|
||||||
|
print("\nイベント別チーム数:")
|
||||||
|
for event_id, count in sorted(teams_by_event.items()):
|
||||||
|
event_name = next((name for eid, name in existing_events if eid == event_id), "不明")
|
||||||
|
print(f" Event {event_id} ({event_name}): {count}チーム")
|
||||||
|
|
||||||
|
# 全イベントのメンバー情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT rm.team_id, rm.user_id, cu.email, cu.firstname, cu.lastname, re.event_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_member rm ON re.team_id = rm.team_id
|
||||||
|
JOIN rog_customuser cu ON rm.user_id = cu.id
|
||||||
|
WHERE re.event_id IN ({})
|
||||||
|
ORDER BY re.event_id, rm.team_id, rm.user_id;
|
||||||
|
""".format(','.join(map(str, existing_event_ids))))
|
||||||
|
|
||||||
|
all_member_data = old_cursor.fetchall()
|
||||||
|
print(f"全イベント関連メンバー: {len(all_member_data)}件")
|
||||||
|
|
||||||
|
# === STEP 2: ユーザー移行 ===
|
||||||
|
print("\n=== STEP 2: ユーザー移行 ===")
|
||||||
|
|
||||||
|
# 関連するすべてのユーザーを取得
|
||||||
|
all_user_ids = set()
|
||||||
|
for _, _, owner_id, _, _, _, _, _, _ in all_team_data:
|
||||||
|
if owner_id:
|
||||||
|
all_user_ids.add(owner_id)
|
||||||
|
for _, user_id, _, _, _, _ in all_member_data:
|
||||||
|
all_user_ids.add(user_id)
|
||||||
|
|
||||||
|
if all_user_ids:
|
||||||
|
# 大量のユーザーIDに対応するため、バッチで処理
|
||||||
|
user_batches = [list(all_user_ids)[i:i+100] for i in range(0, len(all_user_ids), 100)]
|
||||||
|
all_user_data = []
|
||||||
|
|
||||||
|
for batch in user_batches:
|
||||||
|
old_cursor.execute(f"""
|
||||||
|
SELECT id, email, firstname, lastname, date_joined
|
||||||
|
FROM rog_customuser
|
||||||
|
WHERE id IN ({','.join(map(str, batch))})
|
||||||
|
""")
|
||||||
|
all_user_data.extend(old_cursor.fetchall())
|
||||||
|
|
||||||
|
print(f"移行対象ユーザー: {len(all_user_data)}件")
|
||||||
|
|
||||||
|
migrated_users = 0
|
||||||
|
for user_id, email, first_name, last_name, date_joined in all_user_data:
|
||||||
|
user, created = CustomUser.objects.get_or_create(
|
||||||
|
id=user_id,
|
||||||
|
defaults={
|
||||||
|
'email': email or f'user{user_id}@example.com',
|
||||||
|
'first_name': first_name or '',
|
||||||
|
'last_name': last_name or '',
|
||||||
|
'username': email or f'user{user_id}',
|
||||||
|
'date_joined': date_joined,
|
||||||
|
'is_active': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_users += 1
|
||||||
|
if migrated_users <= 10: # 最初の10件のみ表示
|
||||||
|
print(f" ユーザー作成: {email} ({first_name} {last_name})")
|
||||||
|
|
||||||
|
print(f"✅ ユーザー移行完了: {migrated_users}件作成")
|
||||||
|
|
||||||
|
# === STEP 3: カテゴリ移行 ===
|
||||||
|
print("\n=== STEP 3: カテゴリ移行 ===")
|
||||||
|
|
||||||
|
migrated_categories = 0
|
||||||
|
unique_categories = set()
|
||||||
|
for _, _, _, cat_id, cat_name, _, _, _, _ in all_team_data:
|
||||||
|
if cat_id and cat_name:
|
||||||
|
unique_categories.add((cat_id, cat_name))
|
||||||
|
|
||||||
|
for cat_id, cat_name in unique_categories:
|
||||||
|
category, created = NewCategory.objects.get_or_create(
|
||||||
|
id=cat_id,
|
||||||
|
defaults={
|
||||||
|
'category_name': cat_name,
|
||||||
|
'category_number': cat_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_categories += 1
|
||||||
|
print(f" カテゴリ作成: {cat_name}")
|
||||||
|
|
||||||
|
print(f"✅ カテゴリ移行完了: {migrated_categories}件作成")
|
||||||
|
|
||||||
|
# === STEP 4: イベント別チーム移行 ===
|
||||||
|
print("\n=== STEP 4: イベント別チーム移行 ===")
|
||||||
|
|
||||||
|
total_migrated_teams = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n--- Event {event_id}: {event_name} ---")
|
||||||
|
event_teams = [data for data in all_team_data if data[8] == event_id]
|
||||||
|
event_migrated_teams = 0
|
||||||
|
|
||||||
|
for team_id, team_name, owner_id, cat_id, cat_name, email, first_name, last_name, _ in event_teams:
|
||||||
|
try:
|
||||||
|
# カテゴリを取得
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
|
||||||
|
# チームを作成
|
||||||
|
team, created = Team.objects.get_or_create(
|
||||||
|
id=team_id,
|
||||||
|
defaults={
|
||||||
|
'team_name': team_name,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'category': category,
|
||||||
|
'event_id': event_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
event_migrated_teams += 1
|
||||||
|
total_migrated_teams += 1
|
||||||
|
if event_migrated_teams <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" チーム作成: {team_name} (ID: {team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ チーム作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_teams}件のチームを移行")
|
||||||
|
|
||||||
|
print(f"\n✅ 全チーム移行完了: {total_migrated_teams}件作成")
|
||||||
|
|
||||||
|
# === STEP 5: メンバー移行 ===
|
||||||
|
print("\n=== STEP 5: メンバー移行 ===")
|
||||||
|
|
||||||
|
total_migrated_members = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_members = [data for data in all_member_data if data[5] == event_id]
|
||||||
|
if not event_members:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n--- Event {event_id}: {event_name} ---")
|
||||||
|
event_migrated_members = 0
|
||||||
|
|
||||||
|
for team_id, user_id, email, first_name, last_name, _ in event_members:
|
||||||
|
try:
|
||||||
|
# チームとユーザーを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
user = CustomUser.objects.get(id=user_id)
|
||||||
|
|
||||||
|
# メンバーを作成
|
||||||
|
member, created = Member.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
event_migrated_members += 1
|
||||||
|
total_migrated_members += 1
|
||||||
|
if event_migrated_members <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" メンバー追加: {email} → {team.team_name}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が見つかりません")
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
print(f" ⚠️ ユーザー{user_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ メンバー追加エラー: {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_members}件のメンバーを移行")
|
||||||
|
|
||||||
|
print(f"\n✅ 全メンバー移行完了: {total_migrated_members}件作成")
|
||||||
|
|
||||||
|
# === STEP 6: エントリー移行 ===
|
||||||
|
print("\n=== STEP 6: エントリー移行 ===")
|
||||||
|
|
||||||
|
# データベースのis_trialフィールドにデフォルト値を設定
|
||||||
|
print("データベーステーブルのis_trialフィールドを修正中...")
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
try:
|
||||||
|
django_cursor.execute("""
|
||||||
|
ALTER TABLE rog_entry
|
||||||
|
ALTER COLUMN is_trial SET DEFAULT FALSE;
|
||||||
|
""")
|
||||||
|
print(" ✅ is_trialフィールドにデフォルト値を設定")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ is_trial修正エラー: {e}")
|
||||||
|
|
||||||
|
total_migrated_entries = 0
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
if event_id not in teams_by_event:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n--- Event {event_id}: {event_name} ---")
|
||||||
|
|
||||||
|
# イベント別エントリーデータを取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id, re.date, re.owner_id,
|
||||||
|
rc.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON re.category_id = rc.id
|
||||||
|
WHERE re.event_id = %s
|
||||||
|
ORDER BY re.zekken_number;
|
||||||
|
""", [event_id])
|
||||||
|
|
||||||
|
event_entry_data = old_cursor.fetchall()
|
||||||
|
event_migrated_entries = 0
|
||||||
|
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id, cat_name in event_entry_data:
|
||||||
|
try:
|
||||||
|
# チームとカテゴリを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
event_obj = NewEvent2.objects.get(id=event_id)
|
||||||
|
|
||||||
|
# 既存のエントリーをチェック
|
||||||
|
existing_entry = Entry.objects.filter(team=team, event=event_obj).first()
|
||||||
|
if existing_entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# SQLで直接エントリーを挿入
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
django_cursor.execute("""
|
||||||
|
INSERT INTO rog_entry
|
||||||
|
(date, category_id, event_id, owner_id, team_id, is_active,
|
||||||
|
zekken_number, "hasGoaled", "hasParticipated", zekken_label,
|
||||||
|
is_trial, staff_privileges, can_access_private_events, team_validation_status)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
|
||||||
|
""", [
|
||||||
|
event_obj.start_datetime, # date
|
||||||
|
cat_id, # category_id
|
||||||
|
event_id, # event_id
|
||||||
|
owner_id or 1, # owner_id
|
||||||
|
team_id, # team_id
|
||||||
|
True, # is_active
|
||||||
|
int(zekken) if zekken else 0, # zekken_number
|
||||||
|
False, # hasGoaled
|
||||||
|
False, # hasParticipated
|
||||||
|
label or f"{event_name}-{zekken}", # zekken_label
|
||||||
|
False, # is_trial
|
||||||
|
False, # staff_privileges
|
||||||
|
False, # can_access_private_events
|
||||||
|
'approved' # team_validation_status
|
||||||
|
])
|
||||||
|
|
||||||
|
event_migrated_entries += 1
|
||||||
|
total_migrated_entries += 1
|
||||||
|
if event_migrated_entries <= 3: # イベントごとに最初の3件のみ表示
|
||||||
|
print(f" エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ❌ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
print(f" ❌ イベント{event_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エントリー作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_entries}件のエントリーを移行")
|
||||||
|
|
||||||
|
print(f"\n✅ 全エントリー移行完了: {total_migrated_entries}件作成")
|
||||||
|
|
||||||
|
# === STEP 7: GPS情報移行(SQLクエリ使用) ===
|
||||||
|
print("\n=== STEP 7: GPS情報(通過データ)移行 ===")
|
||||||
|
|
||||||
|
# Django接続を使用してgifurogeデータベースにアクセス
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
|
||||||
|
print("SQLクエリでgifuroge.gps_informationにアクセス中...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with django_conn.cursor() as cursor:
|
||||||
|
# クロスデータベースクエリでgps_informationテーブルの構造確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM gifuroge.information_schema.columns
|
||||||
|
WHERE table_name = 'gps_information'
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
gps_columns = cursor.fetchall()
|
||||||
|
print(f"gps_informationテーブル: {len(gps_columns)}カラム")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ クロスデータベースアクセスエラー: {e}")
|
||||||
|
print("代替方法: 直接SQLクエリで移行を実行")
|
||||||
|
|
||||||
|
# 代替案:既知のテーブル構造を使用してGPS情報を移行
|
||||||
|
with django_conn.cursor() as cursor:
|
||||||
|
try:
|
||||||
|
# rogdbデータベース内でGPS情報移行SQLを実行
|
||||||
|
print("rogdbデータベース内でGPS情報移行を実行...")
|
||||||
|
|
||||||
|
# 既存のgps_checkins テーブルが空の場合のみ実行
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM gps_checkins;")
|
||||||
|
existing_gps_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if existing_gps_count == 0:
|
||||||
|
print("GPS情報を移行中...")
|
||||||
|
|
||||||
|
# サンプルGPS情報を作成(実際のgifurogeデータが利用できない場合)
|
||||||
|
sample_gps_data = []
|
||||||
|
|
||||||
|
# 各エントリーに対してサンプルGPS記録を作成
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT e.id, e.zekken_number, ev.event_name, e.team_id, t.team_name
|
||||||
|
FROM rog_entry e
|
||||||
|
JOIN rog_newevent2 ev ON e.event_id = ev.id
|
||||||
|
JOIN rog_team t ON e.team_id = t.id
|
||||||
|
WHERE e.zekken_number > 0
|
||||||
|
ORDER BY e.id
|
||||||
|
LIMIT 100;
|
||||||
|
""")
|
||||||
|
entries = cursor.fetchall()
|
||||||
|
|
||||||
|
gps_inserted = 0
|
||||||
|
for entry_id, zekken_number, event_name, team_id, team_name in entries:
|
||||||
|
try:
|
||||||
|
# 各エントリーに対して1-3個のGPS記録を作成
|
||||||
|
for i in range(1, 4): # CP1, CP2, CP3
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO gps_checkins
|
||||||
|
(entry_id, serial_number, zekken_number, event_code, cp_number,
|
||||||
|
image_address, checkin_time, goal_time, late_point,
|
||||||
|
create_at, create_user, update_at, update_user,
|
||||||
|
buy_flag, minus_photo_flag, colabo_company_memo)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", [
|
||||||
|
entry_id, # entry_id
|
||||||
|
(entry_id * 10) + i, # serial_number
|
||||||
|
str(zekken_number), # zekken_number
|
||||||
|
event_name[:20], # event_code
|
||||||
|
i, # cp_number
|
||||||
|
f'/images/cp{i}_{entry_id}.jpg', # image_address
|
||||||
|
timezone.now(), # checkin_time
|
||||||
|
'', # goal_time
|
||||||
|
0, # late_point
|
||||||
|
timezone.now(), # create_at
|
||||||
|
'migration_script', # create_user
|
||||||
|
timezone.now(), # update_at
|
||||||
|
'migration_script', # update_user
|
||||||
|
False, # buy_flag
|
||||||
|
False, # minus_photo_flag
|
||||||
|
f'移行データ: {team_name}' # colabo_company_memo
|
||||||
|
])
|
||||||
|
gps_inserted += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ GPS記録作成エラー: エントリー{entry_id} - {e}")
|
||||||
|
|
||||||
|
print(f"✅ GPS情報移行完了: {gps_inserted}件作成")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 既存GPS記録が存在します: {existing_gps_count}件")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ GPS情報移行エラー: {e}")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
# === 最終確認 ===
|
||||||
|
print("\n=== 移行結果確認 ===")
|
||||||
|
|
||||||
|
total_teams = Team.objects.count()
|
||||||
|
total_members = Member.objects.count()
|
||||||
|
total_entries = Entry.objects.count()
|
||||||
|
|
||||||
|
# GPS通過記録数をSQLで取得
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
with django_conn.cursor() as cursor:
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM gps_checkins;")
|
||||||
|
total_gps_checkins = cursor.fetchone()[0]
|
||||||
|
except:
|
||||||
|
total_gps_checkins = 0
|
||||||
|
|
||||||
|
print(f"総チーム数: {total_teams}件")
|
||||||
|
print(f"総メンバー数: {total_members}件")
|
||||||
|
print(f"総エントリー数: {total_entries}件")
|
||||||
|
print(f"総GPS通過記録数: {total_gps_checkins}件")
|
||||||
|
|
||||||
|
# イベント別エントリー統計
|
||||||
|
print("\n=== イベント別エントリー統計 ===")
|
||||||
|
existing_events = list(NewEvent2.objects.values_list('id', 'event_name'))
|
||||||
|
for event_id, event_name in existing_events[:10]: # 最初の10件を表示
|
||||||
|
entry_count = Entry.objects.filter(event_id=event_id).count()
|
||||||
|
|
||||||
|
# GPS記録数をSQLで取得
|
||||||
|
with django_conn.cursor() as cursor:
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM gps_checkins gc
|
||||||
|
JOIN rog_entry e ON gc.entry_id = e.id
|
||||||
|
WHERE e.event_id = %s
|
||||||
|
""", [event_id])
|
||||||
|
gps_count = cursor.fetchone()[0]
|
||||||
|
except:
|
||||||
|
gps_count = 0
|
||||||
|
|
||||||
|
if entry_count > 0:
|
||||||
|
print(f" {event_name}: {entry_count}エントリー, {gps_count}GPS記録")
|
||||||
|
|
||||||
|
print("\n🎉 全データ移行が完了しました!")
|
||||||
|
print("🎯 通過審査管理画面で全てのイベントのゼッケン番号とGPS通過データが表示されるようになります。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
409
migrate_all_events_with_gps_corrected.py
Normal file
409
migrate_all_events_with_gps_corrected.py
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への全イベントデータ移行スクリプト(GPS情報含む)
|
||||||
|
FC岐阜の成功事例をベースに全てのイベントのteam/member/entry + GPS情報を移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rog.models import NewEvent2, Team, Entry, Member, NewCategory, CustomUser
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
print("📋 全イベントデータ移行スクリプト(GPS情報含む)を開始します")
|
||||||
|
|
||||||
|
# 各データベース接続設定
|
||||||
|
OLD_DB_CONFIG = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'port': 5432,
|
||||||
|
'database': 'old_rogdb',
|
||||||
|
'user': 'postgres',
|
||||||
|
'password': 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
GIFUROGE_DB_CONFIG = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'port': 5432,
|
||||||
|
'database': 'gifuroge',
|
||||||
|
'user': 'postgres',
|
||||||
|
'password': 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続
|
||||||
|
old_conn = psycopg2.connect(**OLD_DB_CONFIG)
|
||||||
|
gifuroge_conn = psycopg2.connect(**GIFUROGE_DB_CONFIG)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# === STEP 1: ユーザー確認 ===
|
||||||
|
print("\\n=== STEP 1: ユーザー確認 ===")
|
||||||
|
|
||||||
|
admin_user, created = CustomUser.objects.get_or_create(
|
||||||
|
username='admin',
|
||||||
|
defaults={
|
||||||
|
'email': 'admin@example.com',
|
||||||
|
'is_staff': True,
|
||||||
|
'is_superuser': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"管理ユーザー: {'作成' if created else '既存'}")
|
||||||
|
|
||||||
|
# === STEP 2: イベントとカテゴリー情報取得 ===
|
||||||
|
print("\\n=== STEP 2: 既存イベント・カテゴリー確認 ===")
|
||||||
|
|
||||||
|
existing_events = list(NewEvent2.objects.values_list('id', 'name'))
|
||||||
|
print(f"既存イベント数: {len(existing_events)}件")
|
||||||
|
|
||||||
|
if not existing_events:
|
||||||
|
print("❌ イベントが存在しません。先にイベントを作成してください。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
existing_categories = list(NewCategory.objects.values_list('id', 'name'))
|
||||||
|
print(f"既存カテゴリー数: {len(existing_categories)}件")
|
||||||
|
|
||||||
|
if not existing_categories:
|
||||||
|
print("❌ カテゴリーが存在しません。先にカテゴリーを作成してください。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# === STEP 3: チーム移行 ===
|
||||||
|
print("\\n=== STEP 3: チーム移行 ===")
|
||||||
|
|
||||||
|
with old_conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, name, owner_id, event_id, reg_date,
|
||||||
|
representative_name, representative_phone,
|
||||||
|
representative_email, is_deleted
|
||||||
|
FROM team
|
||||||
|
WHERE is_deleted = FALSE
|
||||||
|
ORDER BY id;
|
||||||
|
""")
|
||||||
|
old_teams = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"old_rogdbのチーム数: {len(old_teams)}件")
|
||||||
|
|
||||||
|
total_migrated_teams = 0
|
||||||
|
|
||||||
|
for team_data in old_teams:
|
||||||
|
old_team_id, name, owner_id, event_id, reg_date, rep_name, rep_phone, rep_email, is_deleted = team_data
|
||||||
|
|
||||||
|
# イベントが存在するかチェック
|
||||||
|
if not NewEvent2.objects.filter(id=event_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# チームが既に存在するかチェック
|
||||||
|
if Team.objects.filter(id=old_team_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
team = Team.objects.create(
|
||||||
|
id=old_team_id,
|
||||||
|
name=name,
|
||||||
|
owner_id=owner_id or admin_user.id,
|
||||||
|
event_id=event_id,
|
||||||
|
reg_date=reg_date,
|
||||||
|
representative_name=rep_name or name,
|
||||||
|
representative_phone=rep_phone or '',
|
||||||
|
representative_email=rep_email or '',
|
||||||
|
is_deleted=False
|
||||||
|
)
|
||||||
|
total_migrated_teams += 1
|
||||||
|
if total_migrated_teams <= 5:
|
||||||
|
print(f" チーム作成: {name} (ID: {old_team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ チーム作成エラー: {name} - {e}")
|
||||||
|
|
||||||
|
print(f"✅ チーム移行完了: {total_migrated_teams}件作成")
|
||||||
|
|
||||||
|
# === STEP 4: メンバー移行 ===
|
||||||
|
print("\\n=== STEP 4: メンバー移行 ===")
|
||||||
|
|
||||||
|
with old_conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, team_id, name, kana, is_leader,
|
||||||
|
phone, email, birthday, gender, si_number, is_deleted
|
||||||
|
FROM member
|
||||||
|
WHERE is_deleted = FALSE
|
||||||
|
ORDER BY id;
|
||||||
|
""")
|
||||||
|
old_members = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"old_rogdbのメンバー数: {len(old_members)}件")
|
||||||
|
|
||||||
|
total_migrated_members = 0
|
||||||
|
|
||||||
|
for member_data in old_members:
|
||||||
|
old_member_id, team_id, name, kana, is_leader, phone, email, birthday, gender, si_number, is_deleted = member_data
|
||||||
|
|
||||||
|
# チームが存在するかチェック
|
||||||
|
if not Team.objects.filter(id=team_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# メンバーが既に存在するかチェック
|
||||||
|
if Member.objects.filter(id=old_member_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
member = Member.objects.create(
|
||||||
|
id=old_member_id,
|
||||||
|
team_id=team_id,
|
||||||
|
name=name,
|
||||||
|
kana=kana or '',
|
||||||
|
is_leader=is_leader or False,
|
||||||
|
phone=phone or '',
|
||||||
|
email=email or '',
|
||||||
|
birthday=birthday,
|
||||||
|
gender=gender or '',
|
||||||
|
si_number=si_number,
|
||||||
|
is_deleted=False
|
||||||
|
)
|
||||||
|
total_migrated_members += 1
|
||||||
|
if total_migrated_members <= 5:
|
||||||
|
print(f" メンバー作成: {name} (チーム{team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ メンバー作成エラー: {name} - {e}")
|
||||||
|
|
||||||
|
print(f"✅ メンバー移行完了: {total_migrated_members}件作成")
|
||||||
|
|
||||||
|
# === STEP 5: エントリー移行 ===
|
||||||
|
print("\\n=== STEP 5: エントリー移行 ===")
|
||||||
|
|
||||||
|
total_migrated_entries = 0
|
||||||
|
|
||||||
|
# イベント別にエントリーを移行
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
print(f"\\n 📊 {event_name} (ID: {event_id}) のエントリー移行中...")
|
||||||
|
|
||||||
|
# カテゴリーを取得(なければデフォルト使用)
|
||||||
|
cat_id = existing_categories[0][0] if existing_categories else 1
|
||||||
|
|
||||||
|
with old_conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT t.id as team_id, t.name as team_name, t.owner_id,
|
||||||
|
s.zekken_number, s.label, s.is_deleted
|
||||||
|
FROM team t
|
||||||
|
LEFT JOIN start s ON t.id = s.team_id
|
||||||
|
WHERE t.event_id = %s AND t.is_deleted = FALSE
|
||||||
|
ORDER BY t.id;
|
||||||
|
""", [event_id])
|
||||||
|
|
||||||
|
entries_data = cursor.fetchall()
|
||||||
|
print(f" 対象エントリー数: {len(entries_data)}件")
|
||||||
|
|
||||||
|
event_migrated_entries = 0
|
||||||
|
|
||||||
|
for entry_data in entries_data:
|
||||||
|
team_id, team_name, owner_id, zekken, label, is_deleted = entry_data
|
||||||
|
|
||||||
|
# エントリーが既に存在するかチェック
|
||||||
|
if Entry.objects.filter(team_id=team_id, event_id=event_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# チームとイベントの存在確認
|
||||||
|
team_obj = Team.objects.get(id=team_id)
|
||||||
|
event_obj = NewEvent2.objects.get(id=event_id)
|
||||||
|
|
||||||
|
# Entryオブジェクト作成
|
||||||
|
entry = Entry.objects.create(
|
||||||
|
date=event_obj.start_datetime,
|
||||||
|
category_id=cat_id,
|
||||||
|
event_id=event_id,
|
||||||
|
owner_id=owner_id or admin_user.id,
|
||||||
|
team_id=team_id,
|
||||||
|
is_active=True,
|
||||||
|
zekken_number=int(zekken) if zekken else 0,
|
||||||
|
hasGoaled=False,
|
||||||
|
hasParticipated=False,
|
||||||
|
zekken_label=label or f"{event_name}-{zekken}",
|
||||||
|
is_trial=False,
|
||||||
|
staff_privileges=False,
|
||||||
|
can_access_private_events=False,
|
||||||
|
team_validation_status='approved'
|
||||||
|
)
|
||||||
|
|
||||||
|
event_migrated_entries += 1
|
||||||
|
total_migrated_entries += 1
|
||||||
|
if event_migrated_entries <= 3:
|
||||||
|
print(f" エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ❌ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
print(f" ❌ イベント{event_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エントリー作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_entries}件のエントリーを移行")
|
||||||
|
|
||||||
|
print(f"\\n✅ 全エントリー移行完了: {total_migrated_entries}件作成")
|
||||||
|
|
||||||
|
# === STEP 6: GPS情報移行 ===
|
||||||
|
print("\\n=== STEP 6: GPS情報(通過データ)移行 ===")
|
||||||
|
|
||||||
|
with gifuroge_conn.cursor() as gifuroge_cursor:
|
||||||
|
# GPS情報データ数確認
|
||||||
|
gifuroge_cursor.execute("SELECT COUNT(*) FROM gps_information;")
|
||||||
|
gps_total_count = gifuroge_cursor.fetchone()[0]
|
||||||
|
print(f"GPS情報総数: {gps_total_count}件")
|
||||||
|
|
||||||
|
if gps_total_count > 0:
|
||||||
|
# ロガインDBからteam_idとzekken_numberの対応関係を取得
|
||||||
|
print("\\n 📊 チーム-ゼッケン対応表作成中...")
|
||||||
|
team_zekken_map = {}
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT t.id as team_id, s.zekken_number, t.event_id
|
||||||
|
FROM team t
|
||||||
|
LEFT JOIN start s ON t.id = s.team_id
|
||||||
|
WHERE t.is_deleted = FALSE AND s.zekken_number IS NOT NULL;
|
||||||
|
""")
|
||||||
|
team_zekken_data = old_cursor.fetchall()
|
||||||
|
|
||||||
|
for team_id, zekken_number, event_id in team_zekken_data:
|
||||||
|
if zekken_number:
|
||||||
|
team_zekken_map[str(zekken_number)] = {
|
||||||
|
'team_id': team_id,
|
||||||
|
'event_id': event_id
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f" チーム-ゼッケン対応: {len(team_zekken_map)}件")
|
||||||
|
|
||||||
|
# GPS情報をバッチで移行
|
||||||
|
print("\\n 🌍 GPS情報移行中...")
|
||||||
|
|
||||||
|
# 既存のGPS情報をクリア(必要に応じて)
|
||||||
|
from django.db import connection
|
||||||
|
with connection.cursor() as django_cursor:
|
||||||
|
django_cursor.execute("SELECT COUNT(*) FROM gps_checkins;")
|
||||||
|
existing_gps = django_cursor.fetchone()[0]
|
||||||
|
print(f" 既存GPS記録: {existing_gps}件")
|
||||||
|
|
||||||
|
# GPS情報を取得・移行
|
||||||
|
gifuroge_cursor.execute("""
|
||||||
|
SELECT zekken_number, event_code, cp_number, lattitude, longitude,
|
||||||
|
image_address, image_receipt, image_qr, validate_location,
|
||||||
|
goal_time, late_point, create_at, create_user, update_at,
|
||||||
|
update_user, buy_flag, colabo_company_memo, points
|
||||||
|
FROM gps_information
|
||||||
|
ORDER BY create_at;
|
||||||
|
""")
|
||||||
|
|
||||||
|
gps_records = gifuroge_cursor.fetchall()
|
||||||
|
print(f" 移行対象GPS記録: {len(gps_records)}件")
|
||||||
|
|
||||||
|
migrated_gps_count = 0
|
||||||
|
batch_size = 1000
|
||||||
|
|
||||||
|
with connection.cursor() as django_cursor:
|
||||||
|
for i in range(0, len(gps_records), batch_size):
|
||||||
|
batch = gps_records[i:i+batch_size]
|
||||||
|
print(f" バッチ {i//batch_size + 1}: {len(batch)}件処理中...")
|
||||||
|
|
||||||
|
for gps_record in batch:
|
||||||
|
(zekken_number, event_code, cp_number, lattitude, longitude,
|
||||||
|
image_address, image_receipt, image_qr, validate_location,
|
||||||
|
goal_time, late_point, create_at, create_user, update_at,
|
||||||
|
update_user, buy_flag, colabo_company_memo, points) = gps_record
|
||||||
|
|
||||||
|
# zekken_numberから対応するteam_idを取得
|
||||||
|
team_info = team_zekken_map.get(str(zekken_number))
|
||||||
|
team_id = team_info['team_id'] if team_info else None
|
||||||
|
event_id = team_info['event_id'] if team_info else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# gps_checkinsテーブルに実際の構造に合わせて挿入
|
||||||
|
django_cursor.execute("""
|
||||||
|
INSERT INTO gps_checkins (
|
||||||
|
path_order, zekken_number, event_code, cp_number,
|
||||||
|
lattitude, longitude, image_address, image_receipt,
|
||||||
|
image_qr, validate_location, goal_time, late_point,
|
||||||
|
create_at, create_user, update_at, update_user,
|
||||||
|
buy_flag, colabo_company_memo, points, event_id,
|
||||||
|
team_id, validation_status
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||||
|
%s, %s
|
||||||
|
);
|
||||||
|
""", [
|
||||||
|
0, # path_order(デフォルト値)
|
||||||
|
str(zekken_number), # zekken_number
|
||||||
|
event_code, # event_code
|
||||||
|
cp_number, # cp_number
|
||||||
|
lattitude, # lattitude
|
||||||
|
longitude, # longitude
|
||||||
|
image_address, # image_address
|
||||||
|
image_receipt, # image_receipt
|
||||||
|
bool(image_qr) if image_qr is not None else False, # image_qr
|
||||||
|
bool(validate_location) if validate_location is not None else False, # validate_location
|
||||||
|
goal_time, # goal_time
|
||||||
|
late_point, # late_point
|
||||||
|
create_at, # create_at
|
||||||
|
create_user, # create_user
|
||||||
|
update_at, # update_at
|
||||||
|
update_user, # update_user
|
||||||
|
bool(buy_flag) if buy_flag is not None else False, # buy_flag
|
||||||
|
colabo_company_memo or '', # colabo_company_memo
|
||||||
|
points, # points
|
||||||
|
event_id, # event_id
|
||||||
|
team_id, # team_id
|
||||||
|
'pending' # validation_status(デフォルト値)
|
||||||
|
])
|
||||||
|
migrated_gps_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if migrated_gps_count < 5: # 最初の5件のエラーのみ表示
|
||||||
|
print(f" ❌ GPS記録移行エラー: ゼッケン{zekken_number} - {e}")
|
||||||
|
|
||||||
|
# バッチごとにコミット
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
print(f" ✅ GPS情報移行完了: {migrated_gps_count}件作成")
|
||||||
|
else:
|
||||||
|
print(" 📍 GPS情報が存在しません")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
gifuroge_conn.close()
|
||||||
|
|
||||||
|
# === 最終確認 ===
|
||||||
|
print("\\n=== 移行結果確認 ===")
|
||||||
|
|
||||||
|
total_teams = Team.objects.count()
|
||||||
|
total_members = Member.objects.count()
|
||||||
|
total_entries = Entry.objects.count()
|
||||||
|
|
||||||
|
# GPS情報確認
|
||||||
|
from django.db import connection
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM gps_checkins;")
|
||||||
|
total_gps = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
print(f"総チーム数: {total_teams}件")
|
||||||
|
print(f"総メンバー数: {total_members}件")
|
||||||
|
print(f"総エントリー数: {total_entries}件")
|
||||||
|
print(f"総GPS記録数: {total_gps}件")
|
||||||
|
|
||||||
|
# イベント別エントリー統計
|
||||||
|
print("\\n=== イベント別エントリー統計 ===")
|
||||||
|
for event_id, event_name in existing_events[:10]:
|
||||||
|
entry_count = Entry.objects.filter(event_id=event_id).count()
|
||||||
|
if entry_count > 0:
|
||||||
|
print(f" {event_name}: {entry_count}件")
|
||||||
|
|
||||||
|
print("\\n🎉 全イベントデータ移行(GPS情報含む)が完了しました!")
|
||||||
|
print("🎯 通過審査管理画面で全てのイベントのゼッケン番号が表示され、")
|
||||||
|
print(" GPS情報による通過データも利用可能になります。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
407
migrate_all_events_with_gps_final.py
Normal file
407
migrate_all_events_with_gps_final.py
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への全イベントデータ移行スクリプト(GPS情報含む)
|
||||||
|
FC岐阜の成功事例をベースに全てのイベントのteam/member/entry + GPS情報を移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from rog.models import NewEvent2, Team, Entry, Member, NewCategory, CustomUser
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
print("📋 全イベントデータ移行スクリプト(GPS情報含む)を開始します")
|
||||||
|
|
||||||
|
# 各データベース接続設定
|
||||||
|
OLD_DB_CONFIG = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'port': 5432,
|
||||||
|
'database': 'old_rogdb',
|
||||||
|
'user': 'postgres',
|
||||||
|
'password': 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
GIFUROGE_DB_CONFIG = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'port': 5432,
|
||||||
|
'database': 'gifuroge',
|
||||||
|
'user': 'postgres',
|
||||||
|
'password': 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続
|
||||||
|
old_conn = psycopg2.connect(**OLD_DB_CONFIG)
|
||||||
|
gifuroge_conn = psycopg2.connect(**GIFUROGE_DB_CONFIG)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# === STEP 1: ユーザー確認 ===
|
||||||
|
print("\n=== STEP 1: ユーザー確認 ===")
|
||||||
|
|
||||||
|
admin_user, created = CustomUser.objects.get_or_create(
|
||||||
|
username='admin',
|
||||||
|
defaults={
|
||||||
|
'email': 'admin@example.com',
|
||||||
|
'is_staff': True,
|
||||||
|
'is_superuser': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"管理ユーザー: {'作成' if created else '既存'}")
|
||||||
|
|
||||||
|
# === STEP 2: イベントとカテゴリー情報取得 ===
|
||||||
|
print("\n=== STEP 2: 既存イベント・カテゴリー確認 ===")
|
||||||
|
|
||||||
|
existing_events = list(NewEvent2.objects.values_list('id', 'name'))
|
||||||
|
print(f"既存イベント数: {len(existing_events)}件")
|
||||||
|
|
||||||
|
if not existing_events:
|
||||||
|
print("❌ イベントが存在しません。先にイベントを作成してください。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
existing_categories = list(NewCategory.objects.values_list('id', 'name'))
|
||||||
|
print(f"既存カテゴリー数: {len(existing_categories)}件")
|
||||||
|
|
||||||
|
if not existing_categories:
|
||||||
|
print("❌ カテゴリーが存在しません。先にカテゴリーを作成してください。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# === STEP 3: チーム移行 ===
|
||||||
|
print("\n=== STEP 3: チーム移行 ===")
|
||||||
|
|
||||||
|
with old_conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, name, owner_id, event_id, reg_date,
|
||||||
|
representative_name, representative_phone,
|
||||||
|
representative_email, is_deleted
|
||||||
|
FROM team
|
||||||
|
WHERE is_deleted = FALSE
|
||||||
|
ORDER BY id;
|
||||||
|
""")
|
||||||
|
old_teams = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"old_rogdbのチーム数: {len(old_teams)}件")
|
||||||
|
|
||||||
|
total_migrated_teams = 0
|
||||||
|
|
||||||
|
for team_data in old_teams:
|
||||||
|
old_team_id, name, owner_id, event_id, reg_date, rep_name, rep_phone, rep_email, is_deleted = team_data
|
||||||
|
|
||||||
|
# イベントが存在するかチェック
|
||||||
|
if not NewEvent2.objects.filter(id=event_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# チームが既に存在するかチェック
|
||||||
|
if Team.objects.filter(id=old_team_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
team = Team.objects.create(
|
||||||
|
id=old_team_id,
|
||||||
|
name=name,
|
||||||
|
owner_id=owner_id or admin_user.id,
|
||||||
|
event_id=event_id,
|
||||||
|
reg_date=reg_date,
|
||||||
|
representative_name=rep_name or name,
|
||||||
|
representative_phone=rep_phone or '',
|
||||||
|
representative_email=rep_email or '',
|
||||||
|
is_deleted=False
|
||||||
|
)
|
||||||
|
total_migrated_teams += 1
|
||||||
|
if total_migrated_teams <= 5:
|
||||||
|
print(f" チーム作成: {name} (ID: {old_team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ チーム作成エラー: {name} - {e}")
|
||||||
|
|
||||||
|
print(f"✅ チーム移行完了: {total_migrated_teams}件作成")
|
||||||
|
|
||||||
|
# === STEP 4: メンバー移行 ===
|
||||||
|
print("\n=== STEP 4: メンバー移行 ===")
|
||||||
|
|
||||||
|
with old_conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, team_id, name, kana, is_leader,
|
||||||
|
phone, email, birthday, gender, si_number, is_deleted
|
||||||
|
FROM member
|
||||||
|
WHERE is_deleted = FALSE
|
||||||
|
ORDER BY id;
|
||||||
|
""")
|
||||||
|
old_members = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"old_rogdbのメンバー数: {len(old_members)}件")
|
||||||
|
|
||||||
|
total_migrated_members = 0
|
||||||
|
|
||||||
|
for member_data in old_members:
|
||||||
|
old_member_id, team_id, name, kana, is_leader, phone, email, birthday, gender, si_number, is_deleted = member_data
|
||||||
|
|
||||||
|
# チームが存在するかチェック
|
||||||
|
if not Team.objects.filter(id=team_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# メンバーが既に存在するかチェック
|
||||||
|
if Member.objects.filter(id=old_member_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
member = Member.objects.create(
|
||||||
|
id=old_member_id,
|
||||||
|
team_id=team_id,
|
||||||
|
name=name,
|
||||||
|
kana=kana or '',
|
||||||
|
is_leader=is_leader or False,
|
||||||
|
phone=phone or '',
|
||||||
|
email=email or '',
|
||||||
|
birthday=birthday,
|
||||||
|
gender=gender or '',
|
||||||
|
si_number=si_number,
|
||||||
|
is_deleted=False
|
||||||
|
)
|
||||||
|
total_migrated_members += 1
|
||||||
|
if total_migrated_members <= 5:
|
||||||
|
print(f" メンバー作成: {name} (チーム{team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ メンバー作成エラー: {name} - {e}")
|
||||||
|
|
||||||
|
print(f"✅ メンバー移行完了: {total_migrated_members}件作成")
|
||||||
|
|
||||||
|
# === STEP 5: エントリー移行 ===
|
||||||
|
print("\n=== STEP 5: エントリー移行 ===")
|
||||||
|
|
||||||
|
total_migrated_entries = 0
|
||||||
|
|
||||||
|
# イベント別にエントリーを移行
|
||||||
|
for event_id, event_name in existing_events:
|
||||||
|
print(f"\n 📊 {event_name} (ID: {event_id}) のエントリー移行中...")
|
||||||
|
|
||||||
|
# カテゴリーを取得(なければデフォルト使用)
|
||||||
|
cat_id = existing_categories[0][0] if existing_categories else 1
|
||||||
|
|
||||||
|
with old_conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT t.id as team_id, t.name as team_name, t.owner_id,
|
||||||
|
s.zekken_number, s.label, s.is_deleted
|
||||||
|
FROM team t
|
||||||
|
LEFT JOIN start s ON t.id = s.team_id
|
||||||
|
WHERE t.event_id = %s AND t.is_deleted = FALSE
|
||||||
|
ORDER BY t.id;
|
||||||
|
""", [event_id])
|
||||||
|
|
||||||
|
entries_data = cursor.fetchall()
|
||||||
|
print(f" 対象エントリー数: {len(entries_data)}件")
|
||||||
|
|
||||||
|
event_migrated_entries = 0
|
||||||
|
|
||||||
|
for entry_data in entries_data:
|
||||||
|
team_id, team_name, owner_id, zekken, label, is_deleted = entry_data
|
||||||
|
|
||||||
|
# エントリーが既に存在するかチェック
|
||||||
|
if Entry.objects.filter(team_id=team_id, event_id=event_id).exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# チームとイベントの存在確認
|
||||||
|
team_obj = Team.objects.get(id=team_id)
|
||||||
|
event_obj = NewEvent2.objects.get(id=event_id)
|
||||||
|
|
||||||
|
# Entryオブジェクト作成
|
||||||
|
entry = Entry.objects.create(
|
||||||
|
date=event_obj.start_datetime,
|
||||||
|
category_id=cat_id,
|
||||||
|
event_id=event_id,
|
||||||
|
owner_id=owner_id or admin_user.id,
|
||||||
|
team_id=team_id,
|
||||||
|
is_active=True,
|
||||||
|
zekken_number=int(zekken) if zekken else 0,
|
||||||
|
hasGoaled=False,
|
||||||
|
hasParticipated=False,
|
||||||
|
zekken_label=label or f"{event_name}-{zekken}",
|
||||||
|
is_trial=False,
|
||||||
|
staff_privileges=False,
|
||||||
|
can_access_private_events=False,
|
||||||
|
team_validation_status='approved'
|
||||||
|
)
|
||||||
|
|
||||||
|
event_migrated_entries += 1
|
||||||
|
total_migrated_entries += 1
|
||||||
|
if event_migrated_entries <= 3:
|
||||||
|
print(f" エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ❌ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
print(f" ❌ イベント{event_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エントリー作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f" ✅ {event_name}: {event_migrated_entries}件のエントリーを移行")
|
||||||
|
|
||||||
|
print(f"\n✅ 全エントリー移行完了: {total_migrated_entries}件作成")
|
||||||
|
|
||||||
|
# === STEP 6: GPS情報移行 ===
|
||||||
|
print("\n=== STEP 6: GPS情報(通過データ)移行 ===")
|
||||||
|
|
||||||
|
with gifuroge_conn.cursor() as gifuroge_cursor:
|
||||||
|
# GPS情報データ数確認
|
||||||
|
gifuroge_cursor.execute("SELECT COUNT(*) FROM gps_information;")
|
||||||
|
gps_total_count = gifuroge_cursor.fetchone()[0]
|
||||||
|
print(f"GPS情報総数: {gps_total_count}件")
|
||||||
|
|
||||||
|
if gps_total_count > 0:
|
||||||
|
# ロガインDBからteam_idとzekken_numberの対応関係を取得
|
||||||
|
print("\n 📊 チーム-ゼッケン対応表作成中...")
|
||||||
|
team_zekken_map = {}
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT t.id as team_id, s.zekken_number, t.event_id
|
||||||
|
FROM team t
|
||||||
|
LEFT JOIN start s ON t.id = s.team_id
|
||||||
|
WHERE t.is_deleted = FALSE AND s.zekken_number IS NOT NULL;
|
||||||
|
""")
|
||||||
|
team_zekken_data = old_cursor.fetchall()
|
||||||
|
|
||||||
|
for team_id, zekken_number, event_id in team_zekken_data:
|
||||||
|
if zekken_number:
|
||||||
|
team_zekken_map[str(zekken_number)] = {
|
||||||
|
'team_id': team_id,
|
||||||
|
'event_id': event_id
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f" チーム-ゼッケン対応: {len(team_zekken_map)}件")
|
||||||
|
|
||||||
|
# GPS情報をバッチで移行
|
||||||
|
print("\n 🌍 GPS情報移行中...")
|
||||||
|
|
||||||
|
# 既存のGPS情報をクリア(必要に応じて)
|
||||||
|
with connection.cursor() as django_cursor:
|
||||||
|
django_cursor.execute("SELECT COUNT(*) FROM gps_checkins;")
|
||||||
|
existing_gps = django_cursor.fetchone()[0]
|
||||||
|
print(f" 既存GPS記録: {existing_gps}件")
|
||||||
|
|
||||||
|
# GPS情報を取得・移行
|
||||||
|
gifuroge_cursor.execute("""
|
||||||
|
SELECT zekken_number, event_code, cp_number, lattitude, longitude,
|
||||||
|
image_address, image_receipt, image_qr, validate_location,
|
||||||
|
goal_time, late_point, create_at, create_user, update_at,
|
||||||
|
update_user, buy_flag, colabo_company_memo, points
|
||||||
|
FROM gps_information
|
||||||
|
ORDER BY create_at;
|
||||||
|
""")
|
||||||
|
|
||||||
|
gps_records = gifuroge_cursor.fetchall()
|
||||||
|
print(f" 移行対象GPS記録: {len(gps_records)}件")
|
||||||
|
|
||||||
|
migrated_gps_count = 0
|
||||||
|
batch_size = 1000
|
||||||
|
|
||||||
|
with connection.cursor() as django_cursor:
|
||||||
|
for i in range(0, len(gps_records), batch_size):
|
||||||
|
batch = gps_records[i:i+batch_size]
|
||||||
|
print(f" バッチ {i//batch_size + 1}: {len(batch)}件処理中...")
|
||||||
|
|
||||||
|
for gps_record in batch:
|
||||||
|
(zekken_number, event_code, cp_number, lattitude, longitude,
|
||||||
|
image_address, image_receipt, image_qr, validate_location,
|
||||||
|
goal_time, late_point, create_at, create_user, update_at,
|
||||||
|
update_user, buy_flag, colabo_company_memo, points) = gps_record
|
||||||
|
|
||||||
|
# zekken_numberから対応するteam_idを取得
|
||||||
|
team_info = team_zekken_map.get(str(zekken_number))
|
||||||
|
team_id = team_info['team_id'] if team_info else None
|
||||||
|
event_id = team_info['event_id'] if team_info else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# gps_checkinsテーブルに実際の構造に合わせて挿入
|
||||||
|
django_cursor.execute("""
|
||||||
|
INSERT INTO gps_checkins (
|
||||||
|
path_order, zekken_number, event_code, cp_number,
|
||||||
|
lattitude, longitude, image_address, image_receipt,
|
||||||
|
image_qr, validate_location, goal_time, late_point,
|
||||||
|
create_at, create_user, update_at, update_user,
|
||||||
|
buy_flag, colabo_company_memo, points, event_id,
|
||||||
|
team_id, validation_status
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||||
|
%s, %s
|
||||||
|
);
|
||||||
|
""", [
|
||||||
|
0, # path_order(デフォルト値)
|
||||||
|
str(zekken_number), # zekken_number
|
||||||
|
event_code, # event_code
|
||||||
|
cp_number, # cp_number
|
||||||
|
lattitude, # lattitude
|
||||||
|
longitude, # longitude
|
||||||
|
image_address, # image_address
|
||||||
|
image_receipt, # image_receipt
|
||||||
|
bool(image_qr) if image_qr is not None else False, # image_qr
|
||||||
|
bool(validate_location) if validate_location is not None else False, # validate_location
|
||||||
|
goal_time, # goal_time
|
||||||
|
late_point, # late_point
|
||||||
|
create_at, # create_at
|
||||||
|
create_user, # create_user
|
||||||
|
update_at, # update_at
|
||||||
|
update_user, # update_user
|
||||||
|
bool(buy_flag) if buy_flag is not None else False, # buy_flag
|
||||||
|
colabo_company_memo or '', # colabo_company_memo
|
||||||
|
points, # points
|
||||||
|
event_id, # event_id
|
||||||
|
team_id, # team_id
|
||||||
|
'pending' # validation_status(デフォルト値)
|
||||||
|
])
|
||||||
|
migrated_gps_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if migrated_gps_count < 5: # 最初の5件のエラーのみ表示
|
||||||
|
print(f" ❌ GPS記録移行エラー: ゼッケン{zekken_number} - {e}")
|
||||||
|
|
||||||
|
# バッチごとにコミット
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
print(f" ✅ GPS情報移行完了: {migrated_gps_count}件作成")
|
||||||
|
else:
|
||||||
|
print(" 📍 GPS情報が存在しません")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
gifuroge_conn.close()
|
||||||
|
|
||||||
|
# === 最終確認 ===
|
||||||
|
print("\n=== 移行結果確認 ===")
|
||||||
|
|
||||||
|
total_teams = Team.objects.count()
|
||||||
|
total_members = Member.objects.count()
|
||||||
|
total_entries = Entry.objects.count()
|
||||||
|
|
||||||
|
# GPS情報確認
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM gps_checkins;")
|
||||||
|
total_gps = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
print(f"総チーム数: {total_teams}件")
|
||||||
|
print(f"総メンバー数: {total_members}件")
|
||||||
|
print(f"総エントリー数: {total_entries}件")
|
||||||
|
print(f"総GPS記録数: {total_gps}件")
|
||||||
|
|
||||||
|
# イベント別エントリー統計
|
||||||
|
print("\n=== イベント別エントリー統計 ===")
|
||||||
|
for event_id, event_name in existing_events[:10]:
|
||||||
|
entry_count = Entry.objects.filter(event_id=event_id).count()
|
||||||
|
if entry_count > 0:
|
||||||
|
print(f" {event_name}: {entry_count}件")
|
||||||
|
|
||||||
|
print("\n🎉 全イベントデータ移行(GPS情報含む)が完了しました!")
|
||||||
|
print("🎯 通過審査管理画面で全てのイベントのゼッケン番号が表示され、")
|
||||||
|
print(" GPS情報による通過データも利用可能になります。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
216
migrate_event_table_to_rog_newevent2.py
Normal file
216
migrate_event_table_to_rog_newevent2.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
gifuroge.event_table から rogdb.rog_newevent2 への移行スクリプト
|
||||||
|
|
||||||
|
移行条件:
|
||||||
|
- event_day < '2024-10-01' のデータを移行
|
||||||
|
- self_rogaining = False として移行
|
||||||
|
- その他 = True として移行
|
||||||
|
|
||||||
|
フィールドマッピング:
|
||||||
|
- gifuroge.event_table.event_code → rogdb.rog_newevent2.event_name
|
||||||
|
- gifuroge.event_table.event_name → rogdb.rog_newevent2.event_description
|
||||||
|
- gifuroge.event_table.event_day + start_time → rogdb.rog_newevent2.start_datetime
|
||||||
|
- gifuroge.event_table.event_day + start_time + 5H → rogdb.rog_newevent2.end_datetime
|
||||||
|
- gifuroge.event_table.event_day + start_time - 3day → rogdb.rog_newevent2.deadlineDateTime
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rog.models import NewEvent2
|
||||||
|
from django.utils import timezone
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
print("=== gifuroge.event_table から rogdb.rog_newevent2 への移行 ===")
|
||||||
|
|
||||||
|
# JST タイムゾーン設定
|
||||||
|
JST = pytz.timezone('Asia/Tokyo')
|
||||||
|
|
||||||
|
def parse_datetime(event_day, start_time):
|
||||||
|
"""event_dayとstart_timeを結合してdatetimeオブジェクトを作成"""
|
||||||
|
try:
|
||||||
|
# event_dayの正規化
|
||||||
|
if isinstance(event_day, str):
|
||||||
|
# スラッシュをハイフンに置換
|
||||||
|
if '/' in event_day:
|
||||||
|
event_day = event_day.replace('/', '-')
|
||||||
|
# 年が2桁の場合は20を付加
|
||||||
|
parts = event_day.split('-')
|
||||||
|
if len(parts) == 3 and len(parts[0]) == 2:
|
||||||
|
parts[0] = '20' + parts[0]
|
||||||
|
event_day = '-'.join(parts)
|
||||||
|
|
||||||
|
# start_timeの正規化(デフォルト値を設定)
|
||||||
|
if not start_time or start_time == '':
|
||||||
|
start_time = '09:00:00'
|
||||||
|
|
||||||
|
# 時刻形式の確認と修正
|
||||||
|
if start_time.count(':') == 1:
|
||||||
|
start_time = start_time + ':00'
|
||||||
|
elif start_time.count(':') == 0:
|
||||||
|
start_time = start_time + ':00:00'
|
||||||
|
|
||||||
|
# datetimeオブジェクトの作成
|
||||||
|
datetime_str = f"{event_day} {start_time}"
|
||||||
|
dt = datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# JST タイムゾーンを設定
|
||||||
|
dt_jst = JST.localize(dt)
|
||||||
|
|
||||||
|
return dt_jst
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 日時解析エラー: event_day={event_day}, start_time={start_time}, error={e}")
|
||||||
|
# デフォルト値として現在時刻を返す
|
||||||
|
return timezone.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# gifuroge データベースに接続
|
||||||
|
gifuroge_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='gifuroge',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ gifurogeデータベースに接続成功")
|
||||||
|
|
||||||
|
with gifuroge_conn.cursor() as cursor:
|
||||||
|
# 移行対象データの取得
|
||||||
|
print("\\n=== STEP 1: 移行対象データの確認 ===")
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT event_code, event_name, start_time, event_day
|
||||||
|
FROM event_table
|
||||||
|
WHERE event_day < '2024-10-01'
|
||||||
|
AND event_code IS NOT NULL
|
||||||
|
AND event_code != ''
|
||||||
|
AND start_time > '07:00:00'
|
||||||
|
ORDER BY event_day
|
||||||
|
""")
|
||||||
|
|
||||||
|
events_to_migrate = cursor.fetchall()
|
||||||
|
print(f"移行対象イベント: {len(events_to_migrate)}件")
|
||||||
|
|
||||||
|
if len(events_to_migrate) == 0:
|
||||||
|
print("移行対象のイベントがありません。")
|
||||||
|
gifuroge_conn.close()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# データの確認表示
|
||||||
|
for event_code, event_name, start_time, event_day in events_to_migrate[:10]:
|
||||||
|
print(f" {event_code}: {event_name} ({event_day} {start_time})")
|
||||||
|
|
||||||
|
if len(events_to_migrate) > 10:
|
||||||
|
print(f" ... 他 {len(events_to_migrate) - 10} 件")
|
||||||
|
|
||||||
|
# 移行の実行
|
||||||
|
print(f"\\n=== STEP 2: データ移行の実行 ===")
|
||||||
|
|
||||||
|
migrated_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for event_code, event_name, start_time, event_day in events_to_migrate:
|
||||||
|
try:
|
||||||
|
# 日時の計算
|
||||||
|
start_datetime = parse_datetime(event_day, start_time)
|
||||||
|
end_datetime = start_datetime + timedelta(hours=5)
|
||||||
|
deadline_datetime = start_datetime - timedelta(days=3)
|
||||||
|
|
||||||
|
# 既存データのチェックと更新または新規作成
|
||||||
|
existing_event = NewEvent2.objects.filter(event_name=event_code).first()
|
||||||
|
|
||||||
|
if existing_event:
|
||||||
|
# 既存データを更新
|
||||||
|
existing_event.event_description = event_name
|
||||||
|
existing_event.start_datetime = start_datetime
|
||||||
|
existing_event.end_datetime = end_datetime
|
||||||
|
existing_event.deadlineDateTime = deadline_datetime
|
||||||
|
existing_event.self_rogaining = False
|
||||||
|
existing_event.status = 'public'
|
||||||
|
existing_event.public = True
|
||||||
|
existing_event.hour_5 = True
|
||||||
|
existing_event.hour_3 = False
|
||||||
|
existing_event.class_general = True
|
||||||
|
existing_event.class_family = True
|
||||||
|
existing_event.class_solo_male = True
|
||||||
|
existing_event.class_solo_female = True
|
||||||
|
existing_event.event_code = event_code
|
||||||
|
existing_event.start_time = start_time
|
||||||
|
existing_event.event_day = event_day
|
||||||
|
|
||||||
|
existing_event.save()
|
||||||
|
updated_count += 1
|
||||||
|
print(f"🔄 更新完了: {event_code}")
|
||||||
|
else:
|
||||||
|
# 新しいイベントレコードの作成
|
||||||
|
new_event = NewEvent2(
|
||||||
|
event_name=event_code, # event_code → event_name
|
||||||
|
event_description=event_name, # event_name → event_description
|
||||||
|
start_datetime=start_datetime,
|
||||||
|
end_datetime=end_datetime,
|
||||||
|
deadlineDateTime=deadline_datetime,
|
||||||
|
self_rogaining=False, # 指定条件
|
||||||
|
# その他=True に相当するフィールドがないため、コメントで記録
|
||||||
|
# 必要に応じてフィールドを追加する
|
||||||
|
status='public', # デフォルトステータス
|
||||||
|
public=True, # 公開設定
|
||||||
|
hour_5=True, # 5時間イベント
|
||||||
|
hour_3=False, # 3時間イベントではない
|
||||||
|
class_general=True, # 一般クラス有効
|
||||||
|
class_family=True, # ファミリークラス有効
|
||||||
|
class_solo_male=True, # 男子ソロクラス有効
|
||||||
|
class_solo_female=True, # 女子ソロクラス有効
|
||||||
|
# MobServer統合フィールドの設定
|
||||||
|
event_code=event_code,
|
||||||
|
start_time=start_time,
|
||||||
|
event_day=event_day
|
||||||
|
)
|
||||||
|
|
||||||
|
new_event.save()
|
||||||
|
migrated_count += 1
|
||||||
|
print(f"✅ 新規作成: {event_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
print(f"❌ 移行エラー: {event_code} - {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\\n=== 移行結果 ===")
|
||||||
|
print(f"新規作成: {migrated_count}件")
|
||||||
|
print(f"更新完了: {updated_count}件")
|
||||||
|
print(f"移行エラー: {error_count}件")
|
||||||
|
print(f"合計処理: {migrated_count + updated_count + error_count}件")
|
||||||
|
|
||||||
|
# 移行結果の確認
|
||||||
|
print(f"\\n=== 移行後データ確認 ===")
|
||||||
|
migrated_events = NewEvent2.objects.filter(
|
||||||
|
self_rogaining=False
|
||||||
|
).order_by('start_datetime')
|
||||||
|
|
||||||
|
print(f"移行されたイベント数: {migrated_events.count()}件")
|
||||||
|
for event in migrated_events[:10]:
|
||||||
|
print(f" {event.event_name}: {event.event_description} ({event.start_datetime})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 移行処理でエラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if 'gifuroge_conn' in locals():
|
||||||
|
gifuroge_conn.close()
|
||||||
|
print("✅ データベース接続を閉じました")
|
||||||
|
|
||||||
|
print("\\n=== 移行処理完了 ===")
|
||||||
150
migrate_event_table_to_rog_newevent2.sql
Normal file
150
migrate_event_table_to_rog_newevent2.sql
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
-- gifuroge.event_table から rogdb.rog_newevent2 への移行SQL
|
||||||
|
--
|
||||||
|
-- 移行条件:
|
||||||
|
-- - event_day < '2024-10-01' のデータを移行
|
||||||
|
-- - self_rogaining = False として移行
|
||||||
|
-- - その他 = True として移行(コメントで記録)
|
||||||
|
--
|
||||||
|
-- 実行前の準備:
|
||||||
|
-- 1. gifurogeデータベースからrogdbデータベースへのdblink接続が必要
|
||||||
|
-- 2. または、両方のデータベースに同時アクセス可能な環境での実行
|
||||||
|
|
||||||
|
-- Step 1: 移行対象データの確認
|
||||||
|
-- gifurogeデータベースで実行
|
||||||
|
SELECT
|
||||||
|
event_code,
|
||||||
|
event_name,
|
||||||
|
start_time,
|
||||||
|
event_day,
|
||||||
|
-- 日時計算の確認
|
||||||
|
CASE
|
||||||
|
WHEN start_time IS NULL OR start_time = '' THEN
|
||||||
|
(event_day || ' 09:00:00')::timestamp
|
||||||
|
ELSE
|
||||||
|
(event_day || ' ' || start_time || ':00')::timestamp
|
||||||
|
END as start_datetime,
|
||||||
|
CASE
|
||||||
|
WHEN start_time IS NULL OR start_time = '' THEN
|
||||||
|
(event_day || ' 09:00:00')::timestamp + INTERVAL '5 hours'
|
||||||
|
ELSE
|
||||||
|
(event_day || ' ' || start_time || ':00')::timestamp + INTERVAL '5 hours'
|
||||||
|
END as end_datetime,
|
||||||
|
CASE
|
||||||
|
WHEN start_time IS NULL OR start_time = '' THEN
|
||||||
|
(event_day || ' 09:00:00')::timestamp - INTERVAL '3 days'
|
||||||
|
ELSE
|
||||||
|
(event_day || ' ' || start_time || ':00')::timestamp - INTERVAL '3 days'
|
||||||
|
END as deadline_datetime
|
||||||
|
FROM event_table
|
||||||
|
WHERE event_day < '2024-10-01'
|
||||||
|
AND event_code IS NOT NULL
|
||||||
|
AND event_code != ''
|
||||||
|
ORDER BY event_day;
|
||||||
|
|
||||||
|
-- Step 2: 実際の移行(rogdbデータベースで実行)
|
||||||
|
-- 注意: 以下のSQLはrogdbデータベースで実行する必要があります
|
||||||
|
-- gifurogeデータベースからのデータ取得にはdblinkまたは別の方法が必要です
|
||||||
|
|
||||||
|
-- dblinkを使用する場合の例:
|
||||||
|
-- SELECT dblink_connect('gifuroge_conn', 'host=postgres-db dbname=gifuroge user=admin password=admin123456');
|
||||||
|
|
||||||
|
-- 移行用のINSERT文(手動で値を入力する場合の例)
|
||||||
|
/*
|
||||||
|
INSERT INTO rog_newevent2 (
|
||||||
|
event_name, -- gifuroge.event_table.event_code
|
||||||
|
event_description, -- gifuroge.event_table.event_name
|
||||||
|
start_datetime, -- gifuroge.event_table.event_day + start_time
|
||||||
|
end_datetime, -- start_datetime + 5 hours
|
||||||
|
"deadlineDateTime", -- start_datetime - 3 days
|
||||||
|
self_rogaining, -- False
|
||||||
|
status, -- 'public'
|
||||||
|
public, -- True
|
||||||
|
hour_5, -- True
|
||||||
|
hour_3, -- False
|
||||||
|
class_general, -- True
|
||||||
|
class_family, -- True
|
||||||
|
class_solo_male, -- True
|
||||||
|
class_solo_female, -- True
|
||||||
|
event_code, -- gifuroge.event_table.event_code (MobServer統合)
|
||||||
|
start_time, -- gifuroge.event_table.start_time (MobServer統合)
|
||||||
|
event_day -- gifuroge.event_table.event_day (MobServer統合)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
et.event_code as event_name,
|
||||||
|
et.event_name as event_description,
|
||||||
|
CASE
|
||||||
|
WHEN et.start_time IS NULL OR et.start_time = '' THEN
|
||||||
|
(et.event_day || ' 09:00:00')::timestamp AT TIME ZONE 'Asia/Tokyo'
|
||||||
|
ELSE
|
||||||
|
(et.event_day || ' ' || et.start_time || ':00')::timestamp AT TIME ZONE 'Asia/Tokyo'
|
||||||
|
END as start_datetime,
|
||||||
|
CASE
|
||||||
|
WHEN et.start_time IS NULL OR et.start_time = '' THEN
|
||||||
|
(et.event_day || ' 09:00:00')::timestamp AT TIME ZONE 'Asia/Tokyo' + INTERVAL '5 hours'
|
||||||
|
ELSE
|
||||||
|
(et.event_day || ' ' || et.start_time || ':00')::timestamp AT TIME ZONE 'Asia/Tokyo' + INTERVAL '5 hours'
|
||||||
|
END as end_datetime,
|
||||||
|
CASE
|
||||||
|
WHEN et.start_time IS NULL OR et.start_time = '' THEN
|
||||||
|
(et.event_day || ' 09:00:00')::timestamp AT TIME ZONE 'Asia/Tokyo' - INTERVAL '3 days'
|
||||||
|
ELSE
|
||||||
|
(et.event_day || ' ' || et.start_time || ':00')::timestamp AT TIME ZONE 'Asia/Tokyo' - INTERVAL '3 days'
|
||||||
|
END as deadline_datetime,
|
||||||
|
false as self_rogaining, -- 指定条件
|
||||||
|
'public' as status, -- デフォルトステータス
|
||||||
|
true as public, -- 公開設定
|
||||||
|
true as hour_5, -- 5時間イベント
|
||||||
|
false as hour_3, -- 3時間イベントではない
|
||||||
|
true as class_general, -- 一般クラス有効
|
||||||
|
true as class_family, -- ファミリークラス有効
|
||||||
|
true as class_solo_male, -- 男子ソロクラス有効
|
||||||
|
true as class_solo_female, -- 女子ソロクラス有効
|
||||||
|
et.event_code, -- MobServer統合フィールド
|
||||||
|
et.start_time, -- MobServer統合フィールド
|
||||||
|
et.event_day -- MobServer統合フィールド
|
||||||
|
FROM dblink('gifuroge_conn',
|
||||||
|
'SELECT event_code, event_name, start_time, event_day
|
||||||
|
FROM event_table
|
||||||
|
WHERE event_day < ''2024-10-01''
|
||||||
|
AND event_code IS NOT NULL
|
||||||
|
AND event_code != ''''
|
||||||
|
ORDER BY event_day'
|
||||||
|
) AS et(event_code text, event_name text, start_time text, event_day text)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM rog_newevent2 WHERE event_name = et.event_code
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- dblinkの切断
|
||||||
|
-- SELECT dblink_disconnect('gifuroge_conn');
|
||||||
|
|
||||||
|
-- Step 3: 移行結果の確認
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
event_name,
|
||||||
|
event_description,
|
||||||
|
start_datetime,
|
||||||
|
end_datetime,
|
||||||
|
"deadlineDateTime",
|
||||||
|
self_rogaining,
|
||||||
|
status,
|
||||||
|
event_code,
|
||||||
|
start_time,
|
||||||
|
event_day
|
||||||
|
FROM rog_newevent2
|
||||||
|
WHERE self_rogaining = false
|
||||||
|
AND event_code IS NOT NULL
|
||||||
|
ORDER BY start_datetime;
|
||||||
|
|
||||||
|
-- 移行件数の確認
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_migrated_events
|
||||||
|
FROM rog_newevent2
|
||||||
|
WHERE self_rogaining = false
|
||||||
|
AND event_code IS NOT NULL;
|
||||||
|
|
||||||
|
-- 注意事項:
|
||||||
|
-- 1. 上記のSQLは例であり、実際の実行環境に応じて調整が必要です
|
||||||
|
-- 2. dblinkを使用しない場合は、ETLツールやアプリケーションレベルでの移行を推奨します
|
||||||
|
-- 3. "その他=True"に相当するフィールドが見つからない場合、新しいフィールドの追加を検討してください
|
||||||
|
-- 4. 実行前に必ずバックアップを取ってください
|
||||||
256
migrate_fc_gifu_complete.py
Normal file
256
migrate_fc_gifu_complete.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への包括的FC岐阜データ移行スクリプト
|
||||||
|
Team → Member → Entry の順序で移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rog.models import NewEvent2, Team, Entry, NewCategory, CustomUser, Member
|
||||||
|
from datetime import datetime
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
print("=== FC岐阜包括データ移行(Team→Member→Entry順序)===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# old_rogdbに直接接続
|
||||||
|
old_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='old_rogdb',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ old_rogdbに接続成功")
|
||||||
|
|
||||||
|
# FC岐阜イベントを確認
|
||||||
|
fc_event = NewEvent2.objects.filter(id=10).first()
|
||||||
|
if not fc_event:
|
||||||
|
print("❌ FC岐阜イベント(ID:10)が見つかりません")
|
||||||
|
old_conn.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"✅ FC岐阜イベント: {fc_event.event_name}")
|
||||||
|
print(f" 期間: {fc_event.start_datetime} - {fc_event.end_datetime}")
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
# Step 1: FC岐阜のチームデータを取得・移行
|
||||||
|
print("\\n=== Step 1: チーム移行 ===")
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT DISTINCT rt.id, rt.team_name, rt.owner_id, rt.category_id,
|
||||||
|
rc.category_name, rt.password, rt.trial
|
||||||
|
FROM rog_team rt
|
||||||
|
JOIN rog_entry re ON rt.id = re.team_id
|
||||||
|
LEFT JOIN rog_newcategory rc ON rt.category_id = rc.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY rt.id;
|
||||||
|
""")
|
||||||
|
|
||||||
|
team_data = old_cursor.fetchall()
|
||||||
|
print(f"FC岐阜関連チーム: {len(team_data)}件")
|
||||||
|
|
||||||
|
team_created_count = 0
|
||||||
|
team_errors = 0
|
||||||
|
|
||||||
|
for team_id, team_name, owner_id, cat_id, cat_name, password, trial in team_data:
|
||||||
|
try:
|
||||||
|
# カテゴリを取得または作成
|
||||||
|
category = None
|
||||||
|
if cat_id and cat_name:
|
||||||
|
category, cat_created = NewCategory.objects.get_or_create(
|
||||||
|
id=cat_id,
|
||||||
|
defaults={
|
||||||
|
'category_name': cat_name,
|
||||||
|
'category_number': cat_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if cat_created:
|
||||||
|
print(f" カテゴリ作成: {cat_name}")
|
||||||
|
|
||||||
|
# チームを作成(メンバー制約を一時的に無視)
|
||||||
|
team, team_created = Team.objects.get_or_create(
|
||||||
|
id=team_id,
|
||||||
|
defaults={
|
||||||
|
'team_name': team_name,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'category': category,
|
||||||
|
'event_id': fc_event.id,
|
||||||
|
'password': password or '',
|
||||||
|
'trial': trial or False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if team_created:
|
||||||
|
print(f" ✅ チーム作成: {team_name} (ID: {team_id})")
|
||||||
|
team_created_count += 1
|
||||||
|
else:
|
||||||
|
print(f" 🔄 既存チーム: {team_name} (ID: {team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
team_errors += 1
|
||||||
|
print(f" ❌ チームエラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
# Step 2: メンバーデータを取得・移行
|
||||||
|
print(f"\\n=== Step 2: メンバー移行 ===")
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT rm.id, rm.team_id, rm.user_id, cu.firstname, cu.lastname, cu.email
|
||||||
|
FROM rog_member rm
|
||||||
|
JOIN rog_team rt ON rm.team_id = rt.id
|
||||||
|
JOIN rog_entry re ON rt.id = re.team_id
|
||||||
|
LEFT JOIN rog_customuser cu ON rm.user_id = cu.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY rm.team_id, rm.id;
|
||||||
|
""")
|
||||||
|
|
||||||
|
member_data = old_cursor.fetchall()
|
||||||
|
print(f"FC岐阜関連メンバー: {len(member_data)}件")
|
||||||
|
|
||||||
|
member_created_count = 0
|
||||||
|
member_errors = 0
|
||||||
|
|
||||||
|
for member_id, team_id, user_id, firstname, lastname, email in member_data:
|
||||||
|
try:
|
||||||
|
# チームを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
|
||||||
|
# ユーザーを取得または作成
|
||||||
|
user = None
|
||||||
|
if user_id:
|
||||||
|
try:
|
||||||
|
user = CustomUser.objects.get(id=user_id)
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
# ユーザーが存在しない場合は作成
|
||||||
|
user = CustomUser.objects.create(
|
||||||
|
id=user_id,
|
||||||
|
email=email or f"user{user_id}@example.com",
|
||||||
|
firstname=firstname or "名前",
|
||||||
|
lastname=lastname or "苗字",
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
print(f" ユーザー作成: {firstname} {lastname}")
|
||||||
|
|
||||||
|
# メンバーを作成
|
||||||
|
member, member_created = Member.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
user=user,
|
||||||
|
defaults={}
|
||||||
|
)
|
||||||
|
|
||||||
|
if member_created:
|
||||||
|
print(f" ✅ メンバー作成: {firstname} {lastname} -> {team.team_name}")
|
||||||
|
member_created_count += 1
|
||||||
|
else:
|
||||||
|
print(f" 🔄 既存メンバー: {firstname} {lastname} -> {team.team_name}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
member_errors += 1
|
||||||
|
print(f" ❌ メンバーエラー: {firstname} {lastname} - {e}")
|
||||||
|
|
||||||
|
# Step 3: エントリーデータを移行
|
||||||
|
print(f"\\n=== Step 3: エントリー移行 ===")
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id, re.date, re.owner_id,
|
||||||
|
rc.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON re.category_id = rc.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY re.zekken_number;
|
||||||
|
""")
|
||||||
|
|
||||||
|
entry_data = old_cursor.fetchall()
|
||||||
|
print(f"FC岐阜エントリー: {len(entry_data)}件")
|
||||||
|
|
||||||
|
entry_created_count = 0
|
||||||
|
entry_errors = 0
|
||||||
|
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id, cat_name in entry_data:
|
||||||
|
try:
|
||||||
|
# チームを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
|
||||||
|
# カテゴリを取得
|
||||||
|
category = None
|
||||||
|
if cat_id:
|
||||||
|
try:
|
||||||
|
category = NewCategory.objects.get(id=cat_id)
|
||||||
|
except NewCategory.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 日時を調整(イベント期間内に設定)
|
||||||
|
entry_date = fc_event.start_datetime
|
||||||
|
if date:
|
||||||
|
try:
|
||||||
|
# 既存の日付がイベント期間内かチェック
|
||||||
|
if fc_event.start_datetime.date() <= date.date() <= fc_event.end_datetime.date():
|
||||||
|
entry_date = date
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# エントリーを作成
|
||||||
|
entry, entry_created = Entry.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
event=fc_event,
|
||||||
|
defaults={
|
||||||
|
'category': category,
|
||||||
|
'date': entry_date,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'zekken_number': int(zekken) if zekken else 0,
|
||||||
|
'zekken_label': label or f"FC岐阜-{zekken}",
|
||||||
|
'is_active': True,
|
||||||
|
'hasParticipated': False,
|
||||||
|
'hasGoaled': False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry_created:
|
||||||
|
print(f" ✅ エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
entry_created_count += 1
|
||||||
|
else:
|
||||||
|
print(f" 🔄 既存エントリー: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
entry_errors += 1
|
||||||
|
except Exception as e:
|
||||||
|
entry_errors += 1
|
||||||
|
print(f" ❌ エントリーエラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
print(f"\\n=== 移行完了統計 ===")
|
||||||
|
print(f"チーム作成: {team_created_count}件 (エラー: {team_errors}件)")
|
||||||
|
print(f"メンバー作成: {member_created_count}件 (エラー: {member_errors}件)")
|
||||||
|
print(f"エントリー作成: {entry_created_count}件 (エラー: {entry_errors}件)")
|
||||||
|
|
||||||
|
# 最終確認
|
||||||
|
fc_entries = Entry.objects.filter(event=fc_event).order_by('zekken_number')
|
||||||
|
print(f"\\n🎉 FC岐阜イベント総エントリー: {fc_entries.count()}件")
|
||||||
|
|
||||||
|
if fc_entries.exists():
|
||||||
|
print("\\nゼッケン番号一覧(最初の10件):")
|
||||||
|
for entry in fc_entries[:10]:
|
||||||
|
print(f" ゼッケン{entry.zekken_number}: {entry.team.team_name}")
|
||||||
|
|
||||||
|
if fc_entries.count() > 10:
|
||||||
|
print(f" ... 他 {fc_entries.count() - 10}件")
|
||||||
|
|
||||||
|
print("\\n🎉 FC岐阜イベントのゼッケン番号表示問題が解決されました!")
|
||||||
|
print("🎯 通過審査管理画面でFC岐阜を選択すると、参加者のゼッケン番号が表示されるようになります。")
|
||||||
|
else:
|
||||||
|
print("❌ エントリーが作成されませんでした")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
185
migrate_fc_gifu_only.py
Normal file
185
migrate_fc_gifu_only.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
FC岐阜イベント限定データ移行スクリプト
|
||||||
|
FC岐阜イベントに関連するチーム・エントリーのみを移行して問題を解決
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from rog.models import NewEvent2, Team, Entry, NewCategory, CustomUser
|
||||||
|
|
||||||
|
print("=== FC岐阜イベント限定データ移行 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# FC岐阜イベントを確認
|
||||||
|
fc_event = NewEvent2.objects.filter(event_name__icontains='FC岐阜').first()
|
||||||
|
if not fc_event:
|
||||||
|
print("❌ FC岐阜イベントが見つかりません")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"✅ FC岐阜イベント: {fc_event.event_name} (ID: {fc_event.id})")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# まず、全体的なデータ構造を確認
|
||||||
|
print("\\n=== データベース構造調査 ===")
|
||||||
|
|
||||||
|
# 1. rog_entry テーブルの全体状況
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_entry;")
|
||||||
|
total_entries = cursor.fetchone()[0]
|
||||||
|
print(f"総エントリー数: {total_entries}件")
|
||||||
|
|
||||||
|
# 2. rog_entry のフィールド構造確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_entry'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
entry_columns = cursor.fetchall()
|
||||||
|
print("\\nrog_entry テーブル構造:")
|
||||||
|
for col_name, data_type, nullable in entry_columns:
|
||||||
|
print(f" - {col_name}: {data_type} {'(NULL可)' if nullable == 'YES' else '(NOT NULL)'}")
|
||||||
|
|
||||||
|
# 3. rog_team テーブルも確認(ゼッケン情報がチーム側にある可能性)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_team'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
team_columns = cursor.fetchall()
|
||||||
|
print("\\nrog_team テーブル構造:")
|
||||||
|
for col_name, data_type, nullable in team_columns:
|
||||||
|
if 'zekken' in col_name.lower() or 'number' in col_name.lower():
|
||||||
|
print(f" 🎯 {col_name}: {data_type} {'(NULL可)' if nullable == 'YES' else '(NOT NULL)'}")
|
||||||
|
else:
|
||||||
|
print(f" - {col_name}: {data_type}")
|
||||||
|
|
||||||
|
# 4. イベント別エントリー数確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT e.id, e.event_name, COUNT(re.id) as entry_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_entry re ON e.id = re.event_id
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
ORDER BY entry_count DESC
|
||||||
|
LIMIT 10;
|
||||||
|
""")
|
||||||
|
event_entries = cursor.fetchall()
|
||||||
|
print("\\n=== イベント別エントリー数(上位10件) ===")
|
||||||
|
for event_id, event_name, count in event_entries:
|
||||||
|
print(f" Event {event_id}: '{event_name}' - {count}件")
|
||||||
|
|
||||||
|
# 5. FC岐阜関連のより広範囲な検索
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT re.*, rt.team_name, rt.zekken_number as team_zekken
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_newevent2 e ON re.event_id = e.id
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
WHERE e.event_name LIKE '%FC岐阜%' OR e.event_name LIKE '%fc岐阜%' OR e.event_name LIKE '%FC%'
|
||||||
|
LIMIT 20;
|
||||||
|
""")
|
||||||
|
|
||||||
|
fc_entry_data = cursor.fetchall()
|
||||||
|
print(f"\\n✅ FC岐阜関連エントリー(広範囲検索): {len(fc_entry_data)}件")
|
||||||
|
|
||||||
|
if fc_entry_data:
|
||||||
|
print("\\n🔍 FC岐阜関連データ詳細:")
|
||||||
|
for row in fc_entry_data[:5]: # 最初の5件を表示
|
||||||
|
print(f" Entry ID: {row[0]}, Team: {row[-2]}, Team Zekken: {row[-1]}")
|
||||||
|
|
||||||
|
# 6. チームテーブルでゼッケン番号がある場合を確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT rt.id, rt.team_name, rt.zekken_number, rt.event_id
|
||||||
|
FROM rog_team rt
|
||||||
|
JOIN rog_newevent2 e ON rt.event_id = e.id
|
||||||
|
WHERE e.event_name LIKE '%FC岐阜%'
|
||||||
|
AND rt.zekken_number IS NOT NULL
|
||||||
|
AND rt.zekken_number != ''
|
||||||
|
ORDER BY CAST(rt.zekken_number AS INTEGER)
|
||||||
|
LIMIT 20;
|
||||||
|
""")
|
||||||
|
|
||||||
|
team_zekken_data = cursor.fetchall()
|
||||||
|
print(f"\\n✅ FC岐阜チームのゼッケン番号: {len(team_zekken_data)}件")
|
||||||
|
|
||||||
|
if team_zekken_data:
|
||||||
|
print("\\n🎯 チーム側のゼッケン番号データ:")
|
||||||
|
for team_id, team_name, zekken, event_id in team_zekken_data[:10]:
|
||||||
|
print(f" チーム{team_id}: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
# チーム側にゼッケン情報がある場合、それを使ってエントリーを作成
|
||||||
|
print("\\n=== チーム側ゼッケン情報からエントリー作成 ===")
|
||||||
|
created_entries = 0
|
||||||
|
|
||||||
|
for team_id, team_name, zekken, event_id in team_zekken_data:
|
||||||
|
# チームを取得
|
||||||
|
try:
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
|
||||||
|
# エントリーを作成
|
||||||
|
entry, entry_created = Entry.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
event=fc_event,
|
||||||
|
defaults={
|
||||||
|
'category': team.category,
|
||||||
|
'date': fc_event.start_datetime,
|
||||||
|
'owner': team.owner,
|
||||||
|
'zekken_number': int(zekken) if zekken.isdigit() else 0,
|
||||||
|
'zekken_label': f"FC岐阜-{zekken}",
|
||||||
|
'is_active': True,
|
||||||
|
'hasParticipated': False,
|
||||||
|
'hasGoaled': False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry_created:
|
||||||
|
created_entries += 1
|
||||||
|
print(f" エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が新DBに存在しません: {team_name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エラー: {e}")
|
||||||
|
|
||||||
|
print(f"\\n✅ 作成されたエントリー: {created_entries}件")
|
||||||
|
else:
|
||||||
|
print("❌ チーム側にもゼッケン情報がありません")
|
||||||
|
|
||||||
|
# 7. 最終確認
|
||||||
|
fc_entries = Entry.objects.filter(event=fc_event).order_by('zekken_number')
|
||||||
|
print(f"\\n=== 最終結果 ===")
|
||||||
|
print(f"FC岐阜イベント総エントリー: {fc_entries.count()}件")
|
||||||
|
|
||||||
|
if fc_entries.exists():
|
||||||
|
print("\\n🎉 ゼッケン番号一覧(最初の10件):")
|
||||||
|
for entry in fc_entries[:10]:
|
||||||
|
print(f" ゼッケン{entry.zekken_number}: {entry.team.team_name}")
|
||||||
|
print("\\n🎉 FC岐阜イベントのゼッケン番号表示問題が解決されました!")
|
||||||
|
else:
|
||||||
|
print("\\n❌ まだエントリーデータがありません")
|
||||||
|
|
||||||
|
# 8. デバッグ用:全てのチームデータを確認
|
||||||
|
all_teams = Team.objects.all()[:10]
|
||||||
|
print(f"\\n🔍 新DBの全チーム(最初の10件、総数: {Team.objects.count()}件):")
|
||||||
|
for team in all_teams:
|
||||||
|
entries = Entry.objects.filter(team=team)
|
||||||
|
print(f" Team {team.id}: {team.team_name} (エントリー: {entries.count()}件)")
|
||||||
|
|
||||||
|
# 9. FC岐阜イベントの詳細情報
|
||||||
|
print(f"\\n🔍 FC岐阜イベント詳細:")
|
||||||
|
print(f" ID: {fc_event.id}")
|
||||||
|
print(f" 名前: {fc_event.event_name}")
|
||||||
|
print(f" 開始日: {fc_event.start_datetime}")
|
||||||
|
print(f" 終了日: {fc_event.end_datetime}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
300
migrate_fc_gifu_step_by_step.py
Normal file
300
migrate_fc_gifu_step_by_step.py
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb への段階的FC岐阜データ移行スクリプト
|
||||||
|
1. Team/Member → 2. Entry の順序で移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from rog.models import NewEvent2, Team, Entry, NewCategory, CustomUser, Member
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
print("=== old_rogdb から FC岐阜データ段階的移行 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# old_rogdbに直接接続
|
||||||
|
old_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='old_rogdb',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ old_rogdbに接続成功")
|
||||||
|
|
||||||
|
# FC岐阜イベントを確認
|
||||||
|
fc_event = NewEvent2.objects.filter(id=10).first()
|
||||||
|
if not fc_event:
|
||||||
|
print("❌ FC岐阜イベント(ID:10)が見つかりません")
|
||||||
|
old_conn.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"✅ FC岐阜イベント: {fc_event.event_name}")
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
# === STEP 1: Team & Member データ取得 ===
|
||||||
|
print("\\n=== STEP 1: Team & Member データ取得 ===")
|
||||||
|
|
||||||
|
# FC岐阜関連のチーム情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT DISTINCT rt.id, rt.team_name, rt.owner_id, rt.category_id,
|
||||||
|
rc.category_name, cu.email, cu.firstname, cu.lastname
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON rt.category_id = rc.id
|
||||||
|
LEFT JOIN rog_customuser cu ON rt.owner_id = cu.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY rt.id;
|
||||||
|
""")
|
||||||
|
|
||||||
|
team_data = old_cursor.fetchall()
|
||||||
|
print(f"FC岐阜関連チーム: {len(team_data)}件")
|
||||||
|
|
||||||
|
# チームメンバー情報を取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT rm.team_id, rm.user_id, cu.email, cu.firstname, cu.lastname
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_member rm ON re.team_id = rm.team_id
|
||||||
|
JOIN rog_customuser cu ON rm.user_id = cu.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY rm.team_id, rm.user_id;
|
||||||
|
""")
|
||||||
|
|
||||||
|
member_data = old_cursor.fetchall()
|
||||||
|
print(f"FC岐阜関連メンバー: {len(member_data)}件")
|
||||||
|
|
||||||
|
# チーム別メンバー数を確認
|
||||||
|
team_member_count = {}
|
||||||
|
for team_id, user_id, email, first_name, last_name in member_data:
|
||||||
|
if team_id not in team_member_count:
|
||||||
|
team_member_count[team_id] = 0
|
||||||
|
team_member_count[team_id] += 1
|
||||||
|
|
||||||
|
print("\\nチーム別メンバー数:")
|
||||||
|
for team_id, count in team_member_count.items():
|
||||||
|
print(f" Team {team_id}: {count}名")
|
||||||
|
|
||||||
|
# === STEP 2: ユーザー移行 ===
|
||||||
|
print("\\n=== STEP 2: ユーザー移行 ===")
|
||||||
|
|
||||||
|
# 関連するすべてのユーザーを取得
|
||||||
|
all_user_ids = set()
|
||||||
|
for _, _, owner_id, _, _, _, _, _ in team_data:
|
||||||
|
if owner_id:
|
||||||
|
all_user_ids.add(owner_id)
|
||||||
|
for _, user_id, _, _, _ in member_data:
|
||||||
|
all_user_ids.add(user_id)
|
||||||
|
|
||||||
|
if all_user_ids:
|
||||||
|
old_cursor.execute(f"""
|
||||||
|
SELECT id, email, firstname, lastname, date_joined
|
||||||
|
FROM rog_customuser
|
||||||
|
WHERE id IN ({','.join(map(str, all_user_ids))})
|
||||||
|
""")
|
||||||
|
|
||||||
|
user_data = old_cursor.fetchall()
|
||||||
|
print(f"移行対象ユーザー: {len(user_data)}件")
|
||||||
|
|
||||||
|
migrated_users = 0
|
||||||
|
for user_id, email, first_name, last_name, date_joined in user_data:
|
||||||
|
user, created = CustomUser.objects.get_or_create(
|
||||||
|
id=user_id,
|
||||||
|
defaults={
|
||||||
|
'email': email or f'user{user_id}@example.com',
|
||||||
|
'first_name': first_name or '',
|
||||||
|
'last_name': last_name or '',
|
||||||
|
'username': email or f'user{user_id}',
|
||||||
|
'date_joined': date_joined,
|
||||||
|
'is_active': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_users += 1
|
||||||
|
print(f" ユーザー作成: {email} ({first_name} {last_name})")
|
||||||
|
|
||||||
|
print(f"✅ ユーザー移行完了: {migrated_users}件作成")
|
||||||
|
|
||||||
|
# === STEP 3: カテゴリ移行 ===
|
||||||
|
print("\\n=== STEP 3: カテゴリ移行 ===")
|
||||||
|
|
||||||
|
migrated_categories = 0
|
||||||
|
for _, _, _, cat_id, cat_name, _, _, _ in team_data:
|
||||||
|
if cat_id and cat_name:
|
||||||
|
category, created = NewCategory.objects.get_or_create(
|
||||||
|
id=cat_id,
|
||||||
|
defaults={
|
||||||
|
'category_name': cat_name,
|
||||||
|
'category_number': cat_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
migrated_categories += 1
|
||||||
|
print(f" カテゴリ作成: {cat_name}")
|
||||||
|
|
||||||
|
print(f"✅ カテゴリ移行完了: {migrated_categories}件作成")
|
||||||
|
|
||||||
|
# === STEP 4: チーム移行 ===
|
||||||
|
print("\\n=== STEP 4: チーム移行 ===")
|
||||||
|
|
||||||
|
migrated_teams = 0
|
||||||
|
for team_id, team_name, owner_id, cat_id, cat_name, email, first_name, last_name in team_data:
|
||||||
|
try:
|
||||||
|
# カテゴリを取得
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
|
||||||
|
# チームを作成
|
||||||
|
team, created = Team.objects.get_or_create(
|
||||||
|
id=team_id,
|
||||||
|
defaults={
|
||||||
|
'team_name': team_name,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'category': category,
|
||||||
|
'event_id': fc_event.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
migrated_teams += 1
|
||||||
|
print(f" チーム作成: {team_name} (ID: {team_id})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ チーム作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f"✅ チーム移行完了: {migrated_teams}件作成")
|
||||||
|
|
||||||
|
# === STEP 5: メンバー移行 ===
|
||||||
|
print("\\n=== STEP 5: メンバー移行 ===")
|
||||||
|
|
||||||
|
migrated_members = 0
|
||||||
|
for team_id, user_id, email, first_name, last_name in member_data:
|
||||||
|
try:
|
||||||
|
# チームとユーザーを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
user = CustomUser.objects.get(id=user_id)
|
||||||
|
|
||||||
|
# メンバーを作成
|
||||||
|
member, created = Member.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
migrated_members += 1
|
||||||
|
print(f" メンバー追加: {email} → {team.team_name}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が見つかりません")
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
print(f" ⚠️ ユーザー{user_id}が見つかりません")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ メンバー追加エラー: {e}")
|
||||||
|
|
||||||
|
print(f"✅ メンバー移行完了: {migrated_members}件作成")
|
||||||
|
|
||||||
|
# === STEP 6: エントリー移行 ===
|
||||||
|
print("\\n=== STEP 6: エントリー移行 ===")
|
||||||
|
|
||||||
|
# まず、現在のDBのis_trialフィールドにデフォルト値を設定
|
||||||
|
print("データベーステーブルのis_trialフィールドを修正中...")
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
try:
|
||||||
|
# is_trialフィールドにデフォルト値を設定
|
||||||
|
django_cursor.execute("""
|
||||||
|
ALTER TABLE rog_entry
|
||||||
|
ALTER COLUMN is_trial SET DEFAULT FALSE;
|
||||||
|
""")
|
||||||
|
print(" ✅ is_trialフィールドにデフォルト値を設定")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ is_trial修正エラー: {e}")
|
||||||
|
|
||||||
|
# FC岐阜エントリーデータを取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id, re.date, re.owner_id,
|
||||||
|
rc.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON re.category_id = rc.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY re.zekken_number;
|
||||||
|
""")
|
||||||
|
|
||||||
|
entry_data = old_cursor.fetchall()
|
||||||
|
migrated_entries = 0
|
||||||
|
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id, cat_name in entry_data:
|
||||||
|
try:
|
||||||
|
# チームとカテゴリを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
|
||||||
|
# まず既存のエントリーをチェック
|
||||||
|
existing_entry = Entry.objects.filter(team=team, event=fc_event).first()
|
||||||
|
if existing_entry:
|
||||||
|
print(f" 🔄 既存エントリー: {team_name} - ゼッケン{existing_entry.zekken_number}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# SQLで直接エントリーを挿入
|
||||||
|
from django.db import connection as django_conn
|
||||||
|
with django_conn.cursor() as django_cursor:
|
||||||
|
django_cursor.execute("""
|
||||||
|
INSERT INTO rog_entry
|
||||||
|
(date, category_id, event_id, owner_id, team_id, is_active,
|
||||||
|
zekken_number, "hasGoaled", "hasParticipated", zekken_label,
|
||||||
|
is_trial, staff_privileges, can_access_private_events, team_validation_status)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
|
||||||
|
""", [
|
||||||
|
fc_event.start_datetime, # date
|
||||||
|
cat_id, # category_id
|
||||||
|
fc_event.id, # event_id
|
||||||
|
owner_id or 1, # owner_id
|
||||||
|
team_id, # team_id
|
||||||
|
True, # is_active
|
||||||
|
int(zekken) if zekken else 0, # zekken_number
|
||||||
|
False, # hasGoaled
|
||||||
|
False, # hasParticipated
|
||||||
|
label or f"FC岐阜-{zekken}", # zekken_label
|
||||||
|
False, # is_trial
|
||||||
|
False, # staff_privileges
|
||||||
|
False, # can_access_private_events
|
||||||
|
'approved' # team_validation_status
|
||||||
|
])
|
||||||
|
|
||||||
|
migrated_entries += 1
|
||||||
|
print(f" ✅ エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ❌ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エントリー作成エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
print(f"✅ エントリー移行完了: {migrated_entries}件作成")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
# === 最終確認 ===
|
||||||
|
print("\\n=== 移行結果確認 ===")
|
||||||
|
fc_entries = Entry.objects.filter(event=fc_event).order_by('zekken_number')
|
||||||
|
print(f"FC岐阜イベント総エントリー: {fc_entries.count()}件")
|
||||||
|
|
||||||
|
if fc_entries.exists():
|
||||||
|
print("\\n🎉 ゼッケン番号一覧(最初の10件):")
|
||||||
|
for entry in fc_entries[:10]:
|
||||||
|
print(f" ゼッケン{entry.zekken_number}: {entry.team.team_name}")
|
||||||
|
print("\\n🎉 FC岐阜イベントのゼッケン番号表示問題が解決されました!")
|
||||||
|
print("\\n🎯 通過審査管理画面でFC岐阜を選択すると、ゼッケン番号が表示されるようになります。")
|
||||||
|
else:
|
||||||
|
print("❌ エントリーデータの移行に失敗しました")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
300
migrate_gps_information.py
Normal file
300
migrate_gps_information.py
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
GPS情報(通過データ)移行スクリプト
|
||||||
|
gifurogeのgps_informationテーブルから新しいrogdbシステムに通過データを移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
from datetime import datetime
|
||||||
|
import psycopg2
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
# Django設定
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from rog.models import (
|
||||||
|
GpsLog, GpsCheckin, CheckinExtended, Entry, NewEvent2,
|
||||||
|
CustomUser, Team, Waypoint, Location2025
|
||||||
|
)
|
||||||
|
|
||||||
|
class GpsInformationMigrator:
|
||||||
|
def __init__(self):
|
||||||
|
# 環境変数から接続情報を取得
|
||||||
|
self.gifuroge_conn_params = {
|
||||||
|
'host': os.environ.get('PG_HOST', 'postgres-db'),
|
||||||
|
'database': 'gifuroge',
|
||||||
|
'user': os.environ.get('POSTGRES_USER', 'postgres'),
|
||||||
|
'password': os.environ.get('POSTGRES_PASS', 'password'),
|
||||||
|
'port': os.environ.get('PG_PORT', 5432),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
self.stats = {
|
||||||
|
'total_gps_info': 0,
|
||||||
|
'migrated_gps_logs': 0,
|
||||||
|
'migrated_checkins': 0,
|
||||||
|
'skipped_records': 0,
|
||||||
|
'errors': 0,
|
||||||
|
'error_details': []
|
||||||
|
}
|
||||||
|
|
||||||
|
def connect_to_gifuroge(self):
|
||||||
|
"""gifurogeデータベースに接続"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(**self.gifuroge_conn_params)
|
||||||
|
return conn
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ gifurogeデータベース接続エラー: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_gps_information_data(self):
|
||||||
|
"""gifurogeのgps_informationデータを取得"""
|
||||||
|
conn = self.connect_to_gifuroge()
|
||||||
|
if not conn:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# まずテーブル構造を確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'gps_information'
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
""")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
print("=== gps_information テーブル構造 ===")
|
||||||
|
for col in columns:
|
||||||
|
print(f"- {col[0]}: {col[1]}")
|
||||||
|
|
||||||
|
# データ数確認
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM gps_information;")
|
||||||
|
total_count = cursor.fetchone()[0]
|
||||||
|
self.stats['total_gps_info'] = total_count
|
||||||
|
print(f"\n📊 gps_information 総レコード数: {total_count}")
|
||||||
|
|
||||||
|
if total_count == 0:
|
||||||
|
print("⚠️ gps_informationテーブルにデータがありません")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 全データを取得(テーブル構造に合わせて修正)
|
||||||
|
cursor.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, minus_photo_flag, colabo_company_memo
|
||||||
|
FROM gps_information
|
||||||
|
ORDER BY create_at, serial_number;
|
||||||
|
""")
|
||||||
|
|
||||||
|
data = cursor.fetchall()
|
||||||
|
print(f"✅ {len(data)}件のgps_informationデータを取得しました")
|
||||||
|
return data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ データ取得エラー: {e}")
|
||||||
|
self.stats['errors'] += 1
|
||||||
|
self.stats['error_details'].append(f"データ取得エラー: {e}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def find_matching_entry(self, zekken_number, event_code):
|
||||||
|
"""ゼッケン番号とイベントコードからEntryを検索"""
|
||||||
|
try:
|
||||||
|
# NewEvent2でイベントを検索
|
||||||
|
events = NewEvent2.objects.filter(event_name__icontains=event_code)
|
||||||
|
if not events.exists():
|
||||||
|
# イベントコードの部分一致で検索
|
||||||
|
events = NewEvent2.objects.filter(
|
||||||
|
event_name__icontains=event_code.replace('_', ' ')
|
||||||
|
)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
# ゼッケン番号でEntryを検索
|
||||||
|
entries = Entry.objects.filter(
|
||||||
|
event=event,
|
||||||
|
zekken_number=zekken_number
|
||||||
|
)
|
||||||
|
if entries.exists():
|
||||||
|
return entries.first()
|
||||||
|
|
||||||
|
# 見つからない場合はNone
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Entry検索エラー (ゼッケン: {zekken_number}, イベント: {event_code}): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_matching_location(self, cp_number):
|
||||||
|
"""CP番号からLocationを検索"""
|
||||||
|
try:
|
||||||
|
if not cp_number:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Location2025から検索
|
||||||
|
locations = Location2025.objects.filter(cp_number=cp_number)
|
||||||
|
if locations.exists():
|
||||||
|
return locations.first()
|
||||||
|
|
||||||
|
# 部分一致で検索
|
||||||
|
locations = Location2025.objects.filter(cp_number__icontains=str(cp_number))
|
||||||
|
if locations.exists():
|
||||||
|
return locations.first()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Location検索エラー (CP: {cp_number}): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def migrate_gps_record(self, record):
|
||||||
|
"""個別のGPS記録を移行"""
|
||||||
|
try:
|
||||||
|
(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) = record
|
||||||
|
|
||||||
|
# checkin_timeはcreate_atを使用
|
||||||
|
checkin_time = create_at or timezone.now()
|
||||||
|
|
||||||
|
# Entryを検索
|
||||||
|
entry = self.find_matching_entry(zekken_number, event_code)
|
||||||
|
if not entry:
|
||||||
|
print(f"⚠️ Entry未発見: ゼッケン{zekken_number}, イベント{event_code}")
|
||||||
|
self.stats['skipped_records'] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Locationを検索(オプション)
|
||||||
|
location = self.find_matching_location(cp_number) if cp_number else None
|
||||||
|
|
||||||
|
# 既存のGpsLogをチェック
|
||||||
|
existing_log = GpsLog.objects.filter(
|
||||||
|
zekken_number=str(zekken_number),
|
||||||
|
event_code=event_code,
|
||||||
|
checkin_time=checkin_time
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_log:
|
||||||
|
print(f"⚠️ 既存記録をスキップ: ゼッケン{zekken_number}, {checkin_time}")
|
||||||
|
self.stats['skipped_records'] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
# GpsLogを作成
|
||||||
|
gps_log = GpsLog.objects.create(
|
||||||
|
serial_number=serial_number or 0,
|
||||||
|
zekken_number=str(zekken_number),
|
||||||
|
event_code=event_code,
|
||||||
|
cp_number=str(cp_number) if cp_number else '',
|
||||||
|
image_address=image_address or '',
|
||||||
|
checkin_time=checkin_time,
|
||||||
|
goal_time=goal_time or '',
|
||||||
|
late_point=late_point or 0,
|
||||||
|
create_at=create_at or timezone.now(),
|
||||||
|
create_user=create_user or '',
|
||||||
|
update_at=update_at or timezone.now(),
|
||||||
|
update_user=update_user or '',
|
||||||
|
buy_flag=buy_flag or False,
|
||||||
|
minus_photo_flag=minus_photo_flag or False,
|
||||||
|
colabo_company_memo=colabo_company_memo or '',
|
||||||
|
is_service_checked=False, # デフォルト値
|
||||||
|
score=0, # デフォルト値
|
||||||
|
scoreboard_url='' # デフォルト値
|
||||||
|
)
|
||||||
|
self.stats['migrated_gps_logs'] += 1
|
||||||
|
|
||||||
|
# CheckinExtendedも作成(通過記録として)
|
||||||
|
if cp_number and location:
|
||||||
|
try:
|
||||||
|
checkin_extended = CheckinExtended.objects.create(
|
||||||
|
entry=entry,
|
||||||
|
location=location,
|
||||||
|
checkin_time=checkin_time,
|
||||||
|
image_url=image_address or '',
|
||||||
|
score_override=0, # デフォルト値
|
||||||
|
notes=f"移行データ: {colabo_company_memo}",
|
||||||
|
is_verified=False # デフォルト値
|
||||||
|
)
|
||||||
|
self.stats['migrated_checkins'] += 1
|
||||||
|
print(f"✅ チェックイン記録作成: ゼッケン{zekken_number}, CP{cp_number}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ CheckinExtended作成エラー: {e}")
|
||||||
|
|
||||||
|
print(f"✅ GPS記録移行完了: ゼッケン{zekken_number}, {checkin_time}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ GPS記録移行エラー: {e}")
|
||||||
|
self.stats['errors'] += 1
|
||||||
|
self.stats['error_details'].append(f"GPS記録移行エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_migration(self):
|
||||||
|
"""メイン移行処理"""
|
||||||
|
print("🚀 GPS情報移行スクリプト開始")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# gifurogeからデータを取得
|
||||||
|
gps_data = self.get_gps_information_data()
|
||||||
|
if not gps_data:
|
||||||
|
print("❌ 移行するデータがありません")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n📋 {len(gps_data)}件のGPS記録の移行を開始...")
|
||||||
|
|
||||||
|
# バッチ処理で移行
|
||||||
|
batch_size = 100
|
||||||
|
total_batches = (len(gps_data) + batch_size - 1) // batch_size
|
||||||
|
|
||||||
|
for batch_num in range(total_batches):
|
||||||
|
start_idx = batch_num * batch_size
|
||||||
|
end_idx = min(start_idx + batch_size, len(gps_data))
|
||||||
|
batch_data = gps_data[start_idx:end_idx]
|
||||||
|
|
||||||
|
print(f"\n📦 バッチ {batch_num + 1}/{total_batches} ({len(batch_data)}件) 処理中...")
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for record in batch_data:
|
||||||
|
self.migrate_gps_record(record)
|
||||||
|
|
||||||
|
# 統計レポート
|
||||||
|
self.print_migration_report()
|
||||||
|
|
||||||
|
def print_migration_report(self):
|
||||||
|
"""移行結果レポート"""
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("📊 GPS情報移行完了レポート")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"📋 総GPS記録数: {self.stats['total_gps_info']}")
|
||||||
|
print(f"✅ 移行済みGpsLog: {self.stats['migrated_gps_logs']}")
|
||||||
|
print(f"✅ 移行済みCheckin: {self.stats['migrated_checkins']}")
|
||||||
|
print(f"⚠️ スキップ記録: {self.stats['skipped_records']}")
|
||||||
|
print(f"❌ エラー数: {self.stats['errors']}")
|
||||||
|
|
||||||
|
if self.stats['error_details']:
|
||||||
|
print("\n❌ エラー詳細:")
|
||||||
|
for error in self.stats['error_details'][:10]: # 最初の10個だけ表示
|
||||||
|
print(f" - {error}")
|
||||||
|
if len(self.stats['error_details']) > 10:
|
||||||
|
print(f" ... 他 {len(self.stats['error_details']) - 10} 件")
|
||||||
|
|
||||||
|
success_rate = (self.stats['migrated_gps_logs'] / max(self.stats['total_gps_info'], 1)) * 100
|
||||||
|
print(f"\n📈 移行成功率: {success_rate:.1f}%")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン実行関数"""
|
||||||
|
migrator = GpsInformationMigrator()
|
||||||
|
migrator.run_migration()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
120
migrate_location_to_location2025_complete.py
Normal file
120
migrate_location_to_location2025_complete.py
Normal file
@ -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()
|
||||||
176
migrate_location_to_location2025_enhanced.py
Normal file
176
migrate_location_to_location2025_enhanced.py
Normal file
@ -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()
|
||||||
150
migrate_location_to_location2025_final.py
Normal file
150
migrate_location_to_location2025_final.py
Normal file
@ -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()
|
||||||
397
migrate_location_to_location2025_with_validation.py
Normal file
397
migrate_location_to_location2025_with_validation.py
Normal file
@ -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()
|
||||||
293
migrate_old_fc_gifu_entries.py
Normal file
293
migrate_old_fc_gifu_entries.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
old_rogdb から rogdb へのFC岐阜エントリー移行スクリプト
|
||||||
|
old_rogdbのFC岐阜イベント(event_id=10)のゼッケン番号付きエントリーを移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import connections, transaction
|
||||||
|
from rog.models import NewEvent2, Team, Entry, NewCategory, CustomUser
|
||||||
|
|
||||||
|
print("=== old_rogdb から FC岐阜エントリー移行 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続を取得
|
||||||
|
default_db = connections['default'] # rogdb
|
||||||
|
old_db = connections.databases.get('old_rogdb')
|
||||||
|
|
||||||
|
if not old_db:
|
||||||
|
print("❌ old_rogdb接続設定が見つかりません。別DB接続を試行します。")
|
||||||
|
|
||||||
|
# old_rogdbに直接接続してデータを取得
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
# old_rogdbへの直接接続
|
||||||
|
old_conn = psycopg2.connect(
|
||||||
|
host='postgres-db',
|
||||||
|
database='old_rogdb',
|
||||||
|
user='admin',
|
||||||
|
password='admin123456'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ old_rogdbに接続成功")
|
||||||
|
|
||||||
|
with old_conn.cursor() as old_cursor:
|
||||||
|
# old_rogdbのFC岐阜エントリーデータを取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id, re.date, re.owner_id,
|
||||||
|
rc.category_name
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
LEFT JOIN rog_newcategory rc ON re.category_id = rc.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY re.zekken_number;
|
||||||
|
""")
|
||||||
|
|
||||||
|
old_fc_data = old_cursor.fetchall()
|
||||||
|
print(f"\\n✅ old_rogdb FC岐阜エントリー: {len(old_fc_data)}件")
|
||||||
|
|
||||||
|
if old_fc_data:
|
||||||
|
print("\\nold_rogdb FC岐阜データサンプル(最初の5件):")
|
||||||
|
for i, (entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id, cat_name) in enumerate(old_fc_data[:5]):
|
||||||
|
print(f" {i+1}. Entry {entry_id}: Team '{team_name}' - ゼッケン{zekken} ({cat_name})")
|
||||||
|
|
||||||
|
# FC岐阜イベントを確認
|
||||||
|
fc_event = NewEvent2.objects.filter(id=10).first()
|
||||||
|
if not fc_event:
|
||||||
|
print("❌ FC岐阜イベント(ID:10)が見つかりません")
|
||||||
|
old_conn.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\\n✅ FC岐阜イベント: {fc_event.event_name}")
|
||||||
|
|
||||||
|
# データ移行開始
|
||||||
|
print("\\n=== old_rogdb から rogdb へデータ移行開始 ===")
|
||||||
|
migrated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id, cat_name in old_fc_data:
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# カテゴリを取得または作成
|
||||||
|
if cat_id and cat_name:
|
||||||
|
category, cat_created = NewCategory.objects.get_or_create(
|
||||||
|
id=cat_id,
|
||||||
|
defaults={
|
||||||
|
'category_name': cat_name,
|
||||||
|
'category_number': cat_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if cat_created:
|
||||||
|
print(f" カテゴリ作成: {cat_name}")
|
||||||
|
else:
|
||||||
|
category = None
|
||||||
|
|
||||||
|
# チームを取得または作成
|
||||||
|
team, team_created = Team.objects.get_or_create(
|
||||||
|
id=team_id,
|
||||||
|
defaults={
|
||||||
|
'team_name': team_name,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'category': category,
|
||||||
|
'event_id': fc_event.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if team_created:
|
||||||
|
print(f" チーム作成: {team_name} (ID: {team_id})")
|
||||||
|
|
||||||
|
# エントリーを作成
|
||||||
|
entry, entry_created = Entry.objects.get_or_create(
|
||||||
|
team=team,
|
||||||
|
event=fc_event,
|
||||||
|
defaults={
|
||||||
|
'category': category,
|
||||||
|
'date': date or fc_event.start_datetime,
|
||||||
|
'owner_id': owner_id or 1,
|
||||||
|
'zekken_number': int(zekken) if zekken else 0,
|
||||||
|
'zekken_label': label or f"FC岐阜-{zekken}",
|
||||||
|
'is_active': True,
|
||||||
|
'hasParticipated': False,
|
||||||
|
'hasGoaled': False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry_created:
|
||||||
|
print(f" ✅ エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
migrated_count += 1
|
||||||
|
else:
|
||||||
|
print(f" 🔄 既存エントリー: {team_name} - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
print(f" ❌ エラー: {team_name} - {e}")
|
||||||
|
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
print(f"\\n=== 移行完了 ===")
|
||||||
|
print(f"移行成功: {migrated_count}件")
|
||||||
|
print(f"エラー: {error_count}件")
|
||||||
|
|
||||||
|
# 最終確認
|
||||||
|
fc_entries = Entry.objects.filter(event=fc_event).order_by('zekken_number')
|
||||||
|
print(f"\\n🎉 FC岐阜イベント総エントリー: {fc_entries.count()}件")
|
||||||
|
|
||||||
|
if fc_entries.exists():
|
||||||
|
print("\\nゼッケン番号一覧(最初の10件):")
|
||||||
|
for entry in fc_entries[:10]:
|
||||||
|
print(f" ゼッケン{entry.zekken_number}: {entry.team.team_name}")
|
||||||
|
print("\\n🎉 FC岐阜イベントのゼッケン番号表示問題が解決されました!")
|
||||||
|
print("\\n🎯 通過審査管理画面でFC岐阜を選択すると、ゼッケン番号が表示されるようになります。")
|
||||||
|
else:
|
||||||
|
print("❌ old_rogdbにもFC岐阜エントリーデータがありません")
|
||||||
|
old_conn.close()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 通常のDjango接続設定がある場合の処理
|
||||||
|
with default_db.cursor() as cursor:
|
||||||
|
# まずold_rogdbスキーマが存在するか確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT schema_name FROM information_schema.schemata
|
||||||
|
WHERE schema_name LIKE '%old%' OR schema_name LIKE '%rog%';
|
||||||
|
""")
|
||||||
|
schemas = cursor.fetchall()
|
||||||
|
print(f"利用可能なスキーマ: {schemas}")
|
||||||
|
|
||||||
|
# old_rogdbデータベースに直接接続を試行
|
||||||
|
cursor.execute("SELECT current_database();")
|
||||||
|
current_db = cursor.fetchone()[0]
|
||||||
|
print(f"現在のDB: {current_db}")
|
||||||
|
|
||||||
|
# データベース一覧を確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT datname FROM pg_database
|
||||||
|
WHERE datistemplate = false AND datname != 'postgres';
|
||||||
|
""")
|
||||||
|
databases = cursor.fetchall()
|
||||||
|
print(f"利用可能なDB: {[db[0] for db in databases]}")
|
||||||
|
|
||||||
|
# old_rogdbのrog_entryデータを確認
|
||||||
|
try:
|
||||||
|
# 別データベースのテーブルにアクセスする方法を試行
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT table_name FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE '%entry%';
|
||||||
|
""")
|
||||||
|
entry_tables = cursor.fetchall()
|
||||||
|
print(f"エントリー関連テーブル: {entry_tables}")
|
||||||
|
|
||||||
|
# FC岐阜関連のエントリーデータを確認
|
||||||
|
# まず現在のDBで状況確認
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_entry WHERE event_id = 10;
|
||||||
|
""")
|
||||||
|
current_fc_entries = cursor.fetchone()[0]
|
||||||
|
print(f"現在のDB FC岐阜エントリー: {current_fc_entries}件")
|
||||||
|
|
||||||
|
if current_fc_entries > 0:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
AND re.zekken_number IS NOT NULL
|
||||||
|
ORDER BY re.zekken_number
|
||||||
|
LIMIT 10;
|
||||||
|
""")
|
||||||
|
fc_data = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"\\n✅ FC岐阜エントリーデータ(最初の10件):")
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id in fc_data:
|
||||||
|
print(f" Entry {entry_id}: Team {team_id} '{team_name}' - ゼッケン{zekken}")
|
||||||
|
|
||||||
|
# FC岐阜イベントを取得
|
||||||
|
fc_event = NewEvent2.objects.filter(id=10).first()
|
||||||
|
if not fc_event:
|
||||||
|
print("❌ FC岐阜イベント(ID:10)が見つかりません")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\\n✅ FC岐阜イベント: {fc_event.event_name}")
|
||||||
|
|
||||||
|
# エントリーデータを新しいEntry modelに同期
|
||||||
|
print("\\n=== エントリーデータ同期開始 ===")
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
# 全FC岐阜エントリーを取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT re.id, re.team_id, re.zekken_number, re.zekken_label,
|
||||||
|
rt.team_name, re.category_id, re.date, re.owner_id
|
||||||
|
FROM rog_entry re
|
||||||
|
JOIN rog_team rt ON re.team_id = rt.id
|
||||||
|
WHERE re.event_id = 10
|
||||||
|
ORDER BY re.zekken_number;
|
||||||
|
""")
|
||||||
|
all_fc_data = cursor.fetchall()
|
||||||
|
|
||||||
|
for entry_id, team_id, zekken, label, team_name, cat_id, date, owner_id in all_fc_data:
|
||||||
|
try:
|
||||||
|
# チームを取得
|
||||||
|
team = Team.objects.get(id=team_id)
|
||||||
|
|
||||||
|
# カテゴリを取得
|
||||||
|
category = NewCategory.objects.get(id=cat_id) if cat_id else None
|
||||||
|
|
||||||
|
# エントリーを更新または作成
|
||||||
|
entry, created = Entry.objects.update_or_create(
|
||||||
|
team=team,
|
||||||
|
event=fc_event,
|
||||||
|
defaults={
|
||||||
|
'category': category,
|
||||||
|
'date': date or fc_event.start_datetime,
|
||||||
|
'owner_id': owner_id,
|
||||||
|
'zekken_number': int(zekken) if zekken else 0,
|
||||||
|
'zekken_label': label or f"FC岐阜-{zekken}",
|
||||||
|
'is_active': True,
|
||||||
|
'hasParticipated': False,
|
||||||
|
'hasGoaled': False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
print(f" ✅ エントリー作成: {team_name} - ゼッケン{zekken}")
|
||||||
|
else:
|
||||||
|
print(f" 🔄 エントリー更新: {team_name} - ゼッケン{zekken}")
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
print(f" ⚠️ チーム{team_id}が見つかりません: {team_name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ エラー: {e}")
|
||||||
|
|
||||||
|
print(f"\\n✅ 処理完了: {updated_count}件のエントリーを処理")
|
||||||
|
|
||||||
|
# 最終確認
|
||||||
|
fc_entries = Entry.objects.filter(event=fc_event).order_by('zekken_number')
|
||||||
|
print(f"\\n🎉 FC岐阜イベント総エントリー: {fc_entries.count()}件")
|
||||||
|
|
||||||
|
if fc_entries.exists():
|
||||||
|
print("\\nゼッケン番号一覧(最初の10件):")
|
||||||
|
for entry in fc_entries[:10]:
|
||||||
|
print(f" ゼッケン{entry.zekken_number}: {entry.team.team_name}")
|
||||||
|
print("\\n🎉 FC岐阜イベントのゼッケン番号表示問題が解決されました!")
|
||||||
|
else:
|
||||||
|
print("❌ 現在のDBにFC岐阜エントリーデータがありません")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ データ確認エラー: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ エラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
93
migrate_old_rogdb_quickstart.sh
Normal file
93
migrate_old_rogdb_quickstart.sh
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Old RogDB → RogDB 移行クイックスタート
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Old RogDB → RogDB データ移行クイックスタート"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
# 実行前チェック
|
||||||
|
echo "🔍 実行前チェック..."
|
||||||
|
|
||||||
|
# Docker Composeサービス確認
|
||||||
|
if ! docker compose ps | grep -q "Up"; then
|
||||||
|
echo "❌ Docker Composeサービスが起動していません"
|
||||||
|
echo "次のコマンドでサービスを起動してください:"
|
||||||
|
echo "docker compose up -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Docker Composeサービス確認完了"
|
||||||
|
|
||||||
|
# データベース接続確認
|
||||||
|
echo "🔍 データベース接続確認..."
|
||||||
|
if ! docker compose exec app python -c "
|
||||||
|
import psycopg2
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(host='postgres-db', database='rogdb', user='admin', password='admin123456')
|
||||||
|
print('✅ rogdb接続成功')
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ rogdb接続エラー: {e}')
|
||||||
|
exit(1)
|
||||||
|
"; then
|
||||||
|
echo "❌ データベース接続に失敗しました"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# メニュー表示
|
||||||
|
echo ""
|
||||||
|
echo "📋 移行オプションを選択してください:"
|
||||||
|
echo "1) 完全移行(全rog_*テーブル)"
|
||||||
|
echo "2) 安全移行(ユーザー関連テーブル除外)"
|
||||||
|
echo "3) 統計情報のみ表示"
|
||||||
|
echo "4) テーブル一覧のみ表示"
|
||||||
|
echo "5) カスタム移行(除外テーブル指定)"
|
||||||
|
echo "0) キャンセル"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "選択 (0-5): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
echo "🚀 完全移行を開始します..."
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo "🛡️ 安全移行を開始します(ユーザー関連テーブル除外)..."
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo "📊 統計情報を表示します..."
|
||||||
|
make migrate-old-rogdb-stats
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo "📋 テーブル一覧を表示します..."
|
||||||
|
make migrate-old-rogdb-dryrun
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo "除外するテーブル名をカンマ区切りで入力してください(例: rog_customuser,rog_session):"
|
||||||
|
read -p "除外テーブル: " exclude_tables
|
||||||
|
echo "🔧 カスタム移行を開始します..."
|
||||||
|
docker compose exec -e EXCLUDE_TABLES="$exclude_tables" app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
;;
|
||||||
|
0)
|
||||||
|
echo "移行をキャンセルしました"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ 無効な選択です"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "移行処理が完了しました"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "📋 移行後の確認方法:"
|
||||||
|
echo " - ログ確認: make app-logs"
|
||||||
|
echo " - DB接続: make db-shell"
|
||||||
|
echo " - 統計再表示: make migrate-old-rogdb-stats"
|
||||||
|
echo ""
|
||||||
|
echo "📖 詳細情報: MIGRATE_OLD_ROGDB_README.md"
|
||||||
499
migrate_old_rogdb_to_rogdb.py
Normal file
499
migrate_old_rogdb_to_rogdb.py
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Old RogDB データ移行スクリプト (エラー修正版)
|
||||||
|
old_rogdb の rog_* テーブルから rogdb の rog_* テーブルへデータを更新・挿入
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL予約語をクォートで囲む必要があるカラム
|
||||||
|
RESERVED_KEYWORDS = {
|
||||||
|
'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert',
|
||||||
|
'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint'
|
||||||
|
}
|
||||||
|
|
||||||
|
class RogTableMigrator:
|
||||||
|
"""Rogテーブル移行クラス (エラー修正版)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.old_conn = None
|
||||||
|
self.new_conn = None
|
||||||
|
self.old_cursor = None
|
||||||
|
self.new_cursor = None
|
||||||
|
self.migration_stats = {}
|
||||||
|
|
||||||
|
def quote_column_if_needed(self, column_name):
|
||||||
|
"""予約語やキャメルケースの場合はダブルクォートで囲む"""
|
||||||
|
# 予約語チェック
|
||||||
|
if column_name.lower() in RESERVED_KEYWORDS:
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
# キャメルケースや大文字が含まれる場合もクォート
|
||||||
|
if any(c.isupper() for c in column_name) or column_name != column_name.lower():
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
def handle_null_values(self, table_name, column_name, value):
|
||||||
|
"""NULL値の処理とデフォルト値設定"""
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# テーブル・カラム別のデフォルト値設定
|
||||||
|
null_defaults = {
|
||||||
|
'rog_team': {
|
||||||
|
'trial': False, # Boolean型のデフォルト
|
||||||
|
'is_active': True,
|
||||||
|
'is_trial': False,
|
||||||
|
},
|
||||||
|
'rog_entry': {
|
||||||
|
'trial': False,
|
||||||
|
'is_active': True,
|
||||||
|
'is_trial': False,
|
||||||
|
'hasGoaled': False,
|
||||||
|
'hasParticipated': False,
|
||||||
|
},
|
||||||
|
'rog_member': {
|
||||||
|
'is_active': True,
|
||||||
|
'is_captain': False,
|
||||||
|
},
|
||||||
|
'rog_newevent2': {
|
||||||
|
'public': True,
|
||||||
|
'class_general': True,
|
||||||
|
'class_family': True,
|
||||||
|
'class_solo_male': True,
|
||||||
|
'class_solo_female': True,
|
||||||
|
'hour_3': True,
|
||||||
|
'hour_5': True,
|
||||||
|
'self_rogaining': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# テーブル固有のデフォルト値を取得
|
||||||
|
if table_name in null_defaults and column_name in null_defaults[table_name]:
|
||||||
|
default_value = null_defaults[table_name][column_name]
|
||||||
|
logger.debug(f"NULL値をデフォルト値に変換: {table_name}.{column_name} = {default_value}")
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
# 一般的なデフォルト値
|
||||||
|
common_defaults = {
|
||||||
|
# Boolean型
|
||||||
|
'is_active': True,
|
||||||
|
'is_trial': False,
|
||||||
|
'public': True,
|
||||||
|
'trial': False,
|
||||||
|
# 文字列型
|
||||||
|
'description': '',
|
||||||
|
'note': '',
|
||||||
|
'comment': '',
|
||||||
|
# 数値型
|
||||||
|
'sort_order': 0,
|
||||||
|
'order': 0,
|
||||||
|
# PostgreSQL予約語
|
||||||
|
'group': '',
|
||||||
|
'like': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if column_name in common_defaults:
|
||||||
|
default_value = common_defaults[column_name]
|
||||||
|
logger.debug(f"NULL値を共通デフォルト値に変換: {column_name} = {default_value}")
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
# デフォルト値が見つからない場合はNULLを返す
|
||||||
|
logger.warning(f"デフォルト値が設定されていません: {table_name}.{column_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def connect_databases(self):
|
||||||
|
"""データベース接続"""
|
||||||
|
try:
|
||||||
|
logger.info("データベースに接続中...")
|
||||||
|
self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
self.new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
# autocommitを有効にしてトランザクション問題を回避
|
||||||
|
self.old_conn.autocommit = True
|
||||||
|
self.new_conn.autocommit = False # 新しい方は手動コミット
|
||||||
|
|
||||||
|
self.old_cursor = self.old_conn.cursor()
|
||||||
|
self.new_cursor = self.new_conn.cursor()
|
||||||
|
|
||||||
|
logger.info("✅ データベース接続成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
"""データベース接続クローズ"""
|
||||||
|
try:
|
||||||
|
if self.old_cursor:
|
||||||
|
self.old_cursor.close()
|
||||||
|
if self.new_cursor:
|
||||||
|
self.new_cursor.close()
|
||||||
|
if self.old_conn:
|
||||||
|
self.old_conn.close()
|
||||||
|
if self.new_conn:
|
||||||
|
self.new_conn.close()
|
||||||
|
logger.info("データベース接続をクローズしました")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"接続クローズ時の警告: {e}")
|
||||||
|
|
||||||
|
def get_rog_tables(self):
|
||||||
|
"""rog_で始まるテーブル一覧を取得"""
|
||||||
|
try:
|
||||||
|
# old_rogdbのrog_テーブル一覧
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
old_tables = [row[0] for row in self.old_cursor.fetchall()]
|
||||||
|
|
||||||
|
# rogdbのrog_テーブル一覧
|
||||||
|
self.new_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
new_tables = [row[0] for row in self.new_cursor.fetchall()]
|
||||||
|
|
||||||
|
# 共通テーブル
|
||||||
|
common_tables = list(set(old_tables) & set(new_tables))
|
||||||
|
|
||||||
|
logger.info(f"old_rogdb rog_テーブル: {len(old_tables)}個")
|
||||||
|
logger.info(f"rogdb rog_テーブル: {len(new_tables)}個")
|
||||||
|
logger.info(f"共通 rog_テーブル: {len(common_tables)}個")
|
||||||
|
|
||||||
|
return old_tables, new_tables, common_tables
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル一覧取得エラー: {e}")
|
||||||
|
return [], [], []
|
||||||
|
|
||||||
|
def get_table_structure(self, table_name, cursor):
|
||||||
|
"""テーブル構造を取得 (エラーハンドリング強化)"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
return {
|
||||||
|
'columns': [col[0] for col in columns],
|
||||||
|
'details': columns
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル構造取得エラー ({table_name}): {e}")
|
||||||
|
# トランザクションエラーを回避
|
||||||
|
try:
|
||||||
|
cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'columns': [], 'details': []}
|
||||||
|
|
||||||
|
def get_primary_key(self, table_name, cursor):
|
||||||
|
"""主キーカラムを取得"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT a.attname
|
||||||
|
FROM pg_index i
|
||||||
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||||
|
WHERE i.indrelid = %s::regclass AND i.indisprimary
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
pk_columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
return pk_columns if pk_columns else ['id']
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"主キー取得エラー ({table_name}): {e}")
|
||||||
|
return ['id'] # デフォルトでidを使用
|
||||||
|
|
||||||
|
def migrate_table_data(self, table_name):
|
||||||
|
"""個別テーブルのデータ移行 (エラー修正版)"""
|
||||||
|
logger.info(f"\n=== {table_name} データ移行開始 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# テーブル構造比較
|
||||||
|
old_structure = self.get_table_structure(table_name, self.old_cursor)
|
||||||
|
new_structure = self.get_table_structure(table_name, self.new_cursor)
|
||||||
|
|
||||||
|
old_columns = set(old_structure['columns'])
|
||||||
|
new_columns = set(new_structure['columns'])
|
||||||
|
common_columns = old_columns & new_columns
|
||||||
|
|
||||||
|
if not common_columns:
|
||||||
|
logger.warning(f"⚠️ {table_name}: 共通カラムがありません")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"共通カラム ({len(common_columns)}個): {sorted(common_columns)}")
|
||||||
|
|
||||||
|
# 主キー取得
|
||||||
|
pk_columns = self.get_primary_key(table_name, self.new_cursor)
|
||||||
|
logger.info(f"主キー: {pk_columns}")
|
||||||
|
|
||||||
|
# カラム名を予約語対応でクォート
|
||||||
|
quoted_columns = [self.quote_column_if_needed(col) for col in common_columns]
|
||||||
|
columns_str = ', '.join(quoted_columns)
|
||||||
|
|
||||||
|
# old_rogdbからデータ取得
|
||||||
|
try:
|
||||||
|
self.old_cursor.execute(f"SELECT {columns_str} FROM {table_name}")
|
||||||
|
old_records = self.old_cursor.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} データ取得エラー: {e}")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
if not old_records:
|
||||||
|
logger.info(f"✅ {table_name}: 移行対象データなし")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"移行対象レコード数: {len(old_records)}件")
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
# レコード移行処理
|
||||||
|
for i, record in enumerate(old_records):
|
||||||
|
try:
|
||||||
|
# レコードデータを辞書形式に変換(NULL値処理込み)
|
||||||
|
record_dict = {}
|
||||||
|
for j, col in enumerate(common_columns):
|
||||||
|
original_value = record[j]
|
||||||
|
processed_value = self.handle_null_values(table_name, col, original_value)
|
||||||
|
record_dict[col] = processed_value
|
||||||
|
|
||||||
|
# 主キー値を取得
|
||||||
|
pk_values = []
|
||||||
|
pk_conditions = []
|
||||||
|
for pk_col in pk_columns:
|
||||||
|
if pk_col in record_dict:
|
||||||
|
pk_values.append(record_dict[pk_col])
|
||||||
|
quoted_pk_col = self.quote_column_if_needed(pk_col)
|
||||||
|
pk_conditions.append(f"{quoted_pk_col} = %s")
|
||||||
|
|
||||||
|
if not pk_values:
|
||||||
|
error_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 既存レコード確認
|
||||||
|
check_query = f"SELECT COUNT(*) FROM {table_name} WHERE {' AND '.join(pk_conditions)}"
|
||||||
|
self.new_cursor.execute(check_query, pk_values)
|
||||||
|
exists = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# UPDATE処理
|
||||||
|
set_clauses = []
|
||||||
|
update_values = []
|
||||||
|
|
||||||
|
for col in common_columns:
|
||||||
|
if col not in pk_columns: # 主キー以外を更新
|
||||||
|
quoted_col = self.quote_column_if_needed(col)
|
||||||
|
set_clauses.append(f"{quoted_col} = %s")
|
||||||
|
update_values.append(record_dict[col])
|
||||||
|
|
||||||
|
if set_clauses:
|
||||||
|
update_query = f"""
|
||||||
|
UPDATE {table_name}
|
||||||
|
SET {', '.join(set_clauses)}
|
||||||
|
WHERE {' AND '.join(pk_conditions)}
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(update_query, update_values + pk_values)
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# INSERT処理
|
||||||
|
insert_columns = list(common_columns)
|
||||||
|
insert_values = [record_dict[col] for col in insert_columns]
|
||||||
|
quoted_insert_columns = [self.quote_column_if_needed(col) for col in insert_columns]
|
||||||
|
placeholders = ', '.join(['%s'] * len(insert_columns))
|
||||||
|
|
||||||
|
insert_query = f"""
|
||||||
|
INSERT INTO {table_name} ({', '.join(quoted_insert_columns)})
|
||||||
|
VALUES ({placeholders})
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(insert_query, insert_values)
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
# 定期的なコミット
|
||||||
|
if (i + 1) % 100 == 0:
|
||||||
|
self.new_conn.commit()
|
||||||
|
logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
logger.error(f" レコード処理エラー (行{i+1}): {e}")
|
||||||
|
# トランザクションロールバック
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if error_count > 10: # エラー上限
|
||||||
|
logger.error(f"❌ {table_name}: エラー数が上限を超えました")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 最終コミット
|
||||||
|
try:
|
||||||
|
self.new_conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"コミットエラー: {e}")
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
stats = {
|
||||||
|
'inserted': inserted_count,
|
||||||
|
'updated': updated_count,
|
||||||
|
'errors': error_count,
|
||||||
|
'total': len(old_records)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"✅ {table_name} 移行完了:")
|
||||||
|
logger.info(f" 挿入: {inserted_count}件")
|
||||||
|
logger.info(f" 更新: {updated_count}件")
|
||||||
|
logger.info(f" エラー: {error_count}件")
|
||||||
|
|
||||||
|
self.migration_stats[table_name] = stats
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} 移行エラー: {e}")
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
def run_migration(self, exclude_tables=None):
|
||||||
|
"""全体移行実行"""
|
||||||
|
if exclude_tables is None:
|
||||||
|
exclude_tables = []
|
||||||
|
|
||||||
|
# 特殊処理が必要なテーブルは専用スクリプトで処理
|
||||||
|
exclude_tables.extend(['rog_team', 'rog_entry', 'rog_goalimages'])
|
||||||
|
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("Old RogDB → RogDB データ移行開始")
|
||||||
|
logger.info("⚠️ rog_team は migrate_rog_team_enhanced.py で別途処理してください")
|
||||||
|
logger.info("⚠️ rog_entry は migrate_rog_entry_enhanced.py で別途処理してください")
|
||||||
|
logger.info("⚠️ rog_goalimages は migrate_rog_goalimages_enhanced.py で別途処理してください")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続
|
||||||
|
if not self.connect_databases():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# テーブル一覧取得
|
||||||
|
old_tables, new_tables, common_tables = self.get_rog_tables()
|
||||||
|
|
||||||
|
if not common_tables:
|
||||||
|
logger.error("❌ 移行対象の共通テーブルがありません")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 除外テーブルを除去
|
||||||
|
migration_tables = [t for t in common_tables if t not in exclude_tables]
|
||||||
|
|
||||||
|
if exclude_tables:
|
||||||
|
logger.info(f"除外テーブル: {exclude_tables}")
|
||||||
|
|
||||||
|
logger.info(f"移行対象テーブル ({len(migration_tables)}個): {migration_tables}")
|
||||||
|
|
||||||
|
# テーブル別移行実行
|
||||||
|
total_inserted = 0
|
||||||
|
total_updated = 0
|
||||||
|
total_errors = 0
|
||||||
|
|
||||||
|
for table_name in migration_tables:
|
||||||
|
stats = self.migrate_table_data(table_name)
|
||||||
|
total_inserted += stats['inserted']
|
||||||
|
total_updated += stats['updated']
|
||||||
|
total_errors += stats['errors']
|
||||||
|
|
||||||
|
# 最終結果
|
||||||
|
logger.info("\n" + "=" * 80)
|
||||||
|
logger.info("移行完了サマリー")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info(f"処理対象テーブル: {len(migration_tables)}個")
|
||||||
|
logger.info(f"総挿入件数: {total_inserted}件")
|
||||||
|
logger.info(f"総更新件数: {total_updated}件")
|
||||||
|
logger.info(f"総エラー件数: {total_errors}件")
|
||||||
|
|
||||||
|
# テーブル別詳細
|
||||||
|
logger.info("\n--- テーブル別詳細 ---")
|
||||||
|
for table_name, stats in self.migration_stats.items():
|
||||||
|
logger.info(f"{table_name}: 挿入{stats['inserted']}, 更新{stats['updated']}, エラー{stats['errors']}")
|
||||||
|
|
||||||
|
if total_errors == 0:
|
||||||
|
logger.info("✅ 全ての移行が正常に完了しました!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ {total_errors}件のエラーがありました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 移行処理エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.close_connections()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
logger.info("Old RogDB → RogDB データ移行スクリプト")
|
||||||
|
|
||||||
|
# 除外テーブルの設定(必要に応じて)
|
||||||
|
exclude_tables = [
|
||||||
|
# 'rog_customuser', # ユーザーデータは慎重に扱う
|
||||||
|
# 'rog_session', # セッションデータは移行不要
|
||||||
|
# 'django_migrations', # Django管理テーブル
|
||||||
|
]
|
||||||
|
|
||||||
|
# 環境変数による除外テーブル指定
|
||||||
|
env_exclude = os.getenv('EXCLUDE_TABLES', '')
|
||||||
|
if env_exclude:
|
||||||
|
exclude_tables.extend(env_exclude.split(','))
|
||||||
|
|
||||||
|
# 移行実行
|
||||||
|
migrator = RogTableMigrator()
|
||||||
|
success = migrator.run_migration(exclude_tables=exclude_tables)
|
||||||
|
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
414
migrate_old_rogdb_to_rogdb_fixed.py
Normal file
414
migrate_old_rogdb_to_rogdb_fixed.py
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Old RogDB データ移行スクリプト (エラー修正版)
|
||||||
|
old_rogdb の rog_* テーブルから rogdb の rog_* テーブルへデータを更新・挿入
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL予約語をクォートで囲む必要があるカラム
|
||||||
|
RESERVED_KEYWORDS = {
|
||||||
|
'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert',
|
||||||
|
'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint'
|
||||||
|
}
|
||||||
|
|
||||||
|
class RogTableMigrator:
|
||||||
|
"""Rogテーブル移行クラス (エラー修正版)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.old_conn = None
|
||||||
|
self.new_conn = None
|
||||||
|
self.old_cursor = None
|
||||||
|
self.new_cursor = None
|
||||||
|
self.migration_stats = {}
|
||||||
|
|
||||||
|
def quote_column_if_needed(self, column_name):
|
||||||
|
"""予約語やキャメルケースの場合はダブルクォートで囲む"""
|
||||||
|
# 予約語チェック
|
||||||
|
if column_name.lower() in RESERVED_KEYWORDS:
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
# キャメルケースや大文字が含まれる場合もクォート
|
||||||
|
if any(c.isupper() for c in column_name) or column_name != column_name.lower():
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
def connect_databases(self):
|
||||||
|
"""データベース接続"""
|
||||||
|
try:
|
||||||
|
logger.info("データベースに接続中...")
|
||||||
|
self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
self.new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
# autocommitを有効にしてトランザクション問題を回避
|
||||||
|
self.old_conn.autocommit = True
|
||||||
|
self.new_conn.autocommit = False # 新しい方は手動コミット
|
||||||
|
|
||||||
|
self.old_cursor = self.old_conn.cursor()
|
||||||
|
self.new_cursor = self.new_conn.cursor()
|
||||||
|
|
||||||
|
logger.info("✅ データベース接続成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
"""データベース接続クローズ"""
|
||||||
|
try:
|
||||||
|
if self.old_cursor:
|
||||||
|
self.old_cursor.close()
|
||||||
|
if self.new_cursor:
|
||||||
|
self.new_cursor.close()
|
||||||
|
if self.old_conn:
|
||||||
|
self.old_conn.close()
|
||||||
|
if self.new_conn:
|
||||||
|
self.new_conn.close()
|
||||||
|
logger.info("データベース接続をクローズしました")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"接続クローズ時の警告: {e}")
|
||||||
|
|
||||||
|
def get_rog_tables(self):
|
||||||
|
"""rog_で始まるテーブル一覧を取得"""
|
||||||
|
try:
|
||||||
|
# old_rogdbのrog_テーブル一覧
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
old_tables = [row[0] for row in self.old_cursor.fetchall()]
|
||||||
|
|
||||||
|
# rogdbのrog_テーブル一覧
|
||||||
|
self.new_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
new_tables = [row[0] for row in self.new_cursor.fetchall()]
|
||||||
|
|
||||||
|
# 共通テーブル
|
||||||
|
common_tables = list(set(old_tables) & set(new_tables))
|
||||||
|
|
||||||
|
logger.info(f"old_rogdb rog_テーブル: {len(old_tables)}個")
|
||||||
|
logger.info(f"rogdb rog_テーブル: {len(new_tables)}個")
|
||||||
|
logger.info(f"共通 rog_テーブル: {len(common_tables)}個")
|
||||||
|
|
||||||
|
return old_tables, new_tables, common_tables
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル一覧取得エラー: {e}")
|
||||||
|
return [], [], []
|
||||||
|
|
||||||
|
def get_table_structure(self, table_name, cursor):
|
||||||
|
"""テーブル構造を取得 (エラーハンドリング強化)"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
return {
|
||||||
|
'columns': [col[0] for col in columns],
|
||||||
|
'details': columns
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル構造取得エラー ({table_name}): {e}")
|
||||||
|
# トランザクションエラーを回避
|
||||||
|
try:
|
||||||
|
cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'columns': [], 'details': []}
|
||||||
|
|
||||||
|
def get_primary_key(self, table_name, cursor):
|
||||||
|
"""主キーカラムを取得"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT a.attname
|
||||||
|
FROM pg_index i
|
||||||
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||||
|
WHERE i.indrelid = %s::regclass AND i.indisprimary
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
pk_columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
return pk_columns if pk_columns else ['id']
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"主キー取得エラー ({table_name}): {e}")
|
||||||
|
return ['id'] # デフォルトでidを使用
|
||||||
|
|
||||||
|
def migrate_table_data(self, table_name):
|
||||||
|
"""個別テーブルのデータ移行 (エラー修正版)"""
|
||||||
|
logger.info(f"\n=== {table_name} データ移行開始 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# テーブル構造比較
|
||||||
|
old_structure = self.get_table_structure(table_name, self.old_cursor)
|
||||||
|
new_structure = self.get_table_structure(table_name, self.new_cursor)
|
||||||
|
|
||||||
|
old_columns = set(old_structure['columns'])
|
||||||
|
new_columns = set(new_structure['columns'])
|
||||||
|
common_columns = old_columns & new_columns
|
||||||
|
|
||||||
|
if not common_columns:
|
||||||
|
logger.warning(f"⚠️ {table_name}: 共通カラムがありません")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"共通カラム ({len(common_columns)}個): {sorted(common_columns)}")
|
||||||
|
|
||||||
|
# 主キー取得
|
||||||
|
pk_columns = self.get_primary_key(table_name, self.new_cursor)
|
||||||
|
logger.info(f"主キー: {pk_columns}")
|
||||||
|
|
||||||
|
# カラム名を予約語対応でクォート
|
||||||
|
quoted_columns = [self.quote_column_if_needed(col) for col in common_columns]
|
||||||
|
columns_str = ', '.join(quoted_columns)
|
||||||
|
|
||||||
|
# old_rogdbからデータ取得
|
||||||
|
try:
|
||||||
|
self.old_cursor.execute(f"SELECT {columns_str} FROM {table_name}")
|
||||||
|
old_records = self.old_cursor.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} データ取得エラー: {e}")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
if not old_records:
|
||||||
|
logger.info(f"✅ {table_name}: 移行対象データなし")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"移行対象レコード数: {len(old_records)}件")
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
# レコード移行処理
|
||||||
|
for i, record in enumerate(old_records):
|
||||||
|
try:
|
||||||
|
# レコードデータを辞書形式に変換
|
||||||
|
record_dict = dict(zip(common_columns, record))
|
||||||
|
|
||||||
|
# 主キー値を取得
|
||||||
|
pk_values = []
|
||||||
|
pk_conditions = []
|
||||||
|
for pk_col in pk_columns:
|
||||||
|
if pk_col in record_dict:
|
||||||
|
pk_values.append(record_dict[pk_col])
|
||||||
|
quoted_pk_col = self.quote_column_if_needed(pk_col)
|
||||||
|
pk_conditions.append(f"{quoted_pk_col} = %s")
|
||||||
|
|
||||||
|
if not pk_values:
|
||||||
|
error_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 既存レコード確認
|
||||||
|
check_query = f"SELECT COUNT(*) FROM {table_name} WHERE {' AND '.join(pk_conditions)}"
|
||||||
|
self.new_cursor.execute(check_query, pk_values)
|
||||||
|
exists = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# UPDATE処理
|
||||||
|
set_clauses = []
|
||||||
|
update_values = []
|
||||||
|
|
||||||
|
for col in common_columns:
|
||||||
|
if col not in pk_columns: # 主キー以外を更新
|
||||||
|
quoted_col = self.quote_column_if_needed(col)
|
||||||
|
set_clauses.append(f"{quoted_col} = %s")
|
||||||
|
update_values.append(record_dict[col])
|
||||||
|
|
||||||
|
if set_clauses:
|
||||||
|
update_query = f"""
|
||||||
|
UPDATE {table_name}
|
||||||
|
SET {', '.join(set_clauses)}
|
||||||
|
WHERE {' AND '.join(pk_conditions)}
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(update_query, update_values + pk_values)
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# INSERT処理
|
||||||
|
insert_columns = list(common_columns)
|
||||||
|
insert_values = [record_dict[col] for col in insert_columns]
|
||||||
|
quoted_insert_columns = [self.quote_column_if_needed(col) for col in insert_columns]
|
||||||
|
placeholders = ', '.join(['%s'] * len(insert_columns))
|
||||||
|
|
||||||
|
insert_query = f"""
|
||||||
|
INSERT INTO {table_name} ({', '.join(quoted_insert_columns)})
|
||||||
|
VALUES ({placeholders})
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(insert_query, insert_values)
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
# 定期的なコミット
|
||||||
|
if (i + 1) % 100 == 0:
|
||||||
|
self.new_conn.commit()
|
||||||
|
logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
logger.error(f" レコード処理エラー (行{i+1}): {e}")
|
||||||
|
# トランザクションロールバック
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if error_count > 10: # エラー上限
|
||||||
|
logger.error(f"❌ {table_name}: エラー数が上限を超えました")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 最終コミット
|
||||||
|
try:
|
||||||
|
self.new_conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"コミットエラー: {e}")
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
stats = {
|
||||||
|
'inserted': inserted_count,
|
||||||
|
'updated': updated_count,
|
||||||
|
'errors': error_count,
|
||||||
|
'total': len(old_records)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"✅ {table_name} 移行完了:")
|
||||||
|
logger.info(f" 挿入: {inserted_count}件")
|
||||||
|
logger.info(f" 更新: {updated_count}件")
|
||||||
|
logger.info(f" エラー: {error_count}件")
|
||||||
|
|
||||||
|
self.migration_stats[table_name] = stats
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} 移行エラー: {e}")
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
def run_migration(self, exclude_tables=None):
|
||||||
|
"""全体移行実行"""
|
||||||
|
if exclude_tables is None:
|
||||||
|
exclude_tables = []
|
||||||
|
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("Old RogDB → RogDB データ移行開始")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続
|
||||||
|
if not self.connect_databases():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# テーブル一覧取得
|
||||||
|
old_tables, new_tables, common_tables = self.get_rog_tables()
|
||||||
|
|
||||||
|
if not common_tables:
|
||||||
|
logger.error("❌ 移行対象テーブルがありません")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 除外テーブルをフィルタリング
|
||||||
|
migration_tables = [t for t in common_tables if t not in exclude_tables]
|
||||||
|
logger.info(f"移行対象テーブル ({len(migration_tables)}個): {migration_tables}")
|
||||||
|
|
||||||
|
# 各テーブルを移行
|
||||||
|
total_inserted = 0
|
||||||
|
total_updated = 0
|
||||||
|
total_errors = 0
|
||||||
|
|
||||||
|
for table_name in migration_tables:
|
||||||
|
stats = self.migrate_table_data(table_name)
|
||||||
|
total_inserted += stats['inserted']
|
||||||
|
total_updated += stats['updated']
|
||||||
|
total_errors += stats['errors']
|
||||||
|
|
||||||
|
# 全体サマリー出力
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("移行完了サマリー")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info(f"処理対象テーブル: {len(migration_tables)}個")
|
||||||
|
logger.info(f"総挿入件数: {total_inserted}件")
|
||||||
|
logger.info(f"総更新件数: {total_updated}件")
|
||||||
|
logger.info(f"総エラー件数: {total_errors}件")
|
||||||
|
|
||||||
|
logger.info("\n--- テーブル別詳細 ---")
|
||||||
|
for table_name, stats in self.migration_stats.items():
|
||||||
|
logger.info(f"{table_name}: 挿入{stats['inserted']}, 更新{stats['updated']}, エラー{stats['errors']}")
|
||||||
|
|
||||||
|
if total_errors > 0:
|
||||||
|
logger.warning(f"⚠️ {total_errors}件のエラーがありました")
|
||||||
|
else:
|
||||||
|
logger.info("✅ 全ての移行が正常に完了しました!")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 移行処理エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.close_connections()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
logger.info("Old RogDB → RogDB データ移行スクリプト")
|
||||||
|
|
||||||
|
# 除外テーブル設定
|
||||||
|
exclude_tables = []
|
||||||
|
if os.getenv('EXCLUDE_TABLES'):
|
||||||
|
exclude_tables = [t.strip() for t in os.getenv('EXCLUDE_TABLES').split(',')]
|
||||||
|
logger.info(f"除外テーブル: {exclude_tables}")
|
||||||
|
|
||||||
|
# 移行実行
|
||||||
|
migrator = RogTableMigrator()
|
||||||
|
success = migrator.run_migration(exclude_tables=exclude_tables)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("移行処理が完了しました")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
logger.error("移行処理が失敗しました")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
354
migrate_rog_entry_enhanced.py
Normal file
354
migrate_rog_entry_enhanced.py
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
rog_entry テーブル専用移行スクリプト (予約語・NULL値対応)
|
||||||
|
old_rogdb の rog_entry から rogdb の rog_entry へデータ移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
class RogEntryMigrator:
|
||||||
|
"""rog_entry テーブル専用移行クラス"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.old_conn = None
|
||||||
|
self.new_conn = None
|
||||||
|
self.old_cursor = None
|
||||||
|
self.new_cursor = None
|
||||||
|
|
||||||
|
def connect_databases(self):
|
||||||
|
"""データベース接続"""
|
||||||
|
try:
|
||||||
|
logger.info("データベースに接続中...")
|
||||||
|
self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
self.new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
self.old_conn.autocommit = True
|
||||||
|
self.new_conn.autocommit = False
|
||||||
|
|
||||||
|
self.old_cursor = self.old_conn.cursor()
|
||||||
|
self.new_cursor = self.new_conn.cursor()
|
||||||
|
|
||||||
|
logger.info("✅ データベース接続成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
"""データベース接続クローズ"""
|
||||||
|
try:
|
||||||
|
if self.old_cursor:
|
||||||
|
self.old_cursor.close()
|
||||||
|
if self.new_cursor:
|
||||||
|
self.new_cursor.close()
|
||||||
|
if self.old_conn:
|
||||||
|
self.old_conn.close()
|
||||||
|
if self.new_conn:
|
||||||
|
self.new_conn.close()
|
||||||
|
logger.info("データベース接続をクローズしました")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"接続クローズ時の警告: {e}")
|
||||||
|
|
||||||
|
def quote_column_if_needed(self, column_name):
|
||||||
|
"""予約語やキャメルケースの場合はダブルクォートで囲む"""
|
||||||
|
# camelCaseの場合はクォート
|
||||||
|
if any(c.isupper() for c in column_name):
|
||||||
|
return f'"{column_name}"'
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
def handle_null_values(self, column_name, value):
|
||||||
|
"""NULL値の処理とデフォルト値設定"""
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# rog_entryテーブル固有のデフォルト値
|
||||||
|
defaults = {
|
||||||
|
'is_active': True,
|
||||||
|
'is_trial': False,
|
||||||
|
'hasGoaled': False,
|
||||||
|
'hasParticipated': False,
|
||||||
|
'zekken_label': '',
|
||||||
|
'zekken_number': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if column_name in defaults:
|
||||||
|
default_value = defaults[column_name]
|
||||||
|
logger.debug(f"NULL値をデフォルト値に変換: {column_name} = {default_value}")
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
# デフォルト値が見つからない場合はNULLを返す
|
||||||
|
logger.warning(f"デフォルト値が設定されていません: {column_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_foreign_keys(self):
|
||||||
|
"""外部キー参照の整合性をチェック"""
|
||||||
|
logger.info("外部キー参照の整合性をチェック中...")
|
||||||
|
|
||||||
|
# team_id の存在確認
|
||||||
|
self.old_cursor.execute("SELECT DISTINCT team_id FROM rog_entry WHERE team_id IS NOT NULL")
|
||||||
|
old_team_ids = [row[0] for row in self.old_cursor.fetchall()]
|
||||||
|
|
||||||
|
self.new_cursor.execute("SELECT id FROM rog_team")
|
||||||
|
new_team_ids = [row[0] for row in self.new_cursor.fetchall()]
|
||||||
|
|
||||||
|
missing_teams = set(old_team_ids) - set(new_team_ids)
|
||||||
|
if missing_teams:
|
||||||
|
logger.warning(f"⚠️ 新DBに存在しないteam_id: {missing_teams}")
|
||||||
|
logger.warning("先にrog_teamの移行を完了してください")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# event_id の存在確認
|
||||||
|
self.old_cursor.execute("SELECT DISTINCT event_id FROM rog_entry WHERE event_id IS NOT NULL")
|
||||||
|
old_event_ids = [row[0] for row in self.old_cursor.fetchall()]
|
||||||
|
|
||||||
|
self.new_cursor.execute("SELECT id FROM rog_newevent2")
|
||||||
|
new_event_ids = [row[0] for row in self.new_cursor.fetchall()]
|
||||||
|
|
||||||
|
missing_events = set(old_event_ids) - set(new_event_ids)
|
||||||
|
if missing_events:
|
||||||
|
logger.warning(f"⚠️ 新DBに存在しないevent_id: {missing_events}")
|
||||||
|
logger.warning("先にrog_newevent2の移行を完了してください")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# category_id の存在確認
|
||||||
|
self.old_cursor.execute("SELECT DISTINCT category_id FROM rog_entry WHERE category_id IS NOT NULL")
|
||||||
|
old_category_ids = [row[0] for row in self.old_cursor.fetchall()]
|
||||||
|
|
||||||
|
self.new_cursor.execute("SELECT id FROM rog_newcategory")
|
||||||
|
new_category_ids = [row[0] for row in self.new_cursor.fetchall()]
|
||||||
|
|
||||||
|
missing_categories = set(old_category_ids) - set(new_category_ids)
|
||||||
|
if missing_categories:
|
||||||
|
logger.warning(f"⚠️ 新DBに存在しないcategory_id: {missing_categories}")
|
||||||
|
logger.warning("先にrog_newcategoryの移行を完了してください")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("✅ 外部キー参照の整合性チェック完了")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def migrate_rog_entry(self):
|
||||||
|
"""rog_entry テーブルのデータ移行"""
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("rog_entry テーブルデータ移行開始")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 外部キー整合性チェック
|
||||||
|
if not self.validate_foreign_keys():
|
||||||
|
logger.error("❌ 外部キー整合性チェックに失敗しました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 旧データ取得(camelCaseカラム名をクォート)
|
||||||
|
logger.info("旧rog_entryデータを取得中...")
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT id, date, category_id, event_id, owner_id, team_id,
|
||||||
|
is_active, zekken_number, "hasGoaled", "hasParticipated",
|
||||||
|
zekken_label, is_trial
|
||||||
|
FROM rog_entry
|
||||||
|
ORDER BY id
|
||||||
|
""")
|
||||||
|
old_records = self.old_cursor.fetchall()
|
||||||
|
|
||||||
|
if not old_records:
|
||||||
|
logger.info("✅ 移行対象データがありません")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info(f"移行対象レコード数: {len(old_records)}件")
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
# レコード別処理
|
||||||
|
for i, old_record in enumerate(old_records):
|
||||||
|
try:
|
||||||
|
# レコードデータの展開とNULL値処理
|
||||||
|
entry_id, date, category_id, event_id, owner_id, team_id, \
|
||||||
|
is_active, zekken_number, hasGoaled, hasParticipated, \
|
||||||
|
zekken_label, is_trial = old_record
|
||||||
|
|
||||||
|
# NULL値処理
|
||||||
|
processed_record = {
|
||||||
|
'id': entry_id,
|
||||||
|
'date': date,
|
||||||
|
'category_id': category_id,
|
||||||
|
'event_id': event_id,
|
||||||
|
'owner_id': owner_id,
|
||||||
|
'team_id': team_id,
|
||||||
|
'is_active': self.handle_null_values('is_active', is_active),
|
||||||
|
'zekken_number': self.handle_null_values('zekken_number', zekken_number),
|
||||||
|
'hasGoaled': self.handle_null_values('hasGoaled', hasGoaled),
|
||||||
|
'hasParticipated': self.handle_null_values('hasParticipated', hasParticipated),
|
||||||
|
'zekken_label': self.handle_null_values('zekken_label', zekken_label),
|
||||||
|
'is_trial': self.handle_null_values('is_trial', is_trial)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 既存レコード確認
|
||||||
|
self.new_cursor.execute(
|
||||||
|
"SELECT COUNT(*) FROM rog_entry WHERE id = %s",
|
||||||
|
(entry_id,)
|
||||||
|
)
|
||||||
|
exists = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# UPDATE処理(camelCaseカラムをクォート)
|
||||||
|
update_query = """
|
||||||
|
UPDATE rog_entry SET
|
||||||
|
date = %s,
|
||||||
|
category_id = %s,
|
||||||
|
event_id = %s,
|
||||||
|
owner_id = %s,
|
||||||
|
team_id = %s,
|
||||||
|
is_active = %s,
|
||||||
|
zekken_number = %s,
|
||||||
|
"hasGoaled" = %s,
|
||||||
|
"hasParticipated" = %s,
|
||||||
|
zekken_label = %s,
|
||||||
|
is_trial = %s
|
||||||
|
WHERE id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.new_cursor.execute(update_query, (
|
||||||
|
processed_record['date'],
|
||||||
|
processed_record['category_id'],
|
||||||
|
processed_record['event_id'],
|
||||||
|
processed_record['owner_id'],
|
||||||
|
processed_record['team_id'],
|
||||||
|
processed_record['is_active'],
|
||||||
|
processed_record['zekken_number'],
|
||||||
|
processed_record['hasGoaled'],
|
||||||
|
processed_record['hasParticipated'],
|
||||||
|
processed_record['zekken_label'],
|
||||||
|
processed_record['is_trial'],
|
||||||
|
entry_id
|
||||||
|
))
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# INSERT処理(camelCaseカラムをクォート)
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO rog_entry (
|
||||||
|
id, date, category_id, event_id, owner_id, team_id,
|
||||||
|
is_active, zekken_number, "hasGoaled", "hasParticipated",
|
||||||
|
zekken_label, is_trial
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.new_cursor.execute(insert_query, (
|
||||||
|
processed_record['id'],
|
||||||
|
processed_record['date'],
|
||||||
|
processed_record['category_id'],
|
||||||
|
processed_record['event_id'],
|
||||||
|
processed_record['owner_id'],
|
||||||
|
processed_record['team_id'],
|
||||||
|
processed_record['is_active'],
|
||||||
|
processed_record['zekken_number'],
|
||||||
|
processed_record['hasGoaled'],
|
||||||
|
processed_record['hasParticipated'],
|
||||||
|
processed_record['zekken_label'],
|
||||||
|
processed_record['is_trial']
|
||||||
|
))
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
# 進捗表示とコミット
|
||||||
|
if (i + 1) % 100 == 0:
|
||||||
|
self.new_conn.commit()
|
||||||
|
logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
logger.error(f" レコード処理エラー (ID: {entry_id}): {e}")
|
||||||
|
|
||||||
|
# トランザクションロールバック
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if error_count > 10:
|
||||||
|
logger.error("❌ エラー数が上限を超えました")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 最終コミット
|
||||||
|
self.new_conn.commit()
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("rog_entry 移行完了")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(f"挿入: {inserted_count}件")
|
||||||
|
logger.info(f"更新: {updated_count}件")
|
||||||
|
logger.info(f"エラー: {error_count}件")
|
||||||
|
logger.info(f"総処理: {len(old_records)}件")
|
||||||
|
|
||||||
|
if error_count == 0:
|
||||||
|
logger.info("✅ rog_entry移行が正常に完了しました!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ {error_count}件のエラーがありました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ rog_entry移行エラー: {e}")
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""移行実行"""
|
||||||
|
try:
|
||||||
|
if not self.connect_databases():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.migrate_rog_entry()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.close_connections()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
logger.info("rog_entry テーブル移行スクリプト")
|
||||||
|
|
||||||
|
migrator = RogEntryMigrator()
|
||||||
|
success = migrator.run()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("🎉 移行が正常に完了しました!")
|
||||||
|
else:
|
||||||
|
logger.error("💥 移行中にエラーが発生しました")
|
||||||
|
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
365
migrate_rog_goalimages_enhanced.py
Normal file
365
migrate_rog_goalimages_enhanced.py
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
rog_goalimages テーブル専用移行スクリプト (team_name → zekken_number変換対応)
|
||||||
|
old_rogdb の rog_goalimages から rogdb の rog_goalimages へ
|
||||||
|
zekken_numberがブランクの場合、team_nameからrog_entryを検索してzekken_numberを取得
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
class RogGoalImagesMigrator:
|
||||||
|
"""rog_goalimages テーブル専用移行クラス"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.old_conn = None
|
||||||
|
self.new_conn = None
|
||||||
|
self.old_cursor = None
|
||||||
|
self.new_cursor = None
|
||||||
|
self.team_zekken_cache = {} # team_name → zekken_number キャッシュ
|
||||||
|
|
||||||
|
def connect_databases(self):
|
||||||
|
"""データベース接続"""
|
||||||
|
try:
|
||||||
|
logger.info("データベースに接続中...")
|
||||||
|
self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
self.new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
self.old_conn.autocommit = True
|
||||||
|
self.new_conn.autocommit = False
|
||||||
|
|
||||||
|
self.old_cursor = self.old_conn.cursor()
|
||||||
|
self.new_cursor = self.new_conn.cursor()
|
||||||
|
|
||||||
|
logger.info("✅ データベース接続成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
"""データベース接続クローズ"""
|
||||||
|
try:
|
||||||
|
if self.old_cursor:
|
||||||
|
self.old_cursor.close()
|
||||||
|
if self.new_cursor:
|
||||||
|
self.new_cursor.close()
|
||||||
|
if self.old_conn:
|
||||||
|
self.old_conn.close()
|
||||||
|
if self.new_conn:
|
||||||
|
self.new_conn.close()
|
||||||
|
logger.info("データベース接続をクローズしました")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"接続クローズ時の警告: {e}")
|
||||||
|
|
||||||
|
def build_team_zekken_cache(self):
|
||||||
|
"""team_name → zekken_number のキャッシュを構築"""
|
||||||
|
logger.info("team_name → zekken_number キャッシュを構築中...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 新DBのrog_entryから team_name → zekken_number マッピングを取得
|
||||||
|
self.new_cursor.execute("""
|
||||||
|
SELECT DISTINCT t.team_name, e.zekken_number
|
||||||
|
FROM rog_entry e
|
||||||
|
JOIN rog_team t ON e.team_id = t.id
|
||||||
|
WHERE t.team_name IS NOT NULL
|
||||||
|
AND e.zekken_number IS NOT NULL
|
||||||
|
ORDER BY t.team_name, e.zekken_number
|
||||||
|
""")
|
||||||
|
|
||||||
|
team_zekken_pairs = self.new_cursor.fetchall()
|
||||||
|
|
||||||
|
for team_name, zekken_number in team_zekken_pairs:
|
||||||
|
if team_name not in self.team_zekken_cache:
|
||||||
|
self.team_zekken_cache[team_name] = zekken_number
|
||||||
|
logger.debug(f"キャッシュ追加: {team_name} → {zekken_number}")
|
||||||
|
|
||||||
|
logger.info(f"✅ キャッシュ構築完了: {len(self.team_zekken_cache)}件のteam_name → zekken_numberマッピング")
|
||||||
|
|
||||||
|
# キャッシュ内容の一部をログ出力
|
||||||
|
if self.team_zekken_cache:
|
||||||
|
sample_items = list(self.team_zekken_cache.items())[:5]
|
||||||
|
logger.info(f"キャッシュサンプル: {sample_items}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ キャッシュ構築エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def resolve_zekken_number(self, old_zekken_number, team_name):
|
||||||
|
"""zekken_numberを解決(ブランクの場合はteam_nameから取得)"""
|
||||||
|
# zekken_numberが既に設定されている場合はそのまま使用
|
||||||
|
if old_zekken_number and old_zekken_number.strip():
|
||||||
|
return old_zekken_number.strip()
|
||||||
|
|
||||||
|
# team_nameからzekken_numberを検索
|
||||||
|
if team_name and team_name.strip():
|
||||||
|
clean_team_name = team_name.strip()
|
||||||
|
|
||||||
|
if clean_team_name in self.team_zekken_cache:
|
||||||
|
resolved_zekken = self.team_zekken_cache[clean_team_name]
|
||||||
|
logger.debug(f"team_name '{clean_team_name}' → zekken_number '{resolved_zekken}'")
|
||||||
|
return str(resolved_zekken)
|
||||||
|
else:
|
||||||
|
logger.warning(f"team_name '{clean_team_name}' に対応するzekken_numberが見つかりません")
|
||||||
|
|
||||||
|
# 解決できない場合は空文字列を返す
|
||||||
|
logger.warning(f"zekken_number解決失敗: zekken='{old_zekken_number}', team='{team_name}'")
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_old_goalimages_structure(self):
|
||||||
|
"""旧DBのrog_goalimagesテーブル構造を取得"""
|
||||||
|
try:
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_goalimages'
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""")
|
||||||
|
|
||||||
|
columns = self.old_cursor.fetchall()
|
||||||
|
column_names = [col[0] for col in columns]
|
||||||
|
|
||||||
|
logger.info(f"旧DBのrog_goalimagesカラム: {column_names}")
|
||||||
|
return column_names
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 旧DBテーブル構造取得エラー: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def migrate_rog_goalimages(self):
|
||||||
|
"""rog_goalimages テーブルのデータ移行"""
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("rog_goalimages テーブルデータ移行開始")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# team_name → zekken_number キャッシュ構築
|
||||||
|
if not self.build_team_zekken_cache():
|
||||||
|
logger.error("❌ キャッシュ構築に失敗しました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 旧DBテーブル構造確認
|
||||||
|
old_columns = self.get_old_goalimages_structure()
|
||||||
|
if not old_columns:
|
||||||
|
logger.error("❌ 旧DBのテーブル構造を取得できませんでした")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 旧データ取得
|
||||||
|
logger.info("旧rog_goalimagesデータを取得中...")
|
||||||
|
|
||||||
|
# カラム存在チェック
|
||||||
|
has_zekken_number = 'zekken_number' in old_columns
|
||||||
|
|
||||||
|
if has_zekken_number:
|
||||||
|
select_query = """
|
||||||
|
SELECT id, goalimage, goaltime, team_name, event_code,
|
||||||
|
cp_number, user_id, zekken_number
|
||||||
|
FROM rog_goalimages
|
||||||
|
ORDER BY id
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
select_query = """
|
||||||
|
SELECT id, goalimage, goaltime, team_name, event_code,
|
||||||
|
cp_number, user_id, NULL as zekken_number
|
||||||
|
FROM rog_goalimages
|
||||||
|
ORDER BY id
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.old_cursor.execute(select_query)
|
||||||
|
old_records = self.old_cursor.fetchall()
|
||||||
|
|
||||||
|
if not old_records:
|
||||||
|
logger.info("✅ 移行対象データがありません")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info(f"移行対象レコード数: {len(old_records)}件")
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
zekken_resolved_count = 0
|
||||||
|
|
||||||
|
# レコード別処理
|
||||||
|
for i, old_record in enumerate(old_records):
|
||||||
|
try:
|
||||||
|
# レコードデータの展開
|
||||||
|
record_id, goalimage, goaltime, team_name, event_code, \
|
||||||
|
cp_number, user_id, old_zekken_number = old_record
|
||||||
|
|
||||||
|
# zekken_number解決
|
||||||
|
resolved_zekken_number = self.resolve_zekken_number(old_zekken_number, team_name)
|
||||||
|
|
||||||
|
if not old_zekken_number and resolved_zekken_number:
|
||||||
|
zekken_resolved_count += 1
|
||||||
|
|
||||||
|
# 新レコードデータ
|
||||||
|
new_record = {
|
||||||
|
'id': record_id,
|
||||||
|
'goalimage': goalimage,
|
||||||
|
'goaltime': goaltime,
|
||||||
|
'team_name': team_name or '',
|
||||||
|
'event_code': event_code or '',
|
||||||
|
'cp_number': cp_number or 0,
|
||||||
|
'user_id': user_id,
|
||||||
|
'zekken_number': resolved_zekken_number
|
||||||
|
}
|
||||||
|
|
||||||
|
# 既存レコード確認
|
||||||
|
self.new_cursor.execute(
|
||||||
|
"SELECT COUNT(*) FROM rog_goalimages WHERE id = %s",
|
||||||
|
(record_id,)
|
||||||
|
)
|
||||||
|
exists = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# UPDATE処理
|
||||||
|
update_query = """
|
||||||
|
UPDATE rog_goalimages SET
|
||||||
|
goalimage = %s,
|
||||||
|
goaltime = %s,
|
||||||
|
team_name = %s,
|
||||||
|
event_code = %s,
|
||||||
|
cp_number = %s,
|
||||||
|
user_id = %s,
|
||||||
|
zekken_number = %s
|
||||||
|
WHERE id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.new_cursor.execute(update_query, (
|
||||||
|
new_record['goalimage'],
|
||||||
|
new_record['goaltime'],
|
||||||
|
new_record['team_name'],
|
||||||
|
new_record['event_code'],
|
||||||
|
new_record['cp_number'],
|
||||||
|
new_record['user_id'],
|
||||||
|
new_record['zekken_number'],
|
||||||
|
record_id
|
||||||
|
))
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# INSERT処理
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO rog_goalimages (
|
||||||
|
id, goalimage, goaltime, team_name, event_code,
|
||||||
|
cp_number, user_id, zekken_number
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.new_cursor.execute(insert_query, (
|
||||||
|
new_record['id'],
|
||||||
|
new_record['goalimage'],
|
||||||
|
new_record['goaltime'],
|
||||||
|
new_record['team_name'],
|
||||||
|
new_record['event_code'],
|
||||||
|
new_record['cp_number'],
|
||||||
|
new_record['user_id'],
|
||||||
|
new_record['zekken_number']
|
||||||
|
))
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
# 進捗表示とコミット
|
||||||
|
if (i + 1) % 100 == 0:
|
||||||
|
self.new_conn.commit()
|
||||||
|
logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
logger.error(f" レコード処理エラー (ID: {record_id}): {e}")
|
||||||
|
|
||||||
|
# トランザクションロールバック
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if error_count > 10:
|
||||||
|
logger.error("❌ エラー数が上限を超えました")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 最終コミット
|
||||||
|
self.new_conn.commit()
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("rog_goalimages 移行完了")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(f"挿入: {inserted_count}件")
|
||||||
|
logger.info(f"更新: {updated_count}件")
|
||||||
|
logger.info(f"エラー: {error_count}件")
|
||||||
|
logger.info(f"zekken_number解決: {zekken_resolved_count}件")
|
||||||
|
logger.info(f"総処理: {len(old_records)}件")
|
||||||
|
|
||||||
|
if error_count == 0:
|
||||||
|
logger.info("✅ rog_goalimages移行が正常に完了しました!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ {error_count}件のエラーがありました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ rog_goalimages移行エラー: {e}")
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""移行実行"""
|
||||||
|
try:
|
||||||
|
if not self.connect_databases():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.migrate_rog_goalimages()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.close_connections()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
logger.info("rog_goalimages テーブル移行スクリプト (team_name → zekken_number変換対応)")
|
||||||
|
|
||||||
|
migrator = RogGoalImagesMigrator()
|
||||||
|
success = migrator.run()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("🎉 移行が正常に完了しました!")
|
||||||
|
else:
|
||||||
|
logger.error("💥 移行中にエラーが発生しました")
|
||||||
|
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
407
migrate_rog_team_enhanced.py
Normal file
407
migrate_rog_team_enhanced.py
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
rog_team テーブル専用移行スクリプト (構造変換対応)
|
||||||
|
old_rogdb の rog_team から rogdb の rog_team へ構造変換を行いながらデータ移行
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
class RogTeamMigrator:
|
||||||
|
"""rog_team テーブル専用移行クラス"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.old_conn = None
|
||||||
|
self.new_conn = None
|
||||||
|
self.old_cursor = None
|
||||||
|
self.new_cursor = None
|
||||||
|
self.default_event_id = None
|
||||||
|
|
||||||
|
def connect_databases(self):
|
||||||
|
"""データベース接続"""
|
||||||
|
try:
|
||||||
|
logger.info("データベースに接続中...")
|
||||||
|
self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
self.new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
self.old_conn.autocommit = True
|
||||||
|
self.new_conn.autocommit = False
|
||||||
|
|
||||||
|
self.old_cursor = self.old_conn.cursor()
|
||||||
|
self.new_cursor = self.new_conn.cursor()
|
||||||
|
|
||||||
|
logger.info("✅ データベース接続成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
"""データベース接続クローズ"""
|
||||||
|
try:
|
||||||
|
if self.old_cursor:
|
||||||
|
self.old_cursor.close()
|
||||||
|
if self.new_cursor:
|
||||||
|
self.new_cursor.close()
|
||||||
|
if self.old_conn:
|
||||||
|
self.old_conn.close()
|
||||||
|
if self.new_conn:
|
||||||
|
self.new_conn.close()
|
||||||
|
logger.info("データベース接続をクローズしました")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"接続クローズ時の警告: {e}")
|
||||||
|
|
||||||
|
def get_team_zekken_and_event(self, team_id):
|
||||||
|
"""team_idから最新のrog_entryのzekken_numberとevent_idを取得"""
|
||||||
|
try:
|
||||||
|
# 旧DBのrog_entryから該当team_idの最新レコードを取得
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT zekken_number, event_id, date
|
||||||
|
FROM rog_entry
|
||||||
|
WHERE team_id = %s
|
||||||
|
AND zekken_number IS NOT NULL
|
||||||
|
AND event_id IS NOT NULL
|
||||||
|
ORDER BY date DESC, id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""", (team_id,))
|
||||||
|
|
||||||
|
result = self.old_cursor.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
zekken_number, event_id, entry_date = result
|
||||||
|
logger.debug(f"team_id {team_id}: zekken_number={zekken_number}, event_id={event_id}, date={entry_date}")
|
||||||
|
return str(zekken_number), event_id
|
||||||
|
else:
|
||||||
|
# rog_entryにレコードがない場合はデフォルト値を返す
|
||||||
|
logger.warning(f"team_id {team_id}: rog_entryにレコードが見つかりません")
|
||||||
|
return '', self.default_event_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"team_id {team_id} のzekken_number/event_id取得エラー: {e}")
|
||||||
|
return '', self.default_event_id
|
||||||
|
|
||||||
|
def get_default_event_id(self):
|
||||||
|
"""デフォルトのevent_idを取得または作成"""
|
||||||
|
try:
|
||||||
|
# 既存のイベントを探す
|
||||||
|
self.new_cursor.execute("""
|
||||||
|
SELECT id FROM rog_newevent2
|
||||||
|
WHERE event_name LIKE '%移行%' OR event_name LIKE '%default%'
|
||||||
|
ORDER BY id LIMIT 1
|
||||||
|
""")
|
||||||
|
result = self.new_cursor.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
event_id = result[0]
|
||||||
|
logger.info(f"既存のデフォルトイベントを使用: event_id = {event_id}")
|
||||||
|
return event_id
|
||||||
|
|
||||||
|
# なければ最初のイベントを使用
|
||||||
|
self.new_cursor.execute("""
|
||||||
|
SELECT id FROM rog_newevent2
|
||||||
|
ORDER BY id LIMIT 1
|
||||||
|
""")
|
||||||
|
result = self.new_cursor.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
event_id = result[0]
|
||||||
|
logger.info(f"最初のイベントをデフォルトとして使用: event_id = {event_id}")
|
||||||
|
return event_id
|
||||||
|
|
||||||
|
# イベントがない場合はエラー
|
||||||
|
logger.error("❌ rog_newevent2 テーブルにイベントが存在しません")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ デフォルトevent_id取得エラー: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_category_mapping(self):
|
||||||
|
"""カテゴリIDのマッピングを確認"""
|
||||||
|
try:
|
||||||
|
# 旧DBのカテゴリ
|
||||||
|
self.old_cursor.execute("SELECT id, category_name FROM rog_newcategory ORDER BY id")
|
||||||
|
old_categories = dict(self.old_cursor.fetchall())
|
||||||
|
|
||||||
|
# 新DBのカテゴリ
|
||||||
|
self.new_cursor.execute("SELECT id, category_name FROM rog_newcategory ORDER BY id")
|
||||||
|
new_categories = dict(self.new_cursor.fetchall())
|
||||||
|
|
||||||
|
logger.info(f"旧DB カテゴリ: {old_categories}")
|
||||||
|
logger.info(f"新DB カテゴリ: {new_categories}")
|
||||||
|
|
||||||
|
# 名前ベースでマッピング作成
|
||||||
|
category_mapping = {}
|
||||||
|
for old_id, old_name in old_categories.items():
|
||||||
|
for new_id, new_name in new_categories.items():
|
||||||
|
if old_name == new_name:
|
||||||
|
category_mapping[old_id] = new_id
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# マッチしない場合は最初のカテゴリを使用
|
||||||
|
if new_categories:
|
||||||
|
category_mapping[old_id] = min(new_categories.keys())
|
||||||
|
logger.warning(f"カテゴリマッピング失敗 - デフォルト使用: {old_id} -> {category_mapping[old_id]}")
|
||||||
|
|
||||||
|
return category_mapping
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ カテゴリマッピング取得エラー: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def convert_team_record(self, old_record, category_mapping):
|
||||||
|
"""旧レコードを新レコード形式に変換"""
|
||||||
|
old_id, old_team_name, old_category_id, old_owner_id = old_record
|
||||||
|
|
||||||
|
# team_idから最新のzekken_numberとevent_idを取得
|
||||||
|
zekken_number, event_id = self.get_team_zekken_and_event(old_id)
|
||||||
|
|
||||||
|
# 新しいレコード作成
|
||||||
|
new_record = {
|
||||||
|
'id': old_id,
|
||||||
|
'team_name': old_team_name,
|
||||||
|
'category_id': category_mapping.get(old_category_id, old_category_id),
|
||||||
|
'owner_id': old_owner_id,
|
||||||
|
# 新しいフィールドにデフォルト値を設定
|
||||||
|
'class_name': '', # 空文字列
|
||||||
|
'event_id': event_id, # rog_entryから取得したevent_id
|
||||||
|
'location': None, # PostGIS座標は後で設定可能
|
||||||
|
'password': '', # パスワードなし
|
||||||
|
'trial': False, # 本番チーム
|
||||||
|
'zekken_number': zekken_number, # rog_entryから取得したzekken_number
|
||||||
|
'created_at': datetime.now(timezone.utc),
|
||||||
|
'updated_at': datetime.now(timezone.utc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new_record
|
||||||
|
|
||||||
|
def migrate_rog_team(self):
|
||||||
|
"""rog_team テーブルのデータ移行"""
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("rog_team テーブル構造変換移行開始")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# デフォルトevent_id取得
|
||||||
|
self.default_event_id = self.get_default_event_id()
|
||||||
|
if not self.default_event_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# カテゴリマッピング取得
|
||||||
|
category_mapping = self.get_category_mapping()
|
||||||
|
|
||||||
|
# 旧データ取得
|
||||||
|
logger.info("旧rog_teamデータを取得中...")
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT id, team_name, category_id, owner_id
|
||||||
|
FROM rog_team
|
||||||
|
ORDER BY id
|
||||||
|
""")
|
||||||
|
old_records = self.old_cursor.fetchall()
|
||||||
|
|
||||||
|
if not old_records:
|
||||||
|
logger.info("✅ 移行対象データがありません")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info(f"移行対象レコード数: {len(old_records)}件")
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
zekken_resolved_count = 0
|
||||||
|
constraint_avoided_count = 0
|
||||||
|
|
||||||
|
# レコード別処理
|
||||||
|
for i, old_record in enumerate(old_records):
|
||||||
|
try:
|
||||||
|
# レコード変換
|
||||||
|
new_record = self.convert_team_record(old_record, category_mapping)
|
||||||
|
team_id = new_record['id']
|
||||||
|
|
||||||
|
# zekken_number解決統計
|
||||||
|
if new_record['zekken_number']:
|
||||||
|
zekken_resolved_count += 1
|
||||||
|
|
||||||
|
# 既存レコード確認(IDベース)
|
||||||
|
self.new_cursor.execute(
|
||||||
|
"SELECT COUNT(*) FROM rog_team WHERE id = %s",
|
||||||
|
(team_id,)
|
||||||
|
)
|
||||||
|
exists_by_id = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
# 重複制約確認(zekken_number + event_id の組み合わせ)
|
||||||
|
if new_record['zekken_number']: # zekken_numberが空でない場合のみチェック
|
||||||
|
self.new_cursor.execute(
|
||||||
|
"SELECT COUNT(*) FROM rog_team WHERE zekken_number = %s AND event_id = %s",
|
||||||
|
(new_record['zekken_number'], new_record['event_id'])
|
||||||
|
)
|
||||||
|
exists_by_constraint = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if exists_by_constraint and not exists_by_id:
|
||||||
|
# 制約違反が発生する場合は、zekken_numberを空にしてデフォルトevent_idを使用
|
||||||
|
logger.warning(f"Team ID {team_id}: zekken_number制約回避のため空文字に変更")
|
||||||
|
new_record['zekken_number'] = ''
|
||||||
|
new_record['event_id'] = self.default_event_id
|
||||||
|
constraint_avoided_count += 1
|
||||||
|
|
||||||
|
if exists_by_id:
|
||||||
|
# UPDATE処理
|
||||||
|
update_query = """
|
||||||
|
UPDATE rog_team SET
|
||||||
|
team_name = %s,
|
||||||
|
category_id = %s,
|
||||||
|
owner_id = %s,
|
||||||
|
class_name = %s,
|
||||||
|
event_id = %s,
|
||||||
|
location = %s,
|
||||||
|
password = %s,
|
||||||
|
trial = %s,
|
||||||
|
zekken_number = %s,
|
||||||
|
updated_at = %s
|
||||||
|
WHERE id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.new_cursor.execute(update_query, (
|
||||||
|
new_record['team_name'],
|
||||||
|
new_record['category_id'],
|
||||||
|
new_record['owner_id'],
|
||||||
|
new_record['class_name'],
|
||||||
|
new_record['event_id'],
|
||||||
|
new_record['location'],
|
||||||
|
new_record['password'],
|
||||||
|
new_record['trial'],
|
||||||
|
new_record['zekken_number'],
|
||||||
|
new_record['updated_at'],
|
||||||
|
team_id
|
||||||
|
))
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# INSERT処理
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO rog_team (
|
||||||
|
id, team_name, category_id, owner_id,
|
||||||
|
class_name, event_id, location, password,
|
||||||
|
trial, zekken_number, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.new_cursor.execute(insert_query, (
|
||||||
|
new_record['id'],
|
||||||
|
new_record['team_name'],
|
||||||
|
new_record['category_id'],
|
||||||
|
new_record['owner_id'],
|
||||||
|
new_record['class_name'],
|
||||||
|
new_record['event_id'],
|
||||||
|
new_record['location'],
|
||||||
|
new_record['password'],
|
||||||
|
new_record['trial'],
|
||||||
|
new_record['zekken_number'],
|
||||||
|
new_record['created_at'],
|
||||||
|
new_record['updated_at']
|
||||||
|
))
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
# 進捗表示
|
||||||
|
if (i + 1) % 50 == 0:
|
||||||
|
self.new_conn.commit()
|
||||||
|
logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
logger.error(f" レコード処理エラー (ID: {old_record[0]}): {e}")
|
||||||
|
|
||||||
|
# トランザクションロールバック
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if error_count > 10:
|
||||||
|
logger.error("❌ エラー数が上限を超えました")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 最終コミット
|
||||||
|
self.new_conn.commit()
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("rog_team 移行完了")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(f"挿入: {inserted_count}件")
|
||||||
|
logger.info(f"更新: {updated_count}件")
|
||||||
|
logger.info(f"エラー: {error_count}件")
|
||||||
|
logger.info(f"zekken_number解決: {zekken_resolved_count}件")
|
||||||
|
logger.info(f"制約回避: {constraint_avoided_count}件")
|
||||||
|
logger.info(f"総処理: {len(old_records)}件")
|
||||||
|
|
||||||
|
if error_count == 0:
|
||||||
|
logger.info("✅ rog_team移行が正常に完了しました!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ {error_count}件のエラーがありました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ rog_team移行エラー: {e}")
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""移行実行"""
|
||||||
|
try:
|
||||||
|
if not self.connect_databases():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.migrate_rog_team()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.close_connections()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
logger.info("rog_team テーブル構造変換移行スクリプト")
|
||||||
|
|
||||||
|
migrator = RogTeamMigrator()
|
||||||
|
success = migrator.run()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("🎉 移行が正常に完了しました!")
|
||||||
|
else:
|
||||||
|
logger.error("💥 移行中にエラーが発生しました")
|
||||||
|
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
70
migrate_specification.md
Normal file
70
migrate_specification.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
1. event
|
||||||
|
|
||||||
|
gifuroge: event_table key:event_code
|
||||||
|
rogdb: rog_newevent2 key:id (event_name=event_code)
|
||||||
|
|
||||||
|
1)
|
||||||
|
gifuroge.self_rogaining=Falseで、event_day<'2024-10-01'のデータをrogdb.rog_newevent2 に移行したい。
|
||||||
|
フィールドの移行条件は、
|
||||||
|
gifuroge.event_table.event_code を rogdb.rog_newevent2.event_name へ
|
||||||
|
gifuroge.event_table.event_name を rogdb.rog_newevent2.event_description へ
|
||||||
|
gifuroge.event_table.event_day+start_time を rogdb.rog_newevent2.start_datetime へ
|
||||||
|
gifuroge.event_table.event_day+start_time+5H を rogdb.rog_newevent2.end_datetime へ
|
||||||
|
gifuroge.event_table.event_day+start_time-3day を rogdb.rog_newevent2.deadlineDateTime へ
|
||||||
|
gifuroge.event_table.self_rogaining=False
|
||||||
|
gifuroge.event_table.class_family | class_general | class_solo_female | cla
|
||||||
|
ss_solo_male | hour_3 | hour_5 | public=True
|
||||||
|
である。
|
||||||
|
SQLで更新するようなスクリプトを作成しなさい。
|
||||||
|
=>
|
||||||
|
docker compose exec app python migrate_event_table_to_rog_newevent2.py
|
||||||
|
insert into rog_newevent2 (event_name,start_datetime,end_datetime,"deadlineDateTime",class_family,class_general,class_solo_female,class_solo_male,hour_3,hour_5,public, self_rogaining, event_description) values ('関ケ原','2022-07-30 01:00:00+00','2022-07-30 06:00:00+00','2022-07-25 06:00:00+00',True,True,True,True,True,True,True,False,'岐阜ロゲin関ケ原');
|
||||||
|
|
||||||
|
2. checkpoint
|
||||||
|
|
||||||
|
gifuroge: checkpoint_table key:event_code,cp_number
|
||||||
|
rogdb: rog_location key:id (groupにevent_codeが含まれている)
|
||||||
|
|
||||||
|
===以降はFC岐阜より後のもの(以前のものは別途移行が必要)====
|
||||||
|
|
||||||
|
3. user
|
||||||
|
|
||||||
|
4. team
|
||||||
|
|
||||||
|
gifuroge: team_table key:event_code,zekken_number
|
||||||
|
rogdb: rog_team key:id
|
||||||
|
|
||||||
|
team_table : zekken_number,event_code,team_name,class_name,password,trial
|
||||||
|
rogdb :
|
||||||
|
|
||||||
|
5. member
|
||||||
|
|
||||||
|
gifuroge:
|
||||||
|
rogdb: rog_member key:id, team_id
|
||||||
|
|
||||||
|
6. entry
|
||||||
|
|
||||||
|
gifuroge:
|
||||||
|
rogdb: rog_entry key:id,date,event_id,team_id,zekken_number ,zekken_label (=team.zekken_number)
|
||||||
|
|
||||||
|
7. checkin_history
|
||||||
|
|
||||||
|
gifuroge: (gps_detail) gps_information key:event_code,zekken_number,cp_number
|
||||||
|
rogdb: rog_gpscheckin key:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
. checkin_image
|
||||||
|
|
||||||
|
gifuroge:
|
||||||
|
rogdb:
|
||||||
|
|
||||||
|
8. goal_image
|
||||||
|
|
||||||
|
gifuroge:
|
||||||
|
rogdb:
|
||||||
|
|
||||||
|
9. waypoint
|
||||||
|
|
||||||
|
gifuroge:
|
||||||
|
rogdb:
|
||||||
64
migrate_sub_fields_to_location2025.py
Normal file
64
migrate_sub_fields_to_location2025.py
Normal file
@ -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()
|
||||||
@ -1,147 +1,207 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
既存データ保護版移行プログラム(Location2025対応)
|
GPS記録データマイグレーション (既存データ保護版)
|
||||||
既存のentry、team、memberデータを削除せずに移行データを追加する
|
gifurogeからrogdbへ12,665件のGPSチェックイン記録を移行
|
||||||
Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する
|
既存のアプリケーションデータ(188エントリ、226チーム、388メンバー)は保護
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import psycopg2
|
import psycopg2
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
def get_event_date(event_code):
|
# 設定
|
||||||
"""イベントコードに基づいてイベント日付を返す"""
|
GIFUROGE_DB = {
|
||||||
event_dates = {
|
'host': 'postgres-db',
|
||||||
'美濃加茂': datetime(2024, 5, 19), # 修正済み
|
'database': 'gifuroge',
|
||||||
'岐阜市': datetime(2024, 4, 28),
|
'user': 'admin',
|
||||||
'大垣2': datetime(2024, 4, 20),
|
'password': 'admin123456',
|
||||||
'各務原': datetime(2024, 3, 24),
|
'port': 5432
|
||||||
'下呂': datetime(2024, 3, 10),
|
}
|
||||||
'中津川': datetime(2024, 3, 2),
|
|
||||||
'揖斐川': datetime(2024, 2, 18),
|
|
||||||
'高山': datetime(2024, 2, 11),
|
|
||||||
'大垣': datetime(2024, 1, 27),
|
|
||||||
'多治見': datetime(2024, 1, 20),
|
|
||||||
# 2024年のその他のイベント
|
|
||||||
'養老ロゲ': datetime(2024, 6, 1),
|
|
||||||
'郡上': datetime(2024, 11, 3), # 郡上イベント追加
|
|
||||||
# 2025年新規イベント
|
|
||||||
'岐阜ロゲイニング2025': datetime(2025, 9, 15),
|
|
||||||
}
|
|
||||||
return event_dates.get(event_code)
|
|
||||||
|
|
||||||
def convert_utc_to_jst(utc_timestamp):
|
ROGDB_DB = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'database': 'rogdb',
|
||||||
|
'user': 'admin',
|
||||||
|
'password': 'admin123456',
|
||||||
|
'port': 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_utc_to_jst(utc_time):
|
||||||
"""UTC時刻をJST時刻に変換"""
|
"""UTC時刻をJST時刻に変換"""
|
||||||
if not utc_timestamp:
|
if utc_time is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
utc_tz = pytz.UTC
|
if isinstance(utc_time, str):
|
||||||
jst_tz = pytz.timezone('Asia/Tokyo')
|
utc_time = datetime.fromisoformat(utc_time.replace('Z', '+00:00'))
|
||||||
|
|
||||||
# UTCタイムゾーン情報を付加
|
if utc_time.tzinfo is None:
|
||||||
if utc_timestamp.tzinfo is None:
|
utc_time = utc_time.replace(tzinfo=timezone.utc)
|
||||||
utc_timestamp = utc_tz.localize(utc_timestamp)
|
|
||||||
|
|
||||||
# JSTに変換
|
jst = pytz.timezone('Asia/Tokyo')
|
||||||
return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None)
|
return utc_time.astimezone(jst)
|
||||||
|
|
||||||
|
def get_event_date(event_name: str) -> Optional[datetime]:
|
||||||
|
"""イベント名から開催日を推定"""
|
||||||
|
event_dates = {
|
||||||
|
'岐阜県ロゲイニング大会': datetime(2024, 11, 23),
|
||||||
|
'高山市ロゲイニング大会': datetime(2024, 11, 23),
|
||||||
|
'ロゲイニング大会2024': datetime(2024, 11, 23),
|
||||||
|
'default': datetime(2024, 11, 23)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, date in event_dates.items():
|
||||||
|
if key in event_name:
|
||||||
|
return date
|
||||||
|
|
||||||
|
return event_dates['default']
|
||||||
|
|
||||||
|
def parse_goal_time(goal_time_str: str, event_date_str: str) -> Optional[datetime]:
|
||||||
|
"""goal_time文字列を解析してdatetimeオブジェクトに変換"""
|
||||||
|
try:
|
||||||
|
if ':' in goal_time_str:
|
||||||
|
time_parts = goal_time_str.split(':')
|
||||||
|
hour = int(time_parts[0])
|
||||||
|
minute = int(time_parts[1])
|
||||||
|
|
||||||
|
event_date = datetime.strptime(event_date_str, "%Y-%m-%d")
|
||||||
|
goal_datetime = event_date.replace(hour=hour, minute=minute)
|
||||||
|
|
||||||
|
jst = pytz.timezone('Asia/Tokyo')
|
||||||
|
goal_datetime_jst = jst.localize(goal_datetime)
|
||||||
|
|
||||||
|
return goal_datetime_jst
|
||||||
|
except Exception as e:
|
||||||
|
print(f"goal_time解析エラー: {goal_time_str} - {e}")
|
||||||
|
|
||||||
def parse_goal_time(goal_time_str, event_date_str):
|
|
||||||
"""goal_time文字列を適切なdatetimeに変換"""
|
|
||||||
if not goal_time_str:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def check_database_connectivity():
|
||||||
|
"""データベース接続確認"""
|
||||||
|
print("=== データベース接続確認 ===")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# goal_timeが時刻のみの場合(例: "13:45:00")
|
# gifuroge DB接続確認
|
||||||
goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time()
|
source_conn = psycopg2.connect(**GIFUROGE_DB)
|
||||||
|
source_cursor = source_conn.cursor()
|
||||||
|
source_cursor.execute("SELECT COUNT(*) FROM gps_information")
|
||||||
|
source_count = source_cursor.fetchone()[0]
|
||||||
|
print(f"✅ gifuroge DB接続成功: gps_information {source_count}件")
|
||||||
|
|
||||||
# event_date_strからイベント日付を解析
|
# テーブル構造確認
|
||||||
event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date()
|
source_cursor.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'gps_information'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""")
|
||||||
|
columns = source_cursor.fetchall()
|
||||||
|
print("📋 gps_informationテーブル構造:")
|
||||||
|
for col_name, col_type in columns:
|
||||||
|
print(f" {col_name}: {col_type}")
|
||||||
|
|
||||||
# 日付と時刻を結合
|
source_conn.close()
|
||||||
goal_datetime = datetime.combine(event_date, goal_time)
|
|
||||||
|
|
||||||
# JSTとして解釈
|
# rogdb DB接続確認
|
||||||
jst_tz = pytz.timezone('Asia/Tokyo')
|
target_conn = psycopg2.connect(**ROGDB_DB)
|
||||||
goal_datetime_jst = jst_tz.localize(goal_datetime)
|
target_cursor = target_conn.cursor()
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM gps_checkins")
|
||||||
|
target_count = target_cursor.fetchone()[0]
|
||||||
|
print(f"✅ rogdb DB接続成功: gps_checkins {target_count}件")
|
||||||
|
|
||||||
# UTCに変換して返す
|
# 移行先テーブル構造確認
|
||||||
return goal_datetime_jst.astimezone(pytz.UTC)
|
target_cursor.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'gps_checkins'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""")
|
||||||
|
target_columns = target_cursor.fetchall()
|
||||||
|
print("📋 gps_checkinsテーブル構造:")
|
||||||
|
for col_name, col_type in target_columns:
|
||||||
|
print(f" {col_name}: {col_type}")
|
||||||
|
|
||||||
except (ValueError, TypeError) as e:
|
target_conn.close()
|
||||||
print(f"goal_time変換エラー: {goal_time_str} - {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def clean_target_database_selective(target_cursor):
|
return True
|
||||||
"""ターゲットデータベースの選択的クリーンアップ(既存データを保護)"""
|
|
||||||
print("=== ターゲットデータベースの選択的クリーンアップ ===")
|
|
||||||
|
|
||||||
# 外部キー制約を一時的に無効化
|
except Exception as e:
|
||||||
target_cursor.execute("SET session_replication_role = replica;")
|
print(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
try:
|
|
||||||
# GPSチェックインデータのみクリーンアップ(重複移行防止)
|
|
||||||
target_cursor.execute("DELETE FROM rog_gpscheckin WHERE comment = 'migrated_from_gifuroge'")
|
|
||||||
deleted_checkins = target_cursor.rowcount
|
|
||||||
print(f"過去の移行GPSチェックインデータを削除: {deleted_checkins}件")
|
|
||||||
|
|
||||||
# 注意: rog_entry, rog_team, rog_member, rog_location2025 は削除しない!
|
|
||||||
print("注意: 既存のentry、team、member、location2025データは保護されます")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# 外部キー制約を再有効化
|
|
||||||
target_cursor.execute("SET session_replication_role = DEFAULT;")
|
|
||||||
|
|
||||||
def verify_location2025_compatibility(target_cursor):
|
def verify_location2025_compatibility(target_cursor):
|
||||||
"""Location2025テーブルとの互換性を確認"""
|
"""rog_location2025テーブルとの互換性確認"""
|
||||||
print("\n=== Location2025互換性確認 ===")
|
print("\n=== Location2025互換性確認 ===")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Location2025テーブルの存在確認
|
# テーブル存在確認
|
||||||
target_cursor.execute("""
|
target_cursor.execute("""
|
||||||
SELECT COUNT(*) FROM information_schema.tables
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'rog_location2025'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
table_exists = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if not table_exists:
|
||||||
|
print("⚠️ rog_location2025テーブルが存在しません")
|
||||||
|
return True # テーブルが存在しない場合は互換性チェックをスキップ
|
||||||
|
|
||||||
|
# カラム構造を動的に確認
|
||||||
|
target_cursor.execute("""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
WHERE table_name = 'rog_location2025'
|
WHERE table_name = 'rog_location2025'
|
||||||
|
AND table_schema = 'public'
|
||||||
""")
|
""")
|
||||||
|
columns = [row[0] for row in target_cursor.fetchall()]
|
||||||
|
print(f"検出されたカラム: {columns}")
|
||||||
|
|
||||||
table_exists = target_cursor.fetchone()[0] > 0
|
# event_codeまたはevent_nameカラムの存在確認
|
||||||
|
event_column = None
|
||||||
|
if 'event_code' in columns:
|
||||||
|
event_column = 'event_code'
|
||||||
|
elif 'event_name' in columns:
|
||||||
|
event_column = 'event_name'
|
||||||
|
|
||||||
if table_exists:
|
if event_column:
|
||||||
# Location2025のデータ数確認
|
# 動的にクエリを構築
|
||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025")
|
query = f"""
|
||||||
location2025_count = target_cursor.fetchone()[0]
|
SELECT e.{event_column}, COUNT(l.id) as location_count
|
||||||
print(f"✅ rog_location2025テーブル存在: {location2025_count}件のチェックポイント")
|
FROM rog_entry e
|
||||||
|
LEFT JOIN rog_location2025 l ON e.id = l.entry_id
|
||||||
|
GROUP BY e.{event_column}
|
||||||
|
HAVING COUNT(l.id) > 0
|
||||||
|
"""
|
||||||
|
target_cursor.execute(query)
|
||||||
|
location_data = target_cursor.fetchall()
|
||||||
|
|
||||||
# イベント別チェックポイント数確認
|
print(f"既存のLocation2025データ:")
|
||||||
target_cursor.execute("""
|
for event_id, count in location_data:
|
||||||
SELECT e.event_code, COUNT(l.id) as checkpoint_count
|
print(f" {event_column} {event_id}: {count}件")
|
||||||
FROM rog_location2025 l
|
|
||||||
JOIN rog_newevent2 e ON l.event_id = e.id
|
|
||||||
GROUP BY e.event_code
|
|
||||||
ORDER BY checkpoint_count DESC
|
|
||||||
LIMIT 10
|
|
||||||
""")
|
|
||||||
|
|
||||||
event_checkpoints = target_cursor.fetchall()
|
|
||||||
if event_checkpoints:
|
|
||||||
print("イベント別チェックポイント数(上位10件):")
|
|
||||||
for event_code, count in event_checkpoints:
|
|
||||||
print(f" {event_code}: {count}件")
|
|
||||||
|
|
||||||
|
print("✅ Location2025互換性確認完了")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("⚠️ rog_location2025テーブルが見つかりません")
|
print("⚠️ event_codeもevent_nameも見つかりません")
|
||||||
print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます")
|
return True
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Location2025互換性確認エラー: {e}")
|
print(f"❌ Location2025互換性確認エラー: {e}")
|
||||||
|
# トランザクションエラーの場合はロールバック
|
||||||
|
try:
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def backup_existing_data(target_cursor):
|
def backup_existing_data(target_cursor):
|
||||||
"""既存データのバックアップ状況を確認"""
|
"""既存データのバックアップ状況を確認"""
|
||||||
print("\n=== 既存データ保護確認 ===")
|
print("\n=== 既存データ保護確認 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
# 既存データ数を確認
|
# 既存データ数を確認
|
||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
|
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
|
||||||
entry_count = target_cursor.fetchone()[0]
|
entry_count = target_cursor.fetchone()[0]
|
||||||
@ -152,7 +212,7 @@ def backup_existing_data(target_cursor):
|
|||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
|
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
|
||||||
member_count = target_cursor.fetchone()[0]
|
member_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
target_cursor.execute("SELECT COUNT(*) FROM gps_checkins")
|
||||||
checkin_count = target_cursor.fetchone()[0]
|
checkin_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
# Location2025データ数も確認
|
# Location2025データ数も確認
|
||||||
@ -168,7 +228,7 @@ def backup_existing_data(target_cursor):
|
|||||||
print(f" rog_entry: {entry_count} 件 (保護対象)")
|
print(f" rog_entry: {entry_count} 件 (保護対象)")
|
||||||
print(f" rog_team: {team_count} 件 (保護対象)")
|
print(f" rog_team: {team_count} 件 (保護対象)")
|
||||||
print(f" rog_member: {member_count} 件 (保護対象)")
|
print(f" rog_member: {member_count} 件 (保護対象)")
|
||||||
print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)")
|
print(f" gps_checkins: {checkin_count} 件 (移行対象)")
|
||||||
|
|
||||||
if entry_count > 0 or team_count > 0 or member_count > 0:
|
if entry_count > 0 or team_count > 0 or member_count > 0:
|
||||||
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
|
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
|
||||||
@ -177,21 +237,75 @@ def backup_existing_data(target_cursor):
|
|||||||
print("⚠️ 既存のcore application dataが見つかりません。")
|
print("⚠️ 既存のcore application dataが見つかりません。")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 既存データ確認エラー: {e}")
|
||||||
|
# トランザクションエラーの場合はロールバック
|
||||||
|
try:
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def migrate_gps_data(source_cursor, target_cursor):
|
def migrate_gps_data(source_cursor, target_cursor):
|
||||||
"""GPS記録データのみを移行(写真記録データは除外)"""
|
"""GPS記録データのみを移行(写真記録データは除外)"""
|
||||||
print("\n=== GPS記録データの移行 ===")
|
print("\n=== GPS記録データの移行 ===")
|
||||||
|
|
||||||
# GPS記録のみを取得(不正な写真記録データを除外)
|
try:
|
||||||
|
# 移行元テーブルの構造を確認
|
||||||
source_cursor.execute("""
|
source_cursor.execute("""
|
||||||
SELECT
|
SELECT column_name
|
||||||
serial_number, team_name, cp_number, record_time,
|
FROM information_schema.columns
|
||||||
goal_time, late_point, buy_flag, image_address,
|
WHERE table_name = 'gps_information'
|
||||||
minus_photo_flag, create_user, update_user,
|
ORDER BY ordinal_position
|
||||||
colabo_company_memo
|
""")
|
||||||
|
source_columns = [row[0] for row in source_cursor.fetchall()]
|
||||||
|
print(f"📋 移行元カラム: {source_columns}")
|
||||||
|
|
||||||
|
# 移行先テーブルの構造を確認
|
||||||
|
target_cursor.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'gps_checkins'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""")
|
||||||
|
target_columns = [row[0] for row in target_cursor.fetchall()]
|
||||||
|
print(f"📋 移行先カラム: {target_columns}")
|
||||||
|
|
||||||
|
# 必要なカラムのマッピングを確認
|
||||||
|
column_mapping = {
|
||||||
|
'serial_number': 'serial_number',
|
||||||
|
'team_name': 'team_name' if 'team_name' in source_columns else None,
|
||||||
|
'zekken_number': 'zekken_number' if 'zekken_number' in source_columns else None,
|
||||||
|
'event_code': 'event_code' if 'event_code' in source_columns else None,
|
||||||
|
'cp_number': 'cp_number',
|
||||||
|
'record_time': 'create_at' if 'create_at' in source_columns else 'record_time',
|
||||||
|
'goal_time': 'goal_time',
|
||||||
|
'late_point': 'late_point',
|
||||||
|
'buy_flag': 'buy_flag',
|
||||||
|
'image_address': 'image_address',
|
||||||
|
'minus_photo_flag': 'minus_photo_flag',
|
||||||
|
'create_user': 'create_user',
|
||||||
|
'update_user': 'update_user',
|
||||||
|
'colabo_company_memo': 'colabo_company_memo'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 実際に存在するカラムでクエリを構築
|
||||||
|
select_columns = []
|
||||||
|
for key, column in column_mapping.items():
|
||||||
|
if column and column in source_columns:
|
||||||
|
select_columns.append(f"{column} as {key}")
|
||||||
|
else:
|
||||||
|
select_columns.append(f"NULL as {key}")
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT {', '.join(select_columns)}
|
||||||
FROM gps_information
|
FROM gps_information
|
||||||
WHERE serial_number < 20000 -- GPS専用データのみ
|
WHERE serial_number < 20000 -- GPS専用データのみ
|
||||||
ORDER BY serial_number
|
ORDER BY serial_number
|
||||||
""")
|
"""
|
||||||
|
|
||||||
|
print(f"📋 実行クエリ: {query}")
|
||||||
|
source_cursor.execute(query)
|
||||||
|
|
||||||
gps_records = source_cursor.fetchall()
|
gps_records = source_cursor.fetchall()
|
||||||
print(f"移行対象GPS記録数: {len(gps_records)}件")
|
print(f"移行対象GPS記録数: {len(gps_records)}件")
|
||||||
@ -201,10 +315,25 @@ def migrate_gps_data(source_cursor, target_cursor):
|
|||||||
|
|
||||||
for record in gps_records:
|
for record in gps_records:
|
||||||
try:
|
try:
|
||||||
(serial_number, team_name, cp_number, record_time,
|
# レコードを解析(NULLの場合はデフォルト値を設定)
|
||||||
goal_time, late_point, buy_flag, image_address,
|
record_data = {}
|
||||||
minus_photo_flag, create_user, update_user,
|
for i, key in enumerate(column_mapping.keys()):
|
||||||
colabo_company_memo) = record
|
record_data[key] = record[i] if i < len(record) else None
|
||||||
|
|
||||||
|
serial_number = record_data['serial_number']
|
||||||
|
team_name = record_data['team_name'] or f"Team_{record_data['zekken_number'] or serial_number}"
|
||||||
|
zekken_number = record_data['zekken_number'] or serial_number
|
||||||
|
event_code = record_data['event_code'] or 'unknown'
|
||||||
|
cp_number = record_data['cp_number']
|
||||||
|
record_time = record_data['record_time']
|
||||||
|
goal_time = record_data['goal_time']
|
||||||
|
late_point = record_data['late_point']
|
||||||
|
buy_flag = record_data['buy_flag']
|
||||||
|
image_address = record_data['image_address']
|
||||||
|
minus_photo_flag = record_data['minus_photo_flag']
|
||||||
|
create_user = record_data['create_user']
|
||||||
|
update_user = record_data['update_user']
|
||||||
|
colabo_company_memo = record_data['colabo_company_memo']
|
||||||
|
|
||||||
# UTC時刻をJST時刻に変換
|
# UTC時刻をJST時刻に変換
|
||||||
record_time_jst = convert_utc_to_jst(record_time)
|
record_time_jst = convert_utc_to_jst(record_time)
|
||||||
@ -221,109 +350,120 @@ def migrate_gps_data(source_cursor, target_cursor):
|
|||||||
elif isinstance(goal_time, datetime):
|
elif isinstance(goal_time, datetime):
|
||||||
goal_time_utc = convert_utc_to_jst(goal_time)
|
goal_time_utc = convert_utc_to_jst(goal_time)
|
||||||
|
|
||||||
# rog_gpscheckinに挿入(マイグレーション用マーカー付き)
|
# 移行先テーブルに合わせてINSERT文を動的構築
|
||||||
target_cursor.execute("""
|
insert_columns = ['serial_number', 'cp_number', 'record_time', 'goal_time',
|
||||||
INSERT INTO rog_gpscheckin
|
'late_point', 'buy_flag', 'image_address', 'minus_photo_flag',
|
||||||
(serial_number, team_name, cp_number, record_time, goal_time,
|
'create_user', 'update_user', 'comment']
|
||||||
|
insert_values = [serial_number, cp_number, record_time_jst, goal_time_utc,
|
||||||
late_point, buy_flag, image_address, minus_photo_flag,
|
late_point, buy_flag, image_address, minus_photo_flag,
|
||||||
create_user, update_user, comment)
|
create_user, update_user, f'migrated_from_gifuroge_team_{team_name}_zekken_{zekken_number}_event_{event_code}']
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
||||||
""", (
|
# 移行先テーブルに存在するカラムのみを使用
|
||||||
serial_number, team_name, cp_number, record_time_jst, goal_time_utc,
|
final_columns = []
|
||||||
late_point, buy_flag, image_address, minus_photo_flag,
|
final_values = []
|
||||||
create_user, update_user, 'migrated_from_gifuroge'
|
for i, col in enumerate(insert_columns):
|
||||||
))
|
if col in target_columns:
|
||||||
|
final_columns.append(col)
|
||||||
|
final_values.append(insert_values[i])
|
||||||
|
|
||||||
|
placeholders = ', '.join(['%s'] * len(final_columns))
|
||||||
|
columns_str = ', '.join(final_columns)
|
||||||
|
|
||||||
|
target_cursor.execute(f"""
|
||||||
|
INSERT INTO gps_checkins ({columns_str})
|
||||||
|
VALUES ({placeholders})
|
||||||
|
""", final_values)
|
||||||
|
|
||||||
migrated_count += 1
|
migrated_count += 1
|
||||||
|
|
||||||
if migrated_count % 1000 == 0:
|
if migrated_count % 1000 == 0:
|
||||||
print(f"移行進捗: {migrated_count}/{len(gps_records)}件")
|
print(f" 移行進捗: {migrated_count}/{len(gps_records)} 件")
|
||||||
|
target_cursor.connection.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_count += 1
|
error_count += 1
|
||||||
print(f"移行エラー (record {serial_number}): {e}")
|
print(f" レコード移行エラー(serial_number={serial_number}): {e}")
|
||||||
continue
|
# トランザクションエラーの場合はロールバックして続行
|
||||||
|
try:
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if error_count > 100: # エラー上限
|
||||||
|
print("❌ エラー数が上限を超えました。移行を中止します。")
|
||||||
|
raise
|
||||||
|
|
||||||
print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー")
|
target_cursor.connection.commit()
|
||||||
|
print(f"✅ GPS記録移行完了: {migrated_count}件成功, {error_count}件エラー")
|
||||||
return migrated_count
|
return migrated_count
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ GPS記録移行エラー: {e}")
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""メイン移行処理(既存データ保護版)"""
|
"""メイン移行処理(既存データ保護版)"""
|
||||||
print("=== 既存データ保護版移行プログラム開始 ===")
|
print("=" * 60)
|
||||||
print("注意: 既存のentry、team、memberデータは削除されません")
|
print("GPS記録データ移行スクリプト (既存データ保護版)")
|
||||||
|
print("=" * 60)
|
||||||
# データベース接続設定
|
print("移行対象: gifuroge.gps_information → rogdb.gps_checkins")
|
||||||
source_config = {
|
print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025")
|
||||||
'host': 'localhost',
|
print("=" * 60)
|
||||||
'port': 5432,
|
|
||||||
'database': 'gifuroge',
|
|
||||||
'user': 'admin',
|
|
||||||
'password': 'admin123456'
|
|
||||||
}
|
|
||||||
|
|
||||||
target_config = {
|
|
||||||
'host': 'localhost',
|
|
||||||
'port': 5432,
|
|
||||||
'database': 'rogdb',
|
|
||||||
'user': 'admin',
|
|
||||||
'password': 'admin123456'
|
|
||||||
}
|
|
||||||
|
|
||||||
source_conn = None
|
|
||||||
target_conn = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# データベース接続
|
# 1. データベース接続確認
|
||||||
print("データベースに接続中...")
|
if not check_database_connectivity():
|
||||||
source_conn = psycopg2.connect(**source_config)
|
return False
|
||||||
target_conn = psycopg2.connect(**target_config)
|
|
||||||
|
# 2. 実際の移行処理
|
||||||
|
source_conn = psycopg2.connect(**GIFUROGE_DB)
|
||||||
|
target_conn = psycopg2.connect(**ROGDB_DB)
|
||||||
|
|
||||||
source_cursor = source_conn.cursor()
|
source_cursor = source_conn.cursor()
|
||||||
target_cursor = target_conn.cursor()
|
target_cursor = target_conn.cursor()
|
||||||
|
|
||||||
# Location2025互換性確認
|
# 3. 既存データ確認
|
||||||
location2025_available = verify_location2025_compatibility(target_cursor)
|
|
||||||
|
|
||||||
# 既存データ保護確認
|
|
||||||
has_existing_data = backup_existing_data(target_cursor)
|
has_existing_data = backup_existing_data(target_cursor)
|
||||||
|
|
||||||
# 確認プロンプト
|
# 4. Location2025互換性確認
|
||||||
print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}")
|
is_compatible = verify_location2025_compatibility(target_cursor)
|
||||||
print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}")
|
if not is_compatible:
|
||||||
|
print("❌ Location2025互換性に問題があります。")
|
||||||
|
return False
|
||||||
|
|
||||||
response = input("\n移行を開始しますか? (y/N): ")
|
# 5. 安全確認
|
||||||
if response.lower() != 'y':
|
|
||||||
print("移行を中止しました。")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 選択的クリーンアップ(既存データを保護)
|
|
||||||
clean_target_database_selective(target_cursor)
|
|
||||||
target_conn.commit()
|
|
||||||
|
|
||||||
# GPS記録データ移行
|
|
||||||
migrated_count = migrate_gps_data(source_cursor, target_cursor)
|
|
||||||
target_conn.commit()
|
|
||||||
|
|
||||||
print(f"\n=== 移行完了 ===")
|
|
||||||
print(f"移行されたGPS記録: {migrated_count}件")
|
|
||||||
print(f"Location2025互換性: {'✅ 対応済み' if location2025_available else '⚠️ 要確認'}")
|
|
||||||
if has_existing_data:
|
if has_existing_data:
|
||||||
print("✅ 既存のentry、team、member、location2025データは保護されました")
|
print("\n⚠️ 既存のアプリケーションデータが検出されました。")
|
||||||
else:
|
print("この移行操作は既存データを保護しながらGPS記録のみを移行します。")
|
||||||
print("⚠️ 既存のcore application dataがありませんでした")
|
confirm = input("続行しますか? (yes/no): ")
|
||||||
print(" 別途testdb/rogdb.sqlからの復元が必要です")
|
if confirm.lower() != 'yes':
|
||||||
|
print("移行を中止しました。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 6. GPS記録データ移行
|
||||||
|
migrated_count = migrate_gps_data(source_cursor, target_cursor)
|
||||||
|
|
||||||
|
# 7. 完了確認
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("移行完了サマリー")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"移行されたGPS記録: {migrated_count}件")
|
||||||
|
print("保護された既存データ: rog_entry, rog_team, rog_member, rog_location2025")
|
||||||
|
print("✅ データ移行が完了しました!")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"移行エラー: {e}")
|
print(f"\n❌ 移行処理エラー: {e}")
|
||||||
if target_conn:
|
return False
|
||||||
target_conn.rollback()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if source_conn:
|
try:
|
||||||
source_conn.close()
|
source_conn.close()
|
||||||
if target_conn:
|
|
||||||
target_conn.close()
|
target_conn.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|||||||
373
migration_data_protection_broken.py
Normal file
373
migration_data_protection_broken.py
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
既存データ保護版移行プログラム(Location2025対応)
|
||||||
|
既存のentry、team、memberデータを削除せずに移行データを追加する
|
||||||
|
Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
def get_event_date(event_code):
|
||||||
|
"""イベントコードに基づいてイベント日付を返す"""
|
||||||
|
event_dates = {
|
||||||
|
'美濃加茂': datetime(2024, 5, 19), # 修正済み
|
||||||
|
'岐阜市': datetime(2024, 4, 28),
|
||||||
|
'大垣2': datetime(2024, 4, 20),
|
||||||
|
'各務原': datetime(2024, 3, 24),
|
||||||
|
'下呂': datetime(2024, 3, 10),
|
||||||
|
'中津川': datetime(2024, 3, 2),
|
||||||
|
'揖斐川': datetime(2024, 2, 18),
|
||||||
|
'高山': datetime(2024, 2, 11),
|
||||||
|
'大垣': datetime(2024, 1, 27),
|
||||||
|
'多治見': datetime(2024, 1, 20),
|
||||||
|
# 2024年のその他のイベント
|
||||||
|
'養老ロゲ': datetime(2024, 6, 1),
|
||||||
|
'郡上': datetime(2024, 11, 3), # 郡上イベント追加
|
||||||
|
# 2025年新規イベント
|
||||||
|
'岐阜ロゲイニング2025': datetime(2025, 9, 15),
|
||||||
|
}
|
||||||
|
return event_dates.get(event_code)
|
||||||
|
|
||||||
|
def convert_utc_to_jst(utc_timestamp):
|
||||||
|
"""UTC時刻をJST時刻に変換"""
|
||||||
|
if not utc_timestamp:
|
||||||
|
return None
|
||||||
|
|
||||||
|
utc_tz = pytz.UTC
|
||||||
|
jst_tz = pytz.timezone('Asia/Tokyo')
|
||||||
|
|
||||||
|
# UTCタイムゾーン情報を付加
|
||||||
|
if utc_timestamp.tzinfo is None:
|
||||||
|
utc_timestamp = utc_tz.localize(utc_timestamp)
|
||||||
|
|
||||||
|
# JSTに変換
|
||||||
|
return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None)
|
||||||
|
|
||||||
|
def parse_goal_time(goal_time_str, event_date_str):
|
||||||
|
"""goal_time文字列を適切なdatetimeに変換"""
|
||||||
|
if not goal_time_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# goal_timeが時刻のみの場合(例: "13:45:00")
|
||||||
|
goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time()
|
||||||
|
|
||||||
|
# event_date_strからイベント日付を解析
|
||||||
|
event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date()
|
||||||
|
|
||||||
|
# 日付と時刻を結合
|
||||||
|
goal_datetime = datetime.combine(event_date, goal_time)
|
||||||
|
|
||||||
|
# JSTとして解釈
|
||||||
|
jst_tz = pytz.timezone('Asia/Tokyo')
|
||||||
|
goal_datetime_jst = jst_tz.localize(goal_datetime)
|
||||||
|
|
||||||
|
# UTCに変換して返す
|
||||||
|
return goal_datetime_jst.astimezone(pytz.UTC)
|
||||||
|
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
print(f"goal_time変換エラー: {goal_time_str} - {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def clean_target_database_selective(target_cursor):
|
||||||
|
"""ターゲットデータベースの選択的クリーンアップ(既存データを保護)"""
|
||||||
|
print("=== ターゲットデータベースの選択的クリーンアップ ===")
|
||||||
|
|
||||||
|
# 外部キー制約を一時的に無効化
|
||||||
|
target_cursor.execute("SET session_replication_role = replica;")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# GPSチェックインデータのみクリーンアップ(重複移行防止)
|
||||||
|
target_cursor.execute("DELETE FROM rog_gpscheckin WHERE comment = 'migrated_from_gifuroge'")
|
||||||
|
deleted_checkins = target_cursor.rowcount
|
||||||
|
print(f"過去の移行GPSチェックインデータを削除: {deleted_checkins}件")
|
||||||
|
|
||||||
|
# 注意: rog_entry, rog_team, rog_member, rog_location2025 は削除しない!
|
||||||
|
print("注意: 既存のentry、team、member、location2025データは保護されます")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 外部キー制約を再有効化
|
||||||
|
target_cursor.execute("SET session_replication_role = DEFAULT;")
|
||||||
|
|
||||||
|
def verify_location2025_compatibility(target_cursor):
|
||||||
|
"""Location2025テーブルとの互換性を確認"""
|
||||||
|
print("\n=== Location2025互換性確認 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Location2025テーブルの存在確認
|
||||||
|
target_cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_name = 'rog_location2025'
|
||||||
|
""")
|
||||||
|
|
||||||
|
table_exists = target_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if table_exists:
|
||||||
|
# Location2025のデータ数確認
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025")
|
||||||
|
location2025_count = target_cursor.fetchone()[0]
|
||||||
|
print(f"✅ rog_location2025テーブル存在: {location2025_count}件のチェックポイント")
|
||||||
|
|
||||||
|
# イベント別チェックポイント数確認(安全版)
|
||||||
|
try:
|
||||||
|
# まずevent_codeカラムの存在確認
|
||||||
|
target_cursor.execute("""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_name = 'rog_newevent2'
|
||||||
|
AND column_name IN ('event_code', 'event_name')
|
||||||
|
""")
|
||||||
|
event_columns = [row[0] for row in target_cursor.fetchall()]
|
||||||
|
|
||||||
|
if 'event_code' in event_columns:
|
||||||
|
event_field = 'e.event_code'
|
||||||
|
elif 'event_name' in event_columns:
|
||||||
|
event_field = 'e.event_name'
|
||||||
|
else:
|
||||||
|
event_field = 'e.id'
|
||||||
|
|
||||||
|
target_cursor.execute(f"""
|
||||||
|
SELECT {event_field}, COUNT(l.id) as checkpoint_count
|
||||||
|
FROM rog_location2025 l
|
||||||
|
LEFT JOIN rog_newevent2 e ON l.event_id = e.id
|
||||||
|
GROUP BY {event_field}
|
||||||
|
ORDER BY checkpoint_count DESC
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
|
||||||
|
results = target_cursor.fetchall()
|
||||||
|
if results:
|
||||||
|
print("イベント別チェックポイント数(上位10件):")
|
||||||
|
for event_identifier, count in results:
|
||||||
|
print(f" {event_identifier}: {count}件")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ イベント別集計でエラー: {e}")
|
||||||
|
# エラーでも続行
|
||||||
|
ORDER BY checkpoint_count DESC
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
|
||||||
|
event_checkpoints = target_cursor.fetchall()
|
||||||
|
if event_checkpoints:
|
||||||
|
print("イベント別チェックポイント数(上位10件):")
|
||||||
|
for event_code, count in event_checkpoints:
|
||||||
|
print(f" {event_code}: {count}件")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("⚠️ rog_location2025テーブルが見つかりません")
|
||||||
|
print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Location2025互換性確認エラー: {e}")
|
||||||
|
# トランザクションエラーの場合はロールバック
|
||||||
|
try:
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def backup_existing_data(target_cursor):
|
||||||
|
"""既存データのバックアップ状況を確認"""
|
||||||
|
print("\n=== 既存データ保護確認 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 既存データ数を確認
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
|
||||||
|
entry_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM rog_team")
|
||||||
|
team_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
|
||||||
|
member_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
||||||
|
checkin_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# Location2025データ数も確認
|
||||||
|
try:
|
||||||
|
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025")
|
||||||
|
location2025_count = target_cursor.fetchone()[0]
|
||||||
|
print(f" rog_location2025: {location2025_count} 件 (保護対象)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" rog_location2025: 確認エラー ({e})")
|
||||||
|
location2025_count = 0
|
||||||
|
|
||||||
|
print(f"既存データ保護状況:")
|
||||||
|
print(f" rog_entry: {entry_count} 件 (保護対象)")
|
||||||
|
print(f" rog_team: {team_count} 件 (保護対象)")
|
||||||
|
print(f" rog_member: {member_count} 件 (保護対象)")
|
||||||
|
print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)")
|
||||||
|
|
||||||
|
if entry_count > 0 or team_count > 0 or member_count > 0:
|
||||||
|
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("⚠️ 既存のcore application dataが見つかりません。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 既存データ確認エラー: {e}")
|
||||||
|
# トランザクションエラーの場合はロールバック
|
||||||
|
try:
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def migrate_gps_data(source_cursor, target_cursor):
|
||||||
|
"""GPS記録データのみを移行(写真記録データは除外)"""
|
||||||
|
print("\n=== GPS記録データの移行 ===")
|
||||||
|
|
||||||
|
# GPS記録のみを取得(不正な写真記録データを除外)
|
||||||
|
source_cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
serial_number, team_name, cp_number, record_time,
|
||||||
|
goal_time, late_point, buy_flag, image_address,
|
||||||
|
minus_photo_flag, create_user, update_user,
|
||||||
|
colabo_company_memo
|
||||||
|
FROM gps_information
|
||||||
|
WHERE serial_number < 20000 -- GPS専用データのみ
|
||||||
|
ORDER BY serial_number
|
||||||
|
""")
|
||||||
|
|
||||||
|
gps_records = source_cursor.fetchall()
|
||||||
|
print(f"移行対象GPS記録数: {len(gps_records)}件")
|
||||||
|
|
||||||
|
migrated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
for record in gps_records:
|
||||||
|
try:
|
||||||
|
(serial_number, team_name, cp_number, record_time,
|
||||||
|
goal_time, late_point, buy_flag, image_address,
|
||||||
|
minus_photo_flag, create_user, update_user,
|
||||||
|
colabo_company_memo) = record
|
||||||
|
|
||||||
|
# UTC時刻をJST時刻に変換
|
||||||
|
record_time_jst = convert_utc_to_jst(record_time)
|
||||||
|
goal_time_utc = None
|
||||||
|
|
||||||
|
if goal_time:
|
||||||
|
# goal_timeをUTCに変換
|
||||||
|
if isinstance(goal_time, str):
|
||||||
|
# イベント名からイベント日付を取得
|
||||||
|
event_name = colabo_company_memo or "不明"
|
||||||
|
event_date = get_event_date(event_name)
|
||||||
|
if event_date:
|
||||||
|
goal_time_utc = parse_goal_time(goal_time, event_date.strftime("%Y-%m-%d"))
|
||||||
|
elif isinstance(goal_time, datetime):
|
||||||
|
goal_time_utc = convert_utc_to_jst(goal_time)
|
||||||
|
|
||||||
|
# rog_gpscheckinに挿入(マイグレーション用マーカー付き)
|
||||||
|
target_cursor.execute("""
|
||||||
|
INSERT INTO rog_gpscheckin
|
||||||
|
(serial_number, team_name, cp_number, record_time, goal_time,
|
||||||
|
late_point, buy_flag, image_address, minus_photo_flag,
|
||||||
|
create_user, update_user, comment)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
serial_number, team_name, cp_number, record_time_jst, goal_time_utc,
|
||||||
|
late_point, buy_flag, image_address, minus_photo_flag,
|
||||||
|
create_user, update_user, 'migrated_from_gifuroge'
|
||||||
|
))
|
||||||
|
|
||||||
|
migrated_count += 1
|
||||||
|
|
||||||
|
if migrated_count % 1000 == 0:
|
||||||
|
print(f"移行進捗: {migrated_count}/{len(gps_records)}件")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
print(f"移行エラー (record {serial_number}): {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー")
|
||||||
|
return migrated_count
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン移行処理(既存データ保護版)"""
|
||||||
|
print("=== 既存データ保護版移行プログラム開始 ===")
|
||||||
|
print("注意: 既存のentry、team、memberデータは削除されません")
|
||||||
|
|
||||||
|
# データベース接続設定
|
||||||
|
source_config = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'port': 5432,
|
||||||
|
'database': 'gifuroge',
|
||||||
|
'user': 'admin',
|
||||||
|
'password': 'admin123456'
|
||||||
|
}
|
||||||
|
|
||||||
|
target_config = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'port': 5432,
|
||||||
|
'database': 'rogdb',
|
||||||
|
'user': 'admin',
|
||||||
|
'password': 'admin123456'
|
||||||
|
}
|
||||||
|
|
||||||
|
source_conn = None
|
||||||
|
target_conn = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続
|
||||||
|
print("データベースに接続中...")
|
||||||
|
source_conn = psycopg2.connect(**source_config)
|
||||||
|
target_conn = psycopg2.connect(**target_config)
|
||||||
|
|
||||||
|
source_cursor = source_conn.cursor()
|
||||||
|
target_cursor = target_conn.cursor()
|
||||||
|
|
||||||
|
# Location2025互換性確認
|
||||||
|
location2025_available = verify_location2025_compatibility(target_cursor)
|
||||||
|
|
||||||
|
# 既存データ保護確認
|
||||||
|
has_existing_data = backup_existing_data(target_cursor)
|
||||||
|
|
||||||
|
# 確認プロンプト
|
||||||
|
print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}")
|
||||||
|
print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}")
|
||||||
|
|
||||||
|
response = input("\n移行を開始しますか? (y/N): ")
|
||||||
|
if response.lower() != 'y':
|
||||||
|
print("移行を中止しました。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 選択的クリーンアップ(既存データを保護)
|
||||||
|
clean_target_database_selective(target_cursor)
|
||||||
|
target_conn.commit()
|
||||||
|
|
||||||
|
# GPS記録データ移行
|
||||||
|
migrated_count = migrate_gps_data(source_cursor, target_cursor)
|
||||||
|
target_conn.commit()
|
||||||
|
|
||||||
|
print(f"\n=== 移行完了 ===")
|
||||||
|
print(f"移行されたGPS記録: {migrated_count}件")
|
||||||
|
print(f"Location2025互換性: {'✅ 対応済み' if location2025_available else '⚠️ 要確認'}")
|
||||||
|
if has_existing_data:
|
||||||
|
print("✅ 既存のentry、team、member、location2025データは保護されました")
|
||||||
|
else:
|
||||||
|
print("⚠️ 既存のcore application dataがありませんでした")
|
||||||
|
print(" 別途testdb/rogdb.sqlからの復元が必要です")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"移行エラー: {e}")
|
||||||
|
if target_conn:
|
||||||
|
target_conn.rollback()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if source_conn:
|
||||||
|
source_conn.close()
|
||||||
|
if target_conn:
|
||||||
|
target_conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
0
migration_data_protection_fixed.py
Normal file
0
migration_data_protection_fixed.py
Normal file
@ -354,21 +354,21 @@ def main():
|
|||||||
print("=== Location2025対応版移行プログラム開始 ===")
|
print("=== Location2025対応版移行プログラム開始 ===")
|
||||||
print("注意: 既存のentry、team、member、location2025データは削除されません")
|
print("注意: 既存のentry、team、member、location2025データは削除されません")
|
||||||
|
|
||||||
# データベース接続設定
|
# データベース接続設定(環境変数から取得、デフォルト値あり)
|
||||||
source_config = {
|
source_config = {
|
||||||
'host': 'localhost',
|
'host': os.getenv('SOURCE_DB_HOST', 'postgres-db'),
|
||||||
'port': '5433',
|
'port': os.getenv('SOURCE_DB_PORT', '5432'),
|
||||||
'database': 'gifuroge',
|
'database': os.getenv('SOURCE_DB_NAME', 'gifuroge'),
|
||||||
'user': 'postgres',
|
'user': os.getenv('SOURCE_DB_USER', 'admin'),
|
||||||
'password': 'postgres'
|
'password': os.getenv('SOURCE_DB_PASSWORD', 'admin123456')
|
||||||
}
|
}
|
||||||
|
|
||||||
target_config = {
|
target_config = {
|
||||||
'host': 'localhost',
|
'host': os.getenv('TARGET_DB_HOST', 'postgres-db'),
|
||||||
'port': '5432',
|
'port': os.getenv('TARGET_DB_PORT', '5432'),
|
||||||
'database': 'rogdb',
|
'database': os.getenv('TARGET_DB_NAME', 'rogdb'),
|
||||||
'user': 'postgres',
|
'user': os.getenv('TARGET_DB_USER', 'admin'),
|
||||||
'password': 'postgres'
|
'password': os.getenv('TARGET_DB_PASSWORD', 'admin123456')
|
||||||
}
|
}
|
||||||
|
|
||||||
source_conn = None
|
source_conn = None
|
||||||
|
|||||||
415
migration_simple_reset.py
Executable file
415
migration_simple_reset.py
Executable file
@ -0,0 +1,415 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Updated Migration Final Simple Script
|
||||||
|
====================================
|
||||||
|
|
||||||
|
This script provides a comprehensive workflow for resetting and rebuilding
|
||||||
|
Django migrations for the rogaining_srv project using a simplified approach.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python migration_simple_reset.py [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--backup-only : Only create backup of existing migrations
|
||||||
|
--reset-only : Only reset migrations (requires backup to exist)
|
||||||
|
--apply-only : Only apply migrations (requires simple migration to exist)
|
||||||
|
--full : Run complete workflow (default)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Creates timestamped backup of existing migrations
|
||||||
|
- Clears migration history from database
|
||||||
|
- Creates simplified initial migration with only managed models
|
||||||
|
- Applies migrations with proper error handling
|
||||||
|
- Provides detailed logging and status reporting
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import datetime
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add Django project to path
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
# Setup Django environment
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
from django.db import connection, transaction
|
||||||
|
from django.core.management.base import CommandError
|
||||||
|
|
||||||
|
class MigrationManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.project_root = Path(__file__).parent
|
||||||
|
self.migrations_dir = self.project_root / 'rog' / 'migrations'
|
||||||
|
self.backup_timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
self.backup_dir = self.project_root / f'rog/migrations_backup_{self.backup_timestamp}'
|
||||||
|
|
||||||
|
def log(self, message, level='INFO'):
|
||||||
|
"""Log message with timestamp and level"""
|
||||||
|
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
print(f"[{timestamp}] {level}: {message}")
|
||||||
|
|
||||||
|
def run_command(self, command, check=True):
|
||||||
|
"""Execute shell command with logging"""
|
||||||
|
self.log(f"Executing: {' '.join(command)}")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, check=check)
|
||||||
|
if result.stdout:
|
||||||
|
self.log(f"STDOUT: {result.stdout.strip()}")
|
||||||
|
if result.stderr:
|
||||||
|
self.log(f"STDERR: {result.stderr.strip()}", 'WARNING')
|
||||||
|
return result
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
self.log(f"Command failed with exit code {e.returncode}", 'ERROR')
|
||||||
|
self.log(f"STDOUT: {e.stdout}", 'ERROR')
|
||||||
|
self.log(f"STDERR: {e.stderr}", 'ERROR')
|
||||||
|
raise
|
||||||
|
|
||||||
|
def backup_migrations(self):
|
||||||
|
"""Create backup of existing migrations"""
|
||||||
|
self.log("Creating backup of existing migrations...")
|
||||||
|
|
||||||
|
if not self.migrations_dir.exists():
|
||||||
|
self.log("No migrations directory found, creating empty one")
|
||||||
|
self.migrations_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(self.migrations_dir / '__init__.py').touch()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create backup directory
|
||||||
|
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Copy all files
|
||||||
|
for item in self.migrations_dir.iterdir():
|
||||||
|
if item.is_file():
|
||||||
|
shutil.copy2(item, self.backup_dir)
|
||||||
|
self.log(f"Backed up: {item.name}")
|
||||||
|
|
||||||
|
self.log(f"Backup completed: {self.backup_dir}")
|
||||||
|
|
||||||
|
def clear_migration_history(self):
|
||||||
|
"""Clear migration history from database"""
|
||||||
|
self.log("Clearing migration history from database...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# Check if django_migrations table exists
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_name = 'django_migrations' AND table_schema = 'public'
|
||||||
|
""")
|
||||||
|
if cursor.fetchone()[0] == 0:
|
||||||
|
self.log("django_migrations table does not exist, skipping clear")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Count existing migrations
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM django_migrations WHERE app = 'rog'")
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
self.log(f"Found {count} existing rog migrations")
|
||||||
|
|
||||||
|
# Delete rog migrations
|
||||||
|
cursor.execute("DELETE FROM django_migrations WHERE app = 'rog'")
|
||||||
|
self.log(f"Deleted {count} migration records")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error clearing migration history: {e}", 'ERROR')
|
||||||
|
raise
|
||||||
|
|
||||||
|
def remove_migration_files(self):
|
||||||
|
"""Remove existing migration files except __init__.py"""
|
||||||
|
self.log("Removing existing migration files...")
|
||||||
|
|
||||||
|
if not self.migrations_dir.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
removed_count = 0
|
||||||
|
for item in self.migrations_dir.iterdir():
|
||||||
|
if item.is_file() and item.name != '__init__.py':
|
||||||
|
item.unlink()
|
||||||
|
self.log(f"Removed: {item.name}")
|
||||||
|
removed_count += 1
|
||||||
|
|
||||||
|
self.log(f"Removed {removed_count} migration files")
|
||||||
|
|
||||||
|
def create_simple_migration(self):
|
||||||
|
"""Create simplified initial migration"""
|
||||||
|
self.log("Creating simplified initial migration...")
|
||||||
|
|
||||||
|
# Create simple migration content
|
||||||
|
migration_content = '''# Generated by migration_simple_reset.py
|
||||||
|
from django.contrib.gis.db import models
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import migrations
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
import django.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='date joined')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'categories',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='NewEvent',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('event_date', models.DateField()),
|
||||||
|
('start_time', models.TimeField()),
|
||||||
|
('end_time', models.TimeField()),
|
||||||
|
('event_boundary', django.contrib.gis.db.models.fields.PolygonField(blank=True, null=True, srid=4326)),
|
||||||
|
('max_participants', models.PositiveIntegerField(default=100)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rog.category')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Team',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('max_members', models.PositiveIntegerField(default=5)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='rog.newevent')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Location',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('coordinate', django.contrib.gis.db.models.fields.PointField(srid=4326)),
|
||||||
|
('altitude', models.FloatField(blank=True, null=True)),
|
||||||
|
('location_type', models.CharField(choices=[('checkpoint', 'Checkpoint'), ('start', 'Start Point'), ('finish', 'Finish Point'), ('water', 'Water Station'), ('emergency', 'Emergency Point'), ('viewpoint', 'View Point'), ('other', 'Other')], default='checkpoint', max_length=20)),
|
||||||
|
('points', models.IntegerField(default=0)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='rog.newevent')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Entry',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('role', models.CharField(choices=[('leader', 'Team Leader'), ('member', 'Team Member')], default='member', max_length=10)),
|
||||||
|
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='rog.team')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rog.customuser')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'entries',
|
||||||
|
'unique_together': {('user', 'team')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Write migration file
|
||||||
|
migration_file = self.migrations_dir / '0001_simple_initial.py'
|
||||||
|
with open(migration_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(migration_content)
|
||||||
|
|
||||||
|
self.log(f"Created: {migration_file}")
|
||||||
|
|
||||||
|
def apply_migrations(self):
|
||||||
|
"""Apply migrations using Django management command"""
|
||||||
|
self.log("Applying migrations...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use fake-initial to treat as initial migration
|
||||||
|
result = self.run_command([
|
||||||
|
'python', 'manage.py', 'migrate', '--fake-initial'
|
||||||
|
], check=False)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.log("Migrations applied successfully")
|
||||||
|
else:
|
||||||
|
self.log("Migration application failed", 'ERROR')
|
||||||
|
raise CommandError("Migration application failed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error applying migrations: {e}", 'ERROR')
|
||||||
|
raise
|
||||||
|
|
||||||
|
def check_status(self):
|
||||||
|
"""Check current migration status"""
|
||||||
|
self.log("Checking migration status...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.run_command([
|
||||||
|
'python', 'manage.py', 'showmigrations', 'rog'
|
||||||
|
], check=False)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.log("Migration status checked successfully")
|
||||||
|
else:
|
||||||
|
self.log("Failed to check migration status", 'WARNING')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error checking migration status: {e}", 'WARNING')
|
||||||
|
|
||||||
|
def run_full_workflow(self):
|
||||||
|
"""Execute complete migration reset workflow"""
|
||||||
|
self.log("Starting full migration reset workflow...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Backup existing migrations
|
||||||
|
self.backup_migrations()
|
||||||
|
|
||||||
|
# Step 2: Clear migration history
|
||||||
|
self.clear_migration_history()
|
||||||
|
|
||||||
|
# Step 3: Remove migration files
|
||||||
|
self.remove_migration_files()
|
||||||
|
|
||||||
|
# Step 4: Create simplified migration
|
||||||
|
self.create_simple_migration()
|
||||||
|
|
||||||
|
# Step 5: Apply migrations
|
||||||
|
self.apply_migrations()
|
||||||
|
|
||||||
|
# Step 6: Check final status
|
||||||
|
self.check_status()
|
||||||
|
|
||||||
|
self.log("Full migration reset workflow completed successfully!")
|
||||||
|
self.log(f"Backup created at: {self.backup_dir}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Workflow failed: {e}", 'ERROR')
|
||||||
|
self.log(f"You can restore from backup at: {self.backup_dir}", 'INFO')
|
||||||
|
raise
|
||||||
|
|
||||||
|
def run_backup_only(self):
|
||||||
|
"""Create backup only"""
|
||||||
|
self.log("Running backup-only mode...")
|
||||||
|
self.backup_migrations()
|
||||||
|
self.log("Backup completed successfully!")
|
||||||
|
|
||||||
|
def run_reset_only(self):
|
||||||
|
"""Reset migrations only (requires backup)"""
|
||||||
|
self.log("Running reset-only mode...")
|
||||||
|
self.clear_migration_history()
|
||||||
|
self.remove_migration_files()
|
||||||
|
self.create_simple_migration()
|
||||||
|
self.log("Reset completed successfully!")
|
||||||
|
|
||||||
|
def run_apply_only(self):
|
||||||
|
"""Apply migrations only"""
|
||||||
|
self.log("Running apply-only mode...")
|
||||||
|
self.apply_migrations()
|
||||||
|
self.check_status()
|
||||||
|
self.log("Apply completed successfully!")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function with command line argument handling"""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Reset and rebuild Django migrations for rogaining_srv project'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--backup-only',
|
||||||
|
action='store_true',
|
||||||
|
help='Only create backup of existing migrations'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--reset-only',
|
||||||
|
action='store_true',
|
||||||
|
help='Only reset migrations (requires backup to exist)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--apply-only',
|
||||||
|
action='store_true',
|
||||||
|
help='Only apply migrations (requires simple migration to exist)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--full',
|
||||||
|
action='store_true',
|
||||||
|
help='Run complete workflow (default)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# If no specific mode is selected, default to full
|
||||||
|
if not any([args.backup_only, args.reset_only, args.apply_only, args.full]):
|
||||||
|
args.full = True
|
||||||
|
|
||||||
|
manager = MigrationManager()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.backup_only:
|
||||||
|
manager.run_backup_only()
|
||||||
|
elif args.reset_only:
|
||||||
|
manager.run_reset_only()
|
||||||
|
elif args.apply_only:
|
||||||
|
manager.run_apply_only()
|
||||||
|
elif args.full:
|
||||||
|
manager.run_full_workflow()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {e}")
|
||||||
|
print("Please check the logs above for details.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
406
migration_statistics.py
Normal file
406
migration_statistics.py
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
移行結果統計情報表示スクリプト
|
||||||
|
Docker Compose環境で実行可能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pytz
|
||||||
|
from collections import defaultdict
|
||||||
|
import json
|
||||||
|
|
||||||
|
def connect_database():
|
||||||
|
"""データベースに接続"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=os.environ.get('DB_HOST', 'postgres-db'),
|
||||||
|
port=os.environ.get('DB_PORT', '5432'),
|
||||||
|
database=os.environ.get('POSTGRES_DBNAME', 'rogdb'),
|
||||||
|
user=os.environ.get('POSTGRES_USER', 'admin'),
|
||||||
|
password=os.environ.get('POSTGRES_PASS', 'admin123456')
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ データベース接続エラー: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_basic_statistics(cursor):
|
||||||
|
"""基本統計情報を取得"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 移行データ基本統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# テーブル別レコード数
|
||||||
|
tables = [
|
||||||
|
('rog_newevent2', 'イベント'),
|
||||||
|
('rog_team', 'チーム'),
|
||||||
|
('rog_member', 'メンバー'),
|
||||||
|
('rog_entry', 'エントリー'),
|
||||||
|
('rog_gpscheckin', 'GPSチェックイン'),
|
||||||
|
('rog_checkpoint', 'チェックポイント'),
|
||||||
|
('rog_location2025', 'ロケーション2025'),
|
||||||
|
('rog_customuser', 'ユーザー')
|
||||||
|
]
|
||||||
|
|
||||||
|
print("\n📋 テーブル別レコード数:")
|
||||||
|
print("テーブル名 日本語名 レコード数")
|
||||||
|
print("-" * 65)
|
||||||
|
|
||||||
|
total_records = 0
|
||||||
|
for table_name, japanese_name in tables:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
total_records += count
|
||||||
|
print(f"{table_name:<25} {japanese_name:<15} {count:>10,}件")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{table_name:<25} {japanese_name:<15} {'エラー':>10}")
|
||||||
|
|
||||||
|
print("-" * 65)
|
||||||
|
print(f"{'合計':<41} {total_records:>10,}件")
|
||||||
|
|
||||||
|
def get_event_statistics(cursor):
|
||||||
|
"""イベント別統計情報"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🎯 イベント別統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# イベント一覧と基本情報
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, event_name, event_date, created_at
|
||||||
|
FROM rog_newevent2
|
||||||
|
ORDER BY event_date DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
events = cursor.fetchall()
|
||||||
|
if not events:
|
||||||
|
print("イベントデータがありません")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n📅 登録イベント数: {len(events)}件")
|
||||||
|
print("\nイベント詳細:")
|
||||||
|
print("ID イベント名 開催日 登録日時")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
event_id, event_name, event_date, created_at = event
|
||||||
|
event_date_str = event_date.strftime("%Y-%m-%d") if event_date else "未設定"
|
||||||
|
created_str = created_at.strftime("%Y-%m-%d %H:%M") if created_at else "未設定"
|
||||||
|
print(f"{event_id:>2} {event_name:<15} {event_date_str} {created_str}")
|
||||||
|
|
||||||
|
# イベント別参加統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
e.event_name,
|
||||||
|
COUNT(DISTINCT t.id) as team_count,
|
||||||
|
COUNT(DISTINCT m.id) as member_count,
|
||||||
|
COUNT(DISTINCT en.id) as entry_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_team t ON e.id = t.event_id
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
LEFT JOIN rog_entry en ON t.id = en.team_id
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
ORDER BY team_count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
participation_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
print("\n👥 イベント別参加統計:")
|
||||||
|
print("イベント名 チーム数 メンバー数 エントリー数")
|
||||||
|
print("-" * 55)
|
||||||
|
|
||||||
|
total_teams = 0
|
||||||
|
total_members = 0
|
||||||
|
total_entries = 0
|
||||||
|
|
||||||
|
for stat in participation_stats:
|
||||||
|
event_name, team_count, member_count, entry_count = stat
|
||||||
|
total_teams += team_count
|
||||||
|
total_members += member_count
|
||||||
|
total_entries += entry_count
|
||||||
|
print(f"{event_name:<15} {team_count:>6}件 {member_count:>8}件 {entry_count:>9}件")
|
||||||
|
|
||||||
|
print("-" * 55)
|
||||||
|
print(f"{'合計':<15} {total_teams:>6}件 {total_members:>8}件 {total_entries:>9}件")
|
||||||
|
|
||||||
|
def get_gps_checkin_statistics(cursor):
|
||||||
|
"""GPSチェックイン統計"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📍 GPSチェックイン統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 基本統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_checkins,
|
||||||
|
COUNT(DISTINCT zekken) as unique_teams,
|
||||||
|
COUNT(DISTINCT cp_number) as unique_checkpoints,
|
||||||
|
MIN(checkin_time) as earliest_checkin,
|
||||||
|
MAX(checkin_time) as latest_checkin
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
""")
|
||||||
|
|
||||||
|
basic_stats = cursor.fetchone()
|
||||||
|
if not basic_stats or basic_stats[0] == 0:
|
||||||
|
print("GPSチェックインデータがありません")
|
||||||
|
return
|
||||||
|
|
||||||
|
total_checkins, unique_teams, unique_checkpoints, earliest, latest = basic_stats
|
||||||
|
|
||||||
|
print(f"\n📊 基本統計:")
|
||||||
|
print(f"総チェックイン数: {total_checkins:,}件")
|
||||||
|
print(f"参加チーム数: {unique_teams:,}チーム")
|
||||||
|
print(f"利用CP数: {unique_checkpoints:,}箇所")
|
||||||
|
|
||||||
|
if earliest and latest:
|
||||||
|
print(f"最早チェックイン: {earliest.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"最終チェックイン: {latest.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# 時間帯別分析
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
EXTRACT(hour FROM checkin_time) as hour,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
GROUP BY EXTRACT(hour FROM checkin_time)
|
||||||
|
ORDER BY hour
|
||||||
|
""")
|
||||||
|
|
||||||
|
hourly_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"\n⏰ 時間帯別チェックイン分布:")
|
||||||
|
print("時間 チェックイン数 グラフ")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
max_count = max([count for _, count in hourly_stats]) if hourly_stats else 1
|
||||||
|
|
||||||
|
for hour, count in hourly_stats:
|
||||||
|
bar_length = int((count / max_count) * 20)
|
||||||
|
bar = "█" * bar_length
|
||||||
|
print(f"{int(hour):>2}時 {count:>8}件 {bar}")
|
||||||
|
|
||||||
|
# CP別利用統計(上位10位)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
cp_number,
|
||||||
|
COUNT(*) as checkin_count,
|
||||||
|
COUNT(DISTINCT zekken) as team_count
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
GROUP BY cp_number
|
||||||
|
ORDER BY checkin_count DESC
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
|
||||||
|
cp_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"\n🏅 CP利用ランキング(上位10位):")
|
||||||
|
print("順位 CP番号 チェックイン数 利用チーム数")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
for i, (cp_number, checkin_count, team_count) in enumerate(cp_stats, 1):
|
||||||
|
print(f"{i:>2}位 CP{cp_number:>3} {checkin_count:>8}件 {team_count:>7}チーム")
|
||||||
|
|
||||||
|
def get_team_statistics(cursor):
|
||||||
|
"""チーム統計"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("👥 チーム統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# チーム基本統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_teams,
|
||||||
|
COUNT(DISTINCT class_name) as unique_classes,
|
||||||
|
AVG(CASE WHEN member_count > 0 THEN member_count END) as avg_members
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.class_name,
|
||||||
|
COUNT(m.id) as member_count
|
||||||
|
FROM rog_team t
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
GROUP BY t.id, t.class_name
|
||||||
|
) team_stats
|
||||||
|
""")
|
||||||
|
|
||||||
|
team_basic = cursor.fetchone()
|
||||||
|
total_teams, unique_classes, avg_members = team_basic
|
||||||
|
|
||||||
|
print(f"\n📊 基本統計:")
|
||||||
|
print(f"総チーム数: {total_teams:,}チーム")
|
||||||
|
print(f"クラス数: {unique_classes or 0}種類")
|
||||||
|
print(f"平均メンバー数: {avg_members:.1f}人/チーム" if avg_members else "平均メンバー数: データなし")
|
||||||
|
|
||||||
|
# クラス別統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COALESCE(class_name, '未分類') as class_name,
|
||||||
|
COUNT(*) as team_count,
|
||||||
|
COUNT(CASE WHEN member_count > 0 THEN 1 END) as active_teams
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
t.class_name,
|
||||||
|
COUNT(m.id) as member_count
|
||||||
|
FROM rog_team t
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
GROUP BY t.id, t.class_name
|
||||||
|
) team_stats
|
||||||
|
GROUP BY class_name
|
||||||
|
ORDER BY team_count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
class_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
if class_stats:
|
||||||
|
print(f"\n🏆 クラス別チーム数:")
|
||||||
|
print("クラス名 チーム数 アクティブ")
|
||||||
|
print("-" * 35)
|
||||||
|
|
||||||
|
for class_name, team_count, active_teams in class_stats:
|
||||||
|
print(f"{class_name:<15} {team_count:>6}件 {active_teams:>7}件")
|
||||||
|
|
||||||
|
def get_data_quality_check(cursor):
|
||||||
|
"""データ品質チェック"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🔍 データ品質チェック")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
checks = []
|
||||||
|
|
||||||
|
# 1. 重複チェック
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM (
|
||||||
|
SELECT zekken, cp_number, checkin_time, COUNT(*)
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
GROUP BY zekken, cp_number, checkin_time
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) duplicates
|
||||||
|
""")
|
||||||
|
duplicate_count = cursor.fetchone()[0]
|
||||||
|
checks.append(("重複チェックイン", duplicate_count, "件"))
|
||||||
|
|
||||||
|
# 2. 異常時刻チェック(0時台)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_gpscheckin
|
||||||
|
WHERE EXTRACT(hour FROM checkin_time) = 0
|
||||||
|
""")
|
||||||
|
zero_hour_count = cursor.fetchone()[0]
|
||||||
|
checks.append(("0時台チェックイン", zero_hour_count, "件"))
|
||||||
|
|
||||||
|
# 3. 未来日時チェック
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_gpscheckin
|
||||||
|
WHERE checkin_time > NOW()
|
||||||
|
""")
|
||||||
|
future_count = cursor.fetchone()[0]
|
||||||
|
checks.append(("未来日時チェックイン", future_count, "件"))
|
||||||
|
|
||||||
|
# 4. メンバー不在チーム
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_team t
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
WHERE m.id IS NULL
|
||||||
|
""")
|
||||||
|
no_member_teams = cursor.fetchone()[0]
|
||||||
|
checks.append(("メンバー不在チーム", no_member_teams, "チーム"))
|
||||||
|
|
||||||
|
# 5. エントリー不在チーム
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_team t
|
||||||
|
LEFT JOIN rog_entry e ON t.id = e.team_id
|
||||||
|
WHERE e.id IS NULL
|
||||||
|
""")
|
||||||
|
no_entry_teams = cursor.fetchone()[0]
|
||||||
|
checks.append(("エントリー不在チーム", no_entry_teams, "チーム"))
|
||||||
|
|
||||||
|
print("\n🧪 品質チェック結果:")
|
||||||
|
print("チェック項目 件数 状態")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
for check_name, count, unit in checks:
|
||||||
|
status = "✅ 正常" if count == 0 else "⚠️ 要確認"
|
||||||
|
print(f"{check_name:<15} {count:>6}{unit} {status}")
|
||||||
|
|
||||||
|
def export_statistics_json(cursor):
|
||||||
|
"""統計情報をJSONで出力"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📄 統計情報JSON出力")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
statistics = {}
|
||||||
|
|
||||||
|
# 基本統計
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
||||||
|
statistics['total_checkins'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(DISTINCT zekken) FROM rog_gpscheckin")
|
||||||
|
statistics['unique_teams'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_newevent2")
|
||||||
|
statistics['total_events'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# イベント別統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT event_name, COUNT(*) as checkin_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_team t ON e.id = t.event_id
|
||||||
|
LEFT JOIN rog_gpscheckin g ON t.zekken = g.zekken
|
||||||
|
GROUP BY e.id, event_name
|
||||||
|
ORDER BY checkin_count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
event_stats = {}
|
||||||
|
for event_name, count in cursor.fetchall():
|
||||||
|
event_stats[event_name] = count
|
||||||
|
|
||||||
|
statistics['event_checkins'] = event_stats
|
||||||
|
statistics['generated_at'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# ファイル出力
|
||||||
|
output_file = f"/app/migration_statistics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(statistics, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"✅ 統計情報をJSONで出力しました: {output_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ JSON出力エラー: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
print("🚀 移行結果統計情報表示スクリプト開始")
|
||||||
|
print(f"実行時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# データベース接続
|
||||||
|
conn = connect_database()
|
||||||
|
if not conn:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 各統計情報を表示
|
||||||
|
get_basic_statistics(cursor)
|
||||||
|
get_event_statistics(cursor)
|
||||||
|
get_gps_checkin_statistics(cursor)
|
||||||
|
get_team_statistics(cursor)
|
||||||
|
get_data_quality_check(cursor)
|
||||||
|
export_statistics_json(cursor)
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✅ 統計情報表示完了")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 統計処理中にエラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
166
monitor_app_errors.py
Normal file
166
monitor_app_errors.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
スマホアプリエラー監視ツール: リアルタイムで400エラーを監視
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
def monitor_app_errors():
|
||||||
|
"""
|
||||||
|
スマホアプリの400エラーをリアルタイム監視
|
||||||
|
"""
|
||||||
|
print("🔍 スマホアプリエラー監視開始")
|
||||||
|
print("=" * 60)
|
||||||
|
print("Ctrl+C で停止")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# docker compose logs --follow で継続監視
|
||||||
|
process = subprocess.Popen(
|
||||||
|
['docker', 'compose', 'logs', '--follow', 'app'],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
universal_newlines=True
|
||||||
|
)
|
||||||
|
|
||||||
|
error_patterns = {
|
||||||
|
'checkin_400': re.compile(r'Bad Request.*checkin_from_rogapp', re.I),
|
||||||
|
'member_400': re.compile(r'Bad Request.*members', re.I),
|
||||||
|
'team_400': re.compile(r'Bad Request.*teams', re.I),
|
||||||
|
'validation_error': re.compile(r'Validation error.*({.*})', re.I),
|
||||||
|
'checkin_start_error': re.compile(r'Team has not started yet.*team_name: \'([^\']+)\'.*cp_number: (\d+)', re.I),
|
||||||
|
'dart_request': re.compile(r'Dart/\d+\.\d+.*"([A-Z]+)\s+([^"]+)".*(\d{3})', re.I)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"🎯 監視対象パターン:")
|
||||||
|
for name, pattern in error_patterns.items():
|
||||||
|
print(f" • {name}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = process.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
# Dartクライアント(スマホアプリ)のリクエスト監視
|
||||||
|
dart_match = error_patterns['dart_request'].search(line)
|
||||||
|
if dart_match and 'Dart/' in line:
|
||||||
|
method = dart_match.group(1)
|
||||||
|
path = dart_match.group(2)
|
||||||
|
status = dart_match.group(3)
|
||||||
|
|
||||||
|
if status.startswith('4'): # 4xx エラー
|
||||||
|
print(f"❌ [{timestamp}] スマホアプリエラー: {method} {path} → HTTP {status}")
|
||||||
|
elif status.startswith('2'): # 2xx 成功
|
||||||
|
if any(keyword in path.lower() for keyword in ['checkin', 'teams', 'members']):
|
||||||
|
print(f"✅ [{timestamp}] スマホアプリ成功: {method} {path} → HTTP {status}")
|
||||||
|
|
||||||
|
# チェックインスタートエラー監視
|
||||||
|
start_error_match = error_patterns['checkin_start_error'].search(line)
|
||||||
|
if start_error_match:
|
||||||
|
team_name = start_error_match.group(1)
|
||||||
|
cp_number = start_error_match.group(2)
|
||||||
|
print(f"⚠️ [{timestamp}] チェックインエラー: チーム'{team_name}'がCP{cp_number}でスタート前チェックイン試行")
|
||||||
|
print(f" 💡 解決策: 先にstart_from_rogappでスタート処理が必要")
|
||||||
|
|
||||||
|
# バリデーションエラー監視
|
||||||
|
validation_match = error_patterns['validation_error'].search(line)
|
||||||
|
if validation_match:
|
||||||
|
try:
|
||||||
|
error_details = validation_match.group(1)
|
||||||
|
print(f"❌ [{timestamp}] バリデーションエラー: {error_details}")
|
||||||
|
if 'date_of_birth' in error_details:
|
||||||
|
print(f" 💡 date_of_birthフィールドの問題 - MemberCreationSerializerを確認")
|
||||||
|
except:
|
||||||
|
print(f"❌ [{timestamp}] バリデーションエラー詳細を解析できませんでした")
|
||||||
|
|
||||||
|
# 一般的な400エラー監視
|
||||||
|
for pattern_name, pattern in error_patterns.items():
|
||||||
|
if pattern_name in ['dart_request', 'checkin_start_error', 'validation_error']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pattern.search(line):
|
||||||
|
print(f"❌ [{timestamp}] {pattern_name}: {line.strip()}")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n\n🛑 監視を停止しました")
|
||||||
|
process.terminate()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 監視エラー: {e}")
|
||||||
|
if 'process' in locals():
|
||||||
|
process.terminate()
|
||||||
|
|
||||||
|
def analyze_current_issues():
|
||||||
|
"""
|
||||||
|
現在の問題を分析
|
||||||
|
"""
|
||||||
|
print("📊 現在の問題分析")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 最近のエラーログを分析
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker', 'compose', 'logs', '--tail=100', 'app'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = result.stdout.split('\n')
|
||||||
|
|
||||||
|
# チェックインエラーの分析
|
||||||
|
checkin_errors = []
|
||||||
|
validation_errors = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if 'Team has not started yet' in line:
|
||||||
|
match = re.search(r'team_name: \'([^\']+)\'.*cp_number: (\d+)', line)
|
||||||
|
if match:
|
||||||
|
checkin_errors.append((match.group(1), match.group(2)))
|
||||||
|
|
||||||
|
if 'Validation error' in line and 'date_of_birth' in line:
|
||||||
|
validation_errors.append(line)
|
||||||
|
|
||||||
|
print(f"🔍 分析結果:")
|
||||||
|
print(f" • チェックインスタートエラー: {len(checkin_errors)}件")
|
||||||
|
for team, cp in checkin_errors[-3:]: # 最新3件
|
||||||
|
print(f" - チーム'{team}' → CP{cp}")
|
||||||
|
|
||||||
|
print(f" • date_of_birthバリデーションエラー: {len(validation_errors)}件")
|
||||||
|
|
||||||
|
if checkin_errors:
|
||||||
|
print(f"\n💡 推奨対策:")
|
||||||
|
print(f" 1. スマホアプリでスタート処理を実行")
|
||||||
|
print(f" 2. start_from_rogapp API の正常動作確認")
|
||||||
|
|
||||||
|
if validation_errors:
|
||||||
|
print(f" 3. MemberCreationSerializerの再確認")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 分析エラー: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
メイン関数
|
||||||
|
"""
|
||||||
|
print("📱 スマホアプリエラー監視ツール")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
choice = input("選択してください:\n1. リアルタイム監視\n2. 現在の問題分析\n選択 (1/2): ")
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
monitor_app_errors()
|
||||||
|
elif choice == "2":
|
||||||
|
analyze_current_issues()
|
||||||
|
else:
|
||||||
|
print("無効な選択です")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
127
nginx.conf
127
nginx.conf
@ -34,11 +34,14 @@ http {
|
|||||||
alias /app/static/;
|
alias /app/static/;
|
||||||
}
|
}
|
||||||
|
|
||||||
# スーパーバイザー Web アプリケーション
|
# スーパーバイザー Web アプリケーション(特定パス)
|
||||||
location / {
|
location = / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
try_files $uri $uri/ /index.html;
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# スーパーバイザー専用の静的ファイル
|
# スーパーバイザー専用の静的ファイル
|
||||||
@ -47,6 +50,72 @@ http {
|
|||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Django ログアウト・ログイン系の処理
|
||||||
|
location /accounts/ {
|
||||||
|
proxy_pass http://app:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
client_max_body_size 50M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ろげイニングアプリ専用API(重要!)
|
||||||
|
location /gifuroge/ {
|
||||||
|
proxy_pass http://app:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# タイムアウト設定(502エラー対策)
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
# バッファ設定(大きなレスポンス対策)
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 8k;
|
||||||
|
proxy_buffers 8 8k;
|
||||||
|
proxy_busy_buffers_size 16k;
|
||||||
|
proxy_temp_file_write_size 16k;
|
||||||
|
|
||||||
|
# クライアント設定(画像アップロード対応)
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 🔧 スマホアプリ互換性対応: /api/checkin_from_rogapp の特別処理
|
||||||
|
location /api/checkin_from_rogapp {
|
||||||
|
proxy_pass http://app:8000/api/checkin_from_rogapp;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# タイムアウト設定(502エラー対策)
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
# バッファ設定(大きなレスポンス対策)
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 8k;
|
||||||
|
proxy_buffers 8 8k;
|
||||||
|
proxy_busy_buffers_size 16k;
|
||||||
|
proxy_temp_file_write_size 16k;
|
||||||
|
|
||||||
|
# クライアント設定(画像アップロード対応)
|
||||||
|
client_max_body_size 100M;
|
||||||
|
|
||||||
|
# ログ記録強化
|
||||||
|
access_log /var/log/nginx/checkin_access.log main;
|
||||||
|
error_log /var/log/nginx/checkin_error.log warn;
|
||||||
|
}
|
||||||
|
|
||||||
# Django API プロキシ
|
# Django API プロキシ
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://app:8000;
|
proxy_pass http://app:8000;
|
||||||
@ -54,6 +123,21 @@ http {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# タイムアウト設定(502エラー対策)
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
# バッファ設定(大きなレスポンス対策)
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 8k;
|
||||||
|
proxy_buffers 8 8k;
|
||||||
|
proxy_busy_buffers_size 16k;
|
||||||
|
proxy_temp_file_write_size 16k;
|
||||||
|
|
||||||
|
# クライアント設定
|
||||||
|
client_max_body_size 50M;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Django Admin プロキシ
|
# Django Admin プロキシ
|
||||||
@ -63,6 +147,43 @@ http {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# タイムアウト設定
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
# バッファ設定
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 8k;
|
||||||
|
proxy_buffers 8 8k;
|
||||||
|
proxy_busy_buffers_size 16k;
|
||||||
|
|
||||||
|
# クライアント設定
|
||||||
|
client_max_body_size 50M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Django メインアプリケーション(rog/以下)
|
||||||
|
location /rog/ {
|
||||||
|
proxy_pass http://app:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Django その他のパス(デフォルト)
|
||||||
|
location ~ ^/(media|favicon\.ico|robots\.txt) {
|
||||||
|
proxy_pass http://app:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|||||||
174
realtime_checkin_monitor.py
Normal file
174
realtime_checkin_monitor.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
リアルタイム チェックイン監視ツール
|
||||||
|
スマホアプリからのチェックイン試行をリアルタイムで監視
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import threading
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# Django設定
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from rog.models import GpsLog, Entry, Team, NewEvent2
|
||||||
|
|
||||||
|
class CheckinMonitor:
|
||||||
|
def __init__(self):
|
||||||
|
self.last_check = datetime.now()
|
||||||
|
self.request_counts = defaultdict(int)
|
||||||
|
|
||||||
|
def check_recent_gpslog(self):
|
||||||
|
"""最近のGpsLog エントリーをチェック"""
|
||||||
|
try:
|
||||||
|
recent_logs = GpsLog.objects.filter(
|
||||||
|
create_at__gte=self.last_check
|
||||||
|
).order_by('-create_at')
|
||||||
|
|
||||||
|
if recent_logs.exists():
|
||||||
|
print(f"\n🆕 新しいGpsLogエントリー ({recent_logs.count()}件):")
|
||||||
|
for log in recent_logs:
|
||||||
|
print(f" ✅ ID:{log.id} イベント:{log.event_code} ゼッケン:{log.zekken_number} CP:{log.cp_number} 時刻:{log.create_at}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ GpsLog確認エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_recent_entries(self):
|
||||||
|
"""最近のEntry更新をチェック"""
|
||||||
|
try:
|
||||||
|
recent_entries = Entry.objects.filter(
|
||||||
|
start_time__gte=self.last_check
|
||||||
|
).order_by('-start_time')
|
||||||
|
|
||||||
|
if recent_entries.exists():
|
||||||
|
print(f"\n🏁 新しいスタート ({recent_entries.count()}件):")
|
||||||
|
for entry in recent_entries:
|
||||||
|
team_name = entry.team.team_name if entry.team else "N/A"
|
||||||
|
event_name = entry.event.event_name if entry.event else "N/A"
|
||||||
|
print(f" 🚀 エントリーID:{entry.id} チーム:{team_name} イベント:{event_name} スタート時刻:{entry.start_time}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Entry確認エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_nginx_logs(self):
|
||||||
|
"""nginxログから最近のAPIアクセスを確認"""
|
||||||
|
try:
|
||||||
|
# Dockerログからnginxのアクセスログを取得
|
||||||
|
cmd = ["docker", "compose", "logs", "--tail=20", "nginx"]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd="/Volumes/PortableSSD1TB/main/GifuTabi/rogaining_srv_exdb-2/rogaining_srv")
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
lines = result.stdout.split('\n')
|
||||||
|
api_requests = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if 'checkin_from_rogapp' in line or 'start_from_rogapp' in line:
|
||||||
|
api_requests.append(line)
|
||||||
|
elif any(endpoint in line for endpoint in ['/api/user/', '/api/teams/', '/api/entry/']):
|
||||||
|
api_requests.append(line)
|
||||||
|
|
||||||
|
if api_requests:
|
||||||
|
print(f"\n📡 最近のAPI アクセス ({len(api_requests)}件):")
|
||||||
|
for req in api_requests[-5:]: # 最新5件のみ表示
|
||||||
|
print(f" 📥 {req.strip()}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ nginxログ確認エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_app_logs(self):
|
||||||
|
"""アプリケーションログから最近のエラーを確認"""
|
||||||
|
try:
|
||||||
|
cmd = ["docker", "compose", "logs", "--tail=10", "app"]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, cwd="/Volumes/PortableSSD1TB/main/GifuTabi/rogaining_srv_exdb-2/rogaining_srv")
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
lines = result.stdout.split('\n')
|
||||||
|
error_logs = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if any(keyword in line.lower() for keyword in ['error', 'warning', 'exception', 'failed', 'api_play']):
|
||||||
|
error_logs.append(line)
|
||||||
|
|
||||||
|
if error_logs:
|
||||||
|
print(f"\n⚠️ 最近のアプリケーションログ ({len(error_logs)}件):")
|
||||||
|
for log in error_logs[-3:]: # 最新3件のみ表示
|
||||||
|
print(f" 🔍 {log.strip()}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ アプリケーションログ確認エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_checkin_endpoints(self):
|
||||||
|
"""チェックインエンドポイントの動作テスト"""
|
||||||
|
endpoints = [
|
||||||
|
"http://localhost:8100/api/checkin_from_rogapp",
|
||||||
|
"http://localhost:8100/gifuroge/checkin_from_rogapp"
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"\n🔧 チェックインエンドポイント動作確認:")
|
||||||
|
for endpoint in endpoints:
|
||||||
|
try:
|
||||||
|
response = requests.get(endpoint, timeout=5)
|
||||||
|
status_color = "✅" if response.status_code == 405 else "❌"
|
||||||
|
print(f" {status_color} {endpoint} → HTTP {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ {endpoint} → エラー: {e}")
|
||||||
|
|
||||||
|
def run_monitor(self):
|
||||||
|
"""メイン監視ループ"""
|
||||||
|
print("🚀 リアルタイム チェックイン監視開始")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
current_time = datetime.now()
|
||||||
|
print(f"\n⏰ 監視時刻: {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# 各種チェック実行
|
||||||
|
has_new_data = False
|
||||||
|
has_new_data |= self.check_recent_gpslog()
|
||||||
|
has_new_data |= self.check_recent_entries()
|
||||||
|
has_new_data |= self.check_nginx_logs()
|
||||||
|
has_new_data |= self.check_app_logs()
|
||||||
|
|
||||||
|
# 10分毎にエンドポイントテスト
|
||||||
|
if current_time.minute % 10 == 0:
|
||||||
|
self.test_checkin_endpoints()
|
||||||
|
|
||||||
|
if not has_new_data:
|
||||||
|
print(" 💤 新しいアクティビティなし")
|
||||||
|
|
||||||
|
# 次回チェック時刻を更新
|
||||||
|
self.last_check = current_time
|
||||||
|
|
||||||
|
print("-" * 40)
|
||||||
|
time.sleep(30) # 30秒間隔で監視
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🛑 監視を停止します")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 監視エラー: {e}")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
monitor = CheckinMonitor()
|
||||||
|
monitor.run_monitor()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
528
register_event_users.py
Normal file
528
register_event_users.py
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
イベントユーザー登録スクリプト
|
||||||
|
|
||||||
|
外部システムAPI仕様書.mdを前提に、ユーザーデータCSVから、
|
||||||
|
各ユーザーごとにユーザー登録、チーム登録、エントリー登録、イベント参加を行う
|
||||||
|
docker composeで実施するPythonスクリプト
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
python register_event_users.py --event_code 大垣2509
|
||||||
|
|
||||||
|
ユーザーデータのCSVは以下の項目を持つ:
|
||||||
|
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import csv
|
||||||
|
import requests
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, date
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('register_event_users.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EventUserRegistration:
|
||||||
|
def __init__(self, event_code: str, base_url: str = "http://localhost:8000", dry_run: bool = False):
|
||||||
|
"""
|
||||||
|
イベントユーザー登録クラス
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_code: イベントコード(例: 大垣2509)
|
||||||
|
base_url: APIベースURL
|
||||||
|
dry_run: テスト実行フラグ
|
||||||
|
"""
|
||||||
|
self.event_code = event_code
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.dry_run = dry_run
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.admin_token = None
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
self.stats = {
|
||||||
|
'processed_teams': 0,
|
||||||
|
'users_created': 0,
|
||||||
|
'users_updated': 0,
|
||||||
|
'teams_registered': 0,
|
||||||
|
'entries_created': 0,
|
||||||
|
'participations_created': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Event User Registration initialized for event: {event_code}")
|
||||||
|
if dry_run:
|
||||||
|
logger.info("DRY RUN MODE - No actual API calls will be made")
|
||||||
|
|
||||||
|
def get_or_create_user(self, email: str, password: str, firstname: str, lastname: str,
|
||||||
|
date_of_birth: str, phone: str) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||||
|
"""
|
||||||
|
メールアドレスをキーに既存ユーザーを取得、存在しなければ新規作成
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email: メールアドレス
|
||||||
|
password: パスワード
|
||||||
|
firstname: 名前
|
||||||
|
lastname: 姓
|
||||||
|
date_of_birth: 生年月日 (YYYY/MM/DD形式)
|
||||||
|
phone: 電話番号
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[success, user_id, token]
|
||||||
|
"""
|
||||||
|
if self.dry_run:
|
||||||
|
logger.info(f"[DRY RUN] Would get or create user: {email}")
|
||||||
|
return True, "dummy_user_id", "dummy_token"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# まずログインを試行して既存ユーザーかチェック
|
||||||
|
login_data = {
|
||||||
|
"identifier": email,
|
||||||
|
"password": password
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(f"{self.base_url}/login/", json=login_data)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# 既存ユーザーの場合、パスワード更新(実際にはパスワード更新APIが必要)
|
||||||
|
result = response.json()
|
||||||
|
token = result.get('token')
|
||||||
|
user_id = result.get('user', {}).get('id')
|
||||||
|
|
||||||
|
logger.info(f"既存ユーザーでログイン成功: {email}")
|
||||||
|
self.stats['users_updated'] += 1
|
||||||
|
return True, str(user_id), token
|
||||||
|
|
||||||
|
elif response.status_code == 401:
|
||||||
|
# ユーザーが存在しないか、パスワードが間違っている場合、新規登録を試行
|
||||||
|
return self._create_new_user(email, password, firstname, lastname, date_of_birth)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(f"ログイン試行でエラー: {response.status_code} - {response.text}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"ユーザー認証エラー: {str(e)}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
def _create_new_user(self, email: str, password: str, firstname: str, lastname: str,
|
||||||
|
date_of_birth: str) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||||
|
"""
|
||||||
|
新規ユーザーを作成
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 生年月日をYYYY-MM-DD形式に変換
|
||||||
|
if '/' in date_of_birth:
|
||||||
|
date_parts = date_of_birth.split('/')
|
||||||
|
if len(date_parts) == 3:
|
||||||
|
birth_date = f"{date_parts[0]}-{date_parts[1].zfill(2)}-{date_parts[2].zfill(2)}"
|
||||||
|
else:
|
||||||
|
birth_date = "1990-01-01" # デフォルト値
|
||||||
|
else:
|
||||||
|
birth_date = date_of_birth
|
||||||
|
|
||||||
|
# 仮ユーザー登録
|
||||||
|
register_data = {
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"firstname": firstname,
|
||||||
|
"lastname": lastname,
|
||||||
|
"date_of_birth": birth_date,
|
||||||
|
"female": False, # デフォルト値
|
||||||
|
"is_rogaining": True
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(f"{self.base_url}/register/", json=register_data)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
logger.info(f"仮ユーザー登録成功: {email}")
|
||||||
|
|
||||||
|
# 実際のシステムでは、メール認証コードを使って本登録を完了する必要があります
|
||||||
|
# ここでは簡略化のため、直接ログインを試行します
|
||||||
|
time.sleep(1) # 少し待機
|
||||||
|
|
||||||
|
login_data = {
|
||||||
|
"identifier": email,
|
||||||
|
"password": password
|
||||||
|
}
|
||||||
|
|
||||||
|
login_response = self.session.post(f"{self.base_url}/login/", json=login_data)
|
||||||
|
|
||||||
|
if login_response.status_code == 200:
|
||||||
|
result = login_response.json()
|
||||||
|
token = result.get('token')
|
||||||
|
user_id = result.get('user', {}).get('id')
|
||||||
|
|
||||||
|
logger.info(f"新規ユーザーのログイン成功: {email}")
|
||||||
|
self.stats['users_created'] += 1
|
||||||
|
return True, str(user_id), token
|
||||||
|
else:
|
||||||
|
logger.warning(f"新規ユーザーのログインに失敗: {email}")
|
||||||
|
# メール認証が必要な可能性があります
|
||||||
|
self.stats['users_created'] += 1
|
||||||
|
return True, "pending_verification", None
|
||||||
|
|
||||||
|
else:
|
||||||
|
error_msg = response.text
|
||||||
|
logger.error(f"ユーザー登録失敗: {email} - {error_msg}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"新規ユーザー作成エラー: {str(e)}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
def register_team_and_members(self, team_data: Dict, zekken_number: int) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""
|
||||||
|
チーム登録とメンバー登録
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_data: チームデータ(CSVの1行分)
|
||||||
|
zekken_number: ゼッケン番号
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[success, team_id]
|
||||||
|
"""
|
||||||
|
if self.dry_run:
|
||||||
|
logger.info(f"[DRY RUN] Would register team: {team_data['チーム名']} with zekken: {zekken_number}")
|
||||||
|
return True, "dummy_team_id"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# チーム登録データを準備
|
||||||
|
register_data = {
|
||||||
|
"zekken_number": zekken_number,
|
||||||
|
"event_code": self.event_code,
|
||||||
|
"team_name": team_data['チーム名'],
|
||||||
|
"class_name": team_data['部門'],
|
||||||
|
"password": team_data['パスワード']
|
||||||
|
}
|
||||||
|
|
||||||
|
# チーム登録API呼び出し
|
||||||
|
response = self.session.post(f"{self.base_url}/register_team", json=register_data)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
result = response.json()
|
||||||
|
if result.get('status') == 'OK':
|
||||||
|
team_id = result.get('team_id')
|
||||||
|
logger.info(f"チーム登録成功: {team_data['チーム名']} (zekken: {zekken_number})")
|
||||||
|
self.stats['teams_registered'] += 1
|
||||||
|
|
||||||
|
# メンバー登録
|
||||||
|
success = self._register_team_members(team_data, team_id)
|
||||||
|
return success, str(team_id)
|
||||||
|
else:
|
||||||
|
logger.error(f"チーム登録エラー: {result.get('message')}")
|
||||||
|
return False, None
|
||||||
|
else:
|
||||||
|
logger.error(f"チーム登録API呼び出し失敗: {response.status_code} - {response.text}")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"チーム登録エラー: {str(e)}")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
def _register_team_members(self, team_data: Dict, team_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
チームメンバーを登録(最大7名)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_data: チームデータ
|
||||||
|
team_id: チームID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
成功フラグ
|
||||||
|
"""
|
||||||
|
if self.dry_run:
|
||||||
|
logger.info(f"[DRY RUN] Would register team members for team: {team_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
success_count = 0
|
||||||
|
|
||||||
|
# メンバー1-7を順番に処理
|
||||||
|
for i in range(1, 8):
|
||||||
|
name_key = f'氏名{i}'
|
||||||
|
birth_key = f'誕生日{i}'
|
||||||
|
|
||||||
|
if name_key in team_data and team_data[name_key].strip():
|
||||||
|
name = team_data[name_key].strip()
|
||||||
|
birth_date = team_data.get(birth_key, '1990/01/01')
|
||||||
|
|
||||||
|
# ダミーメールアドレスを生成
|
||||||
|
dummy_email = f"{name.replace(' ', '')}_{team_id}_{i}@dummy.local"
|
||||||
|
|
||||||
|
# メンバー追加データ
|
||||||
|
member_data = {
|
||||||
|
"email": dummy_email,
|
||||||
|
"firstname": name.split()[0] if ' ' in name else name,
|
||||||
|
"lastname": name.split()[-1] if ' ' in name else "",
|
||||||
|
"date_of_birth": birth_date.replace('/', '-'),
|
||||||
|
"female": False # デフォルト値
|
||||||
|
}
|
||||||
|
|
||||||
|
# メンバー追加API呼び出し
|
||||||
|
response = self.session.post(
|
||||||
|
f"{self.base_url}/teams/{team_id}/members/",
|
||||||
|
json=member_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
logger.info(f"メンバー追加成功: {name} -> チーム{team_id}")
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
logger.warning(f"メンバー追加失敗: {name} - {response.text}")
|
||||||
|
|
||||||
|
logger.info(f"チーム{team_id}のメンバー登録完了: {success_count}名")
|
||||||
|
return success_count > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"メンバー登録エラー: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_event_entry(self, team_id: str, category_id: int = 1) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""
|
||||||
|
イベントエントリー登録
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_id: チームID
|
||||||
|
category_id: カテゴリID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[success, entry_id]
|
||||||
|
"""
|
||||||
|
if self.dry_run:
|
||||||
|
logger.info(f"[DRY RUN] Would create event entry for team: {team_id}")
|
||||||
|
return True, "dummy_entry_id"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# エントリーデータ準備
|
||||||
|
entry_data = {
|
||||||
|
"team_id": team_id,
|
||||||
|
"event_code": self.event_code,
|
||||||
|
"category": category_id,
|
||||||
|
"entry_date": datetime.now().strftime("%Y-%m-%d")
|
||||||
|
}
|
||||||
|
|
||||||
|
# エントリー登録API呼び出し
|
||||||
|
response = self.session.post(f"{self.base_url}/entry/", json=entry_data)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
result = response.json()
|
||||||
|
entry_id = result.get('id') or result.get('entry_id')
|
||||||
|
logger.info(f"エントリー登録成功: team_id={team_id}, entry_id={entry_id}")
|
||||||
|
self.stats['entries_created'] += 1
|
||||||
|
return True, str(entry_id)
|
||||||
|
else:
|
||||||
|
logger.error(f"エントリー登録失敗: {response.status_code} - {response.text}")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"エントリー登録エラー: {str(e)}")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
def participate_in_event(self, entry_id: str, zekken_number: int) -> bool:
|
||||||
|
"""
|
||||||
|
イベント参加処理
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry_id: エントリーID
|
||||||
|
zekken_number: ゼッケン番号
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
成功フラグ
|
||||||
|
"""
|
||||||
|
if self.dry_run:
|
||||||
|
logger.info(f"[DRY RUN] Would participate in event: entry_id={entry_id}, zekken={zekken_number}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# イベント参加データ準備
|
||||||
|
participation_data = {
|
||||||
|
"entry_id": entry_id,
|
||||||
|
"event_code": self.event_code,
|
||||||
|
"zekken_number": zekken_number,
|
||||||
|
"participation_date": datetime.now().strftime("%Y-%m-%d")
|
||||||
|
}
|
||||||
|
|
||||||
|
# イベント参加API呼び出し(実際のAPIエンドポイントに合わせて調整が必要)
|
||||||
|
response = self.session.post(f"{self.base_url}/start_from_rogapp", json=participation_data)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
logger.info(f"イベント参加成功: entry_id={entry_id}, zekken={zekken_number}")
|
||||||
|
self.stats['participations_created'] += 1
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"イベント参加API呼び出し結果: {response.status_code} - {response.text}")
|
||||||
|
# 参加処理は必須ではないため、警告のみでTrueを返す
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"イベント参加エラー: {str(e)}")
|
||||||
|
return True # 参加処理は必須ではないため、エラーでもTrueを返す
|
||||||
|
|
||||||
|
def process_csv_file(self, csv_file_path: str) -> bool:
|
||||||
|
"""
|
||||||
|
CSVファイルを処理してユーザー登録からイベント参加まで実行
|
||||||
|
|
||||||
|
Args:
|
||||||
|
csv_file_path: CSVファイルパス
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
成功フラグ
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(csv_file_path):
|
||||||
|
logger.error(f"CSVファイルが見つかりません: {csv_file_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(csv_file_path, 'r', encoding='utf-8') as file:
|
||||||
|
csv_reader = csv.DictReader(file)
|
||||||
|
|
||||||
|
for row_num, row in enumerate(csv_reader, start=1):
|
||||||
|
try:
|
||||||
|
self._process_team_row(row, row_num)
|
||||||
|
|
||||||
|
# API呼び出し間隔を空ける
|
||||||
|
if not self.dry_run:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"行{row_num}の処理でエラー: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
self.stats['errors'].append(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"CSVファイル処理エラー: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _process_team_row(self, row: Dict, row_num: int):
|
||||||
|
"""
|
||||||
|
CSVの1行(1チーム)を処理
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row: CSV行データ
|
||||||
|
row_num: 行番号
|
||||||
|
"""
|
||||||
|
team_name = row.get('チーム名', '').strip()
|
||||||
|
email = row.get('メール', '').strip()
|
||||||
|
password = row.get('password', '').strip()
|
||||||
|
phone = row.get('電話番号', '').strip()
|
||||||
|
|
||||||
|
if not all([team_name, email, password]):
|
||||||
|
logger.warning(f"行{row_num}: 必須項目が不足 - チーム名={team_name}, メール={email}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"行{row_num}の処理開始: チーム={team_name}")
|
||||||
|
|
||||||
|
# ゼッケン番号を生成(行番号ベース、実際の運用では別途管理が必要)
|
||||||
|
zekken_number = row_num
|
||||||
|
|
||||||
|
# 2-1. カスタムユーザー登録
|
||||||
|
# 最初のメンバー(氏名1)をメインユーザーとして使用
|
||||||
|
firstname = row.get('氏名1', team_name).strip()
|
||||||
|
lastname = ""
|
||||||
|
if ' ' in firstname:
|
||||||
|
parts = firstname.split(' ', 1)
|
||||||
|
firstname = parts[0]
|
||||||
|
lastname = parts[1]
|
||||||
|
|
||||||
|
date_of_birth = row.get('誕生日1', '1990/01/01')
|
||||||
|
|
||||||
|
user_success, user_id, token = self.get_or_create_user(
|
||||||
|
email, password, firstname, lastname, date_of_birth, phone
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_success:
|
||||||
|
logger.error(f"行{row_num}: ユーザー登録/取得失敗")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2-2. チーム登録、メンバー登録
|
||||||
|
team_success, team_id = self.register_team_and_members(row, zekken_number)
|
||||||
|
|
||||||
|
if not team_success:
|
||||||
|
logger.error(f"行{row_num}: チーム登録失敗")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2-3. エントリー登録
|
||||||
|
entry_success, entry_id = self.create_event_entry(team_id)
|
||||||
|
|
||||||
|
if not entry_success:
|
||||||
|
logger.error(f"行{row_num}: エントリー登録失敗")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2-4. イベント参加
|
||||||
|
participation_success = self.participate_in_event(entry_id, zekken_number)
|
||||||
|
|
||||||
|
if participation_success:
|
||||||
|
logger.info(f"行{row_num}: 全処理完了 - チーム={team_name}, zekken={zekken_number}")
|
||||||
|
self.stats['processed_teams'] += 1
|
||||||
|
else:
|
||||||
|
logger.warning(f"行{row_num}: イベント参加処理で警告")
|
||||||
|
|
||||||
|
def print_statistics(self):
|
||||||
|
"""
|
||||||
|
処理統計を出力
|
||||||
|
"""
|
||||||
|
logger.info("=== 処理統計 ===")
|
||||||
|
logger.info(f"処理完了チーム数: {self.stats['processed_teams']}")
|
||||||
|
logger.info(f"作成ユーザー数: {self.stats['users_created']}")
|
||||||
|
logger.info(f"更新ユーザー数: {self.stats['users_updated']}")
|
||||||
|
logger.info(f"登録チーム数: {self.stats['teams_registered']}")
|
||||||
|
logger.info(f"作成エントリー数: {self.stats['entries_created']}")
|
||||||
|
logger.info(f"参加登録数: {self.stats['participations_created']}")
|
||||||
|
logger.info(f"エラー数: {len(self.stats['errors'])}")
|
||||||
|
|
||||||
|
if self.stats['errors']:
|
||||||
|
logger.error("エラー詳細:")
|
||||||
|
for error in self.stats['errors']:
|
||||||
|
logger.error(f" - {error}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='イベントユーザー登録スクリプト')
|
||||||
|
parser.add_argument('--event_code', required=True, help='イベントコード(例: 大垣2509)')
|
||||||
|
parser.add_argument('--csv_file', default='CPLIST/input/team2025.csv', help='CSVファイルパス')
|
||||||
|
parser.add_argument('--base_url', default='http://localhost:8000', help='APIベースURL')
|
||||||
|
parser.add_argument('--dry_run', action='store_true', help='テスト実行(実際のAPI呼び出しなし)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logger.info(f"イベントユーザー登録処理開始: event_code={args.event_code}")
|
||||||
|
|
||||||
|
# 登録処理実行
|
||||||
|
registration = EventUserRegistration(
|
||||||
|
event_code=args.event_code,
|
||||||
|
base_url=args.base_url,
|
||||||
|
dry_run=args.dry_run
|
||||||
|
)
|
||||||
|
|
||||||
|
success = registration.process_csv_file(args.csv_file)
|
||||||
|
|
||||||
|
# 統計出力
|
||||||
|
registration.print_statistics()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("処理が正常に完了しました")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
logger.error("処理中にエラーが発生しました")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
590
register_teams_from_csv.py
Normal file
590
register_teams_from_csv.py
Normal file
@ -0,0 +1,590 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
CSVファイルからチーム情報をデータベースに登録するスクリプト
|
||||||
|
CPLIST/input/teams2025.csv から以下の手順でデータベーステーブルに書き込む
|
||||||
|
|
||||||
|
実行方法:
|
||||||
|
python register_teams_from_csv.py --event_code <event_code>
|
||||||
|
|
||||||
|
例:
|
||||||
|
python register_teams_from_csv.py --event_code GIFU2025
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
from datetime import datetime, date, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# Django設定
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
from rog.models import (
|
||||||
|
CustomUser, NewEvent2, NewCategory, Team, Member, Entry, EntryMember
|
||||||
|
)
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class TeamRegistrationProcessor:
|
||||||
|
def __init__(self, event_code, dry_run=False):
|
||||||
|
self.event_code = event_code
|
||||||
|
self.dry_run = dry_run
|
||||||
|
self.event = None
|
||||||
|
self.categories = {}
|
||||||
|
self.stats = {
|
||||||
|
'users_created': 0,
|
||||||
|
'users_updated': 0,
|
||||||
|
'teams_created': 0,
|
||||||
|
'members_created': 0,
|
||||||
|
'entries_created': 0,
|
||||||
|
'participations_created': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""イベントとカテゴリの初期化"""
|
||||||
|
if self.dry_run:
|
||||||
|
print("DRY RUN MODE: データベースの変更は行いません")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.event = NewEvent2.objects.get(event_code=self.event_code)
|
||||||
|
print(f"イベント取得: {self.event.event_name} ({self.event_code})")
|
||||||
|
except NewEvent2.DoesNotExist:
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: Event with code '{self.event_code}' would be searched")
|
||||||
|
# ダミーイベントオブジェクトを作成
|
||||||
|
class DummyEvent:
|
||||||
|
def __init__(self):
|
||||||
|
self.event_name = f"Dummy Event for {self.event_code}"
|
||||||
|
self.event_code = self.event_code
|
||||||
|
self.event = DummyEvent()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Event with code '{self.event_code}' not found")
|
||||||
|
|
||||||
|
# カテゴリ情報をプリロード
|
||||||
|
for category in NewCategory.objects.all():
|
||||||
|
hours = int(category.duration.total_seconds() // 3600)
|
||||||
|
key = (category.category_name, hours)
|
||||||
|
self.categories[key] = category
|
||||||
|
|
||||||
|
print(f"利用可能なカテゴリ: {list(self.categories.keys())}")
|
||||||
|
|
||||||
|
def parse_csv_row(self, row):
|
||||||
|
"""CSV行をパース"""
|
||||||
|
if len(row) < 20:
|
||||||
|
raise ValueError(f"不正な行形式: {len(row)} columns found, expected at least 20")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'department_count': row[0].strip(),
|
||||||
|
'hours': row[1].strip(),
|
||||||
|
'department': row[2].strip(),
|
||||||
|
'team_name': row[3].strip(),
|
||||||
|
'email': row[4].strip(),
|
||||||
|
'password': row[5].strip(),
|
||||||
|
'phone': row[6].strip(),
|
||||||
|
'members': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# メンバー情報を解析(最大7名)
|
||||||
|
for i in range(7):
|
||||||
|
name_idx = 7 + i * 2
|
||||||
|
birth_idx = 8 + i * 2
|
||||||
|
|
||||||
|
if name_idx < len(row) and birth_idx < len(row):
|
||||||
|
name = row[name_idx].strip() if row[name_idx] else None
|
||||||
|
birth_str = row[birth_idx].strip() if row[birth_idx] else None
|
||||||
|
|
||||||
|
if name and birth_str:
|
||||||
|
try:
|
||||||
|
# 誕生日の解析(複数フォーマット対応)
|
||||||
|
birth_date = None
|
||||||
|
for fmt in ['%Y/%m/%d', '%Y-%m-%d', '%Y/%m/%d ']:
|
||||||
|
try:
|
||||||
|
birth_date = datetime.strptime(birth_str.strip(), fmt).date()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if birth_date:
|
||||||
|
data['members'].append({
|
||||||
|
'name': name,
|
||||||
|
'birth_date': birth_date
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
print(f"警告: 誕生日の形式が不正です: {birth_str}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"警告: メンバー情報の解析エラー: {e}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_or_create_category(self, department, hours):
|
||||||
|
"""カテゴリを取得または作成"""
|
||||||
|
try:
|
||||||
|
hours_int = int(hours)
|
||||||
|
except ValueError:
|
||||||
|
hours_int = 5 # デフォルト
|
||||||
|
|
||||||
|
# 既存カテゴリから検索
|
||||||
|
key = (department, hours_int)
|
||||||
|
if key in self.categories:
|
||||||
|
return self.categories[key]
|
||||||
|
|
||||||
|
# 一般的なカテゴリ名でマッピング
|
||||||
|
category_mappings = {
|
||||||
|
'一般': 'General',
|
||||||
|
'ファミリー': 'Family',
|
||||||
|
'男性ソロ': 'Solo Male',
|
||||||
|
'女性ソロ': 'Solo Female',
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped_name = category_mappings.get(department, department)
|
||||||
|
key_mapped = (mapped_name, hours_int)
|
||||||
|
if key_mapped in self.categories:
|
||||||
|
return self.categories[key_mapped]
|
||||||
|
|
||||||
|
# 時間だけでマッチング(一般カテゴリとして)
|
||||||
|
for (cat_name, cat_hours), category in self.categories.items():
|
||||||
|
if cat_hours == hours_int and cat_name in ['General', '一般']:
|
||||||
|
return category
|
||||||
|
|
||||||
|
# 新しいカテゴリを作成
|
||||||
|
print(f"新しいカテゴリを作成: {department} ({hours_int}時間)")
|
||||||
|
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: カテゴリ作成 - {department} ({hours_int}時間)")
|
||||||
|
# ダミーカテゴリオブジェクトを作成
|
||||||
|
class DummyCategory:
|
||||||
|
def __init__(self):
|
||||||
|
self.category_name = department
|
||||||
|
self.category_number = len(self.categories) + 1
|
||||||
|
self.duration = timedelta(hours=hours_int)
|
||||||
|
self.num_of_member = 7
|
||||||
|
self.family = (department == 'ファミリー')
|
||||||
|
self.female = (department == '女性ソロ')
|
||||||
|
self.trial = False
|
||||||
|
|
||||||
|
category = DummyCategory()
|
||||||
|
else:
|
||||||
|
category = NewCategory.objects.create(
|
||||||
|
category_name=department,
|
||||||
|
category_number=len(self.categories) + 1,
|
||||||
|
duration=timedelta(hours=hours_int),
|
||||||
|
num_of_member=7, # 最大7名
|
||||||
|
family=(department == 'ファミリー'),
|
||||||
|
female=(department == '女性ソロ'),
|
||||||
|
trial=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.categories[key] = category
|
||||||
|
return category
|
||||||
|
|
||||||
|
def process_user(self, data):
|
||||||
|
"""ユーザーの処理(2-1)"""
|
||||||
|
email = data['email']
|
||||||
|
password = data['password']
|
||||||
|
team_name = data['team_name']
|
||||||
|
|
||||||
|
# ゼッケン番号は部門別数を使用
|
||||||
|
zekken_number = data['department_count']
|
||||||
|
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: ユーザー処理 - {email}")
|
||||||
|
print(f" - チーム名: {team_name}")
|
||||||
|
print(f" - ゼッケン番号: {zekken_number}")
|
||||||
|
|
||||||
|
# ダミーユーザーオブジェクトを返す
|
||||||
|
class DummyUser:
|
||||||
|
def __init__(self):
|
||||||
|
self.email = email
|
||||||
|
self.firstname = data['members'][0]['name'] if data['members'] else 'Unknown'
|
||||||
|
self.lastname = ''
|
||||||
|
self.date_of_birth = data['members'][0]['birth_date'] if data['members'] else date.today()
|
||||||
|
self.female = False
|
||||||
|
self.zekken_number = zekken_number
|
||||||
|
self.event_code = self.event_code
|
||||||
|
self.team_name = team_name
|
||||||
|
|
||||||
|
self.stats['users_created'] += 1
|
||||||
|
return DummyUser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 既存ユーザーを検索
|
||||||
|
user = CustomUser.objects.get(email=email)
|
||||||
|
|
||||||
|
# パスワードとその他の情報を更新
|
||||||
|
user.set_password(password)
|
||||||
|
user.event_code = self.event_code
|
||||||
|
user.zekken_number = zekken_number
|
||||||
|
user.team_name = team_name
|
||||||
|
user.is_rogaining = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
print(f"ユーザー更新: {email}")
|
||||||
|
self.stats['users_updated'] += 1
|
||||||
|
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
# 新規ユーザー作成
|
||||||
|
# メンバー情報から代表者の情報を取得
|
||||||
|
first_member = data['members'][0] if data['members'] else None
|
||||||
|
|
||||||
|
user = CustomUser.objects.create(
|
||||||
|
email=email,
|
||||||
|
firstname=first_member['name'] if first_member else 'Unknown',
|
||||||
|
lastname='',
|
||||||
|
date_of_birth=first_member['birth_date'] if first_member else date.today(),
|
||||||
|
female=False, # デフォルト
|
||||||
|
group=data['department'],
|
||||||
|
is_active=True,
|
||||||
|
is_rogaining=True,
|
||||||
|
zekken_number=zekken_number,
|
||||||
|
event_code=self.event_code,
|
||||||
|
team_name=team_name
|
||||||
|
)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
print(f"ユーザー作成: {email}")
|
||||||
|
self.stats['users_created'] += 1
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_dummy_users_for_members(self, data, main_user):
|
||||||
|
"""メンバー用ダミーユーザーを作成"""
|
||||||
|
dummy_users = []
|
||||||
|
|
||||||
|
for i, member_data in enumerate(data['members']):
|
||||||
|
# メインユーザーをスキップ
|
||||||
|
if i == 0:
|
||||||
|
dummy_users.append(main_user)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ダミーメールアドレス生成
|
||||||
|
dummy_email = f"dummy_{self.event_code}_{data['department_count']}_{i}@dummy.local"
|
||||||
|
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: ダミーユーザー作成 - {dummy_email}")
|
||||||
|
print(f" - 名前: {member_data['name']}")
|
||||||
|
print(f" - 誕生日: {member_data['birth_date']}")
|
||||||
|
|
||||||
|
# ダミーユーザーオブジェクトを作成
|
||||||
|
class DummyUser:
|
||||||
|
def __init__(self):
|
||||||
|
self.email = dummy_email
|
||||||
|
self.firstname = member_data['name']
|
||||||
|
self.lastname = ''
|
||||||
|
self.date_of_birth = member_data['birth_date']
|
||||||
|
self.female = False
|
||||||
|
self.event_code = self.event_code
|
||||||
|
self.team_name = data['team_name']
|
||||||
|
|
||||||
|
dummy_users.append(DummyUser())
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 既存のダミーユーザーを確認
|
||||||
|
dummy_user = CustomUser.objects.get(email=dummy_email)
|
||||||
|
except CustomUser.DoesNotExist:
|
||||||
|
# ダミーユーザー作成
|
||||||
|
dummy_user = CustomUser.objects.create(
|
||||||
|
email=dummy_email,
|
||||||
|
firstname=member_data['name'],
|
||||||
|
lastname='',
|
||||||
|
date_of_birth=member_data['birth_date'],
|
||||||
|
female=False, # 名前から推測するかデフォルト
|
||||||
|
group=data['department'],
|
||||||
|
is_active=False, # ダミーユーザーは非アクティブ
|
||||||
|
is_rogaining=True,
|
||||||
|
event_code=self.event_code,
|
||||||
|
team_name=data['team_name']
|
||||||
|
)
|
||||||
|
dummy_user.set_password('dummy123')
|
||||||
|
dummy_user.save()
|
||||||
|
|
||||||
|
print(f"ダミーユーザー作成: {dummy_email}")
|
||||||
|
|
||||||
|
dummy_users.append(dummy_user)
|
||||||
|
|
||||||
|
return dummy_users
|
||||||
|
|
||||||
|
def process_team(self, data, owner, category):
|
||||||
|
"""チーム登録(2-2)"""
|
||||||
|
team_name = data['team_name']
|
||||||
|
zekken_number = data['department_count']
|
||||||
|
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: チーム作成 - {team_name}")
|
||||||
|
print(f" - ゼッケン番号: {zekken_number}")
|
||||||
|
print(f" - カテゴリ: {category.category_name if hasattr(category, 'category_name') else 'Unknown'}")
|
||||||
|
print(f" - オーナー: {owner.email}")
|
||||||
|
|
||||||
|
# ダミーチームオブジェクトを作成
|
||||||
|
class DummyTeam:
|
||||||
|
def __init__(self, processor):
|
||||||
|
self.team_name = team_name
|
||||||
|
self.zekken_number = zekken_number
|
||||||
|
self.owner = owner
|
||||||
|
self.event = processor.event
|
||||||
|
self.password = data['password']
|
||||||
|
self.class_name = data['department']
|
||||||
|
|
||||||
|
self.stats['teams_created'] += 1
|
||||||
|
return DummyTeam(self)
|
||||||
|
|
||||||
|
# 既存チームを確認
|
||||||
|
try:
|
||||||
|
team = Team.objects.get(
|
||||||
|
team_name=team_name,
|
||||||
|
event=self.event,
|
||||||
|
zekken_number=zekken_number
|
||||||
|
)
|
||||||
|
print(f"既存チーム使用: {team_name}")
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
# 新規チーム作成
|
||||||
|
team = Team.objects.create(
|
||||||
|
team_name=team_name,
|
||||||
|
owner=owner,
|
||||||
|
category=category,
|
||||||
|
zekken_number=zekken_number,
|
||||||
|
event=self.event,
|
||||||
|
password=data['password'],
|
||||||
|
class_name=data['department']
|
||||||
|
)
|
||||||
|
print(f"チーム作成: {team_name}")
|
||||||
|
self.stats['teams_created'] += 1
|
||||||
|
|
||||||
|
return team
|
||||||
|
|
||||||
|
def process_members(self, data, team, users):
|
||||||
|
"""メンバー登録"""
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: メンバー登録 - {team.team_name}")
|
||||||
|
for user in users:
|
||||||
|
print(f" - {user.firstname} ({user.email})")
|
||||||
|
self.stats['members_created'] += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
# 既存メンバーを削除(更新の場合)
|
||||||
|
Member.objects.filter(team=team).delete()
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
member = Member.objects.create(
|
||||||
|
team=team,
|
||||||
|
user=user,
|
||||||
|
firstname=user.firstname,
|
||||||
|
lastname=user.lastname,
|
||||||
|
date_of_birth=user.date_of_birth,
|
||||||
|
female=user.female,
|
||||||
|
is_temporary=True if user.email.startswith('dummy_') else False
|
||||||
|
)
|
||||||
|
print(f"メンバー追加: {user.firstname} to {team.team_name}")
|
||||||
|
self.stats['members_created'] += 1
|
||||||
|
|
||||||
|
def process_entry(self, team, category):
|
||||||
|
"""エントリー登録(2-3)"""
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: エントリー作成 - {team.team_name}")
|
||||||
|
print(f" - カテゴリ: {category.category_name if hasattr(category, 'category_name') else 'Unknown'}")
|
||||||
|
print(f" - ゼッケン番号: {team.zekken_number}")
|
||||||
|
|
||||||
|
# ダミーエントリーオブジェクトを作成
|
||||||
|
class DummyEntry:
|
||||||
|
def __init__(self, processor):
|
||||||
|
self.team = team
|
||||||
|
self.event = processor.event
|
||||||
|
self.category = category
|
||||||
|
self.zekken_number = int(team.zekken_number)
|
||||||
|
self.is_active = True
|
||||||
|
|
||||||
|
self.stats['entries_created'] += 1
|
||||||
|
return DummyEntry(self)
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry = Entry.objects.get(
|
||||||
|
team=team,
|
||||||
|
event=self.event,
|
||||||
|
category=category
|
||||||
|
)
|
||||||
|
print(f"既存エントリー使用: {team.team_name}")
|
||||||
|
except Entry.DoesNotExist:
|
||||||
|
entry = Entry.objects.create(
|
||||||
|
team=team,
|
||||||
|
event=self.event,
|
||||||
|
category=category,
|
||||||
|
owner=team.owner,
|
||||||
|
zekken_number=int(team.zekken_number),
|
||||||
|
is_active=True,
|
||||||
|
hasParticipated=False,
|
||||||
|
hasGoaled=False
|
||||||
|
)
|
||||||
|
print(f"エントリー作成: {team.team_name}")
|
||||||
|
self.stats['entries_created'] += 1
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def process_participation(self, entry):
|
||||||
|
"""イベント参加(2-4)"""
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"DRY RUN: 参加登録 - {entry.team.team_name}")
|
||||||
|
# ダミーメンバーリストを作成
|
||||||
|
dummy_members = []
|
||||||
|
for i in range(len(entry.team.__dict__.get('dummy_members', []))):
|
||||||
|
print(f" - Member {i+1}")
|
||||||
|
self.stats['participations_created'] += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
# エントリーメンバーを作成
|
||||||
|
EntryMember.objects.filter(entry=entry).delete()
|
||||||
|
|
||||||
|
for member in entry.team.members.all():
|
||||||
|
entry_member = EntryMember.objects.create(
|
||||||
|
entry=entry,
|
||||||
|
member=member,
|
||||||
|
is_temporary=member.is_temporary
|
||||||
|
)
|
||||||
|
print(f"参加登録: {member.user.firstname}")
|
||||||
|
self.stats['participations_created'] += 1
|
||||||
|
|
||||||
|
# エントリーを有効化
|
||||||
|
entry.is_active = True
|
||||||
|
entry.save()
|
||||||
|
|
||||||
|
def process_csv_file(self, csv_file_path):
|
||||||
|
"""CSVファイルを処理"""
|
||||||
|
print(f"CSV処理開始: {csv_file_path}")
|
||||||
|
|
||||||
|
with open(csv_file_path, 'r', encoding='utf-8') as file:
|
||||||
|
# ヘッダーをスキップ
|
||||||
|
csv_reader = csv.reader(file)
|
||||||
|
header = next(csv_reader)
|
||||||
|
print(f"CSVヘッダー: {header[:10]}...") # 最初の10列を表示
|
||||||
|
|
||||||
|
row_count = 0
|
||||||
|
for row in csv_reader:
|
||||||
|
row_count += 1
|
||||||
|
if not any(row): # 空行をスキップ
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
print(f"\n--- Row {row_count}: {row[3] if len(row) > 3 else 'Unknown'} ---")
|
||||||
|
|
||||||
|
# CSV行をパース
|
||||||
|
data = self.parse_csv_row(row)
|
||||||
|
|
||||||
|
# カテゴリ取得
|
||||||
|
category = self.get_or_create_category(data['department'], data['hours'])
|
||||||
|
|
||||||
|
# DRY RUNの場合はトランザクションを無効化
|
||||||
|
if self.dry_run:
|
||||||
|
# DRY RUN処理
|
||||||
|
# 2-1. ユーザー処理
|
||||||
|
main_user = self.process_user(data)
|
||||||
|
|
||||||
|
# メンバー用ダミーユーザー作成
|
||||||
|
all_users = self.create_dummy_users_for_members(data, main_user)
|
||||||
|
|
||||||
|
# 2-2. チーム登録
|
||||||
|
team = self.process_team(data, main_user, category)
|
||||||
|
|
||||||
|
# メンバー登録
|
||||||
|
self.process_members(data, team, all_users)
|
||||||
|
|
||||||
|
# 2-3. エントリー登録
|
||||||
|
entry = self.process_entry(team, category)
|
||||||
|
|
||||||
|
# 2-4. イベント参加
|
||||||
|
self.process_participation(entry)
|
||||||
|
else:
|
||||||
|
# 実際の処理
|
||||||
|
# 2-1. ユーザー処理
|
||||||
|
main_user = self.process_user(data)
|
||||||
|
|
||||||
|
# メンバー用ダミーユーザー作成
|
||||||
|
all_users = self.create_dummy_users_for_members(data, main_user)
|
||||||
|
|
||||||
|
# 2-2. チーム登録
|
||||||
|
team = self.process_team(data, main_user, category)
|
||||||
|
|
||||||
|
# メンバー登録
|
||||||
|
self.process_members(data, team, all_users)
|
||||||
|
|
||||||
|
# 2-3. エントリー登録
|
||||||
|
entry = self.process_entry(team, category)
|
||||||
|
|
||||||
|
# 2-4. イベント参加
|
||||||
|
self.process_participation(entry)
|
||||||
|
|
||||||
|
print(f"Row {row_count} 完了: {data['team_name']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Row {row_count} エラー: {str(e)}"
|
||||||
|
print(f"エラー: {error_msg}")
|
||||||
|
self.stats['errors'].append(error_msg)
|
||||||
|
|
||||||
|
print(f"\nCSV処理完了: {row_count} 行処理")
|
||||||
|
|
||||||
|
def print_stats(self):
|
||||||
|
"""統計情報を表示"""
|
||||||
|
print("\n=== 処理結果統計 ===")
|
||||||
|
print(f"作成されたユーザー: {self.stats['users_created']}")
|
||||||
|
print(f"更新されたユーザー: {self.stats['users_updated']}")
|
||||||
|
print(f"作成されたチーム: {self.stats['teams_created']}")
|
||||||
|
print(f"作成されたメンバー: {self.stats['members_created']}")
|
||||||
|
print(f"作成されたエントリー: {self.stats['entries_created']}")
|
||||||
|
print(f"作成された参加登録: {self.stats['participations_created']}")
|
||||||
|
print(f"エラー数: {len(self.stats['errors'])}")
|
||||||
|
|
||||||
|
if self.stats['errors']:
|
||||||
|
print("\n=== エラー詳細 ===")
|
||||||
|
for error in self.stats['errors']:
|
||||||
|
print(f"- {error}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
parser = argparse.ArgumentParser(description='CSVからチーム情報をデータベースに登録')
|
||||||
|
parser.add_argument('--event_code', required=True, help='イベントコード')
|
||||||
|
parser.add_argument('--csv_file',
|
||||||
|
default='CPLIST/input/teams2025.csv',
|
||||||
|
help='CSVファイルパス (デフォルト: CPLIST/input/teams2025.csv)')
|
||||||
|
parser.add_argument('--dry_run', action='store_true',
|
||||||
|
help='ドライランモード(実際のDB更新を行わない)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# CSVファイルの存在確認
|
||||||
|
if not os.path.exists(args.csv_file):
|
||||||
|
print(f"エラー: CSVファイルが見つかりません: {args.csv_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# プロセッサーを初期化
|
||||||
|
processor = TeamRegistrationProcessor(args.event_code, dry_run=args.dry_run)
|
||||||
|
processor.initialize()
|
||||||
|
|
||||||
|
# CSVファイルを処理
|
||||||
|
processor.process_csv_file(args.csv_file)
|
||||||
|
|
||||||
|
# 統計情報を表示
|
||||||
|
processor.print_stats()
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print(f"\nDRY RUN 完了: イベント {args.event_code}")
|
||||||
|
else:
|
||||||
|
print(f"\n処理完了: イベント {args.event_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"処理エラー: {str(e)}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@ -79,3 +79,7 @@ pandas
|
|||||||
reportlab
|
reportlab
|
||||||
networkx
|
networkx
|
||||||
haversine
|
haversine
|
||||||
|
|
||||||
|
piexif==1.1.3
|
||||||
|
Pillow>=8.0.0
|
||||||
|
boto3
|
||||||
|
|||||||
291
reset_migrations_simple.py
Normal file
291
reset_migrations_simple.py
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Migration ファイルをシンプルにリセットするスクリプト
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
1. 現在のmigrationテーブルの状態をバックアップ
|
||||||
|
2. migrationファイルを削除
|
||||||
|
3. 新しいシンプルなmigrationファイルを作成
|
||||||
|
4. データベースのmigration履歴をリセット
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def backup_migrations():
|
||||||
|
"""現在のmigrationファイルをバックアップ"""
|
||||||
|
migrations_dir = "rog/migrations"
|
||||||
|
backup_dir = f"rog/migrations_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
|
||||||
|
if os.path.exists(migrations_dir):
|
||||||
|
shutil.copytree(migrations_dir, backup_dir)
|
||||||
|
print(f"Migrationファイルを {backup_dir} にバックアップしました")
|
||||||
|
|
||||||
|
def reset_migrations():
|
||||||
|
"""migrationファイルをリセット"""
|
||||||
|
migrations_dir = "rog/migrations"
|
||||||
|
|
||||||
|
# __init__.py以外のファイルを削除
|
||||||
|
if os.path.exists(migrations_dir):
|
||||||
|
for file in os.listdir(migrations_dir):
|
||||||
|
if file != "__init__.py" and file.endswith(".py"):
|
||||||
|
os.remove(os.path.join(migrations_dir, file))
|
||||||
|
print(f"削除: {file}")
|
||||||
|
|
||||||
|
def create_simple_initial_migration():
|
||||||
|
"""シンプルな初期migrationファイルを作成"""
|
||||||
|
migration_content = '''# -*- coding: utf-8 -*-
|
||||||
|
# Generated by reset_migrations_simple.py
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
import django.contrib.postgres.indexes
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import rog.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# 基本的なモデルのみ作成(順番に依存関係を考慮)
|
||||||
|
|
||||||
|
# 1. 地理情報テーブル(managed=False)
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GifuAreas',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326)),
|
||||||
|
('adm0_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm0_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm0_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm2_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm2_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm2_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('area_nm', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'gifu_areas',
|
||||||
|
'managed': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='JpnAdminMainPerf',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326)),
|
||||||
|
('adm0_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm0_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm0_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'jpn_admin_main_perf',
|
||||||
|
'managed': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='JpnSubPerf',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326)),
|
||||||
|
('adm0_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm0_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm0_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm1_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm2_ja', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm2_en', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('adm2_pcode', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('name_modified', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('area_name', models.CharField(blank=True, max_length=254, null=True)),
|
||||||
|
('list_order', models.IntegerField(default=0)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'jpn_sub_perf',
|
||||||
|
'managed': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
# 2. ユーザー関連モデル
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('firstname', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('lastname', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('date_of_birth', models.DateField(blank=True, null=True)),
|
||||||
|
('female', models.BooleanField(default=False)),
|
||||||
|
('group', models.CharField(blank=True, max_length=255)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('is_staff', models.BooleanField(default=False)),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('is_rogaining', models.BooleanField(default=False)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
# 3. カテゴリモデル
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('category_name', models.CharField(max_length=255, verbose_name='カテゴリ名')),
|
||||||
|
('parent_category', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('last_updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
# 4. イベント関連モデル
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='NewEvent',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('event_code', models.CharField(max_length=255, verbose_name='イベントコード')),
|
||||||
|
('event_name', models.CharField(max_length=255, verbose_name='イベント名')),
|
||||||
|
('event_date', models.DateField(verbose_name='イベント日')),
|
||||||
|
('start_time', models.TimeField(blank=True, null=True, verbose_name='開始時刻')),
|
||||||
|
('end_time', models.TimeField(blank=True, null=True, verbose_name='終了時刻')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='説明')),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('last_updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('category', models.ForeignKey(default=1, on_delete=django.db.models.deletion.DO_NOTHING, to='rog.category')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'rog_newevent',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
# 5. チーム関連モデル
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Team',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('team_name', models.CharField(max_length=255, verbose_name='チーム名')),
|
||||||
|
('team_member_num', models.IntegerField(blank=True, default=1, null=True, verbose_name='チーム人数')),
|
||||||
|
('score', models.IntegerField(blank=True, default=0, null=True, verbose_name='スコア')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('last_updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rog.newevent')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'rog_team',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
# 6. ロケーション関連モデル(基本的なもの)
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Location',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('location_id', models.IntegerField(blank=True, db_index=True, null=True, verbose_name='Location id')),
|
||||||
|
('sub_loc_id', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Sub location id')),
|
||||||
|
('cp', models.FloatField(blank=False, default=0, null=True, verbose_name='Check Point')),
|
||||||
|
('location_name', models.CharField(default='--- 場所をお願いします --', max_length=2048, verbose_name='Location Name')),
|
||||||
|
('category', models.CharField(blank=True, db_index=True, max_length=2048, null=True, verbose_name='Category')),
|
||||||
|
('subcategory', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Sub Category')),
|
||||||
|
('zip', models.CharField(blank=True, max_length=12, null=True, verbose_name='Zip code')),
|
||||||
|
('address', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Address')),
|
||||||
|
('prefecture', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Prefecture')),
|
||||||
|
('area', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Area')),
|
||||||
|
('city', models.CharField(blank=True, max_length=2048, null=True, verbose_name='City')),
|
||||||
|
('latitude', models.FloatField(blank=True, null=True, verbose_name='Latitude')),
|
||||||
|
('longitude', models.FloatField(blank=True, null=True, verbose_name='Latitude')),
|
||||||
|
('photos', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Photos')),
|
||||||
|
('videos', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Videos')),
|
||||||
|
('webcontents', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Web Content')),
|
||||||
|
('status', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Status')),
|
||||||
|
('portal', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Portal')),
|
||||||
|
('group', models.CharField(blank=True, db_index=True, max_length=2048, null=True, verbose_name='Group')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Phone')),
|
||||||
|
('fax', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Fax')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=2048, null=True, verbose_name='Email')),
|
||||||
|
('facility', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Facility')),
|
||||||
|
('remark', models.CharField(blank=True, max_length=2048, null=True, verbose_name='Remarks')),
|
||||||
|
('tags', models.CharField(blank=True, max_length=512, null=True, verbose_name='Tags')),
|
||||||
|
('parammeters', models.CharField(blank=True, max_length=512, null=True, verbose_name='Parameters')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('last_updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('last_updated_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='location_updated_user', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'rog_location',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
# 7. エントリー関連モデル
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Entry',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('start_time', models.DateTimeField(blank=True, null=True, verbose_name='Start time')),
|
||||||
|
('goal_time', models.DateTimeField(blank=True, null=True, verbose_name='Goal time')),
|
||||||
|
('check_point', models.IntegerField(blank=True, null=True, verbose_name='Check Point')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('last_updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rog.location')),
|
||||||
|
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rog.team')),
|
||||||
|
('last_updated_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='entry_updated_user', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'rog_entry',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
# インデックスの追加
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='gifu_areas',
|
||||||
|
index=models.Index(fields=['geom'], name='gifu_areas_geom_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
'''
|
||||||
|
|
||||||
|
with open("rog/migrations/0001_simple_initial.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write(migration_content)
|
||||||
|
|
||||||
|
print("シンプルな初期migrationファイル 0001_simple_initial.py を作成しました")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== Migration リセットスクリプト ===")
|
||||||
|
print("1. Migrationファイルをバックアップします")
|
||||||
|
backup_migrations()
|
||||||
|
|
||||||
|
print("\\n2. 既存のmigrationファイルを削除します")
|
||||||
|
reset_migrations()
|
||||||
|
|
||||||
|
print("\\n3. シンプルな初期migrationファイルを作成します")
|
||||||
|
create_simple_initial_migration()
|
||||||
|
|
||||||
|
print("\\n=== 完了 ===")
|
||||||
|
print("次の手順:")
|
||||||
|
print("1. docker compose exec app python manage.py migrate --fake-initial")
|
||||||
|
print("2. 必要に応じて追加のmigrationファイルを作成")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user