Compare commits

79 Commits

Author SHA1 Message Date
4e7d547601 update nginx 2025-09-06 10:11:06 +09:00
45c9b64a78 Auto Start test 2025-09-06 07:03:07 +09:00
316cff3f5f Auto Start 2025-09-06 07:01:07 +09:00
d7d296c33b fix nginx issue 2025-09-06 06:18:27 +09:00
e65da5fd8f checkin status tool 2025-09-06 06:15:35 +09:00
290a5a8c2f Update pass 2025-09-06 04:10:20 +09:00
67db395c3c Fix Login issue 2 2025-09-06 03:38:04 +09:00
023a45f574 Fix Login issue 2025-09-06 03:31:51 +09:00
bcd0bee738 Fix Entry issue 2025-09-06 03:21:36 +09:00
a24a0decb9 Fix validation of team 2025-09-06 03:12:08 +09:00
4761ff9977 Fix nginx error 2 2025-09-06 03:01:44 +09:00
33088234f2 Fix nginx error 2025-09-06 02:56:50 +09:00
49d2aa588b debug log 502 error 2025-09-06 02:49:44 +09:00
4cd3745812 debug log 2025-09-06 02:29:47 +09:00
93768fa4ec add Gpslog log 2025-09-06 02:23:25 +09:00
6d001bf378 Add show_checkin_data_sql.py 2025-09-06 01:56:40 +09:00
716a0be908 Fix S3 issue 2025-09-06 01:29:52 +09:00
66aacbb69e Fix photo upload 2 2025-09-06 01:23:28 +09:00
f50d1e1c79 add photo exif 2025-09-06 01:10:28 +09:00
775f77a440 Fix bulk_upload_checkin_photos 2025-09-06 00:50:30 +09:00
efa51b4fcc Fix API for bulk_upload_checkin_photos 2025-09-06 00:45:51 +09:00
fdc1d66f08 add log for check in 2025-09-06 00:31:53 +09:00
99e4561694 Fix entry registration 2025-09-06 00:11:57 +09:00
86ea3a4b0c Fix history issue 2025-09-05 23:50:50 +09:00
33b122b7e8 Fix history 2025-09-05 23:28:27 +09:00
00bc1cadc9 Fix login issue 2025-09-05 23:20:10 +09:00
d8e1b05d41 password issue 2025-09-05 23:04:40 +09:00
272269431e Email feature 2025-09-05 22:48:15 +09:00
9d11685b65 Fix teams error 2025-09-05 17:29:11 +09:00
4e1ef7c230 add automatic entry script 2025-09-05 16:57:18 +09:00
4a5f6273ed Fix pghba.conf 2025-09-04 19:30:16 +09:00
e0543e2b4e update APIs 2025-09-04 19:25:14 +09:00
32f860af41 fix restore checkin list 2025-09-04 11:27:04 +09:00
3cb0c2daf7 update qr api 2025-09-04 10:34:48 +09:00
7abdfbe903 add submit_qr_points 2025-09-04 10:10:24 +09:00
1698776589 Fix try-except structure and docstring in checkin API
- Correct indentation for except block
- Fix broken docstring formatting
- Ensure proper Python syntax compliance
- Resolve remaining syntax errors
2025-09-03 22:08:08 +09:00
f55f44013f Fix syntax errors in checkin API
- Fix indentation and syntax errors in api_play.py
- Correct try-except block structure
- Fix line endings and code formatting
- Resolve syntax error preventing Django startup
2025-09-03 22:04:19 +09:00
0d6f9024f4 Fix import path for utils module
- Change from .utils import to rog.utils module import
- Update S3Bucket and send_reset_password_email usage
- Fix typo in verification_url variable name
- Resolve ImportError preventing Django app startup
2025-09-03 21:56:40 +09:00
e0635936fe Add missing S3Bucket class to fix import error
- Implement S3Bucket class in utils.py for legacy compatibility
- Add upload_file and get_file_url methods
- Fix ImportError that was preventing app startup
2025-09-03 21:48:22 +09:00
cd8f872f1f Fix Location2025 model attribute error in checkin API
- Replace is_service_cp with default False (attribute not exists in Location2025)
- Update point calculation to use checkin_point from Location2025 model
- Improve error handling for missing attributes
2025-09-03 21:40:40 +09:00
1c36ece232 debug checkin 2025-09-03 20:22:39 +09:00
a0e024b77d update checkin image issue 2025-09-03 07:56:40 +09:00
4901b44f4a Update pg_hba.conf 2025-09-03 04:04:34 +09:00
3c28d33ebf remove logs 2025-09-03 03:58:45 +09:00
bbd655955a update nginx conf for performance 2025-09-02 23:27:34 +09:00
8ffedc177f Fix some APIs 2025-09-02 23:14:14 +09:00
9f27357a3b Fix APIResponseEnhancementMiddleware 2025-09-02 20:52:15 +09:00
3b28f49959 Add logs on API 2025-09-02 20:47:04 +09:00
a8dc2ba3b1 Update new API.. 2025-09-02 20:06:58 +09:00
0acaa6ea1f Fix start/goal processes 2025-09-02 17:10:33 +09:00
d6b40bd0f8 Add log on APIs 2025-09-02 17:01:40 +09:00
c95c8713d4 Fix is_staff issue 2025-09-02 15:29:34 +09:00
70acda8167 add is_staff for user login api 2025-09-02 11:40:17 +09:00
45a29c7b18 Fix inbound2 fields missing 2025-09-02 05:02:16 +09:00
05b9432a90 Fix inbound2 issue 2025-08-31 20:05:27 +09:00
a8c0f52860 Fix /api/locsext API 2025-08-31 17:51:41 +09:00
77acb7c016 Fixed ExtentForLocation API issue 2025-08-31 17:35:06 +09:00
104d39a96b Update location2025 missing fields 2025-08-31 15:04:35 +09:00
619aa4f396 Fix Location2025 missing fields 2025-08-31 14:45:50 +09:00
aa8b39aa99 Fix Location2025 checkin_radius issue 2025-08-31 14:31:31 +09:00
03de478b80 Change Location2025 columns 2025-08-31 14:17:52 +09:00
58165e825b Retry Location2025 search update 3 2025-08-31 14:10:16 +09:00
c8c8d264c9 Retry Location2025 search update 2025-08-31 14:03:47 +09:00
bef4af1086 Fix Location2025 search feature.. 2025-08-31 13:56:20 +09:00
1fe96f6a51 Fix CSV upload for Location2025 2025-08-31 13:49:56 +09:00
e9c6838171 Fix duplicate user registration error 2025-08-31 12:08:36 +09:00
71b073229e Fix Postgres error 2025-08-31 10:01:42 +09:00
0ef0bde5b1 photo_point and cppoint were reverted to checkin_point 2025-08-30 04:39:45 +09:00
cb399f14bf Fix API and admin for location2025 2025-08-30 04:12:55 +09:00
596b7313dd add location migrate 2025-08-30 03:48:07 +09:00
cf0adb34f9 stop unrequired logs 2025-08-30 02:25:01 +09:00
9af1e03523 Convert Location to Location2025 2025-08-30 02:20:25 +09:00
48b09b08da Debug again no image on manage 2025-08-29 21:27:43 +09:00
9c0b8932b5 Fix no image on manage 2025-08-29 21:23:07 +09:00
631c7293fc Fix image location 2025-08-29 21:17:59 +09:00
999ce636ac Fix no images issues 2025-08-29 21:11:00 +09:00
d63f205fa3 Fix API issue 2025-08-29 20:47:11 +09:00
50ebf8847c Update display format for zekken labels 2025-08-29 20:40:32 +09:00
b4d423aa35 Fix event-zekken-list API to use zekken_label instead of zekken_number for proper zekken value selection 2025-08-29 20:39:30 +09:00
102 changed files with 13393 additions and 500 deletions

0
.env.local_akira Normal file
View File

View File

@ -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

View File

@ -0,0 +1 @@
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
1 チーム名 ゼッケン番号 カテゴリー 時間 オーナーメール メンバー数 メンバー一覧 参加登録状況 エントリーID 作成日時

View File

@ -0,0 +1 @@
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
1 チーム名 ゼッケン番号 カテゴリー 時間 オーナーメール メンバー数 メンバー一覧 参加登録状況 エントリーID 作成日時

View File

@ -0,0 +1 @@
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
1 チーム名 ゼッケン番号 カテゴリー 時間 オーナーメール メンバー数 メンバー一覧 参加登録状況 エントリーID 作成日時

View File

@ -0,0 +1 @@
チーム名,ゼッケン番号,カテゴリー,時間,オーナーメール,リーダー,メンバー数,メンバー一覧,参加登録状況,エントリーID,作成日時
1 チーム名 ゼッケン番号 カテゴリー 時間 オーナーメール リーダー メンバー数 メンバー一覧 参加登録状況 エントリーID 作成日時

View 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
1 チーム名 ゼッケン番号 カテゴリー 時間 オーナーメール リーダー メンバー数 メンバー一覧 参加登録状況 エントリーID 作成日時
2 いなりずし 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
3 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
4 きみこうじ 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
5 ウエストサイド 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
6 ベル 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
7 ぐりと愉快な仲間たち 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
8 坂本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
9 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
10 さなっく 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
11 煮込みラーメン 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
12 サウナとビリヤニ 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
13 ひろ君と愉快な仲間たち 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
14 山下和乃 13 女性ソロ-3時間 3.0時間 山下和乃_member_1@dummy.local 山下和乃(2004-04-26) 1 山下和乃(2004-04-26) 完了 560 2025-09-05 07:46:42
15 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
16 しーくん 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
17 風呂の会 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
18 近藤隆 17 男性ソロ-5時間 5.0時間 近藤隆_member_1@dummy.local 近藤隆(1962-06-28) 1 近藤隆(1962-06-28) 完了 564 2025-09-05 07:46:42
19 日吉将大 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
20 東京OLクラブ 19 男性ソロ-3時間 3.0時間 東京olクラブ_member_1@dummy.local 阿部昌隆(1956-04-20) 1 阿部昌隆(1956-04-20) 完了 566 2025-09-05 07:46:42
21 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
22 脇屋貴司 21 男性ソロ-5時間 5.0時間 脇屋貴司_member_1@dummy.local 脇屋貴司(1983-10-26) 1 脇屋貴司(1983-10-26) 完了 568 2025-09-05 07:46:42
23 うぱうぱアイランド 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
24 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
25 チームしぇいや 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

View 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
1 チーム名 ゼッケン番号 カテゴリー 時間 オーナーメール リーダー メンバー数 メンバー一覧 参加登録状況 エントリーID 作成日時
2 いなりずし 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
3 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
4 きみこうじ 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
5 ウエストサイド 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
6 ベル 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
7 ぐりと愉快な仲間たち 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
8 坂本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
9 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
10 さなっく 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
11 煮込みラーメン 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
12 サウナとビリヤニ 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
13 ひろ君と愉快な仲間たち 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
14 山下和乃 13 女性ソロ-3時間 3.0時間 山下和乃_member_1@dummy.local 山下和乃(2004-04-26) 1 山下和乃(2004-04-26) 完了 560 2025-09-05 07:46:42
15 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
16 しーくん 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
17 風呂の会 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
18 近藤隆 17 男性ソロ-5時間 5.0時間 近藤隆_member_1@dummy.local 近藤隆(1962-06-28) 1 近藤隆(1962-06-28) 完了 564 2025-09-05 07:46:42
19 日吉将大 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
20 東京OLクラブ 19 男性ソロ-3時間 3.0時間 東京olクラブ_member_1@dummy.local 阿部昌隆(1956-04-20) 1 阿部昌隆(1956-04-20) 完了 566 2025-09-05 07:46:42
21 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
22 脇屋貴司 21 男性ソロ-5時間 5.0時間 脇屋貴司_member_1@dummy.local 脇屋貴司(1983-10-26) 1 脇屋貴司(1983-10-26) 完了 568 2025-09-05 07:46:42
23 うぱうぱアイランド 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
24 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
25 チームしぇいや 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
26 まゆちー 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
27 ガンバルゾー 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
28 ランエンジョン! 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

View 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
1 チーム名 ゼッケン番号 カテゴリー 時間 オーナーメール リーダー メンバー数 メンバー一覧 参加登録状況 エントリーID 作成日時
2 いなりずし 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
3 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
4 きみこうじ 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
5 ウエストサイド 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
6 ベル 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
7 ぐりと愉快な仲間たち 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
8 坂本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
9 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
10 さなっく 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
11 煮込みラーメン 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
12 サウナとビリヤニ 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
13 ひろ君と愉快な仲間たち 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
14 山下和乃 13 女性ソロ-3時間 3.0時間 山下和乃_member_1@dummy.local 山下和乃(2004-04-26) 1 山下和乃(2004-04-26) 完了 560 2025-09-05 07:46:42
15 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
16 しーくん 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
17 風呂の会 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
18 近藤隆 17 男性ソロ-5時間 5.0時間 近藤隆_member_1@dummy.local 近藤隆(1962-06-28) 1 近藤隆(1962-06-28) 完了 564 2025-09-05 07:46:42
19 日吉将大 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
20 東京OLクラブ 19 男性ソロ-3時間 3.0時間 東京olクラブ_member_1@dummy.local 阿部昌隆(1956-04-20) 1 阿部昌隆(1956-04-20) 完了 566 2025-09-05 07:46:42
21 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
22 脇屋貴司 21 男性ソロ-5時間 5.0時間 脇屋貴司_member_1@dummy.local 脇屋貴司(1983-10-26) 1 脇屋貴司(1983-10-26) 完了 568 2025-09-05 07:46:42
23 うぱうぱアイランド 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
24 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
25 チームしぇいや 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
26 まゆちー 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
27 ガンバルゾー 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
28 ランエンジョン! 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
29 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
30 ポエドリ 29 お試し-5時間 5.0時間 ポエドリ_member_1@dummy.local 高木俊裕(1984-03-09) 1 高木俊裕(1984-03-09) 完了 576 2025-09-05 08:36:18
31 前川一彦 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
View 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,09032840716,小林寿郎,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-kishidaogaki-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,ファミリー,'sファミリー,inukisen@gmail.com,ya1285,09042581285,安田千穂,1984/3/7,安田尚広,1978/1/18,安田雫,2014/9/2,安田葵,2018/5/13,,,,,,,,
1 部門別数 時間 部門 チーム名 メール password 電話番号 氏名1 誕生日1 氏名2 誕生日2 氏名3 誕生日3 氏名4 誕生日4 氏名5 誕生日5 氏名6 誕生日6 氏名7 誕生日7
2 1 3 一般 いなりずし takuyuna1123@icloud.com ko1703 09014701703 児玉優美 1976/12/13 児玉豊久 1973/11/23 田中広美 1975/10/31
3 1 5 一般 Go to the peak! shibashintan@c.vodafone.ne.jp shi0145 090-8499-0145 柴山晋太郎 1974/12/14 後藤克弘 1968/04/07 二村修 1967/06/22
4 2 3 一般 きみこうじ chibi-kimi.706@ezweb.ne.jp sa8309 09062518309 齋藤貴美子 1980/07/06 江口浩次 1968/04/19
5 2 5 一般 ウエストサイド chikachan-5101414@i.softbank.jp go7471 09047997471 後藤睦子 1961/5/1 後藤正寿 1959/7/23 大坪照子 1958/11/11 松村芳美 1964/4/28
6 3 3 一般 ベル kekomura1008@yahoo.co.jp ka3001 090-3564-3001 川村健一 1969/10/08 曽我部知奈美 1973/12/17 伊藤徳幸 1975/02/06 筒井勝児 1976/05/31
7 3 5 一般 ランエンジョン! baycools16@gmail.com ka9749 090÷4790÷9749 河合賢次 1972/12/14 中野真樹 1973/01/23
8 4 3 一般 ぐりと愉快な仲間たち kayochu.v.mame.526@icloud.com na6547 090-1564-6547 長屋香代子 1961/10/27 長屋宣宏 1961/5/26
9 4 5 一般 坂本555 sakamoto180909@yahoo.co.jp sa4396 090-8480-4396 坂本正憲 1972/5/30 坂本彩子 1976/3/29 坂本瑠璃子 2003/8/23
10 5 3 一般 リキとりんごてぃー apple1977tea@yahoo.co.jp te1499 08051241499 鄭寛子 1977/6/13 鄭昌彦 1971/5/26
11 5 5 一般 East Field ryo1hi@outlook.com hi0504 070-8564-0504 東野遼一 1983/09/27 東野智子 1977/03/16
12 6 3 一般 としちんかずちん kazu-chin1998@docomo.ne.jp shi9127 080-2616-9127 渋谷和広 1970/8/1 渋谷敏江 1956/6/16
13 6 5 一般 M sisters with D m.kiyomi.115@gmail.com ma3731 090-4869-3731 前田貴代美 1973/01/15 中濱智恵美 1969/06/16
14 7 3 一般 シマエナガ c6d6.lpbm5-s@ezweb.ne.jp shi1925 090-6336-1925 神谷孫斗 1997/03/02 小栗彩瑚 2001/9/21
15 7 5 一般 さなっく santa04230722@icloud.com ya7192 070-5640-7192 山田朋博 1971/04/23 眞田尚亮 1982/11/30
16 8 3 一般 煮込みラーメン t.nishioka1575tt@gmail.com ni9354 080-8523-9354 西岡嵩倫 1999/1/5 西岡影忠 1971/2/2
17 9 3 一般 そうたとなゆ hmt.sota@gmail.com ho6594 090-1109-6594 甫本創太 1991/06/07 後藤菜友 1994/02/22
18 10 3 一般 KOJ balccitomatochop@gmail.com to5670 090-2181-5670 轟原功樹 1978/08/10 田中美樹 1978/09/07
19 11 3 一般 サウナとビリヤニ bitter_smile107@yahoo.co.jp sa9007 090-4760-9007 坂口祐生 1992/1/7 近藤準 1987/1/25 圓山大貴 1993/5/10
20 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
21 2 3 お試し・一般 フクニシ appleorange100pct@yahoo.co.jp fu2792 080-6954-2792 福西直之 1986/2/5 福西愛 1986/3/2
22 3 3 お試し・一般 あやみち h613-y5m9t-mich@ezweb.ne.jp ya3144 090-4447-3144 谷許文音 2006/07/26 谷許美千代 1976/03/27
23 1 3 お試し・男性ソロ 松村覚司 happy.dreams.come.true923@gmail.com ma3625 090-8186-3625 松村覚司 1967/9/23
24 2 3 お試し・男性ソロ 高野清司 wakano_528@yahoo.co.jp ta5865 090-5603-5865 高野清司 71歳
25 1 3 お試し・ファミリー まゆちー takoyaki_sena@icloud.com a1246 090-6090-1246 浅田舞子 1986/02/22 浅田真結菜 2014/03/30 森美紀 1988/03/06 森千晴 2017/8/4
26 1 5 お試し・ファミリー ポエドリ takagitoshihiro8@yahoo.co.jp ta4245 090-5866-4245 高木俊裕 1984/03/09
27 2 3 お試し・ファミリー ガンバルゾー youkeymr.01@gmail.com mo6605 090-6080-6605 森祐貴 1985/9/26 浅田直之 1987/12/12 浅田晃汰 2014/01/06 森光喜 2015/4/22
28 2 5 お試し・ファミリー fun!fun!うごchan fulayota333@gmail.com ha7384 090-6599-7384 早川宏美 1975/6/15
29 3 3 お試し・ファミリー チームT sphin28420@aim.com te1882 080-6709-1882 寺田剛 1979/06/04 寺田恭子 1985/01/10 寺田向希 2023/11/08
30 1 3 女性ソロ 山下和乃 kazjamster@gmail.com ya2450 090-4229-2450 山下和乃 2004/4/26
31 1 5 女性ソロ Best Wishes thunderhead_56@yahoo.co.jp ha7226 090-5652-7226 長谷川美貴 1973/5/6
32 1 3 男性ソロ しーくん redleif57917913@ezweb.ne.jp mi6827 090-2946-6827 水門茂 1962/12/24
33 1 5 男性ソロ 風呂の会 1845dondon@gmail.com a9050 09096369050 浅井貴弘 1984/07/11
34 2 3 男性ソロ 野田達男 tatchi.sat111@docomo.ne.jp no0873 0901417-0873 野田達男 1950/9/14
35 2 5 男性ソロ 近藤隆 kondo2000gt@yahoo.ne.jp ko0666 09018300666 近藤隆 1962/6/28
36 3 3 男性ソロ 日吉将大 hiyomasa0034@gmail.com hi6343 080-2733-6343 日吉将大 1995/09/14
37 3 5 男性ソロ 松野昌紀 matsubottkuri11994730@gmail.com ma2606 090-1272-2606 松野昌紀 1972/9/30
38 4 3 男性ソロ 東京OLクラブ abe_1755_31@yahoo.co.jp a7102 090-2203-7102 阿部昌隆 1956/4/20
39 4 5 男性ソロ 白木稔人 amida48gan@icloud.com shi6048 090-7302-6048 白木稔人 1972/5/17
40 5 3 男性ソロ 大阪OLC t.okiura1961@gmail.com o1141 090-7888-1141 沖浦徹二 1961/4/29
41 5 5 男性ソロ Best Wishes jovi_bounce14@yahoo.co.jp ko0716 090−3284−0716 小林寿郎 1973/10/26
42 6 3 男性ソロ つるまいOLC junhagi68@gmail.com ha1001 080-3159-1001 萩原淳 1968/3/17
43 6 5 男性ソロ 脇屋貴司 takarinkuririn@gmail.com wa2659 080-3508-2659 脇屋貴司 1983/10/26
44 7 3 男性ソロ ㈱大垣ケーブルテレビ so-kishida@ogaki-tv.co.jp ki1207 0584-82-1207 岸田爽 2001/8/12
45 7 5 男性ソロ 前川一彦 yoshino-chuo@docomo.ne.jp ma2351 090-1074-2351 前川一彦 不明
46 8 3 男性ソロ ㈱大垣ケーブルテレビ ta-shiba@ogaki-tv.co.jp shi1207 芝建 1998/11/9
47 1 3 ファミリー うぱうぱアイランド serukasu@gmail.com i4200 09084584200 伊藤由美子 19920328 伊藤嘉仁 19930825 伊藤嘉利 20220913
48 1 5 ファミリー ながれぼし h2798723ddwyus@i.softbank.jp ta8317 090-1782-8317 高田めぐみ 1982/4/28 高田志穂 2013/12/5
49 2 3 ファミリー Team117 miki.maki0107@gmail.com sa3915 090-7678-3915 佐々木孝好 1970/12/20 佐々木享子 1977/8/25 佐々木実希 2012/1/21 佐々木麻妃 2016/7/1
50 2 5 ファミリー 500えん roumnet@yahoo.co.jp go6814 090-9890-6814 五百木弘道 1972/4/29 五百木芽彩 2015/3/13
51 3 3 ファミリー チームしぇいや rayrain3000@docomo.ne.jp ya2905 090-3056-2905 山本龍也 1976/3/14 山本聖也 2009/9/9 山本輝也 2015/6/3
52 3 5 ファミリー チームユズ livertish_v.g.35@docomo.ne.jp ko7822 090-7311-7822 小出龍 1983/2/27 小出柚希 2019/1/7
53 4 3 ファミリー Y'sファミリー inukisen@gmail.com ya1285 09042581285 安田千穂 1984/3/7 安田尚広 1978/1/18 安田雫 2014/9/2 安田葵 2018/5/13

View File

@ -0,0 +1,2 @@
部門別数,時間,部門,チーム名,メール,password,電話番号,氏名1
2,5,一般,ウエストサイド,hannivalscipio@gmail.com,ka9749,090-4790-9749,宮田 明
1 部門別数 時間 部門 チーム名 メール password 電話番号 氏名1
2 2 5 一般 ウエストサイド hannivalscipio@gmail.com ka9749 090-4790-9749 宮田 明

View 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,,,,,,,,,,,,,,
1 部門別数 時間 部門 チーム名 メール パスワード 電話番号 氏名1 誕生日1 氏名2 誕生日2 氏名3 誕生日3 氏名4 誕生日4 氏名5 誕生日5 氏名6 誕生日6 氏名7 誕生日7
2 2 5 お試し・ファミリー fun!fun!うごchan fulayota333@gmail.com ha7384 090-6599-7384 早川宏美 1975/6/15
3 1 5 お試し・ファミリー ポエドリ takagitoshihiro8@yahoo.co.jp ta4245 090-5866-4245 高木俊裕 1984/03/09
4 7 5 男性ソロ 前川一彦 yoshino-chuo@docomo.ne.jp ma2351 090-1074-2351 前川一彦 1990/1/1

View 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,09032840716,小林寿郎,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-kishidaogaki-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,ファミリー,'sファミリー,inukisen@gmail.com,ya1285,09042581285,安田千穂,1984/3/7,安田尚広,1978/1/18,安田雫,2014/9/2,安田葵,2018/5/13,,,,,,,,
1 部門別数 時間 部門 チーム名 メール password 電話番号 氏名1 誕生日1 氏名2 誕生日2 氏名3 誕生日3 氏名4 誕生日4 氏名5 誕生日5 氏名6 誕生日6 氏名7 誕生日7
2 1 3 一般 いなりずし takuyuna1123@icloud.com ko1703 09014701703 児玉優美 1976/12/13 児玉豊久 1973/11/23 田中広美 1975/10/31
3 1 5 一般 Go to the peak! shibashintan@c.vodafone.ne.jp shi0145 090-8499-0145 柴山晋太郎 1974/12/14 後藤克弘 1968/04/07 二村修 1967/06/22
4 2 3 一般 きみこうじ chibi-kimi.706@ezweb.ne.jp sa8309 09062518309 齋藤貴美子 1980/07/06 江口浩次 1968/04/19
5 2 5 一般 ウエストサイド chikachan-5101414@i.softbank.jp go7471 09047997471 後藤睦子 1961/5/1 後藤正寿 1959/7/23 大坪照子 1958/11/11 松村芳美 1964/4/28
6 3 3 一般 ベル kekomura1008@yahoo.co.jp ka3001 090-3564-3001 川村健一 1969/10/08 曽我部知奈美 1973/12/17 伊藤徳幸 1975/02/06 筒井勝児 1976/05/31
7 3 5 一般 ランエンジョン! baycools16@gmail.com ka9749 090÷4790÷9749 河合賢次 1972/12/14 中野真樹 1973/01/23
8 4 3 一般 ぐりと愉快な仲間たち kayochu.v.mame.526@icloud.com na6547 090-1564-6547 長屋香代子 1961/10/27 長屋宣宏 1961/5/26
9 4 5 一般 坂本555 sakamoto180909@yahoo.co.jp sa4396 090-8480-4396 坂本正憲 1972/5/30 坂本彩子 1976/3/29 坂本瑠璃子 2003/8/23
10 5 3 一般 リキとりんごてぃー apple1977tea@yahoo.co.jp te1499 08051241499 鄭寛子 1977/6/13 鄭昌彦 1971/5/26
11 5 5 一般 East Field ryo1hi@outlook.com hi0504 070-8564-0504 東野遼一 1983/09/27 東野智子 1977/03/16
12 6 3 一般 としちんかずちん kazu-chin1998@docomo.ne.jp shi9127 080-2616-9127 渋谷和広 1970/8/1 渋谷敏江 1956/6/16
13 6 5 一般 M sisters with D m.kiyomi.115@gmail.com ma3731 090-4869-3731 前田貴代美 1973/01/15 中濱智恵美 1969/06/16
14 7 3 一般 シマエナガ c6d6.lpbm5-s@ezweb.ne.jp shi1925 090-6336-1925 神谷孫斗 1997/03/02 小栗彩瑚 2001/9/21
15 7 5 一般 さなっく santa04230722@icloud.com ya7192 070-5640-7192 山田朋博 1971/04/23 眞田尚亮 1982/11/30
16 8 3 一般 煮込みラーメン t.nishioka1575tt@gmail.com ni9354 080-8523-9354 西岡嵩倫 1999/1/5 西岡影忠 1971/2/2
17 9 3 一般 そうたとなゆ hmt.sota@gmail.com ho6594 090-1109-6594 甫本創太 1991/06/07 後藤菜友 1994/02/22
18 10 3 一般 KOJ balccitomatochop@gmail.com to5670 090-2181-5670 轟原功樹 1978/08/10 田中美樹 1978/09/07
19 11 3 一般 サウナとビリヤニ bitter_smile107@yahoo.co.jp sa9007 090-4760-9007 坂口祐生 1992/1/7 近藤準 1987/1/25 圓山大貴 1993/5/10
20 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
21 2 3 お試し・一般 フクニシ appleorange100pct@yahoo.co.jp fu2792 080-6954-2792 福西直之 1986/2/5 福西愛 1986/3/2
22 3 3 お試し・一般 あやみち h613-y5m9t-mich@ezweb.ne.jp ya3144 090-4447-3144 谷許文音 2006/07/26 谷許美千代 1976/03/27
23 1 3 お試し・男性ソロ 松村覚司 happy.dreams.come.true923@gmail.com ma3625 090-8186-3625 松村覚司 1967/9/23
24 2 3 お試し・男性ソロ 高野清司 wakano_528@yahoo.co.jp ta5865 090-5603-5865 高野清司 71歳
25 1 3 お試し・ファミリー まゆちー takoyaki_sena@icloud.com a1246 090-6090-1246 浅田舞子 1986/02/22 浅田真結菜 2014/03/30 森美紀 1988/03/06 森千晴 2017/8/4
26 1 5 お試し・ファミリー ポエドリ takagitoshihiro8@yahoo.co.jp ta4245 090-5866-4245 高木俊裕 1984/03/09
27 2 3 お試し・ファミリー ガンバルゾー youkeymr.01@gmail.com mo6605 090-6080-6605 森祐貴 1985/9/26 浅田直之 1987/12/12 浅田晃汰 2014/01/06 森光喜 2015/4/22
28 2 5 お試し・ファミリー fun!fun!うごchan fulayota333@gmail.com ha7384 090-6599-7384 早川宏美 1975/6/15
29 3 3 お試し・ファミリー チームT sphin28420@aim.com te1882 080-6709-1882 寺田剛 1979/06/04 寺田恭子 1985/01/10 寺田向希 2023/11/08
30 1 3 女性ソロ 山下和乃 kazjamster@gmail.com ya2450 090-4229-2450 山下和乃 2004/4/26
31 1 5 女性ソロ Best Wishes thunderhead_56@yahoo.co.jp ha7226 090-5652-7226 長谷川美貴 1973/5/6
32 1 3 男性ソロ しーくん redleif57917913@ezweb.ne.jp mi6827 090-2946-6827 水門茂 1962/12/24
33 1 5 男性ソロ 風呂の会 1845dondon@gmail.com a9050 09096369050 浅井貴弘 1984/07/11
34 2 3 男性ソロ 野田達男 tatchi.sat111@docomo.ne.jp no0873 0901417-0873 野田達男 1950/9/14
35 2 5 男性ソロ 近藤隆 kondo2000gt@yahoo.ne.jp ko0666 09018300666 近藤隆 1962/6/28
36 3 3 男性ソロ 日吉将大 hiyomasa0034@gmail.com hi6343 080-2733-6343 日吉将大 1995/09/14
37 3 5 男性ソロ 松野昌紀 matsubottkuri11994730@gmail.com ma2606 090-1272-2606 松野昌紀 1972/9/30
38 4 3 男性ソロ 東京OLクラブ abe_1755_31@yahoo.co.jp a7102 090-2203-7102 阿部昌隆 1956/4/20
39 4 5 男性ソロ 白木稔人 amida48gan@icloud.com shi6048 090-7302-6048 白木稔人 1972/5/17
40 5 3 男性ソロ 大阪OLC t.okiura1961@gmail.com o1141 090-7888-1141 沖浦徹二 1961/4/29
41 5 5 男性ソロ Best Wishes jovi_bounce14@yahoo.co.jp ko0716 090−3284−0716 小林寿郎 1973/10/26
42 6 3 男性ソロ つるまいOLC junhagi68@gmail.com ha1001 080-3159-1001 萩原淳 1968/3/17
43 6 5 男性ソロ 脇屋貴司 takarinkuririn@gmail.com wa2659 080-3508-2659 脇屋貴司 1983/10/26
44 7 3 男性ソロ ㈱大垣ケーブルテレビ so-kishida@ogaki-tv.co.jp ki1207 0584-82-1207 岸田爽 2001/8/12
45 7 5 男性ソロ 前川一彦 yoshino-chuo@docomo.ne.jp ma2351 090-1074-2351 前川一彦 不明
46 8 3 男性ソロ ㈱大垣ケーブルテレビ ta-shiba@ogaki-tv.co.jp shi1207 芝建 1998/11/9
47 1 3 ファミリー うぱうぱアイランド serukasu@gmail.com i4200 09084584200 伊藤由美子 19920328 伊藤嘉仁 19930825 伊藤嘉利 20220913
48 1 5 ファミリー ながれぼし h2798723ddwyus@i.softbank.jp ta8317 090-1782-8317 高田めぐみ 1982/4/28 高田志穂 2013/12/5
49 2 3 ファミリー Team117 miki.maki0107@gmail.com sa3915 090-7678-3915 佐々木孝好 1970/12/20 佐々木享子 1977/8/25 佐々木実希 2012/1/21 佐々木麻妃 2016/7/1
50 2 5 ファミリー 500えん roumnet@yahoo.co.jp go6814 090-9890-6814 五百木弘道 1972/4/29 五百木芽彩 2015/3/13
51 3 3 ファミリー チームしぇいや rayrain3000@docomo.ne.jp ya2905 090-3056-2905 山本龍也 1976/3/14 山本聖也 2009/9/9 山本輝也 2015/6/3
52 3 5 ファミリー チームユズ livertish_v.g.35@docomo.ne.jp ko7822 090-7311-7822 小出龍 1983/2/27 小出柚希 2019/1/7
53 4 3 ファミリー Y'sファミリー inukisen@gmail.com ya1285 09042581285 安田千穂 1984/3/7 安田尚広 1978/1/18 安田雫 2014/9/2 安田葵 2018/5/13

View 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,,,,,,,,,,,,
1 部門別数 時間 部門 チーム名 メール パスワード 電話番号 氏名1 誕生日1 氏名2 誕生日2 氏名3 誕生日3 氏名4 誕生日4 氏名5 誕生日5 氏名6 誕生日6 氏名7 誕生日7
2 1 3 一般 いなりずし takuyuna1123@icloud.com ko1703 09014701703 児玉優美 1976/12/13 児玉豊久 1973/11/23 田中広美 1975/10/31
3 1 5 一般 Go to the peak! shibashintan@c.vodafone.ne.jp shi0145 090-8499-0145 柴山晋太郎 1974/12/14 後藤克弘 1968/04/07 二村修 1967/06/22
4 2 3 一般 きみこうじ chibi-kimi.706@ezweb.ne.jp sa8309 09062518309 齋藤貴美子 1980/07/06 江口浩次 1968/04/19

View 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,,,,,,,,,,,,
1 部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
2 1,3,お試し,テスト一人お試し,test_solo_trial@example.com,test123,090-1234-5678,山田太郎,1990/4/15,,,,,,,,,,,,
3 2,5,お試し,テスト一人お試し2,test_solo_trial2@example.com,test456,090-1234-5679,佐藤花子,1985/8/20,,,,,,,,,,,,

View 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,,,,,,,,,,,,
1 部門別数 時間 部門 チーム名 メール パスワード 電話番号 氏名1 誕生日1 氏名2 誕生日2 氏名3 誕生日3 氏名4 誕生日4 氏名5 誕生日5 氏名6 誕生日6 氏名7 誕生日7
2 3 3 お試し・ファミリー まゆちー takoyaki_sena@icloud.com ma0222 090-3309-0222 浅田舞子 1986/02/22 浅田真結菜 2014/03/30 森美紀 1988/03/06 森千晴 2017/8/4
3 4 3 お試し・ファミリー ガンバルゾー youkeymr.01@gmail.com mo3540 090-8962-3540 森祐貴 1985/9/26 浅田直之 1987/12/12 浅田晃汰 2014/01/06 森光喜 2015/4/22
4 7 5 お試し ランエンジョン! baycools16@gmail.com ka9749 090÷4790÷9749 河合賢次 1972/12/14 中野真樹 1973/01/23

View 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"]

243
EMAIL_SENDING_MANUAL.md Normal file
View 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

View 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()` - イベント参加ロジック
## ライセンス
このプロジェクトはロゲイニングシステムの一部です。

245
TEAM_CSV_IMPORT_MANUAL.md Normal file
View 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の列名が正しいか氏名、氏名確認してください。
## サポート
技術的な問題や質問がある場合は、システム開発チームまでお問い合わせください。
---
**作成日:** 2025年9月5日
**バージョン:** 1.0
**対象システム:** 岐阜ロゲイニングサーバー

392
analyze_nginx_logs.py Normal file
View 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()

319
check_checkin_status.py Normal file
View 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()

View File

@ -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',
@ -240,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'
@ -275,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': {
@ -300,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,
},
}, },
} }
@ -326,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/'

View File

@ -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"

103
custom-pg_hba.conf Normal file
View 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
View 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
View 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
View 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()

View File

@ -6,6 +6,7 @@ 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 - ./sqls:/sqls
- ./create_location2025_table.sql:/sql/create_location2025_table.sql - ./create_location2025_table.sql:/sql/create_location2025_table.sql
@ -41,6 +42,11 @@ services:
- 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: healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000')\" || exit 1"] test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000')\" || exit 1"]
interval: 30s interval: 30s

View 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

View 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()

View 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()

View 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()

View 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()

View 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()

166
monitor_app_errors.py Normal file
View 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()

View File

@ -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
View 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
View 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
View 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()

View File

@ -82,3 +82,4 @@ haversine
piexif==1.1.3 piexif==1.1.3
Pillow>=8.0.0 Pillow>=8.0.0
boto3

BIN
rog/.DS_Store vendored

Binary file not shown.

View File

@ -4,7 +4,7 @@ from django.shortcuts import render,redirect
from leaflet.admin import LeafletGeoAdmin from leaflet.admin import LeafletGeoAdmin
from leaflet.admin import LeafletGeoAdminMixin from leaflet.admin import LeafletGeoAdminMixin
from leaflet_admin_list.admin import LeafletAdminListMixin from leaflet_admin_list.admin import LeafletAdminListMixin
from .models import RogUser, Location, Location2025, SystemSettings, JoinedEvent, Favorite, TravelList, TravelPoint, ShapeLayers, Event, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, CustomUser, GifuAreas, UserTracks, templocation, UserUpload, EventUser, GoalImages, CheckinImages, NewEvent2, Team, NewCategory, Entry, Member, TempUser, GifurogeRegister, GpsLog, GpsCheckin, Checkpoint, Waypoint from .models import RogUser, Location2025, SystemSettings, JoinedEvent, Favorite, TravelList, TravelPoint, ShapeLayers, Event, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, CustomUser, GifuAreas, UserTracks, templocation, UserUpload, EventUser, GoalImages, CheckinImages, NewEvent2, Team, NewCategory, Entry, Member, TempUser, GifurogeRegister, GpsLog, GpsCheckin, Checkpoint, Waypoint
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.urls import path,reverse from django.urls import path,reverse
from django.shortcuts import render from django.shortcuts import render
@ -743,15 +743,15 @@ class LocationAdmin(LeafletGeoAdmin):
search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name','group',) search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name','group',)
list_filter = ('event_name', 'group',) list_filter = ('event_name', 'group',)
ordering = ('location_id', 'cp',) ordering = ('location_id', 'cp',)
list_display = ('location_id','sub_loc_id', 'cp', 'location_name', 'photos', 'category', 'group', 'event_name', 'event_active', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',) list_display = ('location_id','sub_loc_id', 'cp', 'location_name', 'photos', 'category', 'group', 'event_name', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',)
def tranfer_to_location(modeladmin, request, queryset): def tranfer_to_location(modeladmin, request, queryset):
tmp_locs = templocation.objects.all(); tmp_locs = templocation.objects.all();
for l in tmp_locs : for l in tmp_locs :
found = Location.objects.filter(location_id = l.location_id).exists() found = Location2025.objects.filter(location_id = l.location_id).exists()
if found: if found:
Location.objects.filter(location_id = l.location_id).update( Location2025.objects.filter(location_id = l.location_id).update(
sub_loc_id = l.sub_loc_id, sub_loc_id = l.sub_loc_id,
cp = l.cp, cp = l.cp,
location_name = l.location_name, location_name = l.location_name,
@ -794,7 +794,7 @@ def tranfer_to_location(modeladmin, request, queryset):
geom=l.geom geom=l.geom
) )
else: else:
loc = Location( loc = Location2025(
location_id=l.location_id, location_id=l.location_id,
sub_loc_id = l.sub_loc_id, sub_loc_id = l.sub_loc_id,
cp = l.cp, cp = l.cp,
@ -848,13 +848,16 @@ class TempLocationAdmin(LeafletGeoAdmin):
search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name',) search_fields = ('location_id', 'cp', 'location_name', 'category', 'event_name',)
list_filter = ('category', 'event_name',) list_filter = ('category', 'event_name',)
ordering = ('location_id', 'cp',) ordering = ('location_id', 'cp',)
list_display = ('location_id','cp', 'location_name', 'category', 'event_name', 'event_active', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',) list_display = ('location_id','cp', 'location_name', 'category', 'event_name', 'auto_checkin', 'checkin_radius', 'checkin_point', 'buy_point',)
actions = [tranfer_to_location,] actions = [tranfer_to_location,]
@admin.register(NewEvent2) @admin.register(NewEvent2)
class NewEvent2Admin(admin.ModelAdmin): class NewEvent2Admin(admin.ModelAdmin):
list_display = ['event_name', 'start_datetime', 'end_datetime', 'csv_upload_button'] list_display = ['event_name', 'start_datetime', 'end_datetime', 'status', 'csv_upload_button']
list_filter = ['status', 'start_datetime']
search_fields = ['event_name', 'event_description', 'event_code'] # 正しいフィールド名に修正
ordering = ['-start_datetime']
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
@ -983,7 +986,6 @@ class CustomUserAdmin(UserAdmin):
admin.site.register(Useractions) admin.site.register(Useractions)
admin.site.register(RogUser, admin.ModelAdmin) admin.site.register(RogUser, admin.ModelAdmin)
admin.site.register(Location, LocationAdmin)
admin.site.register(SystemSettings, admin.ModelAdmin) admin.site.register(SystemSettings, admin.ModelAdmin)
admin.site.register(JoinedEvent, admin.ModelAdmin) admin.site.register(JoinedEvent, admin.ModelAdmin)
admin.site.register(Favorite, admin.ModelAdmin) admin.site.register(Favorite, admin.ModelAdmin)
@ -1003,7 +1005,8 @@ admin.site.register(EventUser, admin.ModelAdmin)
#admin.site.register(ShapeFileLocations, admin.ModelAdmin) #admin.site.register(ShapeFileLocations, admin.ModelAdmin)
#admin.site.register(CustomUser, UserAdminConfig) #admin.site.register(CustomUser, UserAdminConfig)
admin.site.register(templocation, TempLocationAdmin) # 古いtemplocationは無効化 - Location2025を使用
#admin.site.register(templocation, TempLocationAdmin)
admin.site.register(GoalImages, admin.ModelAdmin) admin.site.register(GoalImages, admin.ModelAdmin)
admin.site.register(CheckinImages, admin.ModelAdmin) admin.site.register(CheckinImages, admin.ModelAdmin)
@ -1039,16 +1042,22 @@ class WaypointAdmin(admin.ModelAdmin):
@admin.register(Location2025) @admin.register(Location2025)
class Location2025Admin(LeafletGeoAdmin): class Location2025Admin(LeafletGeoAdmin):
"""Location2025の管理画面""" """Location2025の管理画面(全フィールド対応)"""
list_display = [ list_display = [
'cp_number', 'cp_name', 'event', 'total_point', 'is_active', 'cp_number', 'cp_name', 'event', 'sub_loc_id', 'evaluation_value_display',
'checkin_radius_display', 'total_point', 'has_photos', 'has_videos', 'is_active',
'csv_upload_date', 'created_at' 'csv_upload_date', 'created_at'
] ]
list_filter = [ list_filter = [
'event', 'is_active', 'shop_closed', 'shop_shutdown', 'event', 'is_active', 'shop_closed', 'shop_shutdown',
'category', 'subcategory', 'hidden_location',
'csv_upload_date', 'created_at' 'csv_upload_date', 'created_at'
] ]
search_fields = ['cp_name', 'address', 'description'] search_fields = [
'cp_name', 'address', 'description', 'remark', 'tags',
'sub_loc_id', 'category', 'subcategory', 'evaluation_value',
'event__event_name' # イベント名での検索を追加
]
readonly_fields = [ readonly_fields = [
'csv_source_file', 'csv_upload_date', 'csv_upload_user', 'csv_source_file', 'csv_upload_date', 'csv_upload_user',
'created_at', 'updated_at', 'created_by', 'updated_by' 'created_at', 'updated_at', 'created_by', 'updated_by'
@ -1056,13 +1065,13 @@ class Location2025Admin(LeafletGeoAdmin):
fieldsets = ( fieldsets = (
('基本情報', { ('基本情報', {
'fields': ('cp_number', 'event', 'cp_name', 'is_active', 'sort_order') 'fields': ('cp_number', 'event', 'cp_name', 'category', 'sub_loc_id', 'subcategory', 'is_active', 'sort_order')
}), }),
('位置情報', { ('位置情報', {
'fields': ('latitude', 'longitude', 'location', 'address') 'fields': ('latitude', 'longitude', 'location', 'address', 'zip_code', 'prefecture', 'area', 'city')
}), }),
('ポイント設定', { ('ポイント設定', {
'fields': ('cp_point', 'photo_point', 'buy_point') 'fields': ('checkin_point', 'buy_point')
}), }),
('チェックイン設定', { ('チェックイン設定', {
'fields': ('checkin_radius', 'auto_checkin') 'fields': ('checkin_radius', 'auto_checkin')
@ -1071,7 +1080,15 @@ class Location2025Admin(LeafletGeoAdmin):
'fields': ('shop_closed', 'shop_shutdown', 'opening_hours') 'fields': ('shop_closed', 'shop_shutdown', 'opening_hours')
}), }),
('詳細情報', { ('詳細情報', {
'fields': ('phone', 'website', 'description') 'fields': ('phone', 'website', 'description', 'remark', 'facility')
}),
('メディア・タグ情報', {
'fields': ('photos', 'videos', 'tags', 'evaluation_value'),
'classes': ('wide',)
}),
('高度設定', {
'fields': ('hidden_location',),
'classes': ('collapse',)
}), }),
('CSV情報', { ('CSV情報', {
'fields': ('csv_source_file', 'csv_upload_date', 'csv_upload_user'), 'fields': ('csv_source_file', 'csv_upload_date', 'csv_upload_user'),
@ -1083,6 +1100,46 @@ class Location2025Admin(LeafletGeoAdmin):
}), }),
) )
def has_photos(self, obj):
"""写真データ有無の表示"""
return bool(obj.photos and obj.photos.strip())
has_photos.boolean = True
has_photos.short_description = '写真'
def has_videos(self, obj):
"""動画データ有無の表示"""
return bool(obj.videos and obj.videos.strip())
has_videos.boolean = True
has_videos.short_description = '動画'
def evaluation_value_display(self, obj):
"""evaluation_valueのカスタム表示"""
# CP番号による特別な表示
if obj.cp_number == -2:
return 'スタート'
elif obj.cp_number == -1:
return 'ゴール'
# evaluation_valueによる通常の表示
evaluation_map = {
'0': '通常CP',
'1': '買物CP',
'2': 'QR-CP'
}
value = str(obj.evaluation_value or '0')
return evaluation_map.get(value, value)
evaluation_value_display.short_description = 'CP種別'
def checkin_radius_display(self, obj):
"""checkin_radiusのカスタム表示"""
if obj.checkin_radius == -1:
return '要タップ'
elif obj.checkin_radius is not None and obj.checkin_radius >= 0:
return f'{obj.checkin_radius}m'
else:
return '-'
checkin_radius_display.short_description = 'チェックイン範囲'
# CSV一括アップロード機能 # CSV一括アップロード機能
change_list_template = 'admin/location2025/change_list.html' change_list_template = 'admin/location2025/change_list.html'
@ -1131,9 +1188,13 @@ class Location2025Admin(LeafletGeoAdmin):
return redirect('..') return redirect('..')
# フォーム表示 # フォーム表示 - Location2025システム用
from .models import NewEvent2 from .models import NewEvent2
events = NewEvent2.objects.filter(event_active=True).order_by('-created_at') # スタッフユーザーの場合は全ステータスのイベントを表示
if request.user.is_staff:
events = NewEvent2.objects.all().order_by('-start_datetime')
else:
events = NewEvent2.objects.filter(status='public').order_by('-start_datetime')
return render(request, 'admin/location2025/upload_csv.html', { return render(request, 'admin/location2025/upload_csv.html', {
'events': events, 'events': events,
@ -1141,29 +1202,35 @@ class Location2025Admin(LeafletGeoAdmin):
}) })
def export_csv_view(self, request): def export_csv_view(self, request):
"""CSVエクスポート""" """CSVエクスポート(全フィールド対応)"""
import csv import csv
from django.http import HttpResponse from django.http import HttpResponse
from django.utils import timezone from django.utils import timezone
response = HttpResponse(content_type='text/csv; charset=utf-8') response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = f'attachment; filename="checkpoints_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"' response['Content-Disposition'] = f'attachment; filename="checkpoints_enhanced_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"'
# BOM付きUTF-8で出力 # BOM付きUTF-8で出力
response.write('\ufeff') response.write('\ufeff')
writer = csv.writer(response) writer = csv.writer(response)
# 全フィールドのヘッダー
writer.writerow([ writer.writerow([
'cp_number', 'cp_name', 'latitude', 'longitude', 'cp_point', 'cp_number', 'cp_name', 'latitude', 'longitude', 'checkin_point',
'photo_point', 'buy_point', 'address', 'phone', 'description' 'buy_point', 'address', 'phone', 'description',
'sub_loc_id', 'subcategory', 'photos', 'videos', 'tags',
'evaluation_value', 'remark', 'hidden_location'
]) ])
queryset = self.get_queryset(request) queryset = self.get_queryset(request)
for obj in queryset: for obj in queryset:
writer.writerow([ writer.writerow([
obj.cp_number, obj.cp_name, obj.latitude, obj.longitude, obj.cp_number, obj.cp_name, obj.latitude, obj.longitude,
obj.cp_point, obj.photo_point, obj.buy_point, obj.checkin_point, obj.buy_point,
obj.address, obj.phone, obj.description obj.address or '', obj.phone or '', obj.description or '',
obj.sub_loc_id or '', obj.subcategory or '', obj.photos or '',
obj.videos or '', obj.tags or '', obj.evaluation_value or '',
obj.remark or '', obj.hidden_location
]) ])
return response return response

View File

@ -42,15 +42,24 @@ class EmailOrUsernameModelBackend(ModelBackend):
kwargs = {'username': username} kwargs = {'username': username}
try: try:
user = CustomUser.objects.get(**kwargs) user = CustomUser.objects.get(**kwargs)
if check_password(password, user.password): logger.info(f"User found in database: {username}")
# パスワード検証の詳細ログ
password_valid = check_password(password, user.password)
logger.debug(f"Password validation for {username}: {password_valid}")
if password_valid:
logger.info(f"User authenticated successfully: {username}") logger.info(f"User authenticated successfully: {username}")
return user return user
else: else:
logger.warning(f"Password mismatch for user: {username}") logger.warning(f"Password mismatch for user: {username}")
logger.debug(f"Provided password length: {len(password) if password else 0}")
except CustomUser.DoesNotExist: except CustomUser.DoesNotExist:
logger.warning(f"User does not exist: {username}") logger.warning(f"User does not exist: {username}")
except Exception as e: except Exception as e:
logger.error(f"Authentication error for {username}: {str(e)}") logger.error(f"Authentication error for {username}: {str(e)}")
import traceback
logger.error(f"Authentication traceback: {traceback.format_exc()}")
return None return None
def get_user(self, user_id): def get_user(self, user_id):

View File

@ -0,0 +1 @@
# Django management module

View File

@ -0,0 +1 @@
# Django management commands module

View File

@ -0,0 +1,114 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from rog.models import GpsLog
class Command(BaseCommand):
help = '外部システム登録の状況を確認します'
def add_arguments(self, parser):
parser.add_argument(
'--event-code',
type=str,
help='特定のイベントコードでフィルタ'
)
parser.add_argument(
'--status',
type=str,
choices=['pending', 'success', 'failed', 'retry'],
help='特定のステータスでフィルタ'
)
parser.add_argument(
'--show-details',
action='store_true',
help='詳細情報を表示'
)
def handle(self, *args, **options):
event_code = options.get('event_code')
status = options.get('status')
show_details = options['show_details']
self.stdout.write(
self.style.SUCCESS('外部システム登録状況を確認しています...')
)
# フィルタ条件を構築
queryset = GpsLog.objects.filter(
serial_number=-1,
cp_number="EXTERNAL_REG"
)
if event_code:
queryset = queryset.filter(event_code=event_code)
if status:
queryset = queryset.filter(external_registration_status=status)
# 統計情報
total_count = queryset.count()
pending_count = queryset.filter(external_registration_status='pending').count()
success_count = queryset.filter(external_registration_status='success').count()
failed_count = queryset.filter(external_registration_status='failed').count()
retry_count = queryset.filter(external_registration_status='retry').count()
self.stdout.write(f'\n=== 外部システム登録統計 ===')
self.stdout.write(f'合計記録数: {total_count}')
self.stdout.write(f'保留中: {pending_count}')
self.stdout.write(f'成功: {success_count}')
self.stdout.write(f'失敗: {failed_count}')
self.stdout.write(f'リトライ要求: {retry_count}')
if show_details and queryset.exists():
self.stdout.write(f'\n=== 詳細情報 ===')
for record in queryset.order_by('-create_at')[:20]: # 最新20件
status_color = self.get_status_color(record.external_registration_status)
self.stdout.write(
f'[{record.create_at.strftime("%Y-%m-%d %H:%M")}] '
f'ゼッケン: {record.zekken_number} | '
f'イベント: {record.event_code} | '
f'チーム: {record.team_name} | '
f'ステータス: {status_color(record.external_registration_status)} | '
f'試行回数: {record.external_registration_attempts}'
)
if record.external_registration_error:
self.stdout.write(f' エラー: {record.external_registration_error[:100]}...')
# リトライが必要な記録をハイライト
needs_retry = queryset.filter(
external_registration_status__in=['pending', 'retry'],
external_registration_attempts__lt=3
)
if needs_retry.exists():
self.stdout.write(
self.style.WARNING(
f'\n注意: {needs_retry.count()}件の記録がリトライを待機中です。'
' `python manage.py retry_external_registrations` でリトライできます。'
)
)
# 最大試行回数に達した記録をチェック
max_attempts_reached = queryset.filter(
external_registration_attempts__gte=3,
external_registration_status__in=['pending', 'failed', 'retry']
)
if max_attempts_reached.exists():
self.stdout.write(
self.style.ERROR(
f'\n警告: {max_attempts_reached.count()}件の記録が最大試行回数に達しています。'
' 手動での確認が必要です。'
)
)
def get_status_color(self, status):
"""ステータスに応じた色付き表示を返す"""
if status == 'success':
return self.style.SUCCESS
elif status == 'failed':
return self.style.ERROR
elif status in ['pending', 'retry']:
return self.style.WARNING
else:
return lambda x: x

View File

@ -0,0 +1,36 @@
from django.core.management.base import BaseCommand
from rog.models import CustomUser
from django.contrib.auth import authenticate
class Command(BaseCommand):
help = 'Check user authentication'
def add_arguments(self, parser):
parser.add_argument('email', type=str, help='User email to check')
parser.add_argument('--password', type=str, help='Password to test', default='')
def handle(self, *args, **options):
email = options['email']
password = options.get('password', '')
self.stdout.write(f'Checking user: {email}')
try:
user = CustomUser.objects.get(email=email)
self.stdout.write(f'✅ User found: {user.email}')
self.stdout.write(f' Active: {user.is_active}')
self.stdout.write(f' Has password: {bool(user.password)}')
self.stdout.write(f' Password hash start: {user.password[:30]}...')
if password:
self.stdout.write(f'\n🔐 Testing authentication with provided password...')
auth_user = authenticate(username=email, password=password)
if auth_user:
self.stdout.write('✅ Authentication successful')
else:
self.stdout.write('❌ Authentication failed')
except CustomUser.DoesNotExist:
self.stdout.write(f'❌ User does not exist: {email}')
except Exception as e:
self.stdout.write(f'❌ Error: {e}')

View File

@ -0,0 +1,56 @@
from django.core.management.base import BaseCommand
from rog.models import GpsLog, GpsCheckin, Entry
class Command(BaseCommand):
help = 'Debug checkin data for specific team'
def add_arguments(self, parser):
parser.add_argument('zekken', type=str, help='Zekken number to check')
parser.add_argument('--event', type=str, default='岐阜ロゲイニング2025', help='Event name')
def handle(self, *args, **options):
zekken = options['zekken']
event_name = options['event']
self.stdout.write(f'=== Debugging checkin data for zekken {zekken}, event {event_name} ===')
# Entry確認
self.stdout.write('\n=== Entry Records ===')
entries = Entry.objects.filter(zekken_number=int(zekken), event__event_name=event_name)
self.stdout.write(f'Found {entries.count()} entries')
for entry in entries:
self.stdout.write(f' Team: {entry.team.team_name}, Zekken: {entry.zekken_number}, Event ID: {entry.event.id}')
if not entries.exists():
self.stdout.write('❌ No entries found')
return
entry = entries.first()
# GpsLog確認文字列として検索
self.stdout.write('\n=== GpsLog Records (str zekken) ===')
logs_str = GpsLog.objects.filter(zekken_number=str(zekken), event_code=event_name)
self.stdout.write(f'Found {logs_str.count()} records in GpsLog (str)')
for log in logs_str[:10]:
self.stdout.write(f' ID: {log.id}, CP: {log.cp_number}, Time: {log.checkin_time}, Event: {log.event_code}')
# GpsLog確認整数として検索
self.stdout.write('\n=== GpsLog Records (int zekken) ===')
logs_int = GpsLog.objects.filter(zekken_number=int(zekken), event_code=event_name)
self.stdout.write(f'Found {logs_int.count()} records in GpsLog (int)')
for log in logs_int[:10]:
self.stdout.write(f' ID: {log.id}, CP: {log.cp_number}, Time: {log.checkin_time}, Event: {log.event_code}')
# GpsCheckin確認
self.stdout.write('\n=== GpsCheckin Records ===')
checkins = GpsCheckin.objects.filter(zekken=str(zekken), event_code=event_name)
self.stdout.write(f'Found {checkins.count()} records in GpsCheckin')
for checkin in checkins[:10]:
self.stdout.write(f' ID: {checkin.id}, CP: {checkin.cp_number}, Time: {checkin.checkin_time}, Event: {checkin.event_code}')
# 全てのGpsLogレコードから類似のものを検索
self.stdout.write('\n=== All GpsLog Records for this event ===')
all_logs = GpsLog.objects.filter(event_code=event_name)
self.stdout.write(f'Total records in event: {all_logs.count()}')
for log in all_logs[:5]:
self.stdout.write(f' ID: {log.id}, Zekken: {log.zekken_number} (type: {type(log.zekken_number)}), CP: {log.cp_number}, Event: {log.event_code}')

View File

@ -6,7 +6,7 @@ from django.db import transaction, connections
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rog.models import Member, Team, NewEvent2, Entry, Location,NewCategory #, GpsLog from rog.models import Member, Team, NewEvent2, Entry, Location2025,NewCategory #, GpsLog
CustomUser = get_user_model() CustomUser = get_user_model()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -473,7 +473,7 @@ class Command(BaseCommand):
# 4. Locationテーブルからcheckpoint_tableへの転送 # 4. Locationテーブルからcheckpoint_tableへの転送
self.stdout.write('checkpointデータを転送中...') self.stdout.write('checkpointデータを転送中...')
locations = Location.objects.filter(group=event.event_name) locations = Location2025.objects.filter(group=event.event_name)
# Print the number of location records # Print the number of location records
location_count = locations.count() location_count = locations.count()
self.stdout.write(f'checkpointデータ: {location_count}件を転送中...') self.stdout.write(f'checkpointデータ: {location_count}件を転送中...')
@ -568,7 +568,7 @@ class Command(BaseCommand):
try: try:
# 4. Locationテーブルからcheckpoint_tableへの転送 # 4. Locationテーブルからcheckpoint_tableへの転送
self.stdout.write('checkpointデータを転送中...') self.stdout.write('checkpointデータを転送中...')
locations = Location.objects.filter(event=event) locations = Location2025.objects.filter(event_id=event.id)
for location in locations: for location in locations:
cursor.execute(""" cursor.execute("""

View File

@ -0,0 +1,555 @@
"""
CSVファイルからチームエントリーをインポートするDjango管理コマンド
CPLIST/input/teams2025.csv 本番用対応
Usage:
docker compose exec app python manage.py import_teams --event_code=岐阜ロゲイニング2025 --csv_file=CPLIST/input/teams2025.csv
Author: システム開発チーム
Date: 2025-09-05
"""
import csv
import os
import logging
import re
from datetime import datetime, timedelta
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction, models
from django.contrib.auth.hashers import make_password
from django.utils import timezone
from django.core.exceptions import ValidationError
from rog.models import (
CustomUser, Team, Member, Entry, NewEvent2, NewCategory
)
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'CSVファイルからチームエントリーをインポート'
def add_arguments(self, parser):
parser.add_argument(
'--event_code',
type=str,
required=True,
help='インポート先のイベントコード'
)
parser.add_argument(
'--csv_file',
type=str,
default='CPLIST/input/teams2025.csv',
help='インポートするCSVファイルのパス (default: CPLIST/input/teams2025.csv)'
)
parser.add_argument(
'--dry_run',
action='store_true',
help='実際にはデータベースに書き込まずに処理を確認'
)
def handle(self, *args, **options):
event_code = options['event_code']
csv_file = options['csv_file']
dry_run = options['dry_run']
self.stdout.write(
self.style.SUCCESS(f'CSVインポート開始: event_code={event_code}, csv_file={csv_file}, dry_run={dry_run}')
)
# イベントの存在確認
try:
event = NewEvent2.objects.get(event_name=event_code)
self.stdout.write(f'イベント見つかりました: {event.event_name} (ID: {event.id})')
except NewEvent2.DoesNotExist:
raise CommandError(f'イベントが見つかりません: {event_code}')
# CSVファイルの存在確認
if not os.path.exists(csv_file):
raise CommandError(f'CSVファイルが見つかりません: {csv_file}')
# 統計情報
stats = {
'total_rows': 0,
'users_created': 0,
'users_existing': 0,
'teams_created': 0,
'members_created': 0,
'entries_created': 0,
'errors': []
}
try:
with open(csv_file, 'r', encoding='utf-8-sig') as f: # BOM対応のためutf-8-sigを使用
reader = csv.DictReader(f)
for row_num, row in enumerate(reader, start=2): # ヘッダー行の次から開始
stats['total_rows'] += 1
try:
if dry_run:
self.process_row_dry_run(row, event, stats, row_num)
else:
with transaction.atomic():
self.process_row(row, event, stats, row_num)
except Exception as e:
error_msg = f'{row_num}: {str(e)}'
stats['errors'].append(error_msg)
self.stdout.write(self.style.ERROR(error_msg))
continue
except Exception as e:
raise CommandError(f'CSVファイル読み込みエラー: {str(e)}')
# 結果レポート
self.print_stats(stats, dry_run)
# CSV出力
if not dry_run:
self.export_results_to_csv(event, options['csv_file'])
def export_results_to_csv(self, event, input_csv_file):
"""インポート結果をCSVファイルに出力"""
# 出力ファイル名を生成
input_dir = os.path.dirname(input_csv_file)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
output_file = os.path.join(input_dir, f'import_results_{event.event_code}_{timestamp}.csv')
try:
with open(output_file, 'w', newline='', encoding='utf-8-sig') as csvfile:
fieldnames = [
'チーム名', 'ゼッケン番号', 'カテゴリー', '時間',
'オーナーメール', 'リーダー', 'メンバー数', 'メンバー一覧',
'参加登録状況', 'エントリーID', '作成日時'
]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
# イベントのエントリーを取得
entries = Entry.objects.filter(event=event).select_related(
'team', 'category', 'owner'
).prefetch_related('team__members__user')
for entry in entries:
# メンバー一覧を作成
member_list = []
for member in entry.team.members.all():
member_info = f"{member.user.firstname}"
if member.user.date_of_birth:
member_info += f"({member.user.date_of_birth})"
member_list.append(member_info)
# リーダー情報を取得(チームオーナー)
leader_name = ""
if entry.team.owner:
leader_name = f"{entry.team.owner.firstname}"
if entry.team.owner.date_of_birth:
leader_name += f"({entry.team.owner.date_of_birth})"
writer.writerow({
'チーム名': entry.team.team_name,
'ゼッケン番号': entry.zekken_number,
'カテゴリー': entry.category.category_name if entry.category else '',
'時間': f"{entry.category.duration.total_seconds() // 3600}時間" if entry.category else '',
'オーナーメール': entry.owner.email,
'リーダー': leader_name,
'メンバー数': entry.team.members.count(),
'メンバー一覧': '; '.join(member_list),
'参加登録状況': '完了',
'エントリーID': entry.id,
'作成日時': entry.date.strftime('%Y-%m-%d %H:%M:%S')
})
self.stdout.write(
self.style.SUCCESS(f'インポート結果をCSVに出力しました: {output_file}')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'CSV出力エラー: {str(e)}')
)
def process_row(self, row, event, stats, row_num):
"""CSVの1行を処理実際にデータベースに書き込み"""
team_name = row.get('チーム名', '').strip()
self.stdout.write(f'{row_num} 処理中: チーム={team_name}')
# 2-1. カスタムユーザー登録
user = self.get_or_create_user(row, stats)
# 2-2. チーム登録
team = self.create_team(row, user, event, stats)
# メンバー登録最大7名
members = self.create_members(row, team, stats)
# 2-3. エントリー登録
entry = self.create_entry(row, team, event, stats)
self.stdout.write(self.style.SUCCESS(f'{row_num} 完了: チーム={team.team_name}'))
def process_row_dry_run(self, row, event, stats, row_num):
"""CSVの1行を処理ドライラン - データベースには書き込まない)"""
team_name = row.get('チーム名', '').strip()
self.stdout.write(f'[DRY RUN] 行 {row_num}: チーム={team_name}')
# ユーザーの存在確認のみ
email = row.get('メール', '').strip()
password = row.get('パスワード', '').strip()
if email:
existing_user = CustomUser.objects.filter(email=email).first()
if existing_user:
self.stdout.write(f' ユーザー既存: {email} パスワード:既存')
stats['users_existing'] += 1
else:
display_password = password if password else 'defaultpassword123'
self.stdout.write(f' ユーザー新規作成予定: {email} パスワード:{display_password}')
stats['users_created'] += 1
stats['teams_created'] += 1
stats['entries_created'] += 1
# エントリー情報の表示
category_name = row.get('部門', '').strip()
duration_str = row.get('時間', '').strip()
# 予想されるゼッケン番号を計算(ドライラン用)
max_zekken = Entry.objects.filter(event=event).aggregate(
max_zekken=models.Max('zekken_number')
)['max_zekken'] or 0
predicted_zekken = max_zekken + stats['entries_created']
self.stdout.write(f' エントリー: ゼッケン{predicted_zekken}, カテゴリー:{category_name}, 時間:{duration_str}時間')
# 参加登録状況の確認
existing_entry = Entry.objects.filter(team__team_name=team_name, event=event).first()
if existing_entry:
self.stdout.write(f' 参加登録: 済み既存エントリーID: {existing_entry.id}')
else:
self.stdout.write(f' 参加登録: 新規作成予定')
# メンバー数カウント
member_count = 0
member_names = []
for i in range(1, 8): # 最大7名
# 全角数字を使用
name_key = f'氏名{chr(0xFF10 + i)}' # 全角数字123...を生成
name = row.get(name_key, '').strip()
if name:
member_count += 1
birthday_key = f'誕生日{chr(0xFF10 + i)}'
birthday = row.get(birthday_key, '').strip()
member_names.append(f'{name}({birthday})')
stats['members_created'] += member_count
self.stdout.write(f' メンバー: {member_count}名 [{", ".join(member_names)}]')
def get_or_create_user(self, row, stats):
"""カスタムユーザーの取得または作成"""
email = row.get('メール', '').strip()
password = row.get('パスワード', '').strip()
phone = row.get('電話番号', '').strip()
if not email:
raise ValueError('メールアドレスが必要です')
# 特殊文字のクリーンアップ
email = email.replace('', '@')
phone = self.clean_phone_number(phone)
# 既存ユーザーの検索
user = CustomUser.objects.filter(email=email).first()
if user:
self.stdout.write(f' 既存ユーザー: {email}')
stats['users_existing'] += 1
return user
# 新規ユーザー作成
user = CustomUser.objects.create_user(
email=email,
password=password if password else 'defaultpassword123',
is_active=True
)
self.stdout.write(f' 新規ユーザー作成: {email}')
stats['users_created'] += 1
return user
def clean_phone_number(self, phone):
"""電話番号のクリーンアップ"""
if not phone:
return ''
# 全角文字を半角に変換
phone = phone.replace('', '-').replace('÷', '-')
# 余分な文字を削除
phone = re.sub(r'[^\d\-]', '', phone)
return phone
def create_team(self, row, user, event, stats):
"""チームの作成"""
team_name = row.get('チーム名', '').strip()
category_name = row.get('部門', '').strip()
if not team_name:
raise ValueError('チーム名が必要です')
# チームの重複チェック(同一ユーザー・同一チーム名)
existing_team = Team.objects.filter(team_name=team_name, owner=user).first()
if existing_team:
self.stdout.write(f' 既存チーム使用: {team_name}')
return existing_team
team = Team.objects.create(
team_name=team_name,
owner=user,
class_name=category_name,
event=event, # イベントを設定
created_at=timezone.now()
)
self.stdout.write(f' 新規チーム作成: {team_name}')
stats['teams_created'] += 1
return team
def create_members(self, row, team, stats):
"""メンバーの作成最大7名"""
members = []
for i in range(1, 8): # 最大7名
# 全角数字を使用
name_key = f'氏名{chr(0xFF10 + i)}'
birthday_key = f'誕生日{chr(0xFF10 + i)}'
name = row.get(name_key, '').strip()
birthday_str = row.get(birthday_key, '').strip()
if not name:
continue
# 誕生日の解析
birthday = self.parse_birthday(birthday_str)
# ダミーメールアドレス生成
dummy_email = f"{team.team_name.replace(' ', '_').replace('!', '').replace('?', '').lower()}_member_{i}@dummy.local"
# 既存のダミーユーザーチェック
existing_dummy = CustomUser.objects.filter(email=dummy_email).first()
if existing_dummy:
dummy_user = existing_dummy
else:
# ダミーユーザー作成
dummy_user = CustomUser.objects.create_user(
email=dummy_email,
password='dummypassword123',
firstname=name,
date_of_birth=birthday,
is_active=True
)
# 既存メンバーチェック
existing_member = Member.objects.filter(team=team, user=dummy_user).first()
if existing_member:
member = existing_member
else:
# メンバー作成1番目の人がリーダー
member = Member.objects.create(
team=team,
user=dummy_user,
firstname=name,
date_of_birth=birthday
)
# 1番目の人をチームのオーナーとして設定リーダーの概念
if i == 1 and team.owner != dummy_user:
team.owner = dummy_user
team.save()
stats['members_created'] += 1
members.append(member)
self.stdout.write(f' メンバー{i}作成: {name}')
return members
def parse_birthday(self, birthday_str):
"""誕生日文字列の解析"""
if not birthday_str or birthday_str in ['不明', '']:
return None
# 年齢のみの場合71歳
if '' in birthday_str:
try:
age = int(birthday_str.replace('', ''))
# 現在年から年齢を引いて生年を推定
birth_year = datetime.now().year - age
return datetime(birth_year, 1, 1).date()
except ValueError:
return None
# 日付形式の解析
try:
# YYYY/MM/DD形式
if '/' in birthday_str:
return datetime.strptime(birthday_str, '%Y/%m/%d').date()
# YYYYMMDD形式
elif len(birthday_str) == 8 and birthday_str.isdigit():
return datetime.strptime(birthday_str, '%Y%m%d').date()
# YYYY-MM-DD形式
elif '-' in birthday_str:
return datetime.strptime(birthday_str, '%Y-%m-%d').date()
except ValueError:
pass
self.stdout.write(self.style.WARNING(f' 誕生日形式エラー: {birthday_str}'))
return None
def create_entry(self, row, team, event, stats):
"""エントリーの作成"""
category_name = row.get('部門', '').strip()
duration_str = row.get('時間', '').strip()
# メンバー数を取得
member_count = team.members.count()
# カテゴリーの取得または作成
category = None
if category_name and duration_str:
# 既存のカテゴリーから最適なものを選択
duration_hours = int(duration_str) if duration_str.isdigit() else 3
# お試し部門の判定
is_trial = 'お試し' in category_name
if is_trial:
# お試しカテゴリーを検索(時間が一致するもの)
trial_categories = NewCategory.objects.filter(
trial=True,
duration__exact=timedelta(hours=duration_hours)
).order_by('-num_of_member') # メンバー数が多い順
# メンバー数に適したカテゴリーを選択お試しは1名でも可
for cat in trial_categories:
if cat.num_of_member >= member_count:
category = cat
break
if not category and trial_categories.exists():
# 適切なサイズがない場合は最大のお試しカテゴリーを使用
category = trial_categories.first()
else:
# 1. 完全一致するカテゴリーを探す
full_category_name = f"{category_name}-{duration_hours}時間"
category = NewCategory.objects.filter(category_name=full_category_name).first()
if not category:
# 2. 部分一致するカテゴリーを探す(時間が一致するもの)
categories = NewCategory.objects.filter(
category_name__icontains=category_name,
duration__exact=timedelta(hours=duration_hours),
trial=False # お試しではないもの
).order_by('-num_of_member') # メンバー数が多い順
# メンバー数に適したカテゴリーを選択
for cat in categories:
if cat.num_of_member >= member_count:
category = cat
break
if not category:
# 3. どれも見つからない場合は新規作成(メンバー数を考慮)
if is_trial:
# お試しの場合は1名から最大7名まで許可
max_members = max(7, member_count)
else:
# 通常カテゴリーは既存の制限に従う
max_members = max(7, member_count)
full_category_name = f"{category_name}-{duration_hours}時間"
category = NewCategory.objects.create(
category_name=full_category_name,
duration=timedelta(hours=duration_hours),
num_of_member=max_members,
trial=is_trial, # お試し判定を反映
family='ファミリー' in category_name, # ファミリー判定
category_number=0
)
trial_text = f", お試し={is_trial}" if is_trial else ""
self.stdout.write(f' 新規カテゴリー作成: {full_category_name} (最大{max_members}{trial_text})')
else:
trial_text = ", お試し" if category.trial else ""
self.stdout.write(f' 使用カテゴリー: {category.category_name} (最大{category.num_of_member}{trial_text})')
# 重複エントリーチェック
existing_entry = Entry.objects.filter(team=team, event=event).first()
if existing_entry:
self.stdout.write(f' 既存エントリー使用: {team.team_name}')
return existing_entry
# 最大ゼッケン番号を取得
max_zekken = Entry.objects.filter(event=event).aggregate(
max_zekken=models.Max('zekken_number')
)['max_zekken'] or 0
entry = Entry.objects.create(
team=team,
event=event,
category=category,
owner=team.owner,
zekken_number=max_zekken + 1,
date=timezone.now(),
is_active=True
)
self.stdout.write(f' エントリー作成: ゼッケン{entry.zekken_number}')
stats['entries_created'] += 1
return entry
# 重複エントリーチェック
existing_entry = Entry.objects.filter(team=team, event=event).first()
if existing_entry:
self.stdout.write(f' 既存エントリー使用: {team.team_name}')
return existing_entry
# 最大ゼッケン番号を取得
max_zekken = Entry.objects.filter(event=event).aggregate(
max_zekken=models.Max('zekken_number')
)['max_zekken'] or 0
entry = Entry.objects.create(
team=team,
event=event,
category=category,
owner=team.owner,
zekken_number=max_zekken + 1,
date=timezone.now(),
is_active=True
)
self.stdout.write(f' エントリー作成: ゼッケン{entry.zekken_number}')
stats['entries_created'] += 1
return entry
def print_stats(self, stats, dry_run):
"""統計情報の表示"""
mode = '[DRY RUN] ' if dry_run else ''
self.stdout.write(self.style.SUCCESS('\n' + '='*50))
self.stdout.write(self.style.SUCCESS(f'{mode}インポート結果'))
self.stdout.write(self.style.SUCCESS('='*50))
self.stdout.write(f'処理行数: {stats["total_rows"]}')
self.stdout.write(f'ユーザー新規作成: {stats["users_created"]}')
self.stdout.write(f'ユーザー既存利用: {stats["users_existing"]}')
self.stdout.write(f'チーム作成: {stats["teams_created"]}')
self.stdout.write(f'メンバー作成: {stats["members_created"]}')
self.stdout.write(f'エントリー作成: {stats["entries_created"]}')
if stats['errors']:
self.stdout.write(self.style.ERROR(f'\nエラー数: {len(stats["errors"])}'))
for error in stats['errors']:
self.stdout.write(self.style.ERROR(f' {error}'))
if dry_run:
self.stdout.write(self.style.WARNING('\n※ ドライランのため、実際のデータは作成されていません'))
else:
self.stdout.write(self.style.SUCCESS('\nインポート完了!'))

View File

@ -0,0 +1,638 @@
"""
Django管理コマンド: CSVファイルからチーム情報をデータベースに登録
使用方法:
docker-compose exec app python manage.py register_teams_from_csv --event_code TEST2025 --dry_run
"""
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.contrib.auth import get_user_model
from django.utils import timezone
from datetime import datetime, date, timedelta
import csv
import os
from rog.models import (
CustomUser, NewEvent2, NewCategory, Team, Member, Entry, EntryMember
)
User = get_user_model()
class Command(BaseCommand):
help = 'CSVファイルからチーム情報をデータベースに登録'
def add_arguments(self, parser):
parser.add_argument(
'--event_code',
type=str,
required=True,
help='イベントコード'
)
parser.add_argument(
'--csv_file',
type=str,
default='CPLIST/input/teams2025.csv',
help='CSVファイルパス (デフォルト: CPLIST/input/teams2025.csv)'
)
parser.add_argument(
'--dry_run',
action='store_true',
help='ドライランモード実際のDB更新を行わない'
)
def handle(self, *args, **options):
event_code = options['event_code']
csv_file = options['csv_file']
dry_run = options['dry_run']
# CSVファイルの存在確認
if not os.path.exists(csv_file):
raise CommandError(f'CSVファイルが見つかりません: {csv_file}')
if dry_run:
self.stdout.write(
self.style.WARNING('DRY RUN MODE: データベースの変更は行いません')
)
try:
processor = TeamRegistrationProcessor(
event_code=event_code,
dry_run=dry_run,
stdout=self.stdout,
style=self.style
)
processor.initialize()
processor.process_csv_file(csv_file)
processor.print_stats()
if dry_run:
self.stdout.write(
self.style.SUCCESS(f'DRY RUN 完了: イベント {event_code}')
)
else:
self.stdout.write(
self.style.SUCCESS(f'処理完了: イベント {event_code}')
)
except Exception as e:
raise CommandError(f'処理エラー: {str(e)}')
class TeamRegistrationProcessor:
def __init__(self, event_code, dry_run=False, stdout=None, style=None):
self.event_code = event_code
self.dry_run = dry_run
self.stdout = stdout
self.style = style
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 log(self, message, level='INFO'):
"""ログ出力"""
if self.stdout:
if level == 'ERROR':
self.stdout.write(self.style.ERROR(message))
elif level == 'WARNING':
self.stdout.write(self.style.WARNING(message))
elif level == 'SUCCESS':
self.stdout.write(self.style.SUCCESS(message))
else:
self.stdout.write(message)
else:
print(message)
def initialize(self):
"""イベントとカテゴリの初期化"""
if self.dry_run:
self.log("DRY RUN MODE: データベースの変更は行いません", 'WARNING')
try:
self.event = NewEvent2.objects.get(event_code=self.event_code)
self.log(f"イベント取得: {self.event.event_name} ({self.event_code})")
except NewEvent2.DoesNotExist:
if self.dry_run:
self.log(f"DRY RUN: Event with code '{self.event_code}' would be searched")
# ダミーイベントオブジェクトを作成
class DummyEvent:
def __init__(self, event_code):
self.event_name = f"Dummy Event for {event_code}"
self.event_code = event_code
self.event = DummyEvent(self.event_code)
return
else:
raise ValueError(f"Event with code '{self.event_code}' not found")
# カテゴリ情報をプリロード
if not self.dry_run:
for category in NewCategory.objects.all():
hours = int(category.duration.total_seconds() // 3600)
key = (category.category_name, hours)
self.categories[key] = category
else:
# DRY RUNの場合はダミーカテゴリを作成
dummy_categories = [
('一般', 3), ('一般', 5), ('ファミリー', 3), ('ファミリー', 5),
('男性ソロ', 3), ('男性ソロ', 5), ('女性ソロ', 3), ('女性ソロ', 5)
]
for cat_name, hours in dummy_categories:
class DummyCategory:
def __init__(self, name, hours):
self.category_name = name
self.category_number = len(self.categories) + 1
self.duration = timedelta(hours=hours)
self.num_of_member = 7
self.family = (name == 'ファミリー')
self.female = (name == '女性ソロ')
self.trial = False
self.categories[(cat_name, hours)] = DummyCategory(cat_name, hours)
self.log(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:
self.log(f"警告: 誕生日の形式が不正です: {birth_str}", 'WARNING')
except Exception as e:
self.log(f"警告: メンバー情報の解析エラー: {e}", 'WARNING')
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
# 新しいカテゴリを作成
self.log(f"新しいカテゴリを作成: {department} ({hours_int}時間)")
if self.dry_run:
self.log(f"DRY RUN: カテゴリ作成 - {department} ({hours_int}時間)")
# ダミーカテゴリオブジェクトを作成
class DummyCategory:
def __init__(self, processor):
self.category_name = department
self.category_number = len(processor.categories) + 1
self.duration = timedelta(hours=hours_int)
self.num_of_member = 7
self.family = (department == 'ファミリー')
self.female = (department == '女性ソロ')
self.trial = False
category = DummyCategory(self)
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:
self.log(f"DRY RUN: ユーザー処理 - {email}")
self.log(f" - チーム名: {team_name}")
self.log(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()
self.log(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()
self.log(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:
self.log(f"DRY RUN: ダミーユーザー作成 - {dummy_email}")
self.log(f" - 名前: {member_data['name']}")
self.log(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()
self.log(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:
self.log(f"DRY RUN: チーム作成 - {team_name}")
self.log(f" - ゼッケン番号: {zekken_number}")
self.log(f" - カテゴリ: {category.category_name if hasattr(category, 'category_name') else 'Unknown'}")
self.log(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
)
self.log(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']
)
self.log(f"チーム作成: {team_name}")
self.stats['teams_created'] += 1
return team
def process_members(self, data, team, users):
"""メンバー登録"""
if self.dry_run:
self.log(f"DRY RUN: メンバー登録 - {team.team_name}")
for user in users:
self.log(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 hasattr(user, 'email') and user.email.startswith('dummy_') else False
)
self.log(f"メンバー追加: {user.firstname} to {team.team_name}")
self.stats['members_created'] += 1
def process_entry(self, team, category):
"""エントリー登録2-3"""
if self.dry_run:
self.log(f"DRY RUN: エントリー作成 - {team.team_name}")
self.log(f" - カテゴリ: {category.category_name if hasattr(category, 'category_name') else 'Unknown'}")
self.log(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
)
self.log(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
)
self.log(f"エントリー作成: {team.team_name}")
self.stats['entries_created'] += 1
return entry
def process_participation(self, entry):
"""イベント参加2-4"""
if self.dry_run:
self.log(f"DRY RUN: 参加登録 - {entry.team.team_name}")
# ダミーメンバーリストを作成
for i in range(len(getattr(entry.team, 'dummy_members', [entry.team.owner]))):
self.log(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=getattr(member, 'is_temporary', False)
)
self.log(f"参加登録: {member.user.firstname}")
self.stats['participations_created'] += 1
# エントリーを有効化
entry.is_active = True
entry.save()
def process_csv_file(self, csv_file_path):
"""CSVファイルを処理"""
self.log(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)
self.log(f"CSVヘッダー: {header[:10]}...") # 最初の10列を表示
row_count = 0
for row in csv_reader:
row_count += 1
if not any(row): # 空行をスキップ
continue
try:
# DRY RUNの場合はトランザクションを使用しない
if self.dry_run:
self.log(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'])
# 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)
self.log(f"Row {row_count} 完了: {data['team_name']}")
else:
with transaction.atomic():
self.log(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'])
# 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)
self.log(f"Row {row_count} 完了: {data['team_name']}")
except Exception as e:
error_msg = f"Row {row_count} エラー: {str(e)}"
self.log(f"エラー: {error_msg}", 'ERROR')
self.stats['errors'].append(error_msg)
self.log(f"\nCSV処理完了: {row_count} 行処理")
def print_stats(self):
"""統計情報を表示"""
self.log("\n=== 処理結果統計 ===", 'SUCCESS')
self.log(f"作成されたユーザー: {self.stats['users_created']}")
self.log(f"更新されたユーザー: {self.stats['users_updated']}")
self.log(f"作成されたチーム: {self.stats['teams_created']}")
self.log(f"作成されたメンバー: {self.stats['members_created']}")
self.log(f"作成されたエントリー: {self.stats['entries_created']}")
self.log(f"作成された参加登録: {self.stats['participations_created']}")
self.log(f"エラー数: {len(self.stats['errors'])}")
if self.stats['errors']:
self.log("\n=== エラー詳細 ===", 'WARNING')
for error in self.stats['errors']:
self.log(f"- {error}", 'ERROR')

View File

@ -0,0 +1,84 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from rog.models import GpsLog
class Command(BaseCommand):
help = '失敗した外部システム登録をリトライします'
def add_arguments(self, parser):
parser.add_argument(
'--max-attempts',
type=int,
default=3,
help='最大試行回数 (デフォルト: 3)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='実際の処理を実行せず、処理予定の記録のみを表示'
)
def handle(self, *args, **options):
max_attempts = options['max_attempts']
dry_run = options['dry_run']
self.stdout.write(
self.style.SUCCESS(f'外部システム登録リトライ処理を開始します (最大試行回数: {max_attempts})')
)
# 保留中の外部システム登録を取得
pending_records = GpsLog.get_pending_external_registrations().filter(
external_registration_attempts__lt=max_attempts
)
if not pending_records.exists():
self.stdout.write(
self.style.WARNING('リトライが必要な記録が見つかりませんでした。')
)
return
self.stdout.write(f'リトライ対象の記録数: {pending_records.count()}')
if dry_run:
self.stdout.write(self.style.WARNING('-- DRY RUN モード --'))
for record in pending_records:
self.stdout.write(
f' ゼッケン番号: {record.zekken_number}, '
f'イベント: {record.event_code}, '
f'チーム名: {record.team_name}, '
f'試行回数: {record.external_registration_attempts}'
)
return
# 実際のリトライ処理
result = GpsLog.retry_failed_external_registrations(max_attempts)
self.stdout.write(
self.style.SUCCESS(
f'リトライ完了: 成功 {result["success_count"]}件, '
f'失敗 {result["failed_count"]}件, '
f'合計処理数 {result["total_processed"]}'
)
)
# 最大試行回数に達した記録があるかチェック
max_attempts_reached = GpsLog.get_pending_external_registrations().filter(
external_registration_attempts__gte=max_attempts
)
if max_attempts_reached.exists():
self.stdout.write(
self.style.ERROR(
f'警告: {max_attempts_reached.count()}件の記録が最大試行回数に達しました。'
' 手動での確認が必要です。'
)
)
# 詳細を表示
for record in max_attempts_reached[:5]: # 最初の5件のみ表示
self.stdout.write(
f' ゼッケン番号: {record.zekken_number}, '
f'イベント: {record.event_code}, '
f'エラー: {record.external_registration_error}'
)

View File

@ -0,0 +1,283 @@
"""
CSVファイルからチーム情報を読み込んでメール送信するDjango管理コマンド
Outlook SMTP (rogaining@gifuai.net) での送信に対応
Usage:
# ドライラン(実際には送信しない)
docker compose exec app python manage.py send_team_emails --csv_file="CPLIST/input/team_mail.csv" --dry_run
# 実際のメール送信送信間隔1秒
docker compose exec app python manage.py send_team_emails --csv_file="CPLIST/input/team_mail.csv"
# カスタム送信間隔3秒間隔
docker compose exec app python manage.py send_team_emails --csv_file="CPLIST/input/team_mail.csv" --delay=3
Author: システム開発チーム
Date: 2025-09-05
"""
import csv
import os
import time
from django.core.management.base import BaseCommand, CommandError
from django.core.mail import send_mail
from django.conf import settings
from django.template.loader import render_to_string
from datetime import datetime
class Command(BaseCommand):
help = 'CSVファイルからチーム情報を読み込んでOutlook SMTPでメール送信'
def add_arguments(self, parser):
parser.add_argument(
'--csv_file',
type=str,
required=True,
help='CSVファイルのパス (例: CPLIST/input/team_mail.csv)'
)
parser.add_argument(
'--dry_run',
action='store_true',
help='ドライラン(実際にはメール送信しない)'
)
parser.add_argument(
'--delay',
type=int,
default=1,
help='メール送信間隔(秒)デフォルト: 1秒'
)
parser.add_argument(
'--test_email',
type=str,
help='テスト用メールアドレス(指定した場合、全てのメールをこのアドレスに送信)'
)
def handle(self, *args, **options):
csv_file = options['csv_file']
dry_run = options['dry_run']
delay = options['delay']
test_email = options.get('test_email')
mode = '[DRY RUN] ' if dry_run else ''
self.stdout.write(f'{mode}CSVメール送信開始: csv_file={csv_file}')
if test_email:
self.stdout.write(f'🧪 テストモード: 全メールを {test_email} に送信')
# CSVファイル存在確認
if not os.path.exists(csv_file):
raise CommandError(f'CSVファイルが見つかりません: {csv_file}')
# SMTP設定確認
self.verify_email_settings()
# 統計情報
stats = {
'total_rows': 0,
'emails_sent': 0,
'errors': []
}
# CSVファイル読み込み・メール送信
try:
with open(csv_file, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
for row_num, row in enumerate(reader, start=2):
stats['total_rows'] += 1
try:
# CSVデータ取得
team_data = self.extract_team_data(row)
# メール内容生成
email_content = self.generate_email_content(team_data)
subject = email_content['subject']
body = email_content['body']
# 送信先決定テストモードならtest_emailに
recipient = test_email if test_email else team_data['email']
self.stdout.write(f'{mode}{row_num}: {team_data["team_name"]}{recipient}')
if dry_run:
self.show_email_preview(subject, body)
else:
# 実際のメール送信
self.send_email(subject, body, recipient, team_data)
self.stdout.write(f' ✅ メール送信完了')
# 送信間隔
if delay > 0:
time.sleep(delay)
stats['emails_sent'] += 1
except Exception as e:
error_msg = f'{row_num}: {str(e)}'
stats['errors'].append(error_msg)
self.stdout.write(self.style.ERROR(error_msg))
continue
except Exception as e:
raise CommandError(f'CSVファイル読み込みエラー: {str(e)}')
# 結果レポート
self.print_stats(stats, dry_run)
def verify_email_settings(self):
"""SMTP設定確認"""
required_settings = [
'EMAIL_HOST', 'EMAIL_PORT', 'EMAIL_HOST_USER',
'EMAIL_HOST_PASSWORD', 'DEFAULT_FROM_EMAIL'
]
for setting in required_settings:
if not hasattr(settings, setting) or not getattr(settings, setting):
raise CommandError(f'メール設定が不完全です: {setting}')
self.stdout.write(f'📧 SMTP設定確認完了: {settings.EMAIL_HOST}:{settings.EMAIL_PORT}')
self.stdout.write(f'📧 送信者: {settings.DEFAULT_FROM_EMAIL}')
def extract_team_data(self, row):
"""CSVデータからチーム情報を抽出"""
team_data = {
'team_name': row.get('チーム名', '').strip(),
'email': row.get('メール', '').strip(),
'password': row.get('password', '').strip(),
'category': row.get('部門', '').strip(),
'duration': row.get('時間', '').strip(),
'leader_name': row.get('氏名1', '').strip(),
'phone_number': row.get('電話番号', '').strip(),
}
# 必須項目チェック
if not team_data['team_name']:
raise ValueError('チーム名が必要です')
if not team_data['email']:
raise ValueError('メールアドレスが必要です')
return team_data
def generate_email_content(self, team_data):
"""メール内容生成(外部テンプレートファイル使用)"""
# テンプレートファイルから件名を読み込み
subject = render_to_string(
'emails/team_registration_subject.txt',
team_data
).strip()
# テンプレートファイルから本文を読み込み
body = render_to_string(
'emails/team_registration_body.txt',
team_data
).strip()
return {
'subject': subject,
'body': body
}
def show_email_preview(self, subject, body):
"""メールプレビュー表示(ドライラン用)"""
self.stdout.write(f' 件名: {subject}')
self.stdout.write(f' 本文プレビュー: {body[:100]}...')
def send_email(self, subject, body, recipient, team_data):
"""実際のメール送信"""
try:
send_mail(
subject=subject,
message=body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[recipient],
fail_silently=False
)
except Exception as e:
raise ValueError(f'メール送信エラー ({recipient}): {str(e)}')
def print_stats(self, stats, dry_run):
"""統計情報の表示"""
mode = '[DRY RUN] ' if dry_run else ''
self.stdout.write(self.style.SUCCESS('\n' + '='*50))
self.stdout.write(self.style.SUCCESS(f'{mode}メール送信結果'))
self.stdout.write(self.style.SUCCESS('='*50))
self.stdout.write(f'処理行数: {stats["total_rows"]}')
self.stdout.write(f'メール送信数: {stats["emails_sent"]}')
if stats['errors']:
self.stdout.write(f'エラー数: {len(stats["errors"])}')
for error in stats['errors']:
self.stdout.write(f' {error}')
if not dry_run:
self.stdout.write(self.style.SUCCESS('\nメール送信完了!'))
else:
self.stdout.write(self.style.WARNING('\n※ ドライランのため、実際のメール送信は行われていません'))
def send_email_to_team(self, row, template_content, template_name, row_num, stats):
"""実際のメール送信"""
team_name = row.get('チーム名', '').strip()
email = row.get('メール', '').strip()
category = row.get('部門', '').strip()
duration = row.get('時間', '').strip()
leader_name = row.get('氏名1', '').strip()
phone = row.get('電話番号', '').strip()
if not email:
raise ValueError('メールアドレスが必要です')
if not team_name:
raise ValueError('チーム名が必要です')
# テンプレート処理
context_data = {
'event_name': '岐阜ロゲイニング2025',
'team_name': team_name,
'category': category,
'duration': duration,
'leader_name': leader_name,
'email': email,
'phone': phone,
'password': row.get('パスワード', '').strip()
}
# テンプレートファイルから件名・本文を生成
subject = render_to_string('emails/team_registration_subject.txt', context_data).strip()
body = render_to_string('emails/team_registration_body.txt', context_data).strip()
# メール送信
try:
send_mail(
subject=subject,
message=body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[email],
fail_silently=False,
)
self.stdout.write(f'{row_num}: メール送信完了 {team_name} ({email})')
stats['emails_sent'] += 1
except Exception as e:
raise ValueError(f'メール送信エラー: {str(e)}')
def print_stats(self, stats, dry_run):
"""統計情報の表示"""
mode = '[DRY RUN] ' if dry_run else ''
self.stdout.write(self.style.SUCCESS('\n' + '='*50))
self.stdout.write(self.style.SUCCESS(f'{mode}メール送信結果'))
self.stdout.write(self.style.SUCCESS('='*50))
self.stdout.write(f'処理行数: {stats["total_rows"]}')
self.stdout.write(f'メール送信数: {stats["emails_sent"]}')
if stats['errors']:
self.stdout.write(f'\nエラー数: {len(stats["errors"])}')
for error in stats['errors']:
self.stdout.write(f' {error}')
if dry_run:
self.stdout.write(self.style.WARNING('\n※ ドライランのため、実際にはメールは送信されていません'))
else:
self.stdout.write(self.style.SUCCESS('\nメール送信完了!'))

View File

@ -0,0 +1,22 @@
# Generated manually to add status field to NewEvent2
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rog', '0007_auto_20250829_1836'),
]
operations = [
migrations.AddField(
model_name='newevent2',
name='status',
field=models.CharField(
choices=[('public', 'Public'), ('private', 'Private'), ('draft', 'Draft'), ('closed', 'Closed')],
default='draft',
help_text='イベントステータス',
max_length=20
),
),
]

View File

@ -0,0 +1,34 @@
# Generated manually to add missing timestamp fields to gpscheckin
from django.db import migrations, models
from django.utils import timezone
class Migration(migrations.Migration):
dependencies = [
('rog', '0008_add_status_field'),
]
operations = [
migrations.AddField(
model_name='gpscheckin',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='gpscheckin',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='location2025',
name='sub_loc_id',
field=models.CharField(blank=True, max_length=2048, null=True, verbose_name='サブロケーションID'),
),
migrations.AddField(
model_name='location2025',
name='subcategory',
field=models.CharField(blank=True, max_length=2048, null=True, verbose_name='サブカテゴリ'),
),
]

View File

@ -0,0 +1,43 @@
# Generated manually on 2025-08-30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rog', '0009_add_fields_to_models'),
]
operations = [
migrations.AddField(
model_name='location2025',
name='photos',
field=models.CharField(max_length=2048, blank=True, null=True, verbose_name='写真'),
),
migrations.AddField(
model_name='location2025',
name='videos',
field=models.CharField(max_length=2048, blank=True, null=True, verbose_name='動画'),
),
migrations.AddField(
model_name='location2025',
name='remark',
field=models.TextField(blank=True, null=True, verbose_name='備考'),
),
migrations.AddField(
model_name='location2025',
name='tags',
field=models.CharField(max_length=2048, blank=True, null=True, verbose_name='タグ'),
),
migrations.AddField(
model_name='location2025',
name='evaluation_value',
field=models.CharField(max_length=255, blank=True, null=True, verbose_name='評価値'),
),
migrations.AddField(
model_name='location2025',
name='hidden_location',
field=models.BooleanField(default=False, verbose_name='隠しロケーション'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.9 on 2025-08-29 19:26
import django.contrib.gis.db.models.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rog', '0010_add_missing_fields_to_location2025'),
]
operations = [
# Location2025 field changes only - removing non-existent GpsCheckin operations
migrations.RemoveField(
model_name='location2025',
name='cp_point',
),
migrations.RemoveField(
model_name='location2025',
name='photo_point',
),
migrations.AddField(
model_name='location2025',
name='checkin_point',
field=models.IntegerField(default=10, verbose_name='チェックイン得点'),
),
]

View File

@ -0,0 +1,184 @@
# Generated by Django 3.2.9 on 2025-08-29 19:26
import django.contrib.gis.db.models.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rog', '0010_add_missing_fields_to_location2025'),
]
operations = [
migrations.AlterModelOptions(
name='gpscheckin',
options={'managed': True},
),
migrations.RemoveIndex(
model_name='gpscheckin',
name='idx_create_at',
),
migrations.RemoveIndex(
model_name='gpscheckin',
name='idx_zekken_event',
),
migrations.RemoveField(
model_name='gpscheckin',
name='buy_flag',
),
migrations.RemoveField(
model_name='gpscheckin',
name='checkpoint',
),
migrations.RemoveField(
model_name='gpscheckin',
name='colabo_company_memo',
),
migrations.RemoveField(
model_name='gpscheckin',
name='create_at',
),
migrations.RemoveField(
model_name='gpscheckin',
name='create_user',
),
migrations.RemoveField(
model_name='gpscheckin',
name='goal_time',
),
migrations.RemoveField(
model_name='gpscheckin',
name='image_qr',
),
migrations.RemoveField(
model_name='gpscheckin',
name='image_receipt',
),
migrations.RemoveField(
model_name='gpscheckin',
name='late_point',
),
migrations.RemoveField(
model_name='gpscheckin',
name='minus_photo_flag',
),
migrations.RemoveField(
model_name='gpscheckin',
name='points',
),
migrations.RemoveField(
model_name='gpscheckin',
name='team',
),
migrations.RemoveField(
model_name='gpscheckin',
name='team_name',
),
migrations.RemoveField(
model_name='gpscheckin',
name='update_at',
),
migrations.RemoveField(
model_name='gpscheckin',
name='update_user',
),
migrations.RemoveField(
model_name='gpscheckin',
name='validate_location',
),
migrations.RemoveField(
model_name='gpscheckin',
name='validated_at',
),
migrations.RemoveField(
model_name='gpscheckin',
name='validated_by',
),
migrations.RemoveField(
model_name='gpscheckin',
name='validation_comment',
),
migrations.RemoveField(
model_name='gpscheckin',
name='validation_status',
),
migrations.RemoveField(
model_name='location2025',
name='cp_point',
),
migrations.RemoveField(
model_name='location2025',
name='photo_point',
),
migrations.AddField(
model_name='gpscheckin',
name='checkpoint_id',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='gpscheckin',
name='team_id',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='location2025',
name='checkin_point',
field=models.IntegerField(default=10, verbose_name='チェックイン得点'),
),
migrations.AlterField(
model_name='gpscheckin',
name='checkin_time',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='cp_number',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='event_code',
field=models.CharField(default='', max_length=255),
),
migrations.AlterField(
model_name='gpscheckin',
name='event_id',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='lat',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='lng',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='location',
field=django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326),
),
migrations.AlterField(
model_name='gpscheckin',
name='mobserver_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='record_time',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='serial_number',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AlterField(
model_name='gpscheckin',
name='zekken',
field=models.CharField(blank=True, max_length=20, null=True),
),
]

View File

@ -0,0 +1,43 @@
# Generated manually on 2025-08-31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rog', '0011_auto_20250830_0426'),
]
operations = [
migrations.AddField(
model_name='location2025',
name='area',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='地域'),
),
migrations.AddField(
model_name='location2025',
name='category',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='カテゴリ'),
),
migrations.AddField(
model_name='location2025',
name='city',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='市区町村'),
),
migrations.AddField(
model_name='location2025',
name='facility',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='設備'),
),
migrations.AddField(
model_name='location2025',
name='prefecture',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='都道府県'),
),
migrations.AddField(
model_name='location2025',
name='zip_code',
field=models.CharField(blank=True, max_length=12, null=True, verbose_name='郵便番号'),
),
]

View File

@ -0,0 +1,49 @@
# Generated manually on 2025-09-04 for competition status management
# サーバーAPI変更要求書20250904.md対応
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rog', '0012_location2025_add_missing_fields'),
]
operations = [
migrations.AddField(
model_name='entry',
name='is_in_rog',
field=models.BooleanField(default=False, help_text='ロゲイニング中=スタートしたらTrue'),
),
migrations.AddField(
model_name='entry',
name='rogaining_counted',
field=models.BooleanField(default=False, help_text='ロゲイニングチェックイン履歴あり=一度でもスタート・ゴール以外でチェックインしたらTrue'),
),
migrations.AddField(
model_name='entry',
name='ready_for_goal',
field=models.BooleanField(default=False, help_text='ゴール準備完了=スタートから遠くに移動した際にTrueになる'),
),
migrations.AddField(
model_name='entry',
name='is_at_goal',
field=models.BooleanField(default=False, help_text='ゴール状態=ゴールしたらTrue'),
),
migrations.AddField(
model_name='entry',
name='start_time',
field=models.DateTimeField(null=True, blank=True, help_text='スタート時刻'),
),
migrations.AddField(
model_name='entry',
name='goal_time',
field=models.DateTimeField(null=True, blank=True, help_text='ゴール時刻'),
),
migrations.AddField(
model_name='entry',
name='last_checkin_time',
field=models.DateTimeField(null=True, blank=True, help_text='最後のチェックイン時刻'),
),
]

View File

@ -0,0 +1,58 @@
# Generated manually on 2025-09-06 00:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rog', '0013_add_competition_status_fields'),
]
operations = [
migrations.AddField(
model_name='gpslog',
name='external_registration_status',
field=models.CharField(
choices=[
('pending', 'Pending'),
('success', 'Success'),
('failed', 'Failed'),
('retry', 'Retry Required')
],
default='pending',
help_text='外部システム登録状況',
max_length=20
),
),
migrations.AddField(
model_name='gpslog',
name='external_registration_attempts',
field=models.IntegerField(default=0, help_text='外部システム登録試行回数'),
),
migrations.AddField(
model_name='gpslog',
name='external_registration_error',
field=models.TextField(blank=True, help_text='外部システム登録エラー詳細', null=True),
),
migrations.AddField(
model_name='gpslog',
name='last_external_registration_attempt',
field=models.DateTimeField(blank=True, help_text='最後の外部システム登録試行時刻', null=True),
),
migrations.AddField(
model_name='gpslog',
name='team_name',
field=models.CharField(blank=True, help_text='チーム名(外部システム登録用)', max_length=255, null=True),
),
migrations.AddField(
model_name='gpslog',
name='category_name',
field=models.CharField(blank=True, help_text='カテゴリ名(外部システム登録用)', max_length=255, null=True),
),
migrations.AddField(
model_name='gpslog',
name='user_password_hash',
field=models.TextField(blank=True, help_text='ユーザーパスワードハッシュ(外部システム登録用)', null=True),
),
]

View File

View File

@ -9,6 +9,7 @@ from django.contrib.gis.db import models
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
from django.conf import settings
try: try:
from django.db.models import JSONField from django.db.models import JSONField
except ImportError: except ImportError:
@ -750,6 +751,15 @@ class Entry(models.Model):
staff_privileges = models.BooleanField(default=False, help_text="スタッフ権限フラグ") staff_privileges = models.BooleanField(default=False, help_text="スタッフ権限フラグ")
can_access_private_events = models.BooleanField(default=False, help_text="非公開イベント参加権限") can_access_private_events = models.BooleanField(default=False, help_text="非公開イベント参加権限")
# API変更要求書対応: 競技状態管理 (2025-09-04)
is_in_rog = models.BooleanField(default=False, help_text='ロゲイニング中=スタートしたらTrue')
rogaining_counted = models.BooleanField(default=False, help_text='ロゲイニングチェックイン履歴あり=一度でもスタート・ゴール以外でチェックインしたらTrue')
ready_for_goal = models.BooleanField(default=False, help_text='ゴール準備完了=スタートから遠くに移動した際にTrueになる')
is_at_goal = models.BooleanField(default=False, help_text='ゴール状態=ゴールしたらTrue')
start_time = models.DateTimeField(null=True, blank=True, help_text='スタート時刻')
goal_time = models.DateTimeField(null=True, blank=True, help_text='ゴール時刻')
last_checkin_time = models.DateTimeField(null=True, blank=True, help_text='最後のチェックイン時刻')
VALIDATION_STATUS_CHOICES = [ VALIDATION_STATUS_CHOICES = [
('approved', 'Approved'), ('approved', 'Approved'),
('pending', 'Pending'), ('pending', 'Pending'),
@ -1091,6 +1101,9 @@ class Location2025(models.Model):
cp_number = models.IntegerField(_('CP番号'), db_index=True) cp_number = models.IntegerField(_('CP番号'), db_index=True)
event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE, verbose_name=_('イベント')) event = models.ForeignKey('NewEvent2', on_delete=models.CASCADE, verbose_name=_('イベント'))
cp_name = models.CharField(_('CP名'), max_length=255) cp_name = models.CharField(_('CP名'), max_length=255)
category = models.CharField(_('カテゴリ'), max_length=255, blank=True, null=True)
sub_loc_id = models.CharField(_('サブロケーションID'), max_length=2048, blank=True, null=True)
subcategory = models.CharField(_('サブカテゴリ'), max_length=2048, blank=True, null=True)
# 位置情報 # 位置情報
latitude = models.FloatField(_('緯度'), null=True, blank=True) latitude = models.FloatField(_('緯度'), null=True, blank=True)
@ -1098,8 +1111,7 @@ class Location2025(models.Model):
location = models.PointField(_('位置'), srid=4326, null=True, blank=True) location = models.PointField(_('位置'), srid=4326, null=True, blank=True)
# ポイント情報 # ポイント情報
cp_point = models.IntegerField(_('チェックイン得点'), default=10) checkin_point = models.IntegerField(_('チェックイン得点'), default=10)
photo_point = models.IntegerField(_('写真ポイント'), default=0)
buy_point = models.IntegerField(_('買い物ポイント'), default=0) buy_point = models.IntegerField(_('買い物ポイント'), default=0)
# チェックイン設定 # チェックイン設定
@ -1113,10 +1125,23 @@ class Location2025(models.Model):
# 詳細情報 # 詳細情報
address = models.CharField(_('住所'), max_length=512, blank=True, null=True) address = models.CharField(_('住所'), max_length=512, blank=True, null=True)
zip_code = models.CharField(_('郵便番号'), max_length=12, blank=True, null=True)
prefecture = models.CharField(_('都道府県'), max_length=255, blank=True, null=True)
area = models.CharField(_('地域'), max_length=255, blank=True, null=True)
city = models.CharField(_('市区町村'), max_length=255, blank=True, null=True)
phone = models.CharField(_('電話番号'), max_length=32, blank=True, null=True) phone = models.CharField(_('電話番号'), max_length=32, blank=True, null=True)
website = models.URLField(_('ウェブサイト'), blank=True, null=True) website = models.URLField(_('ウェブサイト'), blank=True, null=True)
facility = models.CharField(_('設備'), max_length=255, blank=True, null=True)
description = models.TextField(_('説明'), blank=True, null=True) description = models.TextField(_('説明'), blank=True, null=True)
# 追加フィールドLocationテーブルから移行
photos = models.CharField(_('写真'), max_length=2048, blank=True, null=True)
videos = models.CharField(_('動画'), max_length=2048, blank=True, null=True)
remark = models.TextField(_('備考'), blank=True, null=True)
tags = models.CharField(_('タグ'), max_length=2048, blank=True, null=True)
evaluation_value = models.CharField(_('評価値'), max_length=255, blank=True, null=True)
hidden_location = models.BooleanField(_('隠しロケーション'), default=False)
# 管理情報 # 管理情報
is_active = models.BooleanField(_('有効'), default=True, db_index=True) is_active = models.BooleanField(_('有効'), default=True, db_index=True)
sort_order = models.IntegerField(_('表示順'), default=0) sort_order = models.IntegerField(_('表示順'), default=0)
@ -1161,19 +1186,21 @@ class Location2025(models.Model):
@property @property
def total_point(self): def total_point(self):
"""総得点を計算""" """総得点を計算"""
return self.cp_point + self.photo_point + self.buy_point return self.checkin_point + self.buy_point
@classmethod @classmethod
def import_from_csv(cls, csv_file, event, user=None): def import_from_csv(cls, csv_file, event, user=None):
""" """
CSVファイルからチェックポイントデータをインポート CSVファイルからチェックポイントデータをインポート(全フィールド対応)
CSV形式: CSV形式:
cp_number,cp_name,latitude,longitude,cp_point,photo_point,buy_point,address,phone,description cp_number,cp_name,latitude,longitude,checkin_point,buy_point,address,phone,description,
sub_loc_id,subcategory,photos,videos,tags,evaluation_value,remark,hidden_location
""" """
import csv import csv
import io import io
from django.utils import timezone from django.utils import timezone
from django.contrib.gis.geos import Point
if isinstance(csv_file, str): if isinstance(csv_file, str):
# ファイルパスの場合 # ファイルパスの場合
@ -1190,21 +1217,89 @@ class Location2025(models.Model):
for row_num, row in enumerate(csv_reader, start=2): for row_num, row in enumerate(csv_reader, start=2):
try: try:
cp_number = int(row.get('cp_number', 0)) # cp列から番号を抽出 (例: "#1(5)" -> 1, "-2" -> -2)
if cp_number <= 0: cp_raw = row.get('cp', row.get('cp_number', ''))
errors.append(f"{row_num}: CP番号が無効です") cp_number = None
if cp_raw:
import re
# #で始まる場合は#の後の数字を抽出 (例: "#1(5)" -> "1")
if cp_raw.startswith('#'):
match = re.search(r'#(-?\d+)', str(cp_raw))
if match:
cp_number = int(match.group(1))
else:
# 直接数値の場合
try:
cp_number = int(cp_raw)
except ValueError:
pass
if cp_number is None:
errors.append(f"{row_num}: CP番号が無効です (値: {cp_raw})")
continue continue
# 緯度経度から位置情報を作成
latitude = float(row['latitude']) if row.get('latitude') else None
longitude = float(row['longitude']) if row.get('longitude') else None
location = None
if latitude and longitude:
location = Point(longitude, latitude)
# hidden_locationのブール値変換
hidden_location = False
if row.get('hidden_location'):
hidden_str = row.get('hidden_location', '').lower()
hidden_location = hidden_str in ['true', '1', 'yes', 'on']
# checkin_radiusの処理
checkin_radius = None
if row.get('checkin_radius'):
radius_str = row.get('checkin_radius', '').strip()
if radius_str and radius_str.lower() not in ['false', '', 'null']:
try:
checkin_radius = float(radius_str)
except ValueError:
# デフォルト値を設定
checkin_radius = -1.0 # 要タップ
else:
checkin_radius = -1.0 # CSVでFALSEや空の場合は要タップ
else:
checkin_radius = -1.0 # デフォルトは要タップ
defaults = { defaults = {
'cp_name': row.get('cp_name', f'CP{cp_number}'), # 基本フィールド
'latitude': float(row['latitude']) if row.get('latitude') else None, 'cp_name': row.get('loc_name', row.get('cp_name', f'CP{cp_number}')),
'longitude': float(row['longitude']) if row.get('longitude') else None, 'latitude': latitude,
'cp_point': int(row.get('cp_point', 10)), 'longitude': longitude,
'photo_point': int(row.get('photo_point', 0)), 'location': location,
'checkin_point': int(row.get('checkin_point', row.get('cp_point', 10))), # 後方互換性のためcp_pointもサポート
'buy_point': int(row.get('buy_point', 0)), 'buy_point': int(row.get('buy_point', 0)),
'address': row.get('address', ''), 'address': row.get('address', ''),
'phone': row.get('phone', ''), 'phone': row.get('phone', ''),
'description': row.get('description', ''), 'description': row.get('description', row.get('remark', '')),
'checkin_radius': checkin_radius, # チェックイン範囲を追加
# 新しいフィールド
'category': row.get('category', ''),
'sub_loc_id': row.get('sub_loc_id', ''),
'subcategory': row.get('subcategory', ''),
'photos': row.get('photos', ''),
'videos': row.get('videos', ''),
'tags': row.get('tags', ''),
'evaluation_value': row.get('evaluation_value', ''),
'remark': row.get('remark', ''),
'hidden_location': hidden_location,
# 追加フィールド
'area': row.get('area', ''),
'zip_code': row.get('zip', ''),
'prefecture': row.get('prefecture', ''),
'city': row.get('city', ''),
'website': row.get('webcontent', ''),
'facility': row.get('facility', ''),
# 管理フィールド
'csv_source_file': getattr(csv_file, 'name', 'uploaded_file.csv'), 'csv_source_file': getattr(csv_file, 'name', 'uploaded_file.csv'),
'csv_upload_date': timezone.now(), 'csv_upload_date': timezone.now(),
'csv_upload_user': user, 'csv_upload_user': user,
@ -1913,6 +2008,27 @@ class GpsLog(models.Model):
score = models.IntegerField(default=0, null=True, blank=True) score = models.IntegerField(default=0, null=True, blank=True)
scoreboard_url = models.URLField(blank=True, null=True) scoreboard_url = models.URLField(blank=True, null=True)
# 外部システム登録管理用フィールド
external_registration_status = models.CharField(
max_length=20,
choices=[
('pending', 'Pending'),
('success', 'Success'),
('failed', 'Failed'),
('retry', 'Retry Required')
],
default='pending',
help_text="外部システム登録状況"
)
external_registration_attempts = models.IntegerField(default=0, help_text="外部システム登録試行回数")
external_registration_error = models.TextField(null=True, blank=True, help_text="外部システム登録エラー詳細")
last_external_registration_attempt = models.DateTimeField(null=True, blank=True, help_text="最後の外部システム登録試行時刻")
# エントリー関連情報(外部システム登録用)
team_name = models.CharField(max_length=255, null=True, blank=True, help_text="チーム名(外部システム登録用)")
category_name = models.CharField(max_length=255, null=True, blank=True, help_text="カテゴリ名(外部システム登録用)")
user_password_hash = models.TextField(null=True, blank=True, help_text="ユーザーパスワードハッシュ(外部システム登録用)")
class Meta: class Meta:
db_table = 'gps_information' db_table = 'gps_information'
# 複合主キーの設定 # 複合主キーの設定
@ -1935,7 +2051,6 @@ class GpsLog(models.Model):
""" """
return cls.objects.create( return cls.objects.create(
serial_number=0, # スタートログを表す特別な値 serial_number=0, # スタートログを表す特別な値
entry=entry,
zekken_number=entry.zekken_number, zekken_number=entry.zekken_number,
event_code=entry.event.event_name, event_code=entry.event.event_name,
cp_number="START", cp_number="START",
@ -1991,6 +2106,102 @@ class GpsLog(models.Model):
return self.create_at return self.create_at
return None return None
@classmethod
def record_external_registration_request(cls, entry):
"""
外部システム登録要求を記録する
Entry作成時に外部システム登録が失敗した場合の情報を保存
"""
return cls.objects.create(
serial_number=-1, # 外部システム登録要求を表す特別な値
zekken_number=str(entry.zekken_number),
event_code=entry.event.event_name,
cp_number="EXTERNAL_REG",
team_name=entry.team.team_name,
category_name=entry.category.category_name,
user_password_hash=entry.team.owner.password,
external_registration_status='pending',
external_registration_attempts=0,
create_at=timezone.now(),
update_at=timezone.now(),
buy_flag=False,
colabo_company_memo="Entry registration - external system pending"
)
def update_external_registration_status(self, status, error_message=None):
"""
外部システム登録状況を更新する
"""
self.external_registration_status = status
self.external_registration_attempts += 1
self.last_external_registration_attempt = timezone.now()
if error_message:
self.external_registration_error = error_message
self.update_at = timezone.now()
self.save()
def retry_external_registration(self):
"""
外部システム登録をリトライする
"""
import requests
api_url = f"{settings.FRONTEND_URL}/gifuroge/register_team"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"zekken_number": self.zekken_number,
"event_code": self.event_code,
"team_name": self.team_name,
"class_name": self.category_name,
"password": self.user_password_hash
}
try:
response = requests.post(api_url, headers=headers, data=data, timeout=30)
response.raise_for_status()
self.update_external_registration_status('success')
return True
except requests.RequestException as e:
self.update_external_registration_status('failed', str(e))
return False
@classmethod
def get_pending_external_registrations(cls):
"""
外部システム登録が保留中の記録を取得する
"""
return cls.objects.filter(
serial_number=-1,
cp_number="EXTERNAL_REG",
external_registration_status__in=['pending', 'retry']
).order_by('create_at')
@classmethod
def retry_failed_external_registrations(cls, max_attempts=3):
"""
失敗した外部システム登録を一括でリトライする
"""
pending_records = cls.get_pending_external_registrations().filter(
external_registration_attempts__lt=max_attempts
)
success_count = 0
failed_count = 0
for record in pending_records:
if record.retry_external_registration():
success_count += 1
else:
failed_count += 1
return {
'success_count': success_count,
'failed_count': failed_count,
'total_processed': success_count + failed_count
}
class Waypoint(models.Model): class Waypoint(models.Model):

View File

@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
import uuid import uuid
from datetime import datetime
from django.db import IntegrityError from django.db import IntegrityError
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
@ -14,7 +15,7 @@ from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from rest_framework_gis.serializers import GeoFeatureModelSerializer from rest_framework_gis.serializers import GeoFeatureModelSerializer
from sqlalchemy.sql.functions import mode from sqlalchemy.sql.functions import mode
from .models import Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, UserTracks, GoalImages, CheckinImages,CustomUser,NewEvent,NewEvent2, Team, NewCategory, Category, Entry, Member, TempUser,EntryMember, AppVersion, UploadedImage from .models import Location2025, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, UserTracks, GoalImages, CheckinImages,CustomUser,NewEvent,NewEvent2, Team, NewCategory, Category, Entry, Member, TempUser,EntryMember, AppVersion, UploadedImage
from drf_extra_fields.fields import Base64ImageField from drf_extra_fields.fields import Base64ImageField
#from django.contrib.auth.models import User #from django.contrib.auth.models import User
@ -32,21 +33,265 @@ logger = logging.getLogger(__name__)
class LocationCatSerializer(serializers.ModelSerializer): class LocationCatSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model=Location model=Location2025
fields=['category',] fields=['category',]
class LocationSerializer(GeoFeatureModelSerializer): class LocationSerializer(serializers.ModelSerializer):
# evaluation_valueに基づくインタラクション情報を追加 # evaluation_valueに基づくインタラクション情報を追加
interaction_type = serializers.SerializerMethodField() interaction_type = serializers.SerializerMethodField()
requires_photo = serializers.SerializerMethodField() requires_photo = serializers.SerializerMethodField()
requires_qr_code = serializers.SerializerMethodField() requires_qr_code = serializers.SerializerMethodField()
interaction_instructions = serializers.SerializerMethodField() interaction_instructions = serializers.SerializerMethodField()
# 追加フィールドのカスタムシリアライズ
has_photos = serializers.SerializerMethodField()
has_videos = serializers.SerializerMethodField()
photos_list = serializers.SerializerMethodField()
videos_list = serializers.SerializerMethodField()
tags_list = serializers.SerializerMethodField()
# 位置情報の緯度経度
latitude_float = serializers.SerializerMethodField()
longitude_float = serializers.SerializerMethodField()
# フィールド名の別名対応
webcontent = serializers.SerializerMethodField()
zip = serializers.SerializerMethodField()
s3_name = serializers.SerializerMethodField()
def get_webcontent(self, obj):
"""websiteフィールドをwebcontentとして返す"""
return obj.website
def get_zip(self, obj):
"""zip_codeフィールドをzipとして返す"""
return obj.zip_code
def get_s3_name(self, obj):
"""S3ファイル名を生成して返すevent_nameのローマ字表記"""
if not obj.event:
return None
# NewEvent2のs3_nameフィールドを参照
if hasattr(obj.event, 's3_name') and obj.event.s3_name:
return obj.event.s3_name
# フォールバック: event_nameをローマ字に変換
if obj.event.event_name:
s3_name = self._convert_japanese_to_romaji(obj.event.event_name)
return s3_name
# 最終フォールバック: イベントコード
return obj.event.event_code
def _convert_japanese_to_romaji(self, japanese_text):
"""日本語をローマ字に変換する"""
# 基本的な日本語→ローマ字変換テーブル
conversion_table = {
# 地名
'岐阜': 'gifu',
'大垣': 'ogaki',
'多治見': 'tajimi',
'': 'seki',
'中津川': 'nakatsugawa',
'美濃': 'mino',
'瑞浪': 'mizunami',
'羽島': 'hashima',
'恵那': 'ena',
'美濃加茂': 'minokamo',
'土岐': 'toki',
'各務原': 'kakamigahara',
'可児': 'kani',
'山県': 'yamagata',
'瑞穂': 'mizuho',
'飛騨': 'hida',
'本巣': 'motosu',
'郡上': 'gujo',
'下呂': 'gero',
'海津': 'kaizu',
# 一般的な単語
'ロゲイニング': 'rogaining',
'ロゲ': 'roge',
'イベント': 'event',
'大会': 'taikai',
'競技': 'kyogi',
'スポーツ': 'sports',
'アウトドア': 'outdoor',
'ハイキング': 'hiking',
'ウォーキング': 'walking',
'ランニング': 'running',
'マラソン': 'marathon',
'トレイル': 'trail',
'オリエンテーリング': 'orienteering',
'ナビゲーション': 'navigation',
'チェックポイント': 'checkpoint',
'ゴール': 'goal',
'スタート': 'start',
'チーム': 'team',
'ファミリー': 'family',
'ジュニア': 'junior',
'シニア': 'senior',
'初心者': 'beginner',
'上級者': 'advanced',
'中級者': 'intermediate',
# 年号
'': '2025',
'2025': '2025',
'': '2024',
'2024': '2024',
'': '2026',
'2026': '2026',
'': '2027',
'2027': '2027',
'': '2028',
'2028': '2028',
'': '2029',
'2029': '2029',
'': '2030',
'2030': '2030',
# 月
'1月': 'january',
'2月': 'february',
'3月': 'march',
'4月': 'april',
'5月': 'may',
'6月': 'june',
'7月': 'july',
'8月': 'august',
'9月': 'september',
'10月': 'october',
'11月': 'november',
'12月': 'december',
# その他
'': 'spring',
'': 'summer',
'': 'autumn',
'': 'winter',
'': 'dai',
'': 'kai',
'': 'hai',
'記念': 'kinen',
'特別': 'tokubetsu',
'公式': 'koshiki',
'練習': 'renshu',
'体験': 'taiken'
}
result = japanese_text.lower()
# 変換テーブルを使用して変換
for japanese, romaji in conversion_table.items():
result = result.replace(japanese, romaji)
# 記号の変換
result = result.replace(' ', '_')
result = result.replace('-', '_')
result = result.replace(' ', '_') # 全角スペース
result = result.replace('', '_')
result = result.replace('/', '_')
result = result.replace('\\', '_')
result = result.replace('(', '_')
result = result.replace(')', '_')
result = result.replace('', '_')
result = result.replace('', '_')
result = result.replace('[', '_')
result = result.replace(']', '_')
result = result.replace('', '_')
result = result.replace('', '_')
result = result.replace(':', '_')
result = result.replace('', '_')
result = result.replace(';', '_')
result = result.replace('', '_')
result = result.replace(',', '_')
result = result.replace('', '_')
result = result.replace('.', '_')
result = result.replace('', '_')
result = result.replace('!', '_')
result = result.replace('', '_')
result = result.replace('?', '_')
result = result.replace('', '_')
# 連続するアンダースコアを単一に
while '__' in result:
result = result.replace('__', '_')
# 先頭と末尾のアンダースコアを削除
result = result.strip('_')
# 空文字列の場合は'event'を返す
if not result:
result = 'event'
return result
class Meta: class Meta:
model=Location model=Location2025
geo_field='geom' fields=[
fields="__all__" # 基本フィールド
'id', 'cp_number', 'event', 'cp_name', 's3_name','category', 'sub_loc_id', 'subcategory',
'latitude', 'longitude', 'location', 'address',
'checkin_point', 'buy_point',
'checkin_radius', 'auto_checkin',
'shop_closed', 'shop_shutdown', 'opening_hours',
'phone', 'website', 'facility', 'description', 'remark',
# 住所詳細フィールド
'zip_code', 'prefecture', 'area', 'city',
# 追加フィールド
'photos', 'videos', 'tags', 'evaluation_value', 'hidden_location',
# 管理フィールド
'is_active', 'sort_order', 'csv_source_file', 'csv_upload_date', 'csv_upload_user',
'created_at', 'updated_at', 'created_by', 'updated_by',
# カスタムフィールド
'interaction_type', 'requires_photo', 'requires_qr_code', 'interaction_instructions',
'has_photos', 'has_videos', 'photos_list', 'videos_list', 'tags_list',
'latitude_float', 'longitude_float', 'webcontent', 'zip'
]
def get_latitude_float(self, obj):
"""位置情報から緯度を取得"""
if obj.location:
return obj.location.y
return obj.latitude
def get_longitude_float(self, obj):
"""位置情報から経度を取得"""
if obj.location:
return obj.location.x
return obj.longitude
def get_has_photos(self, obj):
"""写真データの有無を返す"""
return bool(obj.photos and obj.photos.strip())
def get_has_videos(self, obj):
"""動画データの有無を返す"""
return bool(obj.videos and obj.videos.strip())
def get_photos_list(self, obj):
"""写真ファイル名をリストで返す"""
if not obj.photos or not obj.photos.strip():
return []
# カンマ区切りで分割してリストとして返す
return [photo.strip() for photo in obj.photos.split(',') if photo.strip()]
def get_videos_list(self, obj):
"""動画ファイル名をリストで返す"""
if not obj.videos or not obj.videos.strip():
return []
# カンマ区切りで分割してリストとして返す
return [video.strip() for video in obj.videos.split(',') if video.strip()]
def get_tags_list(self, obj):
"""タグをリストで返す"""
if not obj.tags or not obj.tags.strip():
return []
# カンマ区切りで分割してリストとして返す
return [tag.strip() for tag in obj.tags.split(',') if tag.strip()]
def get_interaction_type(self, obj): def get_interaction_type(self, obj):
"""evaluation_valueに基づくインタラクションタイプを返す""" """evaluation_valueに基づくインタラクションタイプを返す"""
@ -82,7 +327,7 @@ class LocationSerializer(GeoFeatureModelSerializer):
if evaluation_value == "1": if evaluation_value == "1":
return "商品の写真を撮影してください。買い物をすることでポイントを獲得できます" return "商品の写真を撮影してください。買い物をすることでポイントを獲得できます"
elif evaluation_value == "2": elif evaluation_value == "2":
return "QRコードをスキャンしてクイズに答えてください" return "QRコードをスキャンしてください"
else: else:
return "この場所でチェックインしてポイントを獲得してください" return "この場所でチェックインしてポイントを獲得してください"
@ -220,7 +465,7 @@ class TempUserRegistrationSerializer(serializers.ModelSerializer):
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = ('id','email', 'is_rogaining' ,'group', 'zekken_number', 'event_code', 'team_name') fields = ('id','email', 'is_rogaining' ,'group', 'zekken_number', 'event_code', 'team_name', 'is_staff')
class GolaImageSerializer(serializers.ModelSerializer): class GolaImageSerializer(serializers.ModelSerializer):
goalimage = Base64ImageField(max_length=None, use_url=True) goalimage = Base64ImageField(max_length=None, use_url=True)
@ -291,9 +536,10 @@ class LoginUserSerializer(serializers.Serializer):
else: else:
# Check if the user exists # Check if the user exists
try: try:
user_obj = User.objects.get(email=email) from .models import CustomUser
user_obj = CustomUser.objects.get(email=email)
raise serializers.ValidationError("Incorrect password.") raise serializers.ValidationError("Incorrect password.")
except User.DoesNotExist: except CustomUser.DoesNotExist:
raise serializers.ValidationError("User with this email does not exist.") raise serializers.ValidationError("User with this email does not exist.")
else: else:
raise serializers.ValidationError("Must include 'email' and 'password'.") raise serializers.ValidationError("Must include 'email' and 'password'.")
@ -326,7 +572,7 @@ class UserDestinationSerializer(serializers.ModelSerializer):
class LocationEventNameSerializer(serializers.ModelSerializer): class LocationEventNameSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Location model = Location2025
fields = ('id', 'event_name',) fields = ('id', 'event_name',)
@ -509,13 +755,9 @@ class EntrySerializer(serializers.ModelSerializer):
event = serializers.PrimaryKeyRelatedField(queryset=NewEvent2.objects.all()) event = serializers.PrimaryKeyRelatedField(queryset=NewEvent2.objects.all())
category = serializers.PrimaryKeyRelatedField(queryset=NewCategory.objects.all()) category = serializers.PrimaryKeyRelatedField(queryset=NewCategory.objects.all())
owner = serializers.PrimaryKeyRelatedField(read_only=True) owner = serializers.PrimaryKeyRelatedField(read_only=True)
#date = serializers.DateTimeField(input_formats=['%Y-%m-%d']) date = serializers.DateTimeField(required=False, allow_null=True) # DateTimeFieldを使用
date = serializers.DateField(required=False, allow_null=True) # DateTimeFieldではなくDateFieldを使用
zekken_number = serializers.IntegerField() zekken_number = serializers.IntegerField()
#date = serializers.DateTimeField(default_timezone=timezone.get_current_timezone())
class Meta: class Meta:
model = Entry model = Entry
fields = [ fields = [
@ -529,36 +771,24 @@ class EntrySerializer(serializers.ModelSerializer):
def validate_date(self, value): def validate_date(self, value):
if isinstance(value, str): if isinstance(value, str):
try: try:
# 文字列をdatetimeオブジェクトに変換
value = datetime.strptime(value, "%Y-%m-%d") value = datetime.strptime(value, "%Y-%m-%d")
except ValueError: except ValueError:
raise serializers.ValidationError("Invalid date format. Use YYYY-MM-DD.") raise serializers.ValidationError("Invalid date format. Use YYYY-MM-DD.")
if isinstance(value, date): if isinstance(value, date) and not isinstance(value, datetime):
# dateオブジェクトをdatetimeオブジェクトに変換
value = datetime.combine(value, datetime.min.time()) value = datetime.combine(value, datetime.min.time())
if timezone.is_naive(value): if isinstance(value, datetime) and timezone.is_naive(value):
return timezone.make_aware(value, timezone.get_current_timezone()) return timezone.make_aware(value, timezone.get_current_timezone())
return value return value
#if isinstance(value, date):
# # dateオブジェクトをdatetimeオブジェクトに変換
# value = datetime.combine(value, datetime.min.time())
#if timezone.is_naive(value):
# return timezone.make_aware(value, timezone.get_current_timezone())
#return value
def validate_team(self, value): def validate_team(self, value):
if not value.members.exists(): if not value.members.exists():
raise serializers.ValidationError("チームにメンバーが登録されていません。") raise serializers.ValidationError("チームにメンバーが登録されていません。")
return value return value
def validate_date(self, value):
if isinstance(value, datetime):
return value.date()
return value
def validate(self, data): def validate(self, data):
team = data.get('team') team = data.get('team')
event = data.get('event') event = data.get('event')
@ -622,12 +852,22 @@ class EntrySerializer(serializers.ModelSerializer):
def to_representation(self, instance): def to_representation(self, instance):
ret = super().to_representation(instance) ret = super().to_representation(instance)
# instance が辞書の場合(エラー時)は基本情報のみ返す
if isinstance(instance, dict):
return ret
# 正常な場合のみ関連オブジェクトを追加
if hasattr(instance, 'team') and instance.team:
ret['team'] = TeamSerializer(instance.team).data ret['team'] = TeamSerializer(instance.team).data
if hasattr(instance, 'event') and instance.event:
ret['event'] = NewEvent2Serializer(instance.event).data ret['event'] = NewEvent2Serializer(instance.event).data
if hasattr(instance, 'category') and instance.category:
ret['category'] = NewCategorySerializer(instance.category).data ret['category'] = NewCategorySerializer(instance.category).data
if hasattr(instance, 'owner') and instance.owner:
ret['owner'] = CustomUserSerializer(instance.owner).data ret['owner'] = CustomUserSerializer(instance.owner).data
if isinstance(ret['date'], datetime): if isinstance(ret.get('date'), datetime):
ret['date'] = ret['date'].date().isoformat() ret['date'] = ret['date'].date().isoformat()
elif isinstance(ret['date'], date): elif isinstance(ret['date'], date):
ret['date'] = ret['date'].isoformat() ret['date'] = ret['date'].isoformat()
@ -648,7 +888,7 @@ class EntrySerializer(serializers.ModelSerializer):
class CustomUserSerializer(serializers.ModelSerializer): class CustomUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = ['id','email', 'firstname', 'lastname', 'date_of_birth', 'female'] fields = ['id','email', 'firstname', 'lastname', 'date_of_birth', 'female', 'is_staff']
read_only_fields = ['id','email'] read_only_fields = ['id','email']
class TeamDetailSerializer(serializers.ModelSerializer): class TeamDetailSerializer(serializers.ModelSerializer):
@ -658,12 +898,26 @@ class TeamDetailSerializer(serializers.ModelSerializer):
model = Team model = Team
fields = ['id', 'zekken_number', 'team_name', 'category'] fields = ['id', 'zekken_number', 'team_name', 'category']
class UserSerializer(serializers.ModelSerializer): class UserDetailSerializer(serializers.ModelSerializer):
event_date = serializers.SerializerMethodField()
last_goal_time = serializers.SerializerMethodField()
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = ['id','email', 'firstname', 'lastname', 'date_of_birth', 'female', 'is_rogaining', 'zekken_number', 'event_code', 'team_name', 'group'] fields = ['id','email', 'firstname', 'lastname', 'date_of_birth', 'female', 'is_rogaining', 'zekken_number', 'event_code', 'team_name', 'group', 'is_staff', 'event_date', 'last_goal_time']
read_only_fields = ('id', 'email') read_only_fields = ('id', 'email')
def get_event_date(self, obj):
"""イベント日付を取得(ハードコーディングの値を返す)"""
# ハードコーディングされた日付をDateTimeとして返す
from datetime import datetime
return datetime(2025, 5, 17).isoformat()
def get_last_goal_time(self, obj):
"""最後のゴール時間を取得"""
from datetime import datetime
return datetime(2025, 1, 24, 22, 45, 4).isoformat()
class UserUpdateSerializer(serializers.ModelSerializer): class UserUpdateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CustomUser model = CustomUser
@ -682,7 +936,7 @@ class MemberCreationSerializer(serializers.Serializer):
firstname = serializers.CharField(required=False, allow_blank=True) firstname = serializers.CharField(required=False, allow_blank=True)
lastname = serializers.CharField(required=False, allow_blank=True) lastname = serializers.CharField(required=False, allow_blank=True)
date_of_birth = serializers.DateField(required=False) date_of_birth = serializers.DateField(required=False, allow_null=True) # allow_null=True を追加
female = serializers.BooleanField(required=False) female = serializers.BooleanField(required=False)
@ -756,6 +1010,7 @@ class MemberSerializer(serializers.ModelSerializer):
representation['lastname'] = instance.user.lastname representation['lastname'] = instance.user.lastname
representation['date_of_birth'] = instance.user.date_of_birth representation['date_of_birth'] = instance.user.date_of_birth
representation['female'] = instance.user.female representation['female'] = instance.user.female
representation['is_staff'] = instance.user.is_staff
return representation return representation

View File

@ -1,12 +1,14 @@
from sys import prefix from sys import prefix
from rest_framework import urlpatterns from rest_framework import urlpatterns
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import CategoryByNameView, LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel,debug_urls,get_ranking, all_ranking_top3 from .views import CategoryByNameView, LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel,debug_urls,get_ranking, all_ranking_top3, current_entry_info
from .views_apis.api_auth import check_event_code from .views_apis.api_auth import check_event_code
from .views_apis.api_teams import register_team,update_team_name,team_class_changer,team_register,zekken_max_num,zekken_double_check,get_team_list,get_zekken_list from .views_apis.api_teams import register_team,update_team_name,team_class_changer,team_register,zekken_max_num,zekken_double_check,get_team_list,get_zekken_list
from .views_apis.api_play import input_cp,get_checkpoint_list,start_from_rogapp,checkin_from_rogapp,goal_from_rogapp from .views_apis.api_play import input_cp,get_checkpoint_list,start_from_rogapp,checkin_from_rogapp,goal_from_rogapp
from .views_apis.api_edit import remove_checkin_from_rogapp,add_checkin,delete_checkin,move_checkin,goal_checkin,change_goal_time_checkin,change_goal_time_checkin,get_checkin_list,service_check_true,service_check_false,get_yet_check_service_list from .views_apis.api_edit import remove_checkin_from_rogapp,add_checkin,delete_checkin,move_checkin,goal_checkin,change_goal_time_checkin,change_goal_time_checkin,get_checkin_list,service_check_true,service_check_false,get_yet_check_service_list
from .views_apis.api_approval import approve_checkin_history
from .views_apis.api_bulk_photo_upload import bulk_upload_checkin_photos, get_bulk_upload_status
from .views_apis.api_waypoint import get_waypoint_datas_from_rogapp,get_route,fetch_user_locations,get_all_routes from .views_apis.api_waypoint import get_waypoint_datas_from_rogapp,get_route,fetch_user_locations,get_all_routes
from .views_apis.api_routes import top_users_routes,generate_route_image from .views_apis.api_routes import top_users_routes,generate_route_image
from .views_apis.api_events import get_start_point,analyze_point from .views_apis.api_events import get_start_point,analyze_point
@ -15,9 +17,11 @@ from .views_apis.api_ranking import get_ranking,all_ranking_top3
from .views_apis.api_photos import get_photo_list, get_photo_list_prod, get_team_photos from .views_apis.api_photos import get_photo_list, get_photo_list_prod, get_team_photos
from .views_apis.s3_views import upload_checkin_image, upload_standard_image, get_standard_image, list_event_images, delete_image from .views_apis.s3_views import upload_checkin_image, upload_standard_image, get_standard_image, list_event_images, delete_image
from .views_apis.api_scoreboard import get_scoreboard,download_scoreboard,reprint,make_all_scoreboard,make_cp_list_sheet from .views_apis.api_scoreboard import get_scoreboard,download_scoreboard,reprint,make_all_scoreboard,make_cp_list_sheet
from .views_apis.api_qr_points import submit_qr_points, qr_points_status
from .views_apis.api_bulk_upload import bulk_upload_photos, confirm_checkin_validation from .views_apis.api_bulk_upload import bulk_upload_photos, confirm_checkin_validation
from .views_apis.api_admin_validation import get_event_participants_ranking, get_participant_validation_details, get_event_zekken_list from .views_apis.api_admin_validation import get_event_participants_ranking, get_participant_validation_details, get_event_zekken_list
from .views_apis.api_simulator import rogaining_simulator from .views_apis.api_simulator import rogaining_simulator
from .views_apis.api_competition_status import competition_status, update_competition_status, checkpoint_status
from .views_apis.api_test import test_gifuroge,practice from .views_apis.api_test import test_gifuroge,practice
from .views_apis.api_supervisor import get_events_for_supervisor from .views_apis.api_supervisor import get_events_for_supervisor
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -82,9 +86,9 @@ urlpatterns = router.urls
urlpatterns += [ urlpatterns += [
path('inperf/', LocationsInPerf, name="location_perf"), path('inperf/', LocationsInPerf, name="location_perf"),
path('insubperf', LocationsInSubPerf, name='location_subperf'), path('insubperf/', LocationsInSubPerf, name='location_subperf'),
path('inbound', LocationInBound, name='location_bound'), path('inbound/', LocationInBound, name='location_bound'),
path('inbound2', LocationInBound2, name='location_bound'), path('inbound2/', LocationInBound2, name='location_bound2'),
path('location-checkin/', views.LocationCheckinView.as_view(), name='location_checkin'), path('location-checkin/', views.LocationCheckinView.as_view(), name='location_checkin'),
path('location-checkin-test/', views.location_checkin_test, name='location_checkin_test'), path('location-checkin-test/', views.location_checkin_test, name='location_checkin_test'),
path('customarea/', CustomAreaLocations, name='custom_area_location'), path('customarea/', CustomAreaLocations, name='custom_area_location'),
@ -140,6 +144,7 @@ urlpatterns += [
#path('admin/', admin.site.urls), #path('admin/', admin.site.urls),
path('entries/<int:entry_id>/update-status/', update_entry_status, name='update-entry-status'), path('entries/<int:entry_id>/update-status/', update_entry_status, name='update-entry-status'),
path('user/current-entry-info/', views.current_entry_info, name='current-entry-info'),
# for Supervisor Web app # for Supervisor Web app
@ -197,6 +202,13 @@ urlpatterns += [
path('serviceCheckFalse', service_check_false, name='service_check_false'), path('serviceCheckFalse', service_check_false, name='service_check_false'),
path('getYetCheckSeeviceList', get_yet_check_service_list, name='get_yet_check_service_list'), path('getYetCheckSeeviceList', get_yet_check_service_list, name='get_yet_check_service_list'),
## User Approval
path('approve_checkin_history/', approve_checkin_history, name='approve_checkin_history'),
## Bulk Photo Upload & Checkin Correction
path('bulk_upload_checkin_photos/', bulk_upload_checkin_photos, name='bulk_upload_checkin_photos'),
path('get_bulk_upload_status/', get_bulk_upload_status, name='get_bulk_upload_status'),
## Waypoint ## Waypoint
path('get_waypoint_datas_from_rogapp', get_waypoint_datas_from_rogapp, name='get_waypoint_datas_from_rogapp'), path('get_waypoint_datas_from_rogapp', get_waypoint_datas_from_rogapp, name='get_waypoint_datas_from_rogapp'),
path('getRoute', get_route, name='get_route'), path('getRoute', get_route, name='get_route'),
@ -247,6 +259,9 @@ urlpatterns += [
# App Version Management # App Version Management
path('app/version-check/', app_version_check, name='app_version_check'), path('app/version-check/', app_version_check, name='app_version_check'),
path('app/version-check', app_version_check, name='app_version_check_no_slash'), # スラッシュなし対応
path('api/app/version-check/', app_version_check, name='app_version_check_duplicate'), # クライアントの誤ったURL対応
path('api/app/version-check', app_version_check, name='app_version_check_duplicate_no_slash'), # スラッシュなし対応
path('app/version-management/', AppVersionManagementView.as_view(), name='app_version_management'), path('app/version-management/', AppVersionManagementView.as_view(), name='app_version_management'),
# Multi-Image Upload API # Multi-Image Upload API
@ -258,6 +273,15 @@ urlpatterns += [
path('api/routes/gpx-test-data/', gpx_test_data, name='gpx_test_data'), path('api/routes/gpx-test-data/', gpx_test_data, name='gpx_test_data'),
path('api/routes/available/', available_routes, name='available_routes'), path('api/routes/available/', available_routes, name='available_routes'),
# QR Points API
path('submit_qr_points', submit_qr_points, name='submit_qr_points'),
path('qr_points_status', qr_points_status, name='qr_points_status'),
# Competition Status Management API (Added 2025-09-04)
path('api/competition_status/', competition_status, name='competition_status'),
path('api/competition_status/update/', update_competition_status, name='update_competition_status'),
path('api/checkpoint_status/', checkpoint_status, name='checkpoint_status'),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@ -378,3 +378,62 @@ class S3Bucket:
logger.error(f"予期しないエラーが発生しました: {str(e)}") logger.error(f"予期しないエラーが発生しました: {str(e)}")
return False return False
class S3Bucket:
"""
レガシーS3Bucketクラス - 既存コードとの互換性のため
"""
def __init__(self, bucket_name):
self.bucket_name = bucket_name
try:
self.s3_client = boto3.client(
's3',
aws_access_key_id=getattr(settings, 'AWS_ACCESS_KEY', None),
aws_secret_access_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY', None),
region_name=getattr(settings, 'AWS_REGION', 'us-west-2')
)
logger.info(f"S3Bucket initialized for bucket: {self.bucket_name}")
except Exception as e:
logger.error(f"Failed to initialize S3Bucket: {e}")
self.s3_client = None
def upload_file(self, local_file_path, s3_key):
"""
ローカルファイルをS3にアップロード
Args:
local_file_path: アップロードするローカルファイルのパス
s3_key: S3内でのキーファイルパス
Returns:
bool: 成功時True、失敗時False
"""
if not self.s3_client:
logger.error("S3 client not initialized")
return False
try:
self.s3_client.upload_file(
local_file_path,
self.bucket_name,
s3_key,
ExtraArgs={'ACL': 'public-read'}
)
logger.info(f"Successfully uploaded {local_file_path} to s3://{self.bucket_name}/{s3_key}")
return True
except Exception as e:
logger.error(f"Failed to upload {local_file_path} to S3: {e}")
return False
def get_file_url(self, s3_key):
"""
S3ファイルのパブリックURLを生成
Args:
s3_key: S3内でのキーファイルパス
Returns:
str: ファイルのURL
"""
aws_region = getattr(settings, 'AWS_REGION', 'us-west-2')
return f"https://{self.bucket_name}.s3.{aws_region}.amazonaws.com/{s3_key}"

42
rog/utils/__init__.py Normal file
View File

@ -0,0 +1,42 @@
# Python package marker
# rog.utils.pyから必要な関数をインポート
import sys
import os
# 親ディレクトリのutils.pyをインポートできるようにパスを追加
current_dir = os.path.dirname(__file__)
parent_dir = os.path.dirname(current_dir)
utils_py_path = os.path.join(parent_dir, 'utils.py')
if os.path.exists(utils_py_path):
# utils.pyから直接インポート
import importlib.util
spec = importlib.util.spec_from_file_location("rog_utils", utils_py_path)
rog_utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(rog_utils)
# 必要な関数/クラスを公開
S3Bucket = rog_utils.S3Bucket
send_verification_email = rog_utils.send_verification_email
send_invitation_email = rog_utils.send_invitation_email
send_team_join_email = rog_utils.send_team_join_email
send_reset_password_email = rog_utils.send_reset_password_email
else:
# フォールバック: ダミー実装
class S3Bucket:
def __init__(self, bucket_name):
self.bucket_name = bucket_name
def upload_file(self, local_path, s3_key):
return False
def get_file_url(self, s3_key):
return ""
def send_verification_email(*args, **kwargs):
pass
def send_invitation_email(*args, **kwargs):
pass
def send_team_join_email(*args, **kwargs):
pass
def send_reset_password_email(*args, **kwargs):
pass

View File

@ -0,0 +1,163 @@
"""
S3画像アップロードユーティリティ
チェックイン時の画像をS3にアップロードし、URLを生成します
"""
import boto3
import logging
import uuid
from datetime import datetime
from django.conf import settings
from botocore.exceptions import ClientError, NoCredentialsError
import base64
import io
import requests
logger = logging.getLogger(__name__)
class S3ImageUploader:
def __init__(self):
"""S3クライアントを初期化"""
try:
# AWS認証情報の確認
aws_access_key = getattr(settings, 'AWS_ACCESS_KEY_ID', None)
aws_secret_key = getattr(settings, 'AWS_SECRET_ACCESS_KEY', None)
if not aws_access_key or not aws_secret_key:
logger.warning("AWS credentials not configured, S3 upload will be disabled")
self.s3_client = None
self.bucket_name = None
return
self.s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
region_name=getattr(settings, 'AWS_S3_REGION_NAME', 'us-west-2')
)
self.bucket_name = getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'sumasenrogaining')
logger.info(f"S3 client initialized for bucket: {self.bucket_name}")
except Exception as e:
logger.error(f"Failed to initialize S3 client: {e}")
self.s3_client = None
self.bucket_name = None
def upload_checkin_image(self, image_data, event_code, zekken_number, cp_number):
"""
チェックイン画像をS3にアップロード
Args:
image_data: 画像データURLまたはBase64
event_code: イベントコード
zekken_number: ゼッケン番号
cp_number: チェックポイント番号
Returns:
str: S3のURL、失敗時は元のimage_data
"""
if not self.s3_client or not image_data:
logger.warning("S3 client not available or no image data, returning original")
return image_data
try:
# 画像データを取得
image_binary = self._get_image_binary(image_data)
if not image_binary:
logger.error("Failed to get image binary data, returning original")
return image_data
# S3キーを生成: {event_code}/{zekken_number}/{cp_number}.jpg
s3_key = f"{event_code}/{zekken_number}/{cp_number}.jpg"
# S3にアップロード
self.s3_client.put_object(
Bucket=self.bucket_name,
Key=s3_key,
Body=image_binary,
ContentType='image/jpeg',
ACL='public-read' # 公開読み取り可能
)
# S3 URLを生成
aws_region = getattr(settings, 'AWS_REGION', 'us-west-2')
s3_url = f"https://{self.bucket_name}.s3.{aws_region}.amazonaws.com/{s3_key}"
logger.info(f"Successfully uploaded image to S3: {s3_url}")
return s3_url
except ClientError as e:
logger.error(f"S3 upload failed: {e}, returning original URL")
return image_data
except Exception as e:
logger.error(f"Unexpected error during S3 upload: {e}, returning original URL")
return image_data
def _get_image_binary(self, image_data):
"""
画像データからバイナリデータを取得
Args:
image_data: 画像URLHTTPまたはBase64エンコードされた画像データ
Returns:
bytes: 画像のバイナリデータ
"""
try:
if isinstance(image_data, str):
# HTTPURLの場合
if image_data.startswith('http'):
return self._download_image_from_url(image_data)
# Base64の場合
elif self._is_base64(image_data):
return base64.b64decode(image_data)
# data:image/jpeg;base64,の形式の場合
elif image_data.startswith('data:image'):
base64_data = image_data.split(',')[1]
return base64.b64decode(base64_data)
logger.error(f"Unsupported image data format: {type(image_data)}")
return None
except Exception as e:
logger.error(f"Error processing image data: {e}")
return None
def _download_image_from_url(self, url):
"""URLから画像をダウンロード"""
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
return response.content
except Exception as e:
logger.error(f"Failed to download image from URL {url}: {e}")
return None
def _is_base64(self, data):
"""文字列がBase64かどうかをチェック"""
try:
if isinstance(data, str):
base64.b64decode(data, validate=True)
return True
except Exception:
return False
return False
def generate_s3_url(self, event_code, zekken_number, cp_number):
"""
S3 URLを生成アップロード済みの画像用
Args:
event_code: イベントコード
zekken_number: ゼッケン番号
cp_number: チェックポイント番号
Returns:
str: S3のURL
"""
aws_region = getattr(settings, 'AWS_REGION', 'us-west-2')
s3_key = f"{event_code}/{zekken_number}/{cp_number}.jpg"
return f"https://{self.bucket_name}.s3.{aws_region}.amazonaws.com/{s3_key}"
# グローバルインスタンス
s3_uploader = S3ImageUploader()

File diff suppressed because it is too large Load Diff

View File

@ -332,9 +332,14 @@ def get_event_zekken_list(request):
team_name = entry.team.team_name if entry.team else 'チーム名未設定' team_name = entry.team.team_name if entry.team else 'チーム名未設定'
category_name = entry.category.category_name if entry.category else 'クラス未設定' category_name = entry.category.category_name if entry.category else 'クラス未設定'
# zekken_labelが存在する場合はそれを使用、そうでなければzekken_numberを使用
zekken_value = entry.zekken_label if entry.zekken_label else str(entry.zekken_number)
# 表示ラベルをzekken_labelベースに変更
zekken_label = f"{zekken_value} - {team_name}"
zekken_list.append({ zekken_list.append({
'value': str(entry.zekken_number), 'value': zekken_value,
'label': f"{entry.zekken_number} - {team_name}", 'label': zekken_label,
'team_name': team_name, 'team_name': team_name,
'category': category_name 'category': category_name
}) })

View File

@ -0,0 +1,160 @@
"""
通過履歴承認API
ユーザーが自分のチームの通過履歴を確認し、承認確定する処理を行う
"""
import logging
import uuid
from django.http import JsonResponse
from django.utils import timezone
from django.db import transaction
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from knox.auth import TokenAuthentication
from ..models import NewEvent2, Entry, GpsLog
# ログ設定
logger = logging.getLogger(__name__)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def approve_checkin_history(request):
"""
ユーザーがアプリ上で通過履歴を確認し、承認確定する処理
パラメータ:
- event_code: イベントコード (必須)
- zekken_number: ゼッケン番号 (必須)
- checkin_ids: 承認するチェックインIDのリスト (必須)
- approval_comment: 承認コメント (任意)
"""
# リクエストID生成デバッグ用
request_id = str(uuid.uuid4())[:8]
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
logger.info(f"[APPROVE_CHECKIN] 🎯 API call started - ID: {request_id}, User: {request.user.email if request.user.is_authenticated else 'Anonymous'}, Client IP: {client_ip}")
try:
# リクエストデータの取得
data = request.data
event_code = data.get('event_code')
zekken_number = data.get('zekken_number')
checkin_ids = data.get('checkin_ids', [])
approval_comment = data.get('approval_comment', '')
logger.info(f"[APPROVE_CHECKIN] 📝 Request data - ID: {request_id}, event_code: '{event_code}', zekken_number: '{zekken_number}', checkin_ids: {checkin_ids}")
# 必須パラメータの検証
if not all([event_code, zekken_number, checkin_ids]):
logger.warning(f"[APPROVE_CHECKIN] ❌ Missing required parameters - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "イベントコード、ゼッケン番号、チェックインIDが必要です"
}, status=status.HTTP_400_BAD_REQUEST)
if not isinstance(checkin_ids, list) or len(checkin_ids) == 0:
logger.warning(f"[APPROVE_CHECKIN] ❌ Invalid checkin_ids format - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "チェックインIDは空でない配列で指定してください"
}, status=status.HTTP_400_BAD_REQUEST)
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"[APPROVE_CHECKIN] ❌ Event not found - ID: {request_id}, event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[APPROVE_CHECKIN] ✅ Event found - ID: {request_id}, event: '{event_code}', event_id: {event.id}")
# チームの存在確認とオーナー権限の検証
entry = Entry.objects.filter(
event=event,
team__zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"[APPROVE_CHECKIN] ❌ Team not found - ID: {request_id}, zekken_number: '{zekken_number}', event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[APPROVE_CHECKIN] ✅ Team found - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}, entry_id: {entry.id}")
# オーナー権限の確認
if entry.owner != request.user:
logger.warning(f"[APPROVE_CHECKIN] ❌ Permission denied - ID: {request_id}, user: {request.user.email}, team_owner: {entry.owner.email}")
return Response({
"status": "ERROR",
"message": "このチームの通過履歴を承認する権限がありません"
}, status=status.HTTP_403_FORBIDDEN)
# 指定されたチェックインIDの存在確認
existing_checkins = GpsLog.objects.filter(
id__in=checkin_ids,
zekken_number=zekken_number,
event_code=event_code
)
existing_ids = list(existing_checkins.values_list('id', flat=True))
invalid_ids = [cid for cid in checkin_ids if cid not in existing_ids]
if invalid_ids:
logger.warning(f"[APPROVE_CHECKIN] ⚠️ Invalid checkin IDs found - ID: {request_id}, invalid_ids: {invalid_ids}, valid_ids: {existing_ids}")
return Response({
"status": "ERROR",
"message": "指定されたチェックイン記録が見つかりません",
"error_details": {
"invalid_checkin_ids": invalid_ids,
"valid_checkin_ids": existing_ids
}
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[APPROVE_CHECKIN] ✅ All checkin IDs validated - ID: {request_id}, count: {len(existing_ids)}")
# 承認処理現時点ではACK応答のみ
# TODO: 実際のDB更新処理をここに実装
# - validation_statusの更新
# - approval_commentの保存
# - approved_atタイムスタンプの設定
# - approved_byユーザーの記録
approval_time = timezone.now()
approved_checkins = []
for checkin in existing_checkins:
approved_checkins.append({
"checkin_id": checkin.id,
"cp_number": checkin.cp_number,
"approved_at": approval_time.strftime("%Y-%m-%d %H:%M:%S")
})
logger.info(f"[APPROVE_CHECKIN] ✅ Approval completed - ID: {request_id}, approved_count: {len(approved_checkins)}, comment: '{approval_comment[:50]}...' if len(approval_comment) > 50 else approval_comment")
# 成功レスポンス
return Response({
"status": "OK",
"message": "通過履歴の承認が完了しました",
"approved_count": len(approved_checkins),
"approved_checkins": approved_checkins,
"team_info": {
"team_name": entry.team.team_name,
"zekken_number": zekken_number,
"event_code": event_code
}
})
except Exception as e:
logger.error(f"[APPROVE_CHECKIN] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -0,0 +1,860 @@
"""
写真一括アップロード・通過履歴校正API
スマホアルバムから複数の写真をアップロードし、EXIF情報から自動的に通過履歴を生成・校正する
"""
import logging
import uuid
import os
from datetime import datetime
from django.http import JsonResponse
from django.utils import timezone
from django.db import transaction
from django.core.files.storage import default_storage
from django.conf import settings
from rest_framework.decorators import api_view, permission_classes, parser_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from knox.auth import TokenAuthentication
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
import math
import tempfile
import json
from ..models import NewEvent2, Entry, GpsLog, Location2025, GpsCheckin, CheckinImages
# ログ設定
logger = logging.getLogger(__name__)
def upload_photo_to_s3(uploaded_file, event_code, zekken_number, cp_number=None, request_id=None):
"""
アップロードされた写真をS3にアップロードする
Args:
uploaded_file: アップロードされたファイル
event_code: イベントコード
zekken_number: ゼッケン番号
cp_number: チェックポイント番号
request_id: リクエストID
Returns:
dict: {
'success': bool,
's3_url': str,
's3_key': str,
'error': str
}
"""
try:
# S3のバケット設定を取得
bucket_name = getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'rogaining-images')
# S3キーの生成
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
file_extension = os.path.splitext(uploaded_file.name)[1].lower()
if cp_number:
s3_key = f"checkin_photos/{event_code}/{zekken_number}/CP{cp_number}_{timestamp}_{request_id}{file_extension}"
else:
s3_key = f"bulk_upload/{event_code}/{zekken_number}/{timestamp}_{request_id}_{uploaded_file.name}"
# 一時ファイルとして保存
with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
for chunk in uploaded_file.chunks():
temp_file.write(chunk)
temp_file_path = temp_file.name
try:
# AWS認証情報の確認
aws_access_key = getattr(settings, 'AWS_ACCESS_KEY_ID', None)
aws_secret_key = getattr(settings, 'AWS_SECRET_ACCESS_KEY', None)
if not aws_access_key or not aws_secret_key:
logger.warning(f"[S3_UPLOAD] ❌ AWS credentials not configured, falling back to local storage")
return {
'success': False,
's3_url': None,
's3_key': None,
'error': 'AWS credentials not configured, using local storage instead'
}
# S3クライアントの設定環境変数から取得
import boto3
from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
s3_client = boto3.client(
's3',
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
region_name=getattr(settings, 'AWS_S3_REGION_NAME', 'ap-northeast-1')
)
# S3にアップロード
s3_client.upload_file(
temp_file_path,
bucket_name,
s3_key,
ExtraArgs={
'ContentType': uploaded_file.content_type,
'Metadata': {
'event_code': event_code,
'zekken_number': str(zekken_number),
'cp_number': str(cp_number) if cp_number else '',
'original_filename': uploaded_file.name,
'upload_source': 'bulk_upload_api'
}
}
)
# S3 URLの生成
s3_url = f"https://{bucket_name}.s3.{getattr(settings, 'AWS_S3_REGION_NAME', 'ap-northeast-1')}.amazonaws.com/{s3_key}"
logger.info(f"[S3_UPLOAD] ✅ Successfully uploaded to S3 - key: {s3_key}")
return {
'success': True,
's3_url': s3_url,
's3_key': s3_key,
'error': None
}
finally:
# 一時ファイルを削除
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
except ImportError:
logger.warning(f"[S3_UPLOAD] ❌ boto3 not available, saving locally")
return {
'success': False,
's3_url': None,
's3_key': None,
'error': 'S3 upload not available (boto3 not installed)'
}
except Exception as e:
error_message = str(e)
if 'credentials' in error_message.lower():
logger.warning(f"[S3_UPLOAD] ❌ AWS credentials error: {error_message}")
return {
'success': False,
's3_url': None,
's3_key': None,
'error': f'AWS credentials error: {error_message}'
}
else:
logger.error(f"[S3_UPLOAD] ❌ Error uploading to S3: {error_message}")
return {
'success': False,
's3_url': None,
's3_key': None,
'error': str(e)
}
def create_checkin_image_record(gps_checkin, s3_url, s3_key, original_filename, exif_data, request_id, user):
"""
CheckinImagesテーブルにレコードを作成する
Args:
gps_checkin: GpsCheckinオブジェクト
s3_url: S3のURL
s3_key: S3のキー
original_filename: 元のファイル名
exif_data: EXIF情報
request_id: リクエストID
user: ユーザーオブジェクト
Returns:
CheckinImagesオブジェクトまたはNone
"""
try:
# S3 URLがない場合はローカルパスまたは一時的なプレースホルダーを使用
image_url = s3_url if s3_url else f"local://bulk_upload/{original_filename}"
# CheckinImagesレコードを作成
checkin_image = CheckinImages.objects.create(
user=user,
checkinimage=image_url, # S3のURLまたはローカルパスを保存
checkintime=timezone.now(),
team_name=f"Team_{gps_checkin.zekken}", # ゼッケン番号からチーム名を生成
event_code=gps_checkin.event_code,
cp_number=gps_checkin.cp_number
)
if s3_url:
logger.info(f"[CHECKIN_IMAGE] ✅ Created CheckinImages record with S3 URL - ID: {checkin_image.id}, checkin_id: {gps_checkin.id}")
else:
logger.info(f"[CHECKIN_IMAGE] ✅ Created CheckinImages record with local path - ID: {checkin_image.id}, checkin_id: {gps_checkin.id}")
return checkin_image
except Exception as e:
logger.error(f"[CHECKIN_IMAGE] ❌ Error creating CheckinImages record: {str(e)}")
return None
def extract_exif_data(image_file):
"""
画像ファイルからEXIF情報を抽出する
Returns:
dict: {
'latitude': float,
'longitude': float,
'datetime': datetime,
'has_gps': bool
}
"""
try:
# PIL Imageオブジェクトを作成
image = Image.open(image_file)
# EXIF情報を取得
exif_data = image._getexif()
if not exif_data:
logger.warning(f"No EXIF data found in image: {image_file.name}")
return {'has_gps': False, 'latitude': None, 'longitude': None, 'datetime': None}
# GPS情報とDatetime情報を抽出
gps_info = {}
datetime_original = None
for tag_id, value in exif_data.items():
tag = TAGS.get(tag_id, tag_id)
if tag == "GPSInfo":
# GPS情報の詳細を展開
for gps_tag_id, gps_value in value.items():
gps_tag = GPSTAGS.get(gps_tag_id, gps_tag_id)
gps_info[gps_tag] = gps_value
elif tag == "DateTime" or tag == "DateTimeOriginal":
try:
datetime_original = datetime.strptime(str(value), '%Y:%m:%d %H:%M:%S')
except ValueError:
logger.warning(f"Failed to parse datetime: {value}")
# GPS座標の変換
latitude = None
longitude = None
if 'GPSLatitude' in gps_info and 'GPSLongitude' in gps_info:
lat_deg, lat_min, lat_sec = gps_info['GPSLatitude']
lon_deg, lon_min, lon_sec = gps_info['GPSLongitude']
# 度分秒を小数度に変換
latitude = float(lat_deg) + float(lat_min)/60 + float(lat_sec)/3600
longitude = float(lon_deg) + float(lon_min)/60 + float(lon_sec)/3600
# 南緯・西経の場合は負の値にする
if gps_info.get('GPSLatitudeRef') == 'S':
latitude = -latitude
if gps_info.get('GPSLongitudeRef') == 'W':
longitude = -longitude
logger.info(f"EXIF extracted from {image_file.name}: lat={latitude}, lon={longitude}, datetime={datetime_original}")
return {
'has_gps': latitude is not None and longitude is not None,
'latitude': latitude,
'longitude': longitude,
'datetime': datetime_original
}
except Exception as e:
logger.error(f"Error extracting EXIF from {image_file.name}: {str(e)}")
return {'has_gps': False, 'latitude': None, 'longitude': None, 'datetime': None}
def find_nearest_checkpoint(latitude, longitude, event_code, max_distance_m=100):
"""
指定された座標から最も近いチェックポイントを検索する
Args:
latitude: 緯度
longitude: 経度
event_code: イベントコード
max_distance_m: 最大距離(メートル)
Returns:
Location2025オブジェクトまたはNone
"""
try:
# 該当イベントのチェックポイントを取得
checkpoints = Location2025.objects.filter(event__event_name=event_code)
if not checkpoints.exists():
logger.warning(f"No checkpoints found for event: {event_code}")
return None
# 最も近いチェックポイントを検索
nearest_cp = None
min_distance = float('inf')
for cp in checkpoints:
if cp.latitude and cp.longitude:
# ハーバーサイン距離の計算
distance = calculate_distance(latitude, longitude, cp.latitude, cp.longitude)
if distance < min_distance and distance <= max_distance_m:
min_distance = distance
nearest_cp = cp
if nearest_cp:
logger.info(f"Found nearest checkpoint: {nearest_cp.location} (CP{nearest_cp.cp_number}) at distance {min_distance:.1f}m")
else:
logger.info(f"No checkpoint found within {max_distance_m}m of lat={latitude}, lon={longitude}")
return nearest_cp
except Exception as e:
logger.error(f"Error finding nearest checkpoint: {str(e)}")
return None
def calculate_distance(lat1, lon1, lat2, lon2):
"""
ハーバーサイン公式を使用して2点間の距離を計算メートル単位
"""
R = 6371000 # 地球の半径(メートル)
lat1_rad = math.radians(lat1)
lat2_rad = math.radians(lat2)
delta_lat = math.radians(lat2 - lat1)
delta_lon = math.radians(lon2 - lon1)
a = (math.sin(delta_lat/2) * math.sin(delta_lat/2) +
math.cos(lat1_rad) * math.cos(lat2_rad) *
math.sin(delta_lon/2) * math.sin(delta_lon/2))
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
distance = R * c
return distance
def create_checkin_from_photo(entry, checkpoint, photo_datetime, zekken_number, event_code, uploaded_file, exif_data, request_id, user):
"""
写真情報からチェックインデータを作成し、S3アップロードとCheckinImages登録も行う
Args:
entry: Entryオブジェクト
checkpoint: Location2025オブジェクト
photo_datetime: 撮影日時
zekken_number: ゼッケン番号
event_code: イベントコード
uploaded_file: アップロードされたファイル
exif_data: EXIF情報辞書
request_id: リクエストID
user: ユーザーオブジェクト
Returns:
dict: {
'gps_checkin': GpsCheckinオブジェクト,
'checkin_image': CheckinImagesオブジェクト,
's3_info': S3アップロード情報,
'created': bool
}
"""
try:
# 既存のチェックインをチェック(重複防止)
existing_checkin = GpsCheckin.objects.filter(
zekken=str(zekken_number),
event_code=event_code,
cp_number=checkpoint.cp_number
).first()
gps_checkin = None
created = False
if existing_checkin:
logger.info(f"[BULK_UPLOAD] 📍 Existing checkin found - ID: {request_id}, CP: {checkpoint.cp_number}, existing_id: {existing_checkin.id}")
gps_checkin = existing_checkin
else:
# 新規チェックインの作成
# 撮影時刻をJSTに変換
if photo_datetime:
# 撮影時刻をUTCとして扱い、JSTに変換
create_at = timezone.make_aware(photo_datetime, timezone.utc)
else:
create_at = timezone.now()
# シリアル番号を決定(既存のチェックイン数+1
existing_count = GpsCheckin.objects.filter(
zekken=str(zekken_number),
event_code=event_code
).count()
serial_number = existing_count + 1
# チェックインデータを作成
gps_checkin = GpsCheckin.objects.create(
zekken=str(zekken_number),
event_code=event_code,
cp_number=checkpoint.cp_number,
serial_number=serial_number,
create_at=create_at,
update_at=timezone.now(),
validate_location=True, # 写真から作成されたものは自動承認
buy_flag=False,
points=checkpoint.checkin_point if checkpoint.checkin_point else 0,
create_user=f"bulk_upload_{request_id}", # 一括アップロードで作成されたことを示す
update_user=f"bulk_upload_{request_id}",
)
logger.info(f"[BULK_UPLOAD] ✅ Created checkin - ID: {request_id}, checkin_id: {gps_checkin.id}, CP: {checkpoint.cp_number}, time: {create_at}, points: {gps_checkin.points}")
created = True
# S3に写真をアップロード
logger.info(f"[BULK_UPLOAD] 📤 Uploading photo to S3 - ID: {request_id}, CP: {checkpoint.cp_number}")
s3_result = upload_photo_to_s3(
uploaded_file,
event_code,
zekken_number,
checkpoint.cp_number,
request_id
)
checkin_image = None
# S3アップロードが成功した場合も失敗した場合もCheckinImagesレコードを作成
checkin_image = create_checkin_image_record(
gps_checkin,
s3_result['s3_url'], # S3アップロードが失敗した場合はNone
s3_result['s3_key'],
uploaded_file.name,
exif_data,
request_id,
user
)
if s3_result['success']:
logger.info(f"[BULK_UPLOAD] ✅ Photo uploaded to S3 and CheckinImages record created - ID: {request_id}")
else:
logger.warning(f"[BULK_UPLOAD] ⚠️ S3 upload failed but CheckinImages record created with local path - ID: {request_id}, error: {s3_result['error']}")
return {
'gps_checkin': gps_checkin,
'checkin_image': checkin_image,
's3_info': s3_result,
'created': created
}
except Exception as e:
logger.error(f"[BULK_UPLOAD] ❌ Error creating checkin - ID: {request_id}, CP: {checkpoint.cp_number if checkpoint else 'None'}, error: {str(e)}")
return None
@api_view(['POST', 'GET'])
@permission_classes([IsAuthenticated])
@parser_classes([MultiPartParser, FormParser])
def bulk_upload_checkin_photos(request):
"""
スマホアルバムから複数の写真を一括アップロードし、EXIF情報から通過履歴を校正する
パラメータ:
- event_code: イベントコード (必須)
- zekken_number: ゼッケン番号 (必須)
- photos: アップロードする写真ファイルのリスト (必須)
- auto_process: 自動処理を行うかどうか (デフォルト: true)
"""
# リクエストID生成デバッグ用
request_id = str(uuid.uuid4())[:8]
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
logger.info(f"[BULK_UPLOAD] 🎯 API ACCESS CONFIRMED - bulk_upload_checkin_photos called successfully - ID: {request_id}, Method: {request.method}, User: {request.user.email if request.user.is_authenticated else 'Anonymous'}, Client IP: {client_ip}")
logger.info(f"[BULK_UPLOAD] 🔍 Request details - Content-Type: {request.content_type}, POST data keys: {list(request.POST.keys())}, FILES count: {len(request.FILES)}")
# GETリクエストの場合は、APIが動作していることを確認するための情報を返す
if request.method == 'GET':
logger.info(f"[BULK_UPLOAD] 📋 GET request received - returning API status")
return Response({
"status": "ACTIVE",
"message": "一括写真アップロードAPIが動作中です",
"endpoint": "/api/bulk_upload_checkin_photos/",
"method": "POST",
"required_params": ["event_code", "zekken_number", "photos"],
"optional_params": ["auto_process", "skip_team_validation"],
"features": [
"写真からEXIF情報を抽出",
"GPS座標から最寄りチェックポイントを自動検索",
"撮影時刻を使用してチェックインデータを自動作成",
"チーム検証機能"
],
"user": request.user.email if request.user.is_authenticated else 'Not authenticated'
}, status=status.HTTP_200_OK)
try:
# リクエストデータの取得
event_code = request.POST.get('event_code')
zekken_number = request.POST.get('zekken_number')
auto_process = request.POST.get('auto_process', 'true').lower() == 'true'
skip_team_validation = request.POST.get('skip_team_validation', 'false').lower() == 'true'
# アップロードされた写真ファイルの取得
uploaded_files = request.FILES.getlist('photos')
logger.info(f"[BULK_UPLOAD] 📝 Request data - ID: {request_id}, event_code: '{event_code}', zekken_number: '{zekken_number}', files_count: {len(uploaded_files)}, auto_process: {auto_process}, skip_team_validation: {skip_team_validation}")
# 必須パラメータの検証
if not all([event_code, zekken_number]):
logger.warning(f"[BULK_UPLOAD] ❌ Missing required parameters - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "イベントコードとゼッケン番号が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
if not uploaded_files:
logger.warning(f"[BULK_UPLOAD] ❌ No files uploaded - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "アップロードする写真が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
# ファイル数制限の確認
max_files = getattr(settings, 'BULK_UPLOAD_MAX_FILES', 50)
if len(uploaded_files) > max_files:
logger.warning(f"[BULK_UPLOAD] ❌ Too many files - ID: {request_id}, count: {len(uploaded_files)}, max: {max_files}")
return Response({
"status": "ERROR",
"message": f"一度にアップロードできる写真は最大{max_files}枚です"
}, status=status.HTTP_400_BAD_REQUEST)
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"[BULK_UPLOAD] ❌ Event not found - ID: {request_id}, event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[BULK_UPLOAD] ✅ Event found - ID: {request_id}, event: '{event_code}', event_id: {event.id}")
# チームの存在確認とオーナー権限の検証
# zekken_numberは文字列と数値の両方で検索を試行
entry = None
# まず数値として検索
if zekken_number.isdigit():
entry = Entry.objects.filter(
event=event,
zekken_number=int(zekken_number)
).select_related('team').first()
# 見つからない場合は文字列として検索
if not entry:
entry = Entry.objects.filter(
event=event,
zekken_label=zekken_number
).select_related('team').first()
# さらに見つからない場合は文字列での zekken_number 検索
if not entry:
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).select_related('team').first()
logger.info(f"[BULK_UPLOAD] 🔍 Team search - ID: {request_id}, searching for zekken: '{zekken_number}', found entry: {entry.id if entry else 'None'}")
# チーム検証のスキップまたは失敗処理
if not entry and not skip_team_validation:
logger.warning(f"[BULK_UPLOAD] ❌ Team not found - ID: {request_id}, zekken_number: '{zekken_number}', event_code: '{event_code}'")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
elif not entry and skip_team_validation:
logger.warning(f"[BULK_UPLOAD] ⚠️ Team not found but validation skipped - ID: {request_id}, zekken_number: '{zekken_number}', event_code: '{event_code}'")
# ダミーエントリ情報を作成(テスト用)
entry = type('Entry', (), {
'id': f'test_{request_id}',
'team': type('Team', (), {
'team_name': f'Test Team {zekken_number}',
'owner': request.user
})(),
'event': event
})()
logger.info(f"[BULK_UPLOAD] 🧪 Using test entry - ID: {request_id}, test_entry_id: {entry.id}")
if hasattr(entry, 'id') and str(entry.id).startswith('test_'):
logger.info(f"[BULK_UPLOAD] ✅ Test team found - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}, test_entry_id: {entry.id}")
else:
logger.info(f"[BULK_UPLOAD] ✅ Team found - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}, entry_id: {entry.id}")
# オーナー権限の確認 (テストモードではスキップ)
if not skip_team_validation and hasattr(entry, 'owner') and entry.owner != request.user:
logger.warning(f"[BULK_UPLOAD] ❌ Permission denied - ID: {request_id}, user: {request.user.email}, team_owner: {entry.owner.email}")
return Response({
"status": "ERROR",
"message": "このチームの写真をアップロードする権限がありません"
}, status=status.HTTP_403_FORBIDDEN)
# 写真処理の準備
processed_files = []
successful_uploads = 0
failed_uploads = 0
# アップロードディレクトリの準備
upload_dir = f"bulk_checkin_photos/{event_code}/{zekken_number}/"
os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_dir), exist_ok=True)
with transaction.atomic():
for index, uploaded_file in enumerate(uploaded_files):
file_result = {
"filename": uploaded_file.name,
"file_index": index + 1,
"file_size": uploaded_file.size,
"upload_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S")
}
try:
# ファイル形式の確認
allowed_extensions = ['.jpg', '.jpeg', '.png', '.heic']
file_extension = os.path.splitext(uploaded_file.name)[1].lower()
if file_extension not in allowed_extensions:
file_result.update({
"status": "failed",
"error": f"サポートされていないファイル形式: {file_extension}"
})
failed_uploads += 1
processed_files.append(file_result)
continue
# ファイルサイズの確認
max_size = getattr(settings, 'BULK_UPLOAD_MAX_FILE_SIZE', 10 * 1024 * 1024) # 10MB
if uploaded_file.size > max_size:
file_result.update({
"status": "failed",
"error": f"ファイルサイズが大きすぎます: {uploaded_file.size / (1024*1024):.1f}MB"
})
failed_uploads += 1
processed_files.append(file_result)
continue
# ファイルの保存
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
safe_filename = f"{timestamp}_{index+1:03d}_{uploaded_file.name}"
file_path = os.path.join(upload_dir, safe_filename)
# ファイル保存
saved_path = default_storage.save(file_path, uploaded_file)
full_path = os.path.join(settings.MEDIA_ROOT, saved_path)
file_result.update({
"status": "uploaded",
"saved_path": saved_path,
"file_url": f"{settings.MEDIA_URL}{saved_path}"
})
# EXIF情報の抽出とチェックイン作成
if auto_process:
logger.info(f"[BULK_UPLOAD] 🔍 Starting EXIF processing for {uploaded_file.name}")
# ファイルポインタを先頭に戻す
uploaded_file.seek(0)
# EXIF情報の抽出
exif_data = extract_exif_data(uploaded_file)
if exif_data['has_gps']:
logger.info(f"[BULK_UPLOAD] 📍 GPS data found - lat: {exif_data['latitude']}, lon: {exif_data['longitude']}, datetime: {exif_data['datetime']}")
# 最も近いチェックポイントを検索
nearest_checkpoint = find_nearest_checkpoint(
exif_data['latitude'],
exif_data['longitude'],
event_code
)
if nearest_checkpoint:
# ファイルポインタを先頭に戻すS3アップロード用
uploaded_file.seek(0)
# チェックインデータを作成S3アップロードとCheckinImages登録も含む
checkin_result = create_checkin_from_photo(
entry,
nearest_checkpoint,
exif_data['datetime'],
zekken_number,
event_code,
uploaded_file,
exif_data,
request_id,
request.user
)
if checkin_result and checkin_result['gps_checkin']:
gps_checkin = checkin_result['gps_checkin']
checkin_image = checkin_result['checkin_image']
s3_info = checkin_result['s3_info']
file_result.update({
"auto_process_status": "success",
"auto_process_message": f"チェックイン作成完了",
"checkin_info": {
"checkpoint_name": nearest_checkpoint.location,
"cp_number": nearest_checkpoint.cp_number,
"points": gps_checkin.points,
"checkin_time": gps_checkin.create_at.strftime("%Y-%m-%d %H:%M:%S"),
"checkin_id": gps_checkin.id,
"was_existing": not checkin_result['created']
},
"s3_info": {
"uploaded": s3_info['success'],
"s3_url": s3_info['s3_url'],
"error": s3_info['error']
},
"checkin_image_info": {
"created": checkin_image is not None,
"image_id": checkin_image.id if checkin_image else None
},
"gps_info": {
"latitude": exif_data['latitude'],
"longitude": exif_data['longitude'],
"photo_datetime": exif_data['datetime'].strftime("%Y-%m-%d %H:%M:%S") if exif_data['datetime'] else None
}
})
else:
file_result.update({
"auto_process_status": "failed",
"auto_process_message": "チェックイン作成に失敗しました",
"checkpoint_info": {
"checkpoint_name": nearest_checkpoint.location,
"cp_number": nearest_checkpoint.cp_number
}
})
else:
file_result.update({
"auto_process_status": "no_checkpoint",
"auto_process_message": "近くにチェックポイントが見つかりませんでした",
"gps_info": {
"latitude": exif_data['latitude'],
"longitude": exif_data['longitude'],
"photo_datetime": exif_data['datetime'].strftime("%Y-%m-%d %H:%M:%S") if exif_data['datetime'] else None
}
})
else:
file_result.update({
"auto_process_status": "no_gps",
"auto_process_message": "GPS情報が見つかりませんでした",
"exif_info": {
"has_datetime": exif_data['datetime'] is not None,
"photo_datetime": exif_data['datetime'].strftime("%Y-%m-%d %H:%M:%S") if exif_data['datetime'] else None
}
})
successful_uploads += 1
logger.info(f"[BULK_UPLOAD] ✅ File uploaded - ID: {request_id}, filename: {uploaded_file.name}, size: {uploaded_file.size}")
except Exception as file_error:
file_result.update({
"status": "failed",
"error": f"ファイル処理エラー: {str(file_error)}"
})
failed_uploads += 1
logger.error(f"[BULK_UPLOAD] ❌ File processing error - ID: {request_id}, filename: {uploaded_file.name}, error: {str(file_error)}")
processed_files.append(file_result)
# 処理結果のサマリー
logger.info(f"[BULK_UPLOAD] ✅ Upload completed - ID: {request_id}, successful: {successful_uploads}, failed: {failed_uploads}")
# 成功レスポンス
return Response({
"status": "OK",
"message": "写真の一括アップロードとチェックイン処理が完了しました",
"upload_summary": {
"total_files": len(uploaded_files),
"successful_uploads": successful_uploads,
"failed_uploads": failed_uploads,
"upload_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S")
},
"team_info": {
"team_name": entry.team.team_name,
"zekken_number": zekken_number,
"event_code": event_code
},
"processed_files": processed_files,
"auto_process_enabled": auto_process,
"processing_summary": {
"gps_found": len([f for f in processed_files if f.get('auto_process_status') == 'success']),
"checkins_created": len([f for f in processed_files if f.get('checkin_info')]),
"no_gps": len([f for f in processed_files if f.get('auto_process_status') == 'no_gps']),
"no_checkpoint": len([f for f in processed_files if f.get('auto_process_status') == 'no_checkpoint'])
}
})
except Exception as e:
logger.error(f"[BULK_UPLOAD] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_bulk_upload_status(request):
"""
一括アップロードの処理状況を取得する
パラメータ:
- event_code: イベントコード (必須)
- zekken_number: ゼッケン番号 (必須)
"""
request_id = str(uuid.uuid4())[:8]
logger.info(f"[BULK_STATUS] 🎯 API call started - ID: {request_id}, User: {request.user.email}")
try:
event_code = request.GET.get('event_code')
zekken_number = request.GET.get('zekken_number')
if not all([event_code, zekken_number]):
return Response({
"status": "ERROR",
"message": "イベントコードとゼッケン番号が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
# チーム権限の確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
entry = Entry.objects.filter(event=event, team__zekken_number=zekken_number).first()
if not entry or entry.owner != request.user:
return Response({
"status": "ERROR",
"message": "このチームの情報にアクセスする権限がありません"
}, status=status.HTTP_403_FORBIDDEN)
# TODO: 実際の処理状況を取得
# TODO: アップロードされたファイル一覧
# TODO: EXIF解析状況
# TODO: 自動チェックイン生成状況
return Response({
"status": "OK",
"team_info": {
"team_name": entry.team.team_name,
"zekken_number": zekken_number,
"event_code": event_code
},
"upload_status": {
"total_uploaded_files": 0,
"processed_files": 0,
"pending_files": 0,
"auto_checkins_generated": 0,
"manual_review_required": 0
},
"implementation_status": "基本機能実装完了、詳細処理は今後実装予定"
})
except Exception as e:
logger.error(f"[BULK_STATUS] 💥 Unexpected error - ID: {request_id}, error: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -0,0 +1,307 @@
"""
競技状態管理API
サーバーAPI変更要求書20250904.mdに基づく実装
Author: システム開発チーム
Date: 2025-09-04
"""
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
from django.utils import timezone
from rog.models import NewEvent2, Entry, Location2025, GpsLog
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
import logging
logger = logging.getLogger(__name__)
@api_view(['GET'])
def competition_status(request):
"""
競技状態取得API
パラメータ:
- event_code: イベントコード
- zekken_number: ゼッケン番号
レスポンス:
{
"status": "OK",
"data": {
"is_in_rog": true,
"rogaining_counted": false,
"ready_for_goal": false,
"is_at_goal": false,
"start_time": "2025-09-04T09:00:00+09:00",
"goal_time": null,
"last_checkin_time": "2025-09-04T09:30:00+09:00"
}
}
"""
logger.info("competition_status API called")
event_code = request.query_params.get('event_code')
zekken_number = request.query_params.get('zekken_number')
logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}")
# パラメータ検証
if not event_code or not zekken_number:
logger.warning("Missing required parameters")
return Response({
"status": "ERROR",
"message": "イベントコードとゼッケン番号が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
try:
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"Event not found: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# エントリーの存在確認
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のエントリーが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# 競技状態データを返す
data = {
"is_in_rog": entry.is_in_rog,
"rogaining_counted": entry.rogaining_counted,
"ready_for_goal": entry.ready_for_goal,
"is_at_goal": entry.is_at_goal,
"start_time": entry.start_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.start_time else None,
"goal_time": entry.goal_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.goal_time else None,
"last_checkin_time": entry.last_checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") if entry.last_checkin_time else None
}
logger.info(f"Competition status retrieved for zekken {zekken_number} in event {event_code}")
return Response({
"status": "OK",
"data": data
})
except Exception as e:
logger.error(f"Error in competition_status: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['POST'])
def update_competition_status(request):
"""
競技状態更新API
パラメータ:
{
"event_code": "EVENT2025",
"zekken_number": "001",
"is_in_rog": true,
"rogaining_counted": false,
"ready_for_goal": false,
"is_at_goal": false
}
レスポンス:
{
"status": "OK",
"message": "Competition status updated successfully",
"updated_at": "2025-09-04T10:00:00+09:00"
}
"""
logger.info("update_competition_status API called")
event_code = request.data.get('event_code')
zekken_number = request.data.get('zekken_number')
is_in_rog = request.data.get('is_in_rog')
rogaining_counted = request.data.get('rogaining_counted')
ready_for_goal = request.data.get('ready_for_goal')
is_at_goal = request.data.get('is_at_goal')
logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}")
# パラメータ検証
if not event_code or not zekken_number:
logger.warning("Missing required parameters")
return Response({
"status": "ERROR",
"message": "イベントコードとゼッケン番号が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
try:
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"Event not found: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# エントリーの存在確認
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のエントリーが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# トランザクション内で状態更新
with transaction.atomic():
# 状態の更新Noneでない値のみ更新
if is_in_rog is not None:
entry.is_in_rog = is_in_rog
if rogaining_counted is not None:
entry.rogaining_counted = rogaining_counted
if ready_for_goal is not None:
entry.ready_for_goal = ready_for_goal
if is_at_goal is not None:
entry.is_at_goal = is_at_goal
entry.save()
update_time = timezone.now()
logger.info(f"Competition status updated for zekken {zekken_number} in event {event_code}")
return Response({
"status": "OK",
"message": "Competition status updated successfully",
"updated_at": update_time.strftime("%Y-%m-%dT%H:%M:%S%z")
})
except Exception as e:
logger.error(f"Error in update_competition_status: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
def checkpoint_status(request):
"""
チェックポイント状態取得API
パラメータ:
- event_code: イベントコード
- zekken_number: ゼッケン番号
- cp_number: チェックポイント番号
レスポンス:
{
"status": "OK",
"data": {
"cp_number": -2,
"is_checked_in": true,
"checkin_time": "2025-09-04T09:00:00+09:00",
"status": "競技中", // "", "競技中", "競技終了"
"points_earned": 0
}
}
"""
logger.info("checkpoint_status API called")
event_code = request.query_params.get('event_code')
zekken_number = request.query_params.get('zekken_number')
cp_number = request.query_params.get('cp_number')
logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}, cp_number={cp_number}")
# パラメータ検証
if not all([event_code, zekken_number, cp_number]):
logger.warning("Missing required parameters")
return Response({
"status": "ERROR",
"message": "イベントコード、ゼッケン番号、チェックポイント番号が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
try:
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"Event not found: {event_code}")
return Response({
"status": "ERROR",
"message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# エントリーの存在確認
entry = Entry.objects.filter(
event=event,
zekken_number=zekken_number
).first()
if not entry:
logger.warning(f"Entry not found: zekken_number={zekken_number}, event={event_code}")
return Response({
"status": "ERROR",
"message": "指定されたゼッケン番号のエントリーが見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# チェックイン状況の確認
checkin_record = GpsLog.objects.filter(
zekken_number=zekken_number,
event_code=event_code,
cp_number=cp_number
).first()
# チェックポイント情報の取得
checkpoint = Location2025.objects.filter(
event=event,
cp_number=cp_number
).first()
# 競技状況の判定
competition_status = ""
if entry.is_at_goal:
competition_status = "競技終了"
elif entry.is_in_rog:
competition_status = "競技中"
# レスポンスデータ構築
data = {
"cp_number": int(cp_number),
"is_checked_in": bool(checkin_record),
"checkin_time": checkin_record.checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z") if checkin_record else None,
"status": competition_status,
"points_earned": checkpoint.point if checkpoint else 0
}
logger.info(f"Checkpoint status retrieved for CP {cp_number}, zekken {zekken_number} in event {event_code}")
return Response({
"status": "OK",
"data": data
})
except Exception as e:
logger.error(f"Error in checkpoint_status: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -1,13 +1,15 @@
# 既存のインポート部分に追加 # 既存のインポート部分に追加
from datetime import datetime, timezone from datetime import datetime
from django.utils import timezone
# from sqlalchemy import Transaction # 削除 - SQLAlchemy 2.0では利用不可 # from sqlalchemy import Transaction # 削除 - SQLAlchemy 2.0では利用不可
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rog.models import Location, NewEvent2, Entry, GpsLog from rog.models import Location2025, NewEvent2, Entry, GpsLog, GpsCheckin
import logging import logging
import uuid import uuid
import os import os
import json
from django.db.models import F, Q from django.db.models import F, Q
from django.db import transaction from django.db import transaction
from django.conf import settings from django.conf import settings
@ -16,6 +18,12 @@ from urllib.parse import urljoin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_next_serial_number():
"""次のserial_numberを取得"""
from django.db.models import Max
max_serial = GpsLog.objects.aggregate(Max('serial_number'))['serial_number__max']
return (max_serial or 0) + 1
""" """
解説 解説
この実装では以下の処理を行っています: この実装では以下の処理を行っています:
@ -42,18 +50,24 @@ def remove_checkin_from_rogapp(request):
- team_name: チーム名 - team_name: チーム名
- cp_number: チェックポイント番号 - cp_number: チェックポイント番号
""" """
logger.info("remove_checkin_from_rogapp called") # ログ用のリクエストID生成
request_id = uuid.uuid4().hex[:8]
request_time = timezone.now()
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
user_info = f"{request.user.username}({request.user.id})" if request.user.is_authenticated else "Anonymous"
# リクエストからパラメータを取得 # リクエストからパラメータを取得
event_code = request.data.get('event_code') event_code = request.data.get('event_code')
team_name = request.data.get('team_name') team_name = request.data.get('team_name')
cp_number = request.data.get('cp_number') cp_number = request.data.get('cp_number')
logger.info(f"[REMOVE_CHECKIN] 🗑️ API call started - ID: {request_id}, event_code: '{event_code}', team_name: '{team_name}', cp_number: {cp_number}, Client IP: {client_ip}, User: {user_info}")
logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}, cp_number={cp_number}") logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}, cp_number={cp_number}")
# パラメータ検証 # パラメータ検証
if not all([event_code, team_name, cp_number]): if not all([event_code, team_name, cp_number]):
logger.warning("Missing required parameters") logger.error(f"[REMOVE_CHECKIN] ❌ Missing required parameters - ID: {request_id}, event_code: '{event_code}', team_name: '{team_name}', cp_number: {cp_number}, Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "イベントコード、チーム名、チェックポイント番号が必要です" "message": "イベントコード、チーム名、チェックポイント番号が必要です"
@ -63,44 +77,64 @@ def remove_checkin_from_rogapp(request):
# イベントの存在確認 # イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first() event = NewEvent2.objects.filter(event_name=event_code).first()
if not event: if not event:
logger.warning(f"Event not found: {event_code}") logger.error(f"[REMOVE_CHECKIN] ❌ Event not found - ID: {request_id}, event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたイベントが見つかりません" "message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[REMOVE_CHECKIN] ✅ Event found - ID: {request_id}, event: '{event.event_name}', event_id: {event.id}")
# チームの存在確認 # チームの存在確認
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
team_name=team_name team__team_name=team_name
).first() ).first()
if not entry: if not entry:
logger.warning(f"Team not found: {team_name} in event: {event_code}") logger.error(f"[REMOVE_CHECKIN] ❌ Team not found - ID: {request_id}, team_name: '{team_name}', event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたチームが見つかりません" "message": "指定されたチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[REMOVE_CHECKIN] ✅ Team found - ID: {request_id}, team_name: '{team_name}', zekken: {entry.team.zekken_number}, entry_id: {entry.id}")
# 対象のチェックポイント記録を検索 # 対象のチェックポイント記録を検索
checkpoint = GpsLog.objects.filter( checkpoint = GpsLog.objects.filter(
entry=entry, zekken_number=entry.team.zekken_number,
event_code=event_code,
cp_number=cp_number cp_number=cp_number
).first() ).first()
if not checkpoint: if not checkpoint:
logger.warning(f"Checkpoint {cp_number} not found for team: {team_name}") logger.warning(f"[REMOVE_CHECKIN] ⚠️ Checkpoint not found - ID: {request_id}, team_name: '{team_name}', zekken: {entry.team.zekken_number}, cp_number: {cp_number}, Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたチェックポイント記録が見つかりません" "message": "指定されたチェックポイント記録が見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[REMOVE_CHECKIN] ✅ Checkpoint found for removal - ID: {request_id}, checkpoint_id: {checkpoint.id}, checkin_time: {checkpoint.checkin_time}, has_image: {bool(checkpoint.image_address)}")
# チェックポイント記録を削除 # チェックポイント記録を削除
checkin_time = checkpoint.checkin_time checkin_time = checkpoint.checkin_time
checkpoint_id = checkpoint.id checkpoint_id = checkpoint.id
image_address = checkpoint.image_address
with transaction.atomic():
# 拡張情報も一緒に削除(存在する場合)
try:
from ..models import CheckinExtended
extended_info = CheckinExtended.objects.filter(gpslog=checkpoint).first()
if extended_info:
extended_info.delete()
logger.info(f"[REMOVE_CHECKIN] ✅ Extended info also removed - ID: {request_id}, checkpoint_id: {checkpoint_id}")
except Exception as ext_error:
logger.warning(f"[REMOVE_CHECKIN] ⚠️ Failed to remove extended info - ID: {request_id}, Error: {ext_error}")
checkpoint.delete() checkpoint.delete()
logger.info(f"Successfully removed CP {cp_number} for team: {team_name} in event: {event_code}") logger.success(f"[REMOVE_CHECKIN] 🎉 Successfully removed checkpoint - ID: {request_id}, team_name: '{team_name}', zekken: {entry.team.zekken_number}, cp_number: {cp_number}, checkpoint_id: {checkpoint_id}, had_image: {bool(image_address)}, Client IP: {client_ip}")
return Response({ return Response({
"status": "OK", "status": "OK",
@ -112,7 +146,7 @@ def remove_checkin_from_rogapp(request):
}) })
except Exception as e: except Exception as e:
logger.error(f"Error in remove_checkin_from_rogapp: {str(e)}") logger.error(f"[REMOVE_CHECKIN] 💥 ERROR - ID: {request_id}, team_name: '{team_name}', cp_number: {cp_number}, Client IP: {client_ip}, Error: {str(e)}", exc_info=True)
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "サーバーエラーが発生しました" "message": "サーバーエラーが発生しました"
@ -149,17 +183,22 @@ def start_checkin(request):
- event: イベントコード - event: イベントコード
- zekken: ゼッケン番号 - zekken: ゼッケン番号
""" """
logger.info("start_checkin called") # リクエスト詳細情報を取得
client_ip = request.META.get('REMOTE_ADDR', 'Unknown')
user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
user_info = request.user.email if request.user.is_authenticated else 'Anonymous'
logger.info(f"[ADMIN_START] API called - Client IP: {client_ip}, User: {user_info}")
# リクエストからパラメータを取得 # リクエストからパラメータを取得
event_code = request.query_params.get('event') event_code = request.query_params.get('event')
zekken_number = request.query_params.get('zekken') zekken_number = request.query_params.get('zekken')
logger.debug(f"Parameters: event={event_code}, zekken={zekken_number}") logger.info(f"[ADMIN_START] Request parameters - event_code: {event_code}, zekken_number: {zekken_number}, user_agent: {user_agent[:100]}")
# パラメータ検証 # パラメータ検証
if not all([event_code, zekken_number]): if not all([event_code, zekken_number]):
logger.warning("Missing required parameters") logger.warning(f"[ADMIN_START] Missing required parameters - event_code: {event_code}, zekken_number: {zekken_number}, Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "イベントコードとゼッケン番号が必要です" "message": "イベントコードとゼッケン番号が必要です"
@ -188,33 +227,43 @@ def start_checkin(request):
"message": "指定されたゼッケン番号のチームが見つかりません" "message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# 既にスタート済みかチェック # 既にスタート済みかチェックGpsLogでSTARTレコードを確認
if hasattr(entry, 'start_info'): existing_start = GpsLog.objects.filter(
logger.warning(f"Team {entry.team_name} (zekken: {zekken_number}) already started at {entry.start_info.start_time}") zekken_number=zekken_number,
event_code=event_code,
cp_number="START",
serial_number=0
).first()
if existing_start:
logger.warning(f"Team {entry.team.team_name} (zekken: {zekken_number}) already started at {existing_start.checkin_time}")
return Response({ return Response({
"status": "WARNING", "status": "WARNING",
"message": "このチームは既にスタートしています", "message": "このチームは既にスタートしています",
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S"), "start_time": existing_start.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
"team_name": entry.team_name "team_name": entry.team.team_name
}) })
# トランザクション開始 # トランザクション開始
with transaction.atomic(): with transaction.atomic():
# スタート情報を登録 # スタート情報をGpsLogとして登録
start_info = TeamStart.objects.create( start_info = GpsLog.objects.create(
entry=entry, zekken_number=zekken_number,
start_time=timezone.now() event_code=event_code,
cp_number="START",
serial_number=0,
checkin_time=timezone.now()
) )
logger.info(f"Team {entry.team_name} (zekken: {zekken_number}) started at {start_info.start_time}") logger.info(f"Team {entry.team.team_name} (zekken: {zekken_number}) started at {start_info.checkin_time}")
return Response({ return Response({
"status": "OK", "status": "OK",
"message": "スタート処理が完了しました", "message": "スタート処理が完了しました",
"team_name": entry.team_name, "team_name": entry.team.team_name,
"zekken_number": zekken_number, "zekken_number": zekken_number,
"event_code": event_code, "event_code": event_code,
"start_time": start_info.start_time.strftime("%Y-%m-%d %H:%M:%S") "start_time": start_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S")
}) })
except Exception as e: except Exception as e:
@ -251,18 +300,23 @@ def add_checkin(request):
- zekken: ゼッケン番号 - zekken: ゼッケン番号
- list: カンマ区切りのチェックポイント番号リスト - list: カンマ区切りのチェックポイント番号リスト
""" """
logger.info("add_checkin called") # ログ用のリクエストID生成
request_id = uuid.uuid4().hex[:8]
request_time = timezone.now()
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
user_info = f"{request.user.username}({request.user.id})" if request.user.is_authenticated else "Anonymous"
# リクエストからパラメータを取得 # リクエストからパラメータを取得
event_code = request.query_params.get('event') event_code = request.query_params.get('event')
zekken_number = request.query_params.get('zekken') zekken_number = request.query_params.get('zekken')
cp_list_string = request.query_params.get('list') cp_list_string = request.query_params.get('list')
logger.debug(f"Parameters: event={event_code}, zekken={zekken_number}, list={cp_list_string}") logger.info(f"[ADMIN_CHECKIN] 📝 Bulk checkin API call started - ID: {request_id}, event_code: '{event_code}', zekken_number: {zekken_number}, cp_list: '{cp_list_string}', Client IP: {client_ip}, User: {user_info}, User-Agent: {user_agent[:100]}")
# パラメータ検証 # パラメータ検証
if not all([event_code, zekken_number, cp_list_string]): if not all([event_code, zekken_number, cp_list_string]):
logger.warning("Missing required parameters") logger.error(f"[ADMIN_CHECKIN] ❌ Missing required parameters - ID: {request_id}, event_code: '{event_code}', zekken_number: {zekken_number}, cp_list: '{cp_list_string}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "イベントコード、ゼッケン番号、チェックポイントリストが必要です" "message": "イベントコード、ゼッケン番号、チェックポイントリストが必要です"
@ -272,12 +326,14 @@ def add_checkin(request):
# イベントの存在確認 # イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first() event = NewEvent2.objects.filter(event_name=event_code).first()
if not event: if not event:
logger.warning(f"Event not found: {event_code}") logger.error(f"[ADMIN_CHECKIN] ❌ Event not found - ID: {request_id}, event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたイベントが見つかりません" "message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[ADMIN_CHECKIN] ✅ Event found - ID: {request_id}, event: '{event.event_name}', event_id: {event.id}")
# チームの存在確認 # チームの存在確認
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
@ -285,19 +341,33 @@ def add_checkin(request):
).first() ).first()
if not entry: if not entry:
logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}") logger.error(f"[ADMIN_CHECKIN] ❌ Team not found - ID: {request_id}, zekken: {zekken_number}, event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません" "message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[ADMIN_CHECKIN] ✅ Team found - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}, entry_id: {entry.id}")
# チームがスタートしているか確認(オプション) # チームがスタートしているか確認(オプション)
if not hasattr(entry, 'start_info'): start_record = GpsLog.objects.filter(
zekken_number=zekken_number,
event_code=event_code,
cp_number="START",
serial_number=0
).first()
if not start_record:
# スタート情報がない場合は自動的にスタートさせる # スタート情報がない場合は自動的にスタートさせる
# 注意: 管理画面からの操作なので、自動スタートを許可 # 注意: 管理画面からの操作なので、自動スタートを許可
from rog.models import TeamStart GpsLog.objects.create(
TeamStart.objects.create(entry=entry, start_time=timezone.now()) serial_number=get_next_serial_number(),
logger.info(f"Auto-started team {entry.team_name} (zekken: {zekken_number})") zekken_number=zekken_number,
event_code=event_code,
cp_number="START",
checkin_time=timezone.now()
)
logger.info(f"[ADMIN_CHECKIN] ✅ Auto-started team - ID: {request_id}, team_name: '{entry.team.team_name}', zekken: {zekken_number}")
# チェックポイントリストを解析 # チェックポイントリストを解析
cp_list = [cp.strip() for cp in cp_list_string.split(',') if cp.strip()] cp_list = [cp.strip() for cp in cp_list_string.split(',') if cp.strip()]
@ -320,7 +390,8 @@ def add_checkin(request):
for cp_number in cp_list: for cp_number in cp_list:
# 既に同じCPを登録済みかチェック # 既に同じCPを登録済みかチェック
existing_checkpoint = GpsLog.objects.filter( existing_checkpoint = GpsLog.objects.filter(
entry=entry, zekken_number=zekken_number,
event_code=event_code,
cp_number=cp_number cp_number=cp_number
).first() ).first()
@ -332,8 +403,8 @@ def add_checkin(request):
# イベントのチェックポイント定義を確認(必要に応じて) # イベントのチェックポイント定義を確認(必要に応じて)
event_cp = None event_cp = None
try: try:
event_cp = Location.objects.filter( event_cp = Location2025.objects.filter(
event=event, event_id=event.id,
cp_number=cp_number cp_number=cp_number
).first() ).first()
except: except:
@ -341,10 +412,11 @@ def add_checkin(request):
# チェックポイント登録 # チェックポイント登録
checkpoint = GpsLog.objects.create( checkpoint = GpsLog.objects.create(
entry=entry, serial_number=get_next_serial_number(),
zekken_number=zekken_number,
event_code=event_code,
cp_number=cp_number, cp_number=cp_number,
checkin_time=timezone.now(), checkin_time=timezone.now()
is_service_checked=event_cp.is_service_cp if event_cp else False
) )
logger.info(f"Successfully registered CP {cp_number} for team: {entry.team_name} (zekken: {zekken_number})") logger.info(f"Successfully registered CP {cp_number} for team: {entry.team_name} (zekken: {zekken_number})")
@ -725,22 +797,41 @@ def goal_checkin(request):
"message": "指定されたゼッケン番号のチームが見つかりません" "message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# チームがスタートしているか確認 # チームがスタートしているか確認GpsLogでSTARTレコードを確認
if not hasattr(entry, 'start_info'): start_record = GpsLog.objects.filter(
entry=entry,
cp_number="START",
serial_number=0
).first()
if not start_record:
# 管理画面からの操作なので、自動的にスタートさせる # 管理画面からの操作なので、自動的にスタートさせる
from rog.models import TeamStart GpsLog.objects.create(
TeamStart.objects.create(entry=entry, start_time=timezone.now()) entry=entry,
cp_number="START",
serial_number=0,
latitude=0.0,
longitude=0.0,
checkin_time=timezone.now(),
extra_data={"auto_start": True, "source": "admin_goal"}
)
logger.info(f"Auto-started team {entry.team_name} (zekken: {zekken_number})") logger.info(f"Auto-started team {entry.team_name} (zekken: {zekken_number})")
# 既にゴールしているかチェック # 既にゴールしているかチェックGpsLogでGOALレコードを確認
if hasattr(entry, 'goal_info'): existing_goal = GpsLog.objects.filter(
logger.warning(f"Team {entry.team_name} (zekken: {zekken_number}) already reached goal at {entry.goal_info.goal_time}") entry=entry,
cp_number="GOAL",
serial_number=9999
).first()
if existing_goal:
logger.warning(f"Team {entry.team_name} (zekken: {zekken_number}) already reached goal at {existing_goal.checkin_time}")
return Response({ return Response({
"status": "WARNING", "status": "WARNING",
"message": "このチームは既にゴールしています", "message": "このチームは既にゴールしています",
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"), "goal_time": existing_goal.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
"team_name": entry.team_name, "team_name": entry.team_name,
"scoreboard_url": entry.goal_info.scoreboard_url "scoreboard_url": existing_goal.extra_data.get('scoreboard_url', '') if existing_goal.extra_data else ''
}) })
# ゴール時間の処理 # ゴール時間の処理
@ -773,12 +864,18 @@ def goal_checkin(request):
# スコアボードへのURL # スコアボードへのURL
scoreboard_url = f"{settings.MEDIA_URL}scoreboards/{scoreboard_filename}" scoreboard_url = f"{settings.MEDIA_URL}scoreboards/{scoreboard_filename}"
# ゴール情報を登録 # ゴール情報をGpsLogとして登録
goal_info = TeamGoal.objects.create( goal_info = GpsLog.objects.create(
entry=entry, entry=entry,
goal_time=goal_time, cp_number="GOAL",
score=score, serial_number=9999,
scoreboard_url=scoreboard_url latitude=0.0,
longitude=0.0,
checkin_time=goal_time,
extra_data={
"score": score,
"scoreboard_url": scoreboard_url
}
) )
logger.info(f"Team {entry.team_name} (zekken: {zekken_number}) reached goal at {goal_time} with score {score}") logger.info(f"Team {entry.team_name} (zekken: {zekken_number}) reached goal at {goal_time} with score {score}")
@ -804,7 +901,10 @@ def goal_checkin(request):
def calculate_team_score(entry): def calculate_team_score(entry):
"""チームのスコアを計算する補助関数""" """チームのスコアを計算する補助関数"""
# チームが通過したチェックポイントを取得 # チームが通過したチェックポイントを取得
checkpoints = GpsLog.objects.filter(entry=entry) checkpoints = GpsLog.objects.filter(
zekken_number=entry.team.zekken_number,
event_code=entry.event.event_code
)
total_score = 0 total_score = 0
@ -812,9 +912,9 @@ def calculate_team_score(entry):
# チェックポイントの得点を取得 # チェックポイントの得点を取得
cp_point = 0 cp_point = 0
try: try:
# Location モデルが存在する場合はそこから得点を取得 # Location2025 モデルが存在する場合はそこから得点を取得
event_cp = Location.objects.filter( event_cp = Location2025.objects.filter(
event=entry.event, event_id=entry.event.id,
cp_number=cp.cp_number cp_number=cp.cp_number
).first() ).first()
if event_cp: if event_cp:
@ -988,11 +1088,33 @@ def get_checkin_list(request):
""" """
logger.info("get_checkin_list called") logger.info("get_checkin_list called")
# リクエストからパラメータを取得 # リクエストからパラメータを取得GET/POSTの両方に対応
zekken_number = request.query_params.get('zekken') # 両方のパラメータ名に対応
event_code = request.query_params.get('event') if request.method == 'GET':
zekken_number = request.GET.get('zekken_number') or request.GET.get('zekken')
event_code = request.GET.get('event_code') or request.GET.get('event')
else: # POST
try:
data = json.loads(request.body)
zekken_number = data.get('zekken_number') or data.get('zekken')
event_code = data.get('event_code') or data.get('event')
except:
data = request.POST
zekken_number = data.get('zekken_number') or data.get('zekken')
event_code = data.get('event_code') or data.get('event')
logger.debug(f"Parameters: zekken={zekken_number}, event={event_code}") logger.info(f"[GET_CHECKIN_LIST] Request method: {request.method}")
logger.info(f"[GET_CHECKIN_LIST] Parameters received - zekken: {zekken_number}, event: {event_code}")
logger.info(f"[GET_CHECKIN_LIST] Full GET params: {dict(request.GET.items())}")
logger.info(f"[GET_CHECKIN_LIST] Full POST params: {dict(request.POST.items()) if request.method == 'POST' else 'N/A'}")
# POSTの場合のリクエストボディも確認
if request.method == 'POST':
try:
body_content = request.body.decode('utf-8')
logger.info(f"[GET_CHECKIN_LIST] Request body: {body_content}")
except:
logger.info(f"[GET_CHECKIN_LIST] Could not decode request body")
# パラメータ検証 # パラメータ検証
if not all([zekken_number, event_code]): if not all([zekken_number, event_code]):
@ -1013,23 +1135,83 @@ def get_checkin_list(request):
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# チームの存在確認 # チームの存在確認
logger.info(f"[GET_CHECKIN_LIST] Searching for event: {event.event_name} (ID: {event.id})")
logger.info(f"[GET_CHECKIN_LIST] Searching for zekken_number: {zekken_number}")
# まず、このイベントのすべてのEntryを確認
all_entries = Entry.objects.filter(event=event)
logger.info(f"[GET_CHECKIN_LIST] Total entries in event: {all_entries.count()}")
# ゼッケン番号でのチーム検索(複数の方法で試す)
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
zekken_number=zekken_number zekken_number=zekken_number
).first() ).first()
if not entry:
# team__zekken_numberでも試してみる
entry = Entry.objects.filter(
event=event,
team__zekken_number=zekken_number
).first()
if not entry: if not entry:
logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}") logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}")
# デバッグ用:存在するゼッケン番号を表示
existing_zekkens = list(Entry.objects.filter(event=event).values_list('zekken_number', flat=True))
logger.info(f"[GET_CHECKIN_LIST] Existing zekken numbers: {existing_zekkens[:10]}") # 最初の10件のみ
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたゼッケン番号のチームが見つかりません" "message": "指定されたゼッケン番号のチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# チェックイン記録を取得 # チェックイン記録を取得
checkpoints = GpsLog.objects.filter( logger.info(f"[GET_CHECKIN_LIST] Found entry: {entry.team.team_name} (zekken: {entry.zekken_number})")
entry=entry
# 複数のテーブルからチェックイン記録を取得
# GpsLogテーブルから
gps_checkpoints = GpsLog.objects.filter(
zekken_number=str(entry.zekken_number),
event_code=event_code
).order_by('checkin_time') ).order_by('checkin_time')
# GpsCheckinテーブルからも取得
gps_checkins = GpsCheckin.objects.filter(
zekken=str(entry.zekken_number),
event_code=event_code
).order_by('checkin_time')
logger.info(f"[GET_CHECKIN_LIST] Found {gps_checkpoints.count()} records in GpsLog")
logger.info(f"[GET_CHECKIN_LIST] Found {gps_checkins.count()} records in GpsCheckin")
# すべてのチェックイン記録を統合
all_checkpoints = []
# GpsLogからの記録を追加
for cp in gps_checkpoints:
all_checkpoints.append({
'source': 'GpsLog',
'id': cp.id,
'cp_number': cp.cp_number,
'checkin_time': cp.checkin_time,
'image_address': getattr(cp, 'image_address', None),
'is_service_checked': getattr(cp, 'is_service_checked', False)
})
# GpsCheckinからの記録を追加
for cp in gps_checkins:
all_checkpoints.append({
'source': 'GpsCheckin',
'id': cp.id,
'cp_number': cp.cp_number,
'checkin_time': cp.checkin_time,
'image_address': None, # GpsCheckinには画像URLがない
'is_service_checked': False
})
# 時間順にソート
all_checkpoints.sort(key=lambda x: x['checkin_time'] or timezone.now())
# スタート情報を取得 # スタート情報を取得
start_info = None start_info = None
if hasattr(entry, 'start_info'): if hasattr(entry, 'start_info'):
@ -1048,26 +1230,27 @@ def get_checkin_list(request):
# チェックイン記録をシリアライズ # チェックイン記録をシリアライズ
checkpoint_list = [] checkpoint_list = []
for cp in checkpoints: for cp in all_checkpoints:
checkpoint_data = { checkpoint_data = {
"id": cp.id, "id": cp['id'],
"cp_number": cp.cp_number, "cp_number": cp['cp_number'],
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None, "checkin_time": cp['checkin_time'].strftime("%Y-%m-%d %H:%M:%S") if cp['checkin_time'] else None,
"image_url": cp.image_address, "image_url": cp['image_address'],
"is_service_checked": cp.is_service_checked if hasattr(cp, 'is_service_checked') else False "is_service_checked": cp['is_service_checked'],
"source": cp['source']
} }
# チェックポイントの得点情報を取得( Location モデルがある場合) # チェックポイントの得点情報を取得( Location2025 モデルがある場合)
try: try:
event_cp = Location.objects.filter( event_cp = Location2025.objects.filter(
event=event, event_id=event.id,
cp_number=cp.cp_number cp_number=cp['cp_number']
).first() ).first()
if event_cp: if event_cp:
checkpoint_data["cp_point"] = event_cp.cp_point checkpoint_data["cp_point"] = event_cp.cp_point
checkpoint_data["cp_name"] = event_cp.cp_name checkpoint_data["cp_name"] = event_cp.cp_name
checkpoint_data["is_service_cp"] = event_cp.is_service_cp checkpoint_data["is_service_cp"] = event_cp.buy_point > 0 # buy_pointが0より大きい場合はサービスポイント
except: except:
# Location モデルが存在しない場合はスキップ # Location モデルが存在しない場合はスキップ
pass pass
@ -1078,9 +1261,9 @@ def get_checkin_list(request):
return Response({ return Response({
"status": "OK", "status": "OK",
"team_info": { "team_info": {
"team_name": entry.team_name, "team_name": entry.team.team_name,
"zekken_number": zekken_number, "zekken_number": zekken_number,
"class_name": entry.class_name, "class_name": entry.category.category_name,
"event_code": event_code "event_code": event_code
}, },
"start_info": start_info, "start_info": start_info,
@ -1327,15 +1510,15 @@ def get_yet_check_service_list(request):
is_service_cp = False is_service_cp = False
try: try:
event_cp = Location.objects.filter( event_cp = Location2025.objects.filter(
event=event, event_id=event.id,
cp_number=cp.cp_number cp_number=cp.cp_number
).first() ).first()
if event_cp and event_cp.is_service_cp: if event_cp and event_cp.buy_point > 0: # buy_pointが0より大きい場合はサービスポイント
is_service_cp = True is_service_cp = True
except: except:
# Location モデルがない場合は、チェックポイントのプロパティだけで判断 # Location2025 モデルがない場合は、チェックポイントのプロパティだけで判断
pass pass
# サービスチェックが必要なチェックポイントならリストに追加 # サービスチェックが必要なチェックポイントならリストに追加
@ -1343,10 +1526,10 @@ def get_yet_check_service_list(request):
# チェックポイントをリストに追加 # チェックポイントをリストに追加
pending_service_checks.append({ pending_service_checks.append({
"id": cp.id, "id": cp.id,
"team_name": entry.team_name, "team_name": entry.team.team_name,
"zekken_number": entry.zekken_number, "zekken_number": entry.zekken_number,
"cp_number": cp.cp_number, "cp_number": cp.cp_number,
"class_name": entry.class_name, "class_name": entry.category.category_name,
"checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None, "checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None,
"image_url": cp.image_address "image_url": cp.image_address
}) })

View File

@ -2,16 +2,31 @@
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rog.models import NewEvent2, Entry, Location2025 from rog.models import NewEvent2, Entry, Location2025, GpsLog
from rog.models import GpsLog from rog.models import GpsLog
import logging import logging
from django.db.models import F, Q from django.db.models import F, Q, Max
from django.conf import settings from django.conf import settings
import os import os
from urllib.parse import urljoin from urllib.parse import urljoin
from django.db import transaction
from django.utils import timezone
from datetime import datetime
import uuid
import time
from django.http import JsonResponse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# S3アップローダーを安全にインポート
try:
from rog.utils.s3_image_uploader import s3_uploader
S3_AVAILABLE = True
except ImportError as e:
logger.warning(f"S3 uploader not available: {e}")
s3_uploader = None
S3_AVAILABLE = False
""" """
解説 解説
この実装では以下の処理を行っています: この実装では以下の処理を行っています:
@ -82,12 +97,13 @@ def input_cp(request):
# 既に同じCPを登録済みかチェック # 既に同じCPを登録済みかチェック
existing_checkpoint = GpsLog.objects.filter( existing_checkpoint = GpsLog.objects.filter(
entry=entry, zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number=cp_number cp_number=cp_number
).first() ).first()
if existing_checkpoint: if existing_checkpoint:
logger.warning(f"Checkpoint {cp_number} already registered for team: {entry.team_name}") logger.warning(f"Checkpoint {cp_number} already registered for team: {entry.team.team_name}")
return Response({ return Response({
"status": "WARNING", "status": "WARNING",
"message": "このチェックポイントは既に登録されています", "message": "このチェックポイントは既に登録されています",
@ -97,14 +113,26 @@ def input_cp(request):
# トランザクション開始 # トランザクション開始
with transaction.atomic(): with transaction.atomic():
# チェックポイント登録 # チェックポイント登録
# serial_numberを自動生成既存の最大値+1
max_serial = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name
).aggregate(max_serial=Max('serial_number'))['max_serial'] or 0
checkpoint = GpsLog.objects.create( checkpoint = GpsLog.objects.create(
entry=entry, serial_number=max_serial + 1,
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number=cp_number, cp_number=cp_number,
image_address=image_address, image_address=image_address,
checkin_time=timezone.now() checkin_time=timezone.now(),
create_at=timezone.now(),
update_at=timezone.now(),
buy_flag=False,
colabo_company_memo=""
) )
logger.info(f"Successfully registered CP {cp_number} for team: {entry.team_name} " logger.info(f"Successfully registered CP {cp_number} for team: {entry.team.team_name} "
f"with zekken: {zekken_number}") f"with zekken: {zekken_number}")
return Response({ return Response({
@ -172,7 +200,7 @@ def get_checkpoint_list(request):
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# イベントのチェックポイント情報を取得 # イベントのチェックポイント情報を取得
checkpoints = Location2025.objects.filter(event=event).order_by('cp_number') checkpoints = Location2025.objects.filter(event_id=event.id).order_by('cp_number')
checkpoint_list = [] checkpoint_list = []
for cp in checkpoints: for cp in checkpoints:
@ -183,7 +211,7 @@ def get_checkpoint_list(request):
"latitude": cp.latitude, "latitude": cp.latitude,
"longitude": cp.longitude, "longitude": cp.longitude,
"cp_description": cp.description, "cp_description": cp.description,
"is_service_cp": cp.is_service_cp "is_service_cp": cp.buy_point > 0 # buy_pointが0より大きい場合はサービスポイント
} }
checkpoint_list.append(checkpoint_info) checkpoint_list.append(checkpoint_info)
@ -225,78 +253,201 @@ def start_from_rogapp(request):
- event_code: イベントコード - event_code: イベントコード
- team_name: チーム名 - team_name: チーム名
""" """
logger.info("start_from_rogapp called") # リクエスト詳細情報を取得
client_ip = request.META.get('REMOTE_ADDR', 'Unknown')
user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
user_info = request.user.email if request.user.is_authenticated else 'Anonymous'
request_time = timezone.now()
request_id = str(uuid.uuid4())[:8]
# リクエストからパラメータを取得 # 初期ログ出力
event_code = request.data.get('event_code') logger.info(f"[START_API] 🚀 REQUEST_START - ID: {request_id}")
team_name = request.data.get('team_name') logger.info(f"[START_API] Request details - Time: {request_time}, Client: {client_ip}, User: {user_info}")
logger.info(f"[START_API] Headers - User-Agent: {user_agent[:200]}")
logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}") logger.info(f"[START_API] Content-Type: {request.content_type}")
logger.info(f"[START_API] Method: {request.method}")
# パラメータ検証
if not all([event_code, team_name]):
logger.warning("Missing required parameters")
return Response({
"status": "ERROR",
"message": "イベントコードとチーム名が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
try: try:
# リクエストデータの解析
logger.info(f"[START_API] Raw request body type: {type(request.body)}")
logger.info(f"[START_API] Raw request body length: {len(request.body) if request.body else 0}")
# リクエストからパラメータを取得
logger.info(f"[START_API] Attempting to parse request data...")
event_code = request.data.get('event_code')
team_name = request.data.get('team_name')
latitude = request.data.get('latitude', 0.0)
longitude = request.data.get('longitude', 0.0)
extra_data = request.data.get('extra_data', {})
logger.info(f"[START_API] Parameters parsed - ID: {request_id}")
logger.info(f"[START_API] event_code: '{event_code}' (type: {type(event_code)})")
logger.info(f"[START_API] team_name: '{team_name}' (type: {type(team_name)})")
logger.info(f"[START_API] GPS: lat={latitude}, lon={longitude}")
logger.info(f"[START_API] extra_data: {extra_data}")
# パラメータ検証
if not event_code:
logger.warning(f"[START_API] ❌ Missing event_code - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "イベントコードが必要です"
}, status=status.HTTP_400_BAD_REQUEST)
if not team_name:
logger.warning(f"[START_API] ❌ Missing team_name - ID: {request_id}")
return Response({
"status": "ERROR",
"message": "チーム名が必要です"
}, status=status.HTTP_400_BAD_REQUEST)
logger.info(f"[START_API] ✅ Parameter validation passed - ID: {request_id}")
# データベース操作開始
logger.info(f"[START_API] Starting database operations - ID: {request_id}")
# イベントの存在確認 # イベントの存在確認
logger.info(f"[START_API] Searching for event: '{event_code}' - ID: {request_id}")
event = NewEvent2.objects.filter(event_name=event_code).first() event = NewEvent2.objects.filter(event_name=event_code).first()
if not event: if not event:
logger.warning(f"Event not found: {event_code}") logger.warning(f"[START_API] ❌ Event not found - ID: {request_id}, event_code: '{event_code}'")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたイベントが見つかりません" "message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[START_API] ✅ Event found - ID: {request_id}, Event ID: {event.id}, Name: '{event.event_name}'")
logger.info(f"[START_API] Event details - Start: {event.start_datetime}, End: {event.end_datetime}")
# チームの存在確認 # チームの存在確認
logger.info(f"[START_API] Searching for team: '{team_name}' in event: '{event_code}' - ID: {request_id}")
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
team_name=team_name team__team_name=team_name
).first() ).first()
if not entry: if not entry:
logger.warning(f"Team not found: {team_name} in event: {event_code}") logger.warning(f"[START_API] ❌ Team not found - ID: {request_id}, team: '{team_name}', event: '{event_code}'")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたチームが見つかりません" "message": "指定されたチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[START_API] ✅ Team found - ID: {request_id}, Entry ID: {entry.id}")
logger.info(f"[START_API] Team details - Zekken: {entry.zekken_number}, Category: '{entry.category.category_name if entry.category else 'N/A'}'")
# 既にスタート済みかチェック # 既にスタート済みかチェック
if hasattr(entry, 'start_info'): logger.info(f"[START_API] Checking if team already started - ID: {request_id}")
logger.warning(f"Team {team_name} already started at {entry.start_info.start_time}") existing_start = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=event.event_name,
cp_number="START",
serial_number=0
).first()
if existing_start:
logger.warning(f"[START_API] ⚠️ Team already started - ID: {request_id}, start_time: {existing_start.checkin_time}")
return Response({ return Response({
"status": "WARNING", "status": "WARNING",
"message": "このチームは既にスタートしています", "message": "このチームは既にスタートしています",
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S") "start_time": existing_start.checkin_time.strftime("%Y-%m-%d %H:%M:%S")
}) })
logger.info(f"[START_API] ✅ Team not started yet - ID: {request_id}")
# トランザクション開始 # トランザクション開始
logger.info(f"[START_API] Starting database transaction - ID: {request_id}")
with transaction.atomic(): with transaction.atomic():
# スタート情報を登録 # スタート情報をGpsLogとして登録
start_info = TeamStart.objects.create( logger.info(f"[START_API] Creating start record - ID: {request_id}")
entry=entry, start_time = timezone.now()
start_time=timezone.now() start_info = GpsLog.objects.create(
zekken_number=entry.zekken_number,
event_code=event.event_name,
cp_number="START",
serial_number=0,
checkin_time=start_time,
create_at=start_time,
update_at=start_time,
buy_flag=False,
colabo_company_memo=""
) )
logger.info(f"Team {team_name} started at {start_info.start_time}") # 競技状態を更新
entry.is_in_rog = True
entry.start_time = start_time
entry.last_checkin_time = start_time
entry.save()
return Response({ logger.info(f"[START_API] ✅ Start record created - ID: {request_id}, GpsLog ID: {start_info.id}")
logger.info(f"[START_API] ✅ Competition status updated - is_in_rog: True")
# 統計情報取得
try:
total_checkpoints = Location2025.objects.filter(event=event).count()
logger.info(f"[START_API] Total checkpoints available: {total_checkpoints} - ID: {request_id}")
except Exception as e:
logger.warning(f"[START_API] Could not get checkpoint count: {str(e)} - ID: {request_id}")
total_checkpoints = 0
# 成功レスポンス準備
response_data = {
"status": "OK", "status": "OK",
"message": "スタート処理が完了しました", "message": "スタート処理が完了しました",
"competition_status": {
"is_in_rog": entry.is_in_rog,
"rogaining_counted": entry.rogaining_counted,
"ready_for_goal": entry.ready_for_goal,
"is_at_goal": entry.is_at_goal,
"start_time": start_info.checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z")
},
"checkin_record": {
"id": start_info.id,
"cp_number": "START",
"checkin_time": start_info.checkin_time.strftime("%Y-%m-%dT%H:%M:%S%z")
},
"team_name": team_name, "team_name": team_name,
"event_code": event_code, "event_code": event_code,
"start_time": start_info.start_time.strftime("%Y-%m-%d %H:%M:%S") "zekken_number": entry.zekken_number,
}) "entry_id": entry.id
}
logger.info(f"[START_API] 🎉 SUCCESS - ID: {request_id}, Team: '{team_name}', Zekken: {entry.zekken_number}")
logger.info(f"[START_API] Response data: {response_data}")
return Response(response_data)
except Exception as e: except Exception as e:
logger.error(f"Error in start_from_rogapp: {str(e)}") # 詳細なエラー情報をログに出力
logger.error(f"[START_API] 💥 CRITICAL ERROR - ID: {request_id}")
logger.error(f"[START_API] Error type: {type(e).__name__}")
logger.error(f"[START_API] Error message: {str(e)}")
logger.error(f"[START_API] Request data: event_code='{event_code if 'event_code' in locals() else 'UNDEFINED'}', team_name='{team_name if 'team_name' in locals() else 'UNDEFINED'}'")
logger.error(f"[START_API] Client: {client_ip}, User: {user_info}")
logger.error(f"[START_API] Full traceback:", exc_info=True)
# エラーレスポンス
try:
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "サーバーエラーが発生しました" "message": "サーバーエラーが発生しました",
"error_id": request_id
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as response_error:
logger.error(f"[START_API] 💥 DOUBLE ERROR - Could not create error response: {str(response_error)}")
# 最後の手段として、シンプルなJSONレスポンスを返す
return JsonResponse({
"status": "ERROR",
"message": "サーバーエラーが発生しました",
"error_id": request_id
}, status=500)
finally:
# 処理完了ログ
end_time = timezone.now()
duration = (end_time - request_time).total_seconds()
logger.info(f"[START_API] 🏁 REQUEST_END - ID: {request_id}, Duration: {duration:.3f}s")
""" """
@ -333,7 +484,17 @@ def checkin_from_rogapp(request):
- gps_coordinates: GPS座標情報 (新規) - gps_coordinates: GPS座標情報 (新規)
- camera_metadata: カメラメタデータ (新規) - camera_metadata: カメラメタデータ (新規)
""" """
logger.info("checkin_from_rogapp called") # リクエスト詳細情報を取得
client_ip = request.META.get('REMOTE_ADDR', 'Unknown')
user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
user_info = request.user.email if request.user.is_authenticated else 'Anonymous'
request_time = timezone.now()
request_id = str(uuid.uuid4())[:8]
# APIの最初にログを記録問題の特定のため
logger.info(f"[CHECKIN] 🚀 API Request Received - ID: {request_id}, Time: {request_time}, Client IP: {client_ip}, User: {user_info}, S3_Available: {S3_AVAILABLE}")
logger.info(f"[CHECKIN] 📍 API Request Started - ID: {request_id}, Time: {request_time}, Client IP: {client_ip}, User: {user_info}")
# リクエストからパラメータを取得 # リクエストからパラメータを取得
event_code = request.data.get('event_code') event_code = request.data.get('event_code')
@ -346,12 +507,40 @@ def checkin_from_rogapp(request):
gps_coordinates = request.data.get('gps_coordinates', {}) gps_coordinates = request.data.get('gps_coordinates', {})
camera_metadata = request.data.get('camera_metadata', {}) camera_metadata = request.data.get('camera_metadata', {})
logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}, " # 🔍 詳細なパラメータログQRコード問題調査用
f"cp_number={cp_number}, image={image_url}") logger.info(f"[CHECKIN] 🔍 DETAILED PARAMS - ID: {request_id}")
logger.info(f"[CHECKIN] 📊 Basic params: event_code='{event_code}', team_name='{team_name}', cp_number={cp_number}")
logger.info(f"[CHECKIN] 🖼️ Image params: has_image={bool(image_url)}, image_size={len(image_url) if image_url else 0}, image_type={type(image_url)}")
logger.info(f"[CHECKIN] 🛒 Purchase params: buy_flag={buy_flag} (type: {type(buy_flag)})")
logger.info(f"[CHECKIN] 📱 Client params: user_agent='{user_agent[:100]}...', client_ip='{client_ip}'")
logger.info(f"[CHECKIN] 🔐 Auth params: user_authenticated={request.user.is_authenticated}, user='{user_info}'")
# 全リクエストデータをダンプQRコード問題調査用
try:
import json
request_data_safe = {}
for key, value in request.data.items():
if key == 'image' and value:
request_data_safe[key] = f"[IMAGE_DATA:{len(str(value))}chars]"
else:
request_data_safe[key] = value
logger.info(f"[CHECKIN] 📥 FULL REQUEST DATA: {json.dumps(request_data_safe, ensure_ascii=False, indent=2)}")
except Exception as e:
logger.warning(f"[CHECKIN] Failed to log request data: {e}")
logger.info(f"[CHECKIN] Request parameters - ID: {request_id}, event_code: '{event_code}', team_name: '{team_name}', cp_number: {cp_number}, has_image: {bool(image_url)}, image_size: {len(image_url) if image_url else 0} chars, buy_flag: {buy_flag}, user_agent: '{user_agent[:100]}'")
# GPS座標情報をログに記録
if gps_coordinates:
logger.info(f"[CHECKIN] GPS coordinates - ID: {request_id}, lat: {gps_coordinates.get('latitude')}, lng: {gps_coordinates.get('longitude')}, accuracy: {gps_coordinates.get('accuracy')}m, timestamp: {gps_coordinates.get('timestamp')}")
# カメラメタデータをログに記録
if camera_metadata:
logger.info(f"[CHECKIN] Camera metadata - ID: {request_id}, capture_time: {camera_metadata.get('capture_time')}, device: {camera_metadata.get('device_info')}")
# パラメータ検証 # パラメータ検証
if not all([event_code, team_name, cp_number]): if not all([event_code, team_name, cp_number]):
logger.warning("Missing required parameters") logger.warning(f"[CHECKIN] ❌ Missing required parameters - ID: {request_id}, event_code: '{event_code}', team_name: '{team_name}', cp_number: {cp_number}, Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "イベントコード、チーム名、チェックポイント番号が必要です" "message": "イベントコード、チーム名、チェックポイント番号が必要です"
@ -361,41 +550,75 @@ def checkin_from_rogapp(request):
# イベントの存在確認 # イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first() event = NewEvent2.objects.filter(event_name=event_code).first()
if not event: if not event:
logger.warning(f"Event not found: {event_code}") logger.warning(f"[CHECKIN] ❌ Event not found - ID: {request_id}, event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたイベントが見つかりません" "message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[CHECKIN] ✅ Event found - ID: {request_id}, Event ID: {event.id}, Name: '{event.event_name}'")
# チームの存在確認 # チームの存在確認
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
team_name=team_name team__team_name=team_name
).first() ).first()
if not entry: if not entry:
logger.warning(f"Team not found: {team_name} in event: {event_code}") logger.warning(f"[CHECKIN] ❌ Team not found - ID: {request_id}, team_name: '{team_name}', event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたチームが見つかりません" "message": "指定されたチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[CHECKIN] ✅ Team found - ID: {request_id}, Entry ID: {entry.id}, Team: '{team_name}', Zekken: {entry.zekken_number}, Category: '{entry.category.category_name if entry.category else 'N/A'}'")
# チームがスタートしているか確認 # チームがスタートしているか確認
if not hasattr(entry, 'start_info'): start_record = GpsLog.objects.filter(
logger.warning(f"Team {team_name} has not started yet") zekken_number=entry.zekken_number,
return Response({ event_code=entry.event.event_name,
"status": "ERROR", cp_number="START",
"message": "このチームはまだスタートしていません。先にスタート処理を行ってください。" serial_number=0
}, status=status.HTTP_400_BAD_REQUEST) ).first()
if not start_record:
# 🚀 AUTO-START機能: スタートしていない場合、自動的にスタート処理を実行
logger.info(f"[CHECKIN] 🚀 Team has not started yet, auto-starting - ID: {request_id}, team_name: '{team_name}', zekken: {entry.zekken_number}")
with transaction.atomic():
# スタートレコードを作成
start_record = GpsLog.objects.create(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="START",
serial_number=0,
checkin_time=timezone.now(),
create_at=timezone.now(),
create_user="AUTO_START",
image_address="AUTO_START",
buy_flag=False,
score=0
)
# エントリー状況を更新
entry.is_in_rog = True
entry.rogaining_counted = True
entry.start_time = timezone.now()
entry.save()
logger.info(f"[CHECKIN] ✅ Auto-start completed - ID: {request_id}, start_record_id: {start_record.id}, start_time: {start_record.checkin_time}")
else:
logger.info(f"[CHECKIN] ✅ Team has already started - ID: {request_id}, start_time: {start_record.checkin_time}")
# 既に同じCPを登録済みかチェック # 既に同じCPを登録済みかチェック
existing_checkpoint = GpsLog.objects.filter( existing_checkpoint = GpsLog.objects.filter(
entry=entry, zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number=cp_number cp_number=cp_number
).first() ).first()
if existing_checkpoint: if existing_checkpoint:
logger.warning(f"Checkpoint {cp_number} already registered for team: {team_name}") logger.warning(f"[CHECKIN] ⚠️ Checkpoint already registered - ID: {request_id}, team_name: '{team_name}', zekken: {entry.zekken_number}, cp_number: {cp_number}, previous_checkin_time: {existing_checkpoint.checkin_time}, Client IP: {client_ip}")
return Response({ return Response({
"status": "WARNING", "status": "WARNING",
"message": "このチェックポイントは既に登録されています", "message": "このチェックポイントは既に登録されています",
@ -410,24 +633,88 @@ def checkin_from_rogapp(request):
event=event, event=event,
cp_number=cp_number cp_number=cp_number
).first() ).first()
except: if event_cp:
logger.info(f"Location2025 model not available or CP {cp_number} not defined for event") logger.info(f"[CHECKIN] ✅ Event checkpoint found - ID: {request_id}, CP: {cp_number}, Name: '{event_cp.cp_name}', Points: {getattr(event_cp, 'checkin_point', 'N/A')}, Category: '{event_cp.category}'")
else:
logger.info(f"[CHECKIN] ⚠️ Event checkpoint not defined - ID: {request_id}, CP: {cp_number}")
except Exception as e:
logger.warning(f"[CHECKIN] Location2025 model issue - ID: {request_id}, CP: {cp_number}, Error: {e}")
# トランザクション開始 # トランザクション開始
logger.info(f"[GPSLOG] 🔄 Starting database transaction - ID: {request_id}")
with transaction.atomic(): with transaction.atomic():
# チェックポイント登録 logger.info(f"[GPSLOG] 🔒 Database transaction started successfully - ID: {request_id}")
checkpoint = GpsLog.objects.create(
entry=entry, # S3に画像をアップロードし、S3 URLを取得
cp_number=cp_number, s3_image_url = image_url
image_address=image_url, if image_url and S3_AVAILABLE and s3_uploader:
checkin_time=timezone.now(), try:
is_service_checked=event_cp.is_service_cp if event_cp else False logger.info(f"[GPSLOG] 📤 Starting S3 upload - ID: {request_id}, image_size: {len(image_url)} chars")
s3_image_url = s3_uploader.upload_checkin_image(
image_data=image_url,
event_code=entry.event.event_name,
zekken_number=entry.zekken_number,
cp_number=cp_number
) )
logger.info(f"[CHECKIN] S3 upload - Original: {image_url[:50]}..., S3: {s3_image_url}")
logger.info(f"[GPSLOG] ✅ S3 upload completed - ID: {request_id}, new_url: {s3_image_url[:50]}...")
except Exception as e:
logger.error(f"[CHECKIN] S3 upload failed, using original URL: {e}")
logger.error(f"[GPSLOG] ❌ S3 upload failed - ID: {request_id}, error: {e}")
s3_image_url = image_url
elif image_url:
logger.info(f"[CHECKIN] S3 not available, using original URL")
logger.info(f"[GPSLOG] S3 not available - ID: {request_id}, using original URL")
logger.info(f"Successfully registered CP {cp_number} for team: {team_name} in event: {event_code}") # serial_numberを自動生成既存の最大値+1
logger.info(f"[GPSLOG] 🔢 Calculating serial number for zekken: {entry.zekken_number}, event: {entry.event.event_name}")
max_serial = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name
).aggregate(max_serial=Max('serial_number'))['max_serial'] or 0
# 獲得ポイントの計算イベントCPが定義されている場合 new_serial = max_serial + 1
point_value = event_cp.cp_point if event_cp else 0 logger.info(f"[GPSLOG] 📊 Serial number calculation - max_existing: {max_serial}, new_serial: {new_serial}")
# GpsLogテーブルへの書き込み準備
gpslog_data = {
'serial_number': new_serial,
'zekken_number': entry.zekken_number,
'event_code': entry.event.event_name,
'cp_number': cp_number,
'image_address': s3_image_url, # S3 URLを保存
'checkin_time': timezone.now(),
'create_at': timezone.now(),
'update_at': timezone.now(),
'buy_flag': False,
'is_service_checked': False, # Location2025にはis_service_cpがないので、デフォルトでFalse
'colabo_company_memo': ""
}
logger.info(f"[GPSLOG] 📝 Preparing GpsLog record - ID: {request_id}")
logger.info(f"[GPSLOG] 🏷️ Data: serial={new_serial}, zekken={entry.zekken_number}, event={entry.event.event_name}, cp={cp_number}")
logger.info(f"[GPSLOG] 🖼️ Image: has_image={bool(s3_image_url)}, url_length={len(s3_image_url) if s3_image_url else 0}")
logger.info(f"[GPSLOG] ⏰ Timestamps: checkin_time={gpslog_data['checkin_time']}")
# チェックポイント登録S3 URLを使用
try:
checkpoint = GpsLog.objects.create(**gpslog_data)
logger.info(f"[GPSLOG] ✅ GpsLog record created successfully - ID: {checkpoint.id}, request_id: {request_id}")
logger.info(f"[GPSLOG] 🎯 Created record details - DB_ID: {checkpoint.id}, serial: {checkpoint.serial_number}, checkin_time: {checkpoint.checkin_time}")
# 作成されたレコードの検証
if checkpoint.id:
logger.info(f"[GPSLOG] 🔍 Verification - Record exists in database with ID: {checkpoint.id}")
else:
logger.warning(f"[GPSLOG] ⚠️ Warning - Record created but ID is None")
except Exception as create_error:
logger.error(f"[GPSLOG] ❌ Failed to create GpsLog record - ID: {request_id}, Error: {create_error}")
logger.error(f"[GPSLOG] 📊 Failed data: {gpslog_data}")
raise create_error
# 獲得ポイントの計算Location2025から取得
point_value = event_cp.checkin_point if event_cp else 0
bonus_points = 0 bonus_points = 0
scoring_breakdown = { scoring_breakdown = {
"base_points": point_value, "base_points": point_value,
@ -435,18 +722,34 @@ def checkin_from_rogapp(request):
"total_points": point_value "total_points": point_value
} }
logger.info(f"[GPSLOG] 🎯 Point calculation - base_points: {point_value}, bonus_points: {bonus_points}")
# カメラボーナス計算 # カメラボーナス計算
if image_url and event_cp and hasattr(event_cp, 'evaluation_value'): if image_url and event_cp and hasattr(event_cp, 'evaluation_value'):
if event_cp.evaluation_value == "1": # 写真撮影必須ポイント if event_cp.evaluation_value == "1": # 写真撮影必須ポイント
bonus_points += 5 bonus_points += 5
scoring_breakdown["camera_bonus"] = 5 scoring_breakdown["camera_bonus"] = 5
scoring_breakdown["total_points"] += 5 scoring_breakdown["total_points"] += 5
logger.info(f"[GPSLOG] 📸 Camera bonus applied - additional_points: 5, total: {scoring_breakdown['total_points']}")
logger.info(f"[CHECKIN] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, CP: {cp_number}, Points: {point_value}, Bonus: {bonus_points}, Time: {checkpoint.checkin_time}, Has Image: {bool(image_url)}, Buy Flag: {buy_flag}, Client IP: {client_ip}, User: {user_info}")
logger.info(f"[GPSLOG] 🏆 FINAL RESULT - GpsLog_ID: {checkpoint.id}, Serial: {checkpoint.serial_number}, Total_Points: {scoring_breakdown['total_points']}, Request_ID: {request_id}")
# 競技状態を更新(スタート・ゴール以外のチェックイン時)
if cp_number not in ["START", "GOAL", -2, -1]:
entry.rogaining_counted = True
entry.last_checkin_time = checkpoint.checkin_time
entry.save()
logger.info(f"[CHECKIN] ✅ Competition status updated - rogaining_counted: True, last_checkin_time: {checkpoint.checkin_time}")
logger.info(f"[GPSLOG] 🎮 Entry updated - entry_id: {entry.id}, rogaining_counted: {entry.rogaining_counted}")
else:
logger.info(f"[GPSLOG] Special checkpoint ({cp_number}) - Entry status not updated")
# 拡張情報があれば保存 # 拡張情報があれば保存
if gps_coordinates or camera_metadata: if gps_coordinates or camera_metadata:
try: try:
from ..models import CheckinExtended from ..models import CheckinExtended
CheckinExtended.objects.create( extended_record = CheckinExtended.objects.create(
gpslog=checkpoint, gpslog=checkpoint,
gps_latitude=gps_coordinates.get('latitude'), gps_latitude=gps_coordinates.get('latitude'),
gps_longitude=gps_coordinates.get('longitude'), gps_longitude=gps_coordinates.get('longitude'),
@ -457,10 +760,14 @@ def checkin_from_rogapp(request):
bonus_points=bonus_points, bonus_points=bonus_points,
scoring_breakdown=scoring_breakdown scoring_breakdown=scoring_breakdown
) )
logger.info(f"[GPSLOG] 📋 Extended info saved - CheckinExtended_ID: {extended_record.id}")
except Exception as ext_error: except Exception as ext_error:
logger.warning(f"Failed to save extended checkin info: {ext_error}") logger.warning(f"[GPSLOG] ⚠️ Failed to save extended checkin info: {ext_error}")
else:
logger.info(f"[GPSLOG] No extended GPS/camera data to save")
return Response({ # レスポンス作成
response_data = {
"status": "OK", "status": "OK",
"message": "チェックポイントが正常に登録されました", "message": "チェックポイントが正常に登録されました",
"team_name": team_name, "team_name": team_name,
@ -471,32 +778,76 @@ def checkin_from_rogapp(request):
"bonus_points": bonus_points, "bonus_points": bonus_points,
"scoring_breakdown": scoring_breakdown, "scoring_breakdown": scoring_breakdown,
"validation_status": "pending", "validation_status": "pending",
"requires_manual_review": bool(gps_coordinates.get('accuracy', 0) > 10) # 10m以上は要審査 "requires_manual_review": bool(gps_coordinates.get('accuracy', 0) > 10), # 10m以上は要審査
}) "image_url": s3_image_url, # S3画像URLを返す
"competition_status": {
"is_in_rog": entry.is_in_rog,
"rogaining_counted": entry.rogaining_counted,
"ready_for_goal": entry.ready_for_goal,
"is_at_goal": entry.is_at_goal
}
}
logger.info(f"[GPSLOG] 📤 Creating response - ID: {request_id}")
logger.info(f"[GPSLOG] 📊 Response summary - checkpoint_id: {checkpoint.id}, total_points: {scoring_breakdown['total_points']}, status: OK")
logger.info(f"[GPSLOG] 🔄 Transaction completed successfully - ID: {request_id}")
return Response(response_data)
except Exception as e: except Exception as e:
logger.error(f"Error in checkin_from_rogapp: {str(e)}") # より詳細なエラー情報をログに記録
import traceback
error_details = {
"error_type": type(e).__name__,
"error_message": str(e),
"traceback": traceback.format_exc(),
"request_data": request.data if hasattr(request, 'data') else "No data",
"user_agent": user_agent if 'user_agent' in locals() else "Unknown",
"client_ip": client_ip if 'client_ip' in locals() else "Unknown"
}
logger.error(f"[CHECKIN] ❌ DETAILED ERROR: {error_details}")
logger.error(f"[GPSLOG] ❌ GpsLog creation failed - ID: {request_id if 'request_id' in locals() else 'Unknown'}, Error: {type(e).__name__}: {str(e)}")
# GpsLogトランザクション失敗の詳細
if 'checkpoint' in locals():
logger.error(f"[GPSLOG] 💾 Transaction state - checkpoint created: True, checkpoint_id: {getattr(checkpoint, 'id', 'Unknown')}")
else:
logger.error(f"[GPSLOG] 💾 Transaction state - checkpoint created: False, transaction rolled back")
# より具体的なエラーメッセージを返す
if "データベース" in str(e).lower() or "database" in str(e).lower():
error_message = "データベースの構造に問題があります。アプリを再起動してください。"
elif "s3" in str(e).lower() or "boto" in str(e).lower():
error_message = "画像アップロードに問題があります。しばらく後に再試行してください。"
elif "import" in str(e).lower() or "module" in str(e).lower():
error_message = "サーバーモジュールの読み込みエラーです。管理者にお問い合わせください。"
else:
error_message = "サーバーエラーが発生しました。しばらく後に再試行してください。"
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "サーバーエラーが発生しました" "message": error_message,
"error_code": "CHECKIN_API_ERROR"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
""" """
解説 Goal API from rogapp - 解説
この実装では以下の処理を行っています: この実装では以下の処理を行っています:
1.イベントコード、チーム名、画像URL、ゴール時間のパラメータを受け取ります 1. イベントコード、チーム名、画像URL、ゴール時間のパラメータを受け取ります
2.パラメータが不足している場合はエラーを返します 2. パラメータが不足している場合はエラーを返します
3.指定されたイベントとチームの存在を確認します 3. 指定されたイベントとチームの存在を確認します
4.チームがスタートしているかを確認します 4. チームがスタートしているかを確認します
- スタートしていない場合はエラーを返します - スタートしていない場合はエラーを返します
5.既にゴールしているかチェックします 5. 既にゴールしているかチェックします
- ゴール済みの場合は警告メッセージを返します - ゴール済みの場合は警告メッセージを返します
6.ゴール時間を処理します(提供されていない場合は現在時刻を使用) 6. ゴール時間を処理します(提供されていない場合は現在時刻を使用)
7.チームのスコアを計算します 7. チームのスコアを計算します
8.スコアボードを生成します(実際の生成ロジックは実装によって異なります) 8. スコアボードを生成します(実際の生成ロジックは実装によって異なります)
9.ゴール情報を登録します 9. ゴール情報を登録します
10.成功した場合、ゴール情報、スコア、スコアボードURLを含む成功メッセージを返します 10. 成功した場合、ゴール情報、スコア、スコアボードURLを含む成功メッセージを返します
スコアボードの生成部分は、実際のシステムの要件に合わせて詳細に実装する必要があります。 スコアボードの生成部分は、実際のシステムの要件に合わせて詳細に実装する必要があります。
この例では、単純にPDFファイルのパスとURLを生成していますが、 この例では、単純にPDFファイルのパスとURLを生成していますが、
@ -514,7 +865,12 @@ def goal_from_rogapp(request):
- image: 画像URL - image: 画像URL
- goal_time: ゴール時間 - goal_time: ゴール時間
""" """
logger.info("goal_from_rogapp called") # ログ用のリクエストID生成
request_id = uuid.uuid4().hex[:8]
request_time = time.time()
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR', 'Unknown'))
user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
user_info = f"{request.user.username}({request.user.id})" if request.user.is_authenticated else "Anonymous"
# リクエストからパラメータを取得 # リクエストからパラメータを取得
event_code = request.data.get('event_code') event_code = request.data.get('event_code')
@ -522,12 +878,11 @@ def goal_from_rogapp(request):
image_url = request.data.get('image') image_url = request.data.get('image')
goal_time_str = request.data.get('goal_time') goal_time_str = request.data.get('goal_time')
logger.debug(f"Parameters: event_code={event_code}, team_name={team_name}, " logger.info(f"[GOAL] 🏁 API call started - ID: {request_id}, event_code: '{event_code}', team_name: '{team_name}', has_image: {bool(image_url)}, goal_time: '{goal_time_str}', Client IP: {client_ip}, User: {user_info}, User-Agent: {user_agent[:100]}")
f"image={image_url}, goal_time={goal_time_str}")
# パラメータ検証 # パラメータ検証
if not all([event_code, team_name]): if not all([event_code, team_name]):
logger.warning("Missing required parameters") logger.error(f"[GOAL] ❌ Missing required parameters - ID: {request_id}, event_code: '{event_code}', team_name: '{team_name}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "イベントコードとチーム名が必要です" "message": "イベントコードとチーム名が必要です"
@ -537,41 +892,59 @@ def goal_from_rogapp(request):
# イベントの存在確認 # イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first() event = NewEvent2.objects.filter(event_name=event_code).first()
if not event: if not event:
logger.warning(f"Event not found: {event_code}") logger.error(f"[GOAL] ❌ Event not found - ID: {request_id}, event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたイベントが見つかりません" "message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.info(f"[GOAL] ✅ Event found - ID: {request_id}, event: '{event.event_name}', event_id: {event.id}")
# チームの存在確認 # チームの存在確認
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
team_name=team_name team__team_name=team_name
).first() ).first()
if not entry: if not entry:
logger.warning(f"Team not found: {team_name} in event: {event_code}") logger.error(f"[GOAL] ❌ Team not found - ID: {request_id}, team_name: '{team_name}', event_code: '{event_code}', Client IP: {client_ip}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "指定されたチームが見つかりません" "message": "指定されたチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# チームがスタートしているか確認 logger.info(f"[GOAL] ✅ Team found - ID: {request_id}, team_name: '{team_name}', zekken: {entry.zekken_number}, entry_id: {entry.id}")
if not hasattr(entry, 'start_info'):
# チームがスタートしているか確認GpsLogでSTARTレコードを確認
start_record = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="START",
serial_number=0
).first()
if not start_record:
logger.warning(f"Team {team_name} has not started yet") logger.warning(f"Team {team_name} has not started yet")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "このチームはまだスタートしていません。先にスタート処理を行ってください。" "message": "このチームはまだスタートしていません。先にスタート処理を行ってください。"
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
# 既にゴールしているかチェック # 既にゴールしているかチェックGpsLogでGOALレコードを確認
if hasattr(entry, 'goal_info'): existing_goal = GpsLog.objects.filter(
logger.warning(f"Team {team_name} already reached goal at {entry.goal_info.goal_time}") zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="GOAL",
serial_number=9999
).first()
if existing_goal:
logger.warning(f"Team {team_name} already reached goal at {existing_goal.checkin_time}")
return Response({ return Response({
"status": "WARNING", "status": "WARNING",
"message": "このチームは既にゴールしています", "message": "このチームは既にゴールしています",
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"), "goal_time": existing_goal.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
"scoreboard_url": entry.goal_info.scoreboard_url "scoreboard_url": existing_goal.extra_data.get('scoreboard_url', '') if existing_goal.extra_data else ''
}) })
# ゴール時間の処理 # ゴール時間の処理
@ -600,28 +973,49 @@ def goal_from_rogapp(request):
# スコアボードへのURL # スコアボードへのURL
scoreboard_url = f"{settings.MEDIA_URL}scoreboards/{scoreboard_filename}" scoreboard_url = f"{settings.MEDIA_URL}scoreboards/{scoreboard_filename}"
# ゴール情報を登録 # ゴール情報をGpsLogとして登録
goal_info = TeamGoal.objects.create( goal_info = GpsLog.objects.create(
entry=entry, zekken_number=entry.zekken_number,
goal_time=goal_time, event_code=entry.event.event_name,
image_url=image_url, cp_number="GOAL",
serial_number=9999, # ゴール記録の固定シリアル番号
checkin_time=goal_time,
image_address=image_url,
create_at=timezone.now(),
update_at=timezone.now(),
buy_flag=False,
score=score, score=score,
scoreboard_url=scoreboard_url scoreboard_url=scoreboard_url,
colabo_company_memo=""
) )
logger.info(f"Team {team_name} reached goal at {goal_time} with score {score}") # 競技状態を更新
entry.is_at_goal = True
entry.goal_time = goal_time
entry.last_checkin_time = goal_time
entry.save()
logger.info(f"[GOAL] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, Event: {event_code}, Goal Time: {goal_time}, Score: {score}, Has Image: {bool(image_url)}, Client IP: {client_ip}, User: {user_info}")
logger.info(f"[GOAL] ✅ Competition status updated - is_at_goal: True")
return Response({ return Response({
"status": "OK", "status": "OK",
"message": "ゴール処理が正常に完了しました", "message": "ゴール処理が正常に完了しました",
"competition_status": {
"is_in_rog": entry.is_in_rog,
"rogaining_counted": entry.rogaining_counted,
"ready_for_goal": entry.ready_for_goal,
"is_at_goal": entry.is_at_goal,
"goal_time": goal_time.strftime("%Y-%m-%dT%H:%M:%S%z")
},
"team_name": team_name, "team_name": team_name,
"goal_time": goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"), "goal_time": goal_info.checkin_time.strftime("%Y-%m-%d %H:%M:%S"),
"score": score, "score": score,
"scoreboard_url": scoreboard_url "scoreboard_url": scoreboard_url
}) })
except Exception as e: except Exception as e:
logger.error(f"Error in goal_from_rogapp: {str(e)}") logger.error(f"[GOAL] ❌ ERROR - team_name: {team_name}, event_code: {event_code}, Client IP: {client_ip}, Error: {str(e)}")
return Response({ return Response({
"status": "ERROR", "status": "ERROR",
"message": "サーバーエラーが発生しました" "message": "サーバーエラーが発生しました"
@ -630,7 +1024,10 @@ def goal_from_rogapp(request):
def calculate_team_score(entry): def calculate_team_score(entry):
"""チームのスコアを計算する補助関数""" """チームのスコアを計算する補助関数"""
# チームが通過したチェックポイントを取得 # チームが通過したチェックポイントを取得
checkpoints = GpsLog.objects.filter(entry=entry) checkpoints = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name
)
total_score = 0 total_score = 0

View File

@ -0,0 +1,354 @@
"""
QRコードサービスポイント処理API
このモジュールは、QRコードを使用したサービスポイントの登録・処理を行います。
"""
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rog.models import NewEvent2, Entry, Location2025, GpsCheckin
from django.db import transaction
from django.utils import timezone
from datetime import datetime
import logging
import json
import uuid
logger = logging.getLogger(__name__)
@api_view(['POST'])
def submit_qr_points(request):
"""
QRコードサービスポイント登録API
パラメータ:
- event_code: イベントコード
- team_name: チーム名
- qr_code_data: QRコードデータ
- latitude: 緯度(オプション)
- longitude: 経度(オプション)
- image: 画像データ(オプション)
- cp_number: チェックポイント番号(オプション)
"""
# リクエストIDを生成してログで追跡できるようにする
request_id = str(uuid.uuid4())[:8]
logger.info(f"[QR_SUBMIT] 🚀 Starting QR points submission - ID: {request_id}")
# クライアント情報を取得
user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
client_ip = request.META.get('HTTP_X_FORWARDED_FOR') or request.META.get('REMOTE_ADDR', 'Unknown')
user_info = str(request.user) if request.user.is_authenticated else 'Anonymous'
# リクエストからパラメータを取得
event_code = request.data.get('event_code')
team_name = request.data.get('team_name')
qr_code_data = request.data.get('qr_code_data')
latitude = request.data.get('latitude')
longitude = request.data.get('longitude')
image_data = request.data.get('image')
cp_number = request.data.get('cp_number')
point_value = request.data.get('point_value')
location_id = request.data.get('location_id')
timestamp = request.data.get('timestamp')
qr_scan = request.data.get('qr_scan')
# 📋 パラメータをログ出力(デバッグ用)
logger.info(f"[QR_SUBMIT] ID: {request_id} - 📋 Request Parameters:")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 🏷️ Event Code: '{event_code}'")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 👥 Team Name: '{team_name}'")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 📱 QR Code Data: '{qr_code_data}'")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 📍 Latitude: {latitude}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 🌍 Longitude: {longitude}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 🏁 CP Number: {cp_number}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - <20> Point Value: {point_value}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 📍 Location ID: {location_id}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - ⏰ Timestamp: {timestamp}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 📱 QR Scan: {qr_scan}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - <20>📸 Has Image: {image_data is not None}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 🌐 Client IP: {client_ip}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 👤 User: {user_info}")
logger.info(f"[QR_SUBMIT] ID: {request_id} - 🔧 User Agent: {user_agent[:100]}...")
# 全リクエストデータをログ出力(セキュリティ上重要でないデータのみ)
safe_data = {k: v for k, v in request.data.items() if k not in ['image', 'password']}
logger.info(f"[QR_SUBMIT] ID: {request_id} - 📊 Full Request Data: {json.dumps(safe_data, ensure_ascii=False, indent=2)}")
cp_number = request.data.get('cp_number')
# 📊 詳細なパラメータログ
logger.info(f"[QR_SUBMIT] 📊 DETAILED PARAMS - ID: {request_id}")
logger.info(f"[QR_SUBMIT] 🏷️ Basic params: event_code='{event_code}', team_name='{team_name}'")
logger.info(f"[QR_SUBMIT] 📱 QR params: qr_code_data='{qr_code_data}', cp_number={cp_number}")
logger.info(f"[QR_SUBMIT] 🌍 Location params: lat={latitude}, lng={longitude}")
logger.info(f"[QR_SUBMIT] 🖼️ Image params: has_image={bool(image_data)}, image_size={len(str(image_data)) if image_data else 0}")
logger.info(f"[QR_SUBMIT] 📱 Client params: user_agent='{user_agent[:100]}...', client_ip='{client_ip}'")
logger.info(f"[QR_SUBMIT] 🔐 Auth params: user_authenticated={request.user.is_authenticated}, user='{user_info}'")
# 全リクエストデータをダンプ(デバッグ用)
try:
request_data_safe = {}
for key, value in request.data.items():
if key == 'image' and value:
request_data_safe[key] = f"[IMAGE_DATA:{len(str(value))}chars]"
else:
request_data_safe[key] = value
logger.info(f"[QR_SUBMIT] 📥 FULL REQUEST DATA: {json.dumps(request_data_safe, ensure_ascii=False, indent=2)}")
except Exception as e:
logger.warning(f"[QR_SUBMIT] Failed to log request data: {e}")
# パラメータ検証 - 実際のリクエストデータに基づく
# 基本的な必須パラメータ: event_code, team_name
# オプション: qr_code_data, cp_number, point_value, location_id, qr_scan
if not all([event_code, team_name]):
missing_params = []
if not event_code:
missing_params.append('event_code')
if not team_name:
missing_params.append('team_name')
logger.warning(f"[QR_SUBMIT] ❌ Missing required parameters: {missing_params} - ID: {request_id}")
return Response({
"status": "ERROR",
"message": f"必須パラメータが不足しています: {', '.join(missing_params)}",
"missing_parameters": missing_params,
"request_id": request_id
}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
logger.warning(f"[QR_SUBMIT] ❌ Event not found: {event_code} - ID: {request_id}")
return Response({
"status": "ERROR",
"message": f"指定されたイベント '{event_code}' が見つかりません",
"request_id": request_id
}, status=status.HTTP_404_NOT_FOUND)
# チームの存在確認
entry = Entry.objects.filter(
event=event,
team__team_name=team_name
).first()
if not entry:
logger.warning(f"[QR_SUBMIT] ❌ Team not found: {team_name} in event: {event_code} - ID: {request_id}")
return Response({
"status": "ERROR",
"message": f"指定されたチーム '{team_name}' がイベント '{event_code}' に見つかりません",
"request_id": request_id
}, status=status.HTTP_404_NOT_FOUND)
# QRコードデータの解析オプション
qr_data = None
if qr_code_data:
try:
if isinstance(qr_code_data, str):
# JSON文字列の場合はパース
if qr_code_data.startswith('{'):
qr_data = json.loads(qr_code_data)
else:
# 単純な文字列の場合
qr_data = {"code": qr_code_data}
else:
qr_data = qr_code_data
logger.info(f"[QR_SUBMIT] 📱 Parsed QR data: {qr_data} - ID: {request_id}")
except json.JSONDecodeError as e:
logger.warning(f"[QR_SUBMIT] ⚠️ Invalid QR code data format: {e}, using as string - ID: {request_id}")
qr_data = {"code": str(qr_code_data)}
# チェックポイント情報の取得
location = None
calculated_point_value = 0
# location_idが指定されている場合、それを優先
if location_id:
location = Location2025.objects.filter(
id=location_id,
event_id=event.id
).first()
if location:
calculated_point_value = location.cp_point or 0
logger.info(f"[QR_SUBMIT] 📍 Found location by ID: {location_id} - CP{location.cp_number} - {location.cp_name} - Points: {calculated_point_value} - ID: {request_id}")
else:
logger.warning(f"[QR_SUBMIT] ⚠️ Location not found for location_id: {location_id} - ID: {request_id}")
# cp_numberが指定されている場合も確認
elif cp_number:
location = Location2025.objects.filter(
event_id=event.id,
cp_number=cp_number
).first()
if location:
calculated_point_value = location.cp_point or 0
logger.info(f"[QR_SUBMIT] 📍 Found location by CP: CP{cp_number} - {location.cp_name} - Points: {calculated_point_value} - ID: {request_id}")
else:
logger.warning(f"[QR_SUBMIT] ⚠️ Location not found for CP{cp_number} - ID: {request_id}")
# point_valueが明示的に指定されている場合、それを使用
final_point_value = point_value if point_value is not None else calculated_point_value
logger.info(f"[QR_SUBMIT] 💯 Point calculation: provided={point_value}, calculated={calculated_point_value}, final={final_point_value} - ID: {request_id}")
# QRポイント登録処理
current_time = timezone.now()
# timestampが提供されている場合は使用、そうでなければ現在時刻
if timestamp:
try:
# ISO形式のタイムスタンプをパース
checkin_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
if checkin_time.tzinfo is None:
checkin_time = timezone.make_aware(checkin_time)
logger.info(f"[QR_SUBMIT] ⏰ Using provided timestamp: {checkin_time} - ID: {request_id}")
except (ValueError, AttributeError) as e:
logger.warning(f"[QR_SUBMIT] ⚠️ Invalid timestamp format, using current time: {e} - ID: {request_id}")
checkin_time = current_time
else:
checkin_time = current_time
# GpsCheckinレコードを作成
checkin_data = {
'event': event,
'entry': entry,
'zekken_number': entry.zekken_number,
'cp_number': cp_number or (location.cp_number if location else 0),
'checkin_time': checkin_time,
'is_service_checked': True, # QRコードはサービスポイントとして扱う
}
# 位置情報が提供されている場合は追加
if latitude and longitude:
checkin_data['latitude'] = float(latitude)
checkin_data['longitude'] = float(longitude)
logger.info(f"[QR_SUBMIT] 🌍 GPS coordinates recorded: {latitude}, {longitude} - ID: {request_id}")
# QRコードデータを格納qr_scanフラグも含む
qr_metadata = {
'qr_scan': qr_scan,
'location_id': location_id,
'point_value': final_point_value,
'original_qr_data': qr_data,
'timestamp': timestamp
}
if hasattr(GpsCheckin, 'qr_code_data'):
checkin_data['qr_code_data'] = qr_metadata
# 画像データを格納URLまたはパスの場合
if image_data:
if hasattr(GpsCheckin, 'image_url'):
checkin_data['image_url'] = image_data
logger.info(f"[QR_SUBMIT] 🖼️ Image data recorded - ID: {request_id}")
# レコードを作成
gps_checkin = GpsCheckin.objects.create(**checkin_data)
# 成功レスポンス
response_data = {
"status": "OK",
"message": "QRコードサービスポイントが正常に登録されました",
"request_id": request_id,
"data": {
"event_code": event_code,
"team_name": team_name,
"zekken_number": entry.zekken_number,
"cp_number": cp_number or (location.cp_number if location else 0),
"point_value": final_point_value,
"location_id": location_id,
"checkin_time": checkin_time.isoformat(),
"qr_scan": qr_scan,
"qr_code_processed": True,
"has_location": bool(latitude and longitude),
"has_image": bool(image_data),
"checkin_id": gps_checkin.id if gps_checkin else None
}
}
logger.info(f"[QR_SUBMIT] ✅ SUCCESS - Team: {team_name}, Zekken: {entry.zekken_number}, CP: {cp_number or (location.cp_number if location else 0)}, Points: {final_point_value}, QR: {qr_scan}, Location ID: {location_id}, Client IP: {client_ip}, User: {user_info} - ID: {request_id}")
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"[QR_SUBMIT] ❌ EXCEPTION - ID: {request_id}, Error: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "QRコードサービスポイント処理中にエラーが発生しました",
"request_id": request_id,
"error_detail": str(e) if logger.getEffectiveLevel() <= logging.DEBUG else None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
def qr_points_status(request):
"""
QRポイント処理状況確認API
パラメータ:
- event_code: イベントコード
- team_name: チーム名(オプション)
"""
event_code = request.query_params.get('event_code')
team_name = request.query_params.get('team_name')
logger.info(f"[QR_STATUS] QR points status check - event: {event_code}, team: {team_name}")
if not event_code:
return Response({
"status": "ERROR",
"message": "イベントコードが必要です"
}, status=status.HTTP_400_BAD_REQUEST)
try:
# イベントの存在確認
event = NewEvent2.objects.filter(event_name=event_code).first()
if not event:
return Response({
"status": "ERROR",
"message": f"指定されたイベント '{event_code}' が見つかりません"
}, status=status.HTTP_404_NOT_FOUND)
# QRコード関連のチェックイン状況を取得
query = GpsCheckin.objects.filter(event=event, is_service_checked=True)
if team_name:
query = query.filter(entry__team__team_name=team_name)
qr_checkins = query.order_by('-checkin_time')[:50] # 最新50件
checkin_list = []
for checkin in qr_checkins:
checkin_list.append({
"checkin_id": checkin.id,
"team_name": checkin.entry.team.team_name,
"zekken_number": checkin.zekken_number,
"cp_number": checkin.cp_number,
"checkin_time": checkin.checkin_time.isoformat(),
"has_qr_data": hasattr(checkin, 'qr_code_data') and bool(checkin.qr_code_data),
"has_location": bool(checkin.latitude and checkin.longitude),
"is_service_checked": checkin.is_service_checked
})
return Response({
"status": "OK",
"message": "QRポイント状況を取得しました",
"data": {
"event_code": event_code,
"total_count": len(checkin_list),
"checkins": checkin_list
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"[QR_STATUS] Error getting QR points status: {str(e)}", exc_info=True)
return Response({
"status": "ERROR",
"message": "QRポイント状況取得中にエラーが発生しました",
"error_detail": str(e) if logger.getEffectiveLevel() <= logging.DEBUG else None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -2,7 +2,7 @@
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rog.models import NewEvent2, Entry, Location, GpsLog from rog.models import NewEvent2, Entry, Location2025, GpsLog
import logging import logging
from django.db.models import F, Q from django.db.models import F, Q
from django.conf import settings from django.conf import settings
@ -166,8 +166,8 @@ def top_users_routes(request):
# チェックポイントの座標情報を取得 # チェックポイントの座標情報を取得
try: try:
event_cp = Location.objects.filter( event_cp = Location2025.objects.filter(
event=event, event_id=event.id,
cp_number=cp.cp_number cp_number=cp.cp_number
).first() ).first()
@ -469,7 +469,7 @@ def generate_route_image(request):
# チェックポイント情報の取得(座標) # チェックポイント情報の取得(座標)
cp_locations = {} cp_locations = {}
try: try:
event_cps = Location.objects.filter(event=event) event_cps = Location2025.objects.filter(event_id=event.id)
for cp in event_cps: for cp in event_cps:
if cp.latitude is not None and cp.longitude is not None: if cp.latitude is not None and cp.longitude is not None:

View File

@ -7,7 +7,7 @@ pip install openpyxl
# 既存のインポート部分に追加 # 既存のインポート部分に追加
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from django.http import HttpResponse, FileResponse from django.http import HttpResponse, FileResponse
from rog.models import Location, NewEvent2, Entry, GpsLog from rog.models import Location2025, NewEvent2, Entry, GpsLog
import logging import logging
import openpyxl import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
@ -96,7 +96,7 @@ def get_scoreboard(request):
# イベントのチェックポイント定義を取得 # イベントのチェックポイント定義を取得
cp_definitions = {} cp_definitions = {}
try: try:
event_cps = Location.objects.filter(event=event) event_cps = Location2025.objects.filter(event_id=event.id)
for cp in event_cps: for cp in event_cps:
cp_definitions[cp.cp_number] = { cp_definitions[cp.cp_number] = {
@ -106,7 +106,7 @@ def get_scoreboard(request):
'longitude': cp.longitude 'longitude': cp.longitude
} }
except: except:
# Locationモデルが存在しない場合 # Location2025モデルが存在しない場合
pass pass
# スタート・ゴール情報を取得 # スタート・ゴール情報を取得
@ -457,7 +457,7 @@ def download_scoreboard(request):
# イベントのチェックポイント定義を取得 # イベントのチェックポイント定義を取得
cp_definitions = {} cp_definitions = {}
try: try:
event_cps = Location.objects.filter(event=event) event_cps = Location2025.objects.filter(event_id=event.id)
for cp in event_cps: for cp in event_cps:
cp_definitions[cp.cp_number] = { cp_definitions[cp.cp_number] = {
@ -467,7 +467,7 @@ def download_scoreboard(request):
'longitude': cp.longitude 'longitude': cp.longitude
} }
except: except:
# Locationモデルが存在しない場合 # Location2025モデルが存在しない場合
pass pass
# スタート・ゴール情報を取得 # スタート・ゴール情報を取得

View File

@ -2,9 +2,11 @@
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rog.models import NewEvent2, Entry, Location, GpsLog from rog.models import NewEvent2, Entry, Location2025, GpsLog
import logging import logging
import json
from django.db.models import F, Q from django.db.models import F, Q
from django.db import transaction
from django.conf import settings from django.conf import settings
import os import os
from urllib.parse import urljoin from urllib.parse import urljoin
@ -108,7 +110,7 @@ def get_waypoint_datas_from_rogapp(request):
# チームの存在確認 # チームの存在確認
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
team_name=team_name team__team_name=team_name
).first() ).first()
if not entry: if not entry:
@ -118,8 +120,15 @@ def get_waypoint_datas_from_rogapp(request):
"message": "指定されたチーム名のエントリーが見つかりません" "message": "指定されたチーム名のエントリーが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# チームがスタートしているか確認(オプション) # チームがスタートしているか確認
if not hasattr(entry, 'start_info'): start_record = GpsLog.objects.filter(
zekken_number=entry.zekken_number,
event_code=entry.event.event_name,
cp_number="START",
serial_number=0
).first()
if not start_record:
logger.warning(f"Team {team_name} has not started yet") logger.warning(f"Team {team_name} has not started yet")
# 必要に応じてエラーを返すか、自動的にスタート処理を行う # 必要に応じてエラーを返すか、自動的にスタート処理を行う
@ -294,10 +303,12 @@ def get_route(request):
"message": "指定されたイベントが見つかりません" "message": "指定されたイベントが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
logger.debug(f"Event found: {event.event_name} (id: {event.id})")
# チームの存在確認 # チームの存在確認
entry = Entry.objects.filter( entry = Entry.objects.filter(
event=event, event=event,
team_name=team_name team__team_name=team_name
).first() ).first()
if not entry: if not entry:
@ -307,30 +318,23 @@ def get_route(request):
"message": "指定されたチームが見つかりません" "message": "指定されたチームが見つかりません"
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
# ウェイポイントデータを取得(時間順) logger.debug(f"Entry found: {entry.id}, team: {entry.team.team_name}")
waypoints = Waypoint.objects.filter(
entry=entry
).order_by('recorded_at')
# チェックポイント通過情報を取得(時間順 # 簡略化されたレスポンスでテストDBクエリなし
checkpoints = GpsLog.objects.filter( return Response({
entry=entry "status": "OK",
).order_by('checkin_time') "message": "get_route function is working",
"team_name": team_name,
"event_code": event_code,
"entry_id": entry.id
})
# スタート情報を取得 except Exception as e:
start_info = None logger.error(f"Error in get_route: {str(e)}")
if hasattr(entry, 'start_info'): return Response({
start_info = { "status": "ERROR",
"start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S") "message": "サーバーエラーが発生しました"
} }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ゴール情報を取得
goal_info = None
if hasattr(entry, 'goal_info'):
goal_info = {
"goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"),
"score": entry.goal_info.score
}
# ウェイポイントを処理 # ウェイポイントを処理
route_points = [] route_points = []
@ -362,10 +366,10 @@ def get_route(request):
"is_service_checked": getattr(cp, 'is_service_checked', False) "is_service_checked": getattr(cp, 'is_service_checked', False)
} }
# チェックポイントの座標情報を取得Locationモデルがある場合 # チェックポイントの座標情報を取得Location2025モデルがある場合)
try: try:
event_cp = Location.objects.filter( event_cp = Location2025.objects.filter(
event=event, event_id=event.id,
cp_number=cp.cp_number cp_number=cp.cp_number
).first() ).first()
@ -453,8 +457,8 @@ def get_route(request):
"status": "OK", "status": "OK",
"team_info": { "team_info": {
"team_name": team_name, "team_name": team_name,
"zekken_number": entry.zekken_number, "zekken_number": entry.team.zekken_number,
"class_name": entry.class_name, "class_name": entry.team.class_name,
"event_code": event_code "event_code": event_code
}, },
"start_info": start_info, "start_info": start_info,
@ -853,8 +857,8 @@ def get_all_routes(request):
# チェックポイントの座標情報を取得 # チェックポイントの座標情報を取得
try: try:
event_cp = Location.objects.filter( event_cp = Location2025.objects.filter(
event=event, event_id=event.id,
cp_number=cp.cp_number cp_number=cp.cp_number
).first() ).first()

95
run_event_registration.sh Executable file
View File

@ -0,0 +1,95 @@
#!/bin/bash
# イベントユーザー登録実行スクリプト
#
# 使用方法:
# ./run_event_registration.sh [EVENT_CODE] [OPTIONS]
#
# 例:
# ./run_event_registration.sh 大垣2509
# ./run_event_registration.sh 大垣2509 --dry-run
# ./run_event_registration.sh 大垣2509 --csv-file CPLIST/input/custom_teams.csv
set -e
# デフォルト値
EVENT_CODE=${1:-"大垣2509"}
CSV_FILE="CPLIST/input/team2025.csv"
BASE_URL="http://localhost:8000"
DRY_RUN=""
# コマンドライン引数を解析
shift
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN="true"
shift
;;
--csv-file)
CSV_FILE="$2"
shift 2
;;
--base-url)
BASE_URL="$2"
shift 2
;;
--help)
echo "使用方法: $0 [EVENT_CODE] [OPTIONS]"
echo ""
echo "オプション:"
echo " --dry-run テスト実行実際のAPI呼び出しなし"
echo " --csv-file FILE CSVファイルパスデフォルト: CPLIST/input/team2025.csv"
echo " --base-url URL APIベースURLデフォルト: http://localhost:8000"
echo " --help このヘルプを表示"
echo ""
echo "例:"
echo " $0 大垣2509"
echo " $0 大垣2509 --dry-run"
echo " $0 大垣2509 --csv-file CPLIST/input/custom_teams.csv"
exit 0
;;
*)
echo "不明なオプション: $1"
echo "ヘルプを表示するには --help を使用してください"
exit 1
;;
esac
done
echo "=== イベントユーザー登録処理 ==="
echo "イベントコード: $EVENT_CODE"
echo "CSVファイル: $CSV_FILE"
echo "APIベースURL: $BASE_URL"
echo "テスト実行: ${DRY_RUN:-false}"
echo "================================"
# CSVファイルの存在確認
if [ ! -f "$CSV_FILE" ]; then
echo "エラー: CSVファイルが見つかりません: $CSV_FILE"
exit 1
fi
# Docker Composeファイルの存在確認
if [ ! -f "docker-compose.event-registration.yml" ]; then
echo "エラー: docker-compose.event-registration.yml が見つかりません"
exit 1
fi
# ログディレクトリを作成
mkdir -p logs
# 環境変数を設定してDocker Composeを実行
export EVENT_CODE="$EVENT_CODE"
export CSV_FILE="$CSV_FILE"
export BASE_URL="$BASE_URL"
export DRY_RUN="$DRY_RUN"
echo "Docker Composeでイベントユーザー登録処理を開始します..."
# Docker Composeを実行
docker-compose -f docker-compose.event-registration.yml up --build --remove-orphans
echo ""
echo "=== 処理完了 ==="
echo "ログファイルを確認してください: logs/register_event_users.log"

247
show_checkin_data_sql.py Normal file
View File

@ -0,0 +1,247 @@
#!/usr/bin/env python
"""
SQL直接実行版: 最近のGpsLogとCheckinImagesデータを表示
Docker環境で動作
"""
import subprocess
import sys
import json
from datetime import datetime, timedelta
def run_db_query(query):
"""
PostgreSQLクエリを実行
"""
try:
cmd = [
'docker-compose', 'exec', '-T', 'postgres-db',
'psql', '-U', 'admin', '-d', 'rogdb',
'-c', query
]
result = subprocess.run(
cmd,
capture_output=True,
text=True
)
if result.returncode == 0:
return result.stdout
else:
print(f"❌ SQLエラー: {result.stderr}")
return None
except Exception as e:
print(f"❌ 実行エラー: {e}")
return None
def show_recent_gpslog(days=7):
"""
最近のGpsLogデータを表示
"""
print(f"\n📍 GpsLog (過去{days}日間)")
print("-" * 80)
query = f"""
SELECT
id,
to_char(checkin_time, 'MM-DD HH24:MI:SS') as time,
zekken_number,
event_code,
cp_number,
CASE
WHEN image_address IS NOT NULL AND image_address != '' THEN ''
ELSE ''
END as has_image,
CASE
WHEN length(image_address) > 50 THEN left(image_address, 47) || '...'
ELSE coalesce(image_address, '')
END as image_preview
FROM rog_gpslog
WHERE checkin_time >= NOW() - INTERVAL '{days} days'
ORDER BY checkin_time DESC
LIMIT 20;
"""
result = run_db_query(query)
if result:
print(result)
def show_recent_checkinimages(days=7):
"""
最近のCheckinImagesデータを表示
"""
print(f"\n🖼️ CheckinImages (過去{days}日間)")
print("-" * 80)
query = f"""
SELECT
ci.id,
to_char(ci.checkintime, 'MM-DD HH24:MI:SS') as time,
left(cu.email, 15) as user_email,
left(ci.team_name, 12) as team,
left(ci.event_code, 10) as event,
ci.cp_number,
CASE
WHEN ci.checkinimage IS NOT NULL AND ci.checkinimage != '' THEN ''
ELSE ''
END as has_file,
CASE
WHEN length(ci.checkinimage::text) > 30 THEN left(ci.checkinimage::text, 27) || '...'
ELSE coalesce(ci.checkinimage::text, '')
END as file_preview
FROM rog_checkinimages ci
LEFT JOIN auth_user cu ON ci.user_id = cu.id
WHERE ci.checkintime >= NOW() - INTERVAL '{days} days'
ORDER BY ci.checkintime DESC
LIMIT 20;
"""
result = run_db_query(query)
if result:
print(result)
def show_table_stats():
"""
テーブル統計情報を表示
"""
print(f"\n📊 テーブル統計")
print("-" * 50)
query = """
SELECT
'GpsLog' as table_name,
count(*) as total_count,
count(CASE WHEN image_address IS NOT NULL AND image_address != '' THEN 1 END) as with_image,
to_char(min(checkin_time), 'YYYY-MM-DD') as oldest_date,
to_char(max(checkin_time), 'YYYY-MM-DD HH24:MI') as latest_date
FROM rog_gpslog
UNION ALL
SELECT
'CheckinImages' as table_name,
count(*) as total_count,
count(CASE WHEN checkinimage IS NOT NULL AND checkinimage != '' THEN 1 END) as with_file,
to_char(min(checkintime), 'YYYY-MM-DD') as oldest_date,
to_char(max(checkintime), 'YYYY-MM-DD HH24:MI') as latest_date
FROM rog_checkinimages;
"""
result = run_db_query(query)
if result:
print(result)
def show_event_stats():
"""
イベント別統計を表示
"""
print(f"\n🎯 イベント別統計 (GpsLog)")
print("-" * 40)
query = """
SELECT
event_code,
count(*) as checkin_count,
count(CASE WHEN image_address IS NOT NULL AND image_address != '' THEN 1 END) as with_image_count,
to_char(min(checkin_time), 'MM-DD') as first_checkin,
to_char(max(checkin_time), 'MM-DD') as last_checkin
FROM rog_gpslog
GROUP BY event_code
ORDER BY checkin_count DESC
LIMIT 10;
"""
result = run_db_query(query)
if result:
print(result)
print(f"\n🖼️ イベント別統計 (CheckinImages)")
print("-" * 40)
query = """
SELECT
event_code,
count(*) as image_count,
count(DISTINCT team_name) as unique_teams,
to_char(min(checkintime), 'MM-DD') as first_image,
to_char(max(checkintime), 'MM-DD') as last_image
FROM rog_checkinimages
GROUP BY event_code
ORDER BY image_count DESC
LIMIT 10;
"""
result = run_db_query(query)
if result:
print(result)
def show_recent_activity():
"""
最近のアクティビティを時系列で表示
"""
print(f"\n⏰ 最近のアクティビティ (時系列)")
print("-" * 60)
query = """
(
SELECT
'GpsLog' as source,
checkin_time as timestamp,
zekken_number as identifier,
event_code,
cp_number,
CASE WHEN image_address IS NOT NULL AND image_address != '' THEN 'with_image' ELSE 'no_image' END as note
FROM rog_gpslog
WHERE checkin_time >= NOW() - INTERVAL '24 hours'
)
UNION ALL
(
SELECT
'CheckinImages' as source,
checkintime as timestamp,
team_name as identifier,
event_code,
cp_number::text,
'image_upload' as note
FROM rog_checkinimages
WHERE checkintime >= NOW() - INTERVAL '24 hours'
)
ORDER BY timestamp DESC
LIMIT 15;
"""
result = run_db_query(query)
if result:
print(result)
def main():
"""
メイン関数
"""
print("🏃‍♂️ ロゲイニング チェックインデータ表示ツール (SQL版)")
print("=" * 80)
# 引数処理
days = 7
if len(sys.argv) > 1:
try:
days = int(sys.argv[1])
except ValueError:
print("⚠️ 日数は数値で指定してください (デフォルト: 7日)")
days = 7
print(f"📅 過去{days}日間のデータを表示します")
# データ表示
show_recent_gpslog(days)
show_recent_checkinimages(days)
show_table_stats()
show_event_stats()
show_recent_activity()
print(f"\n✅ 完了")
print(f"\n使用方法: python3 {sys.argv[0]} [日数]")
print(f"例: python3 {sys.argv[0]} 3 # 過去3日間のデータ")
if __name__ == '__main__':
main()

169
show_django_data.py Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python
"""
Django管理コマンド: 最近のGpsLogとCheckinImagesデータを表示
docker-compose exec app python manage.py shell で実行
"""
import os
import sys
import django
from datetime import datetime, timedelta
# Django設定
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.utils import timezone
from rog.models import GpsLog, CheckinImages, CustomUser
from django.db import connection
def show_recent_data(days=7):
"""
最近のチェックインデータを表示
Args:
days (int): 過去何日分のデータを表示するか
"""
# 基準日時を計算
since_date = timezone.now() - timedelta(days=days)
print(f"=== 過去{days}日間のチェックインデータ ===")
print(f"検索期間: {since_date.strftime('%Y-%m-%d %H:%M:%S')} 以降")
print("-" * 80)
# GpsLogの最近のデータ
print("\n📍 GpsLog (チェックイン記録)")
print("-" * 50)
recent_gpslogs = GpsLog.objects.filter(
checkin_time__gte=since_date
).order_by('-checkin_time')[:20]
if recent_gpslogs:
print(f"件数: {len(recent_gpslogs)}")
print()
print("ID | 時刻 | ゼッケン | イベント | CP | 画像URL")
print("-" * 80)
for log in recent_gpslogs:
image_status = "" if log.image_address else ""
image_url_preview = log.image_address[:40] + "..." if log.image_address and len(log.image_address) > 40 else log.image_address or ""
print(f"{log.id:5d} | {log.checkin_time.strftime('%m-%d %H:%M:%S'):15s} | {log.zekken_number:8s} | {log.event_code:11s} | {log.cp_number:4s} | {image_status} {image_url_preview}")
else:
print("データなし")
# CheckinImagesの最近のデータ
print(f"\n🖼️ CheckinImages (写真記録)")
print("-" * 50)
recent_images = CheckinImages.objects.filter(
checkintime__gte=since_date
).order_by('-checkintime')[:20]
if recent_images:
print(f"件数: {len(recent_images)}")
print()
print("ID | 時刻 | ユーザー | チーム名 | イベント | CP | 画像ファイル")
print("-" * 100)
for img in recent_images:
user_name = img.user.email[:12] if img.user else "Unknown"
team_name = img.team_name[:12] if img.team_name else ""
event_code = img.event_code[:11] if img.event_code else ""
image_file = str(img.checkinimage)[:30] + "..." if len(str(img.checkinimage)) > 30 else str(img.checkinimage)
print(f"{img.id:5d} | {img.checkintime.strftime('%m-%d %H:%M:%S'):15s} | {user_name:12s} | {team_name:12s} | {event_code:11s} | {img.cp_number:4d} | {image_file}")
else:
print("データなし")
def show_summary_stats():
"""
チェックインデータの統計情報を表示
"""
print(f"\n📊 データベース統計")
print("-" * 50)
# 各テーブルの総件数
gpslog_total = GpsLog.objects.count()
checkinimages_total = CheckinImages.objects.count()
print(f"GpsLog 総件数: {gpslog_total:,}")
print(f"CheckinImages 総件数: {checkinimages_total:,}")
# 最新・最古のデータ
if gpslog_total > 0:
latest_gpslog = GpsLog.objects.order_by('-checkin_time').first()
oldest_gpslog = GpsLog.objects.order_by('checkin_time').first()
print(f"GpsLog 最新: {latest_gpslog.checkin_time.strftime('%Y-%m-%d %H:%M:%S') if latest_gpslog else 'N/A'}")
print(f"GpsLog 最古: {oldest_gpslog.checkin_time.strftime('%Y-%m-%d %H:%M:%S') if oldest_gpslog else 'N/A'}")
if checkinimages_total > 0:
latest_image = CheckinImages.objects.order_by('-checkintime').first()
oldest_image = CheckinImages.objects.order_by('checkintime').first()
print(f"CheckinImages 最新: {latest_image.checkintime.strftime('%Y-%m-%d %H:%M:%S') if latest_image else 'N/A'}")
print(f"CheckinImages 最古: {oldest_image.checkintime.strftime('%Y-%m-%d %H:%M:%S') if oldest_image else 'N/A'}")
# 画像有りのGpsLog件数
gpslog_with_images = GpsLog.objects.exclude(image_address__isnull=True).exclude(image_address='').count()
print(f"画像付きGpsLog: {gpslog_with_images:,} / {gpslog_total:,} ({gpslog_with_images/gpslog_total*100:.1f}%)" if gpslog_total > 0 else "画像付きGpsLog: 0")
def show_event_breakdown():
"""
イベント別のチェックイン件数を表示
"""
print(f"\n🎯 イベント別チェックイン件数")
print("-" * 50)
# GpsLogのイベント別集計
print("GpsLog:")
with connection.cursor() as cursor:
cursor.execute("""
SELECT event_code, COUNT(*) as count
FROM rog_gpslog
GROUP BY event_code
ORDER BY count DESC
LIMIT 10
""")
for row in cursor.fetchall():
print(f" {row[0]:15s}: {row[1]:,}")
# CheckinImagesのイベント別集計
print("\nCheckinImages:")
with connection.cursor() as cursor:
cursor.execute("""
SELECT event_code, COUNT(*) as count
FROM rog_checkinimages
GROUP BY event_code
ORDER BY count DESC
LIMIT 10
""")
for row in cursor.fetchall():
print(f" {row[0]:15s}: {row[1]:,}")
# 引数処理
days = 7
if len(sys.argv) > 1:
try:
days = int(sys.argv[1])
except ValueError:
print("⚠️ 日数は数値で指定してください")
days = 7
print("🏃‍♂️ ロゲイニング チェックインデータ表示ツール")
print("=" * 80)
try:
# データ表示
show_recent_data(days)
show_summary_stats()
show_event_breakdown()
print(f"\n✅ 完了")
except Exception as e:
print(f"❌ エラーが発生しました: {e}")
import traceback
traceback.print_exc()

175
show_recent_checkin_data.py Normal file
View File

@ -0,0 +1,175 @@
#!/usr/bin/env python
"""
最近のGpsLogとCheckinImagesデータを表示するスクリプト
"""
import os
import sys
import django
from datetime import datetime, timedelta
from django.utils import timezone
# Django設定
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from rog.models import GpsLog, CheckinImages, CustomUser
from django.db import connection
def show_recent_data(days=7):
"""
最近のチェックインデータを表示
Args:
days (int): 過去何日分のデータを表示するか
"""
# 基準日時を計算
since_date = timezone.now() - timedelta(days=days)
print(f"=== 過去{days}日間のチェックインデータ ===")
print(f"検索期間: {since_date.strftime('%Y-%m-%d %H:%M:%S')} 以降")
print("-" * 80)
# GpsLogの最近のデータ
print("\n📍 GpsLog (チェックイン記録)")
print("-" * 50)
recent_gpslogs = GpsLog.objects.filter(
checkin_time__gte=since_date
).order_by('-checkin_time')[:20]
if recent_gpslogs:
print(f"件数: {recent_gpslogs.count()}")
print()
print("ID | 時刻 | ゼッケン | イベント | CP | 画像URL")
print("-" * 80)
for log in recent_gpslogs:
image_status = "" if log.image_address else ""
image_url_preview = log.image_address[:40] + "..." if log.image_address and len(log.image_address) > 40 else log.image_address or ""
print(f"{log.id:5d} | {log.checkin_time.strftime('%m-%d %H:%M:%S'):15s} | {log.zekken_number:8s} | {log.event_code:11s} | {log.cp_number:4s} | {image_status} {image_url_preview}")
else:
print("データなし")
# CheckinImagesの最近のデータ
print(f"\n🖼️ CheckinImages (写真記録)")
print("-" * 50)
recent_images = CheckinImages.objects.filter(
checkintime__gte=since_date
).order_by('-checkintime')[:20]
if recent_images:
print(f"件数: {recent_images.count()}")
print()
print("ID | 時刻 | ユーザー | チーム名 | イベント | CP | 画像ファイル")
print("-" * 100)
for img in recent_images:
user_name = img.user.email[:12] if img.user else "Unknown"
team_name = img.team_name[:12] if img.team_name else ""
event_code = img.event_code[:11] if img.event_code else ""
image_file = str(img.checkinimage)[:30] + "..." if len(str(img.checkinimage)) > 30 else str(img.checkinimage)
print(f"{img.id:5d} | {img.checkintime.strftime('%m-%d %H:%M:%S'):15s} | {user_name:12s} | {team_name:12s} | {event_code:11s} | {img.cp_number:4d} | {image_file}")
else:
print("データなし")
def show_summary_stats():
"""
チェックインデータの統計情報を表示
"""
print(f"\n📊 データベース統計")
print("-" * 50)
# 各テーブルの総件数
gpslog_total = GpsLog.objects.count()
checkinimages_total = CheckinImages.objects.count()
print(f"GpsLog 総件数: {gpslog_total:,}")
print(f"CheckinImages 総件数: {checkinimages_total:,}")
# 最新・最古のデータ
if gpslog_total > 0:
latest_gpslog = GpsLog.objects.order_by('-checkin_time').first()
oldest_gpslog = GpsLog.objects.order_by('checkin_time').first()
print(f"GpsLog 最新: {latest_gpslog.checkin_time.strftime('%Y-%m-%d %H:%M:%S') if latest_gpslog else 'N/A'}")
print(f"GpsLog 最古: {oldest_gpslog.checkin_time.strftime('%Y-%m-%d %H:%M:%S') if oldest_gpslog else 'N/A'}")
if checkinimages_total > 0:
latest_image = CheckinImages.objects.order_by('-checkintime').first()
oldest_image = CheckinImages.objects.order_by('checkintime').first()
print(f"CheckinImages 最新: {latest_image.checkintime.strftime('%Y-%m-%d %H:%M:%S') if latest_image else 'N/A'}")
print(f"CheckinImages 最古: {oldest_image.checkintime.strftime('%Y-%m-%d %H:%M:%S') if oldest_image else 'N/A'}")
# 画像有りのGpsLog件数
gpslog_with_images = GpsLog.objects.exclude(image_address__isnull=True).exclude(image_address='').count()
print(f"画像付きGpsLog: {gpslog_with_images:,} / {gpslog_total:,} ({gpslog_with_images/gpslog_total*100:.1f}%)" if gpslog_total > 0 else "画像付きGpsLog: 0")
def show_event_breakdown():
"""
イベント別のチェックイン件数を表示
"""
print(f"\n🎯 イベント別チェックイン件数")
print("-" * 50)
# GpsLogのイベント別集計
print("GpsLog:")
with connection.cursor() as cursor:
cursor.execute("""
SELECT event_code, COUNT(*) as count
FROM rog_gpslog
GROUP BY event_code
ORDER BY count DESC
LIMIT 10
""")
for row in cursor.fetchall():
print(f" {row[0]:15s}: {row[1]:,}")
# CheckinImagesのイベント別集計
print("\nCheckinImages:")
with connection.cursor() as cursor:
cursor.execute("""
SELECT event_code, COUNT(*) as count
FROM rog_checkinimages
GROUP BY event_code
ORDER BY count DESC
LIMIT 10
""")
for row in cursor.fetchall():
print(f" {row[0]:15s}: {row[1]:,}")
def main():
"""
メイン関数
"""
print("🏃‍♂️ ロゲイニング チェックインデータ表示ツール")
print("=" * 80)
try:
# 引数処理
days = 7
if len(sys.argv) > 1:
try:
days = int(sys.argv[1])
except ValueError:
print("⚠️ 日数は数値で指定してください")
days = 7
# データ表示
show_recent_data(days)
show_summary_stats()
show_event_breakdown()
print(f"\n✅ 完了")
except Exception as e:
print(f"❌ エラーが発生しました: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()

View File

@ -930,6 +930,13 @@
const teamData = await teamResponse.json(); const teamData = await teamResponse.json();
const checkinsData = await checkinsResponse.json(); const checkinsData = await checkinsResponse.json();
// デバッグ: APIレスポンスの内容を確認
console.log('Checkins API Response:', checkinsData);
if (checkinsData.length > 0) {
console.log('First checkin sample:', checkinsData[0]);
console.log('Image address sample:', checkinsData[0].image_address);
}
// ゴール時刻の表示を更新 // ゴール時刻の表示を更新
updateGoalTimeDisplay(teamData.end_datetime); updateGoalTimeDisplay(teamData.end_datetime);
original_goal_time = teamData.end_datetime; original_goal_time = teamData.end_datetime;
@ -1001,6 +1008,11 @@
tr.dataset.path_order = index+1; tr.dataset.path_order = index+1;
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : ''; const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : '';
// デバッグ: 画像アドレスを確認
if (checkin.image_address) {
console.log(`CP ${checkin.cp_number}: image_address = ${checkin.image_address}`);
}
tr.innerHTML = ` tr.innerHTML = `
<td class="px-1 py-3 cursor-move"> <td class="px-1 py-3 cursor-move">
<i class="fas fa-bars text-gray-400"></i> <i class="fas fa-bars text-gray-400"></i>
@ -1011,11 +1023,12 @@
`<img src="/media/compressed/${checkin.photos}" class="h-20 w-20 object-cover rounded" onclick="showLargeImage(this.src)">` : ''} `<img src="/media/compressed/${checkin.photos}" class="h-20 w-20 object-cover rounded" onclick="showLargeImage(this.src)">` : ''}
</td> </td>
<td class="px-2 py-3"> <td class="px-2 py-3">
${checkin.cp_number===-1 || checkin.image_address===null ? "" : ${checkin.image_address ?
`<img src="${checkin.image_address}" `<img src="${checkin.image_address}"
class="h-20 w-20 object-cover rounded" class="h-20 w-20 object-cover rounded"
onclick="showLargeImage(this.src)" onclick="showLargeImage(this.src)"
onerror="this.parentElement.innerHTML=''">`} onerror="this.style.display='none'; this.parentElement.innerHTML='<div class=\\'text-xs text-gray-500\\'>画像なし</div>'">`
: '<div class="text-xs text-gray-500">画像なし</div>'}
</td> </td>
<td class="px-2 py-3 ${bgColor}"> <td class="px-2 py-3 ${bgColor}">
<div class="font-bold">${checkin.sub_loc_id}</div> <div class="font-bold">${checkin.sub_loc_id}</div>

View File

@ -4,6 +4,20 @@
{% block content %} {% block content %}
<h1>チェックポイントCSVアップロード</h1> <h1>チェックポイントCSVアップロード</h1>
{% if user.is_staff %}
<div class="messagelist">
<div class="info">
<strong>スタッフユーザー:</strong> 全ステータスdraft, private, publicのイベントが表示されます。
</div>
</div>
{% else %}
<div class="messagelist">
<div class="info">
<strong>一般ユーザー:</strong> publicステータスのイベントのみが表示されます。
</div>
</div>
{% endif %}
<div class="module"> <div class="module">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
@ -14,7 +28,7 @@
<select name="event" required> <select name="event" required>
<option value="">-- イベントを選択 --</option> <option value="">-- イベントを選択 --</option>
{% for event in events %} {% for event in events %}
<option value="{{ event.id }}">{{ event.event_name }}</option> <option value="{{ event.id }}">{{ event.event_name }} ({{ event.status }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -38,9 +52,9 @@
<h2>CSVフォーマット</h2> <h2>CSVフォーマット</h2>
<p>以下の形式でCSVファイルを作成してください</p> <p>以下の形式でCSVファイルを作成してください</p>
<pre> <pre>
cp_number,cp_name,latitude,longitude,cp_point,photo_point,buy_point,address,phone,description cp_number,cp_name,latitude,longitude,cp_point,photo_point,buy_point,address,phone,description,sub_loc_id,subcategory,photos,videos,tags,evaluation_value,remark,hidden_location
1,岐阜駅前,35.4091,136.7581,10,0,0,岐阜県岐阜市橋本町1-10-1,058-123-4567,JR岐阜駅前広場 1,岐阜駅前,35.4091,136.7581,10,0,0,岐阜県岐阜市橋本町1-10-1,058-123-4567,JR岐阜駅前広場,#1(10),交通,IMG_001.JPG,,駅を背景に撮影,50,岐阜市の中心駅です,false
2,岐阜城,35.4329,136.7817,15,5,0,岐阜県岐阜市金華山天守閣18,058-263-4853,金華山の頂上にある歴史ある城 2,岐阜城,35.4329,136.7817,15,5,0,岐阜県岐阜市金華山天守閣18,058-263-4853,金華山の頂上にある歴史ある城,#2(20),史跡,IMG_002.JPG,VID_001.MP4,城と景色を撮影,85,織田信長ゆかりの名城,false
</pre> </pre>
<h3>項目説明</h3> <h3>項目説明</h3>
@ -54,8 +68,30 @@ cp_number,cp_name,latitude,longitude,cp_point,photo_point,buy_point,address,phon
<li><strong>buy_point</strong>: 買い物ポイント(デフォルト: 0</li> <li><strong>buy_point</strong>: 買い物ポイント(デフォルト: 0</li>
<li><strong>address</strong>: 住所(任意)</li> <li><strong>address</strong>: 住所(任意)</li>
<li><strong>phone</strong>: 電話番号(任意)</li> <li><strong>phone</strong>: 電話番号(任意)</li>
<li><strong>description</strong>: 説明(任意)</li> <li><strong>description</strong>: 基本説明(任意)</li>
<li><strong>sub_loc_id</strong>: サブロケーションID任意、例: #1(10)</li>
<li><strong>subcategory</strong>: サブカテゴリ(任意、例: 史跡、観光名所、神社など)</li>
<li><strong>photos</strong>: 写真ファイル名(任意、複数の場合はカンマ区切り)</li>
<li><strong>videos</strong>: 動画ファイル名(任意、複数の場合はカンマ区切り)</li>
<li><strong>tags</strong>: 撮影タグ(任意、例: 建物を背景に撮影)</li>
<li><strong>evaluation_value</strong>: 評価値(任意、数値)</li>
<li><strong>remark</strong>: 詳細説明(任意、長文可)</li>
<li><strong>hidden_location</strong>: 隠しロケーションtrue/false、デフォルト: false</li>
</ul> </ul>
<div class="help">
<h4>📝 新フィールドの使用例</h4>
<ul>
<li><strong>sub_loc_id</strong>: "#CP001(50)" - CP番号とポイントの組み合わせ</li>
<li><strong>subcategory</strong>: "史跡", "観光名所", "神社", "グルメ", "自然" など</li>
<li><strong>photos</strong>: "IMG_001.JPG,IMG_002.JPG" - 複数ファイルはカンマ区切り</li>
<li><strong>videos</strong>: "VID_001.MP4" - 動画ファイル名</li>
<li><strong>tags</strong>: "建物を背景に撮影", "看板と一緒に" など撮影指示</li>
<li><strong>evaluation_value</strong>: "85" - 0-100の数値評価</li>
<li><strong>remark</strong>: 詳細な歴史的背景や訪問時の注意事項など</li>
<li><strong>hidden_location</strong>: "true" - 一般公開しない特別なロケーション</li>
</ul>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,21 @@
{{ team_name }} 代表者 {{leader_name}} 様
岐阜ロゲ in 大垣 へのご参加ありがとうございます。
ご連絡が大変遅くなり、申し訳ございません。
以下の内容でパスワードをお送りいたしますので、よろしくお願い申し上げます。
■ チーム情報
チーム名: {{ team_name }}
部門: {{ category }}{{ duration }}時間)
ユーザー名: {{ email }}
パスワード: {{ password }}
--
岐阜ロゲ in 大垣
運営NPO 岐阜aiネットワーク
Email: info@gifuai.net

View File

@ -0,0 +1 @@
【岐阜ロゲ in 大垣】チーム「{{ team_name }}」パスワードのご連絡

109
test_auto_start.py Normal file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
自動スタート機能テストツール
大垣2509の宮だみーゼッケン9999でチェックイン→自動スタート機能をテスト
"""
import requests
import json
import time
def test_auto_start_checkin():
"""自動スタート機能のテスト"""
print("🚀 自動スタート機能テスト開始")
print("=" * 60)
# テストデータ大垣2509の宮だみー
test_data = {
"event_code": "大垣2509",
"team_name": "宮だみー",
"cp_number": "1", # 最初のチェックポイント
"image": "data:image/jpeg;base64,auto_start_test",
"buy_flag": False
}
print(f"📊 テストデータ:")
print(f" イベント: {test_data['event_code']}")
print(f" チーム: {test_data['team_name']}")
print(f" チェックポイント: {test_data['cp_number']}")
# チェックインAPIを実行
url = "http://localhost:8100/api/checkin_from_rogapp"
print(f"\n🎯 チェックインAPI実行:")
print(f" URL: {url}")
print(f" Method: POST")
try:
response = requests.post(url, json=test_data, timeout=10)
print(f"\n📥 レスポンス:")
print(f" ステータス: HTTP {response.status_code}")
# レスポンス内容を整形して表示
try:
response_data = response.json()
print(f" 内容: {json.dumps(response_data, ensure_ascii=False, indent=2)}")
# 成功判定
if response.status_code == 200 and response_data.get('status') == 'OK':
print(f"\n✅ 自動スタート機能テスト成功!")
return True
else:
print(f"\n❌ 自動スタート機能テスト失敗")
return False
except json.JSONDecodeError:
print(f" 内容: {response.text}")
return False
except Exception as e:
print(f"\n❌ APIリクエストエラー: {e}")
return False
def check_database_status():
"""データベースの状況確認"""
print(f"\n📊 データベース確認:")
print(f" 以下のSQLを実行して確認してください:")
print(f"")
print(f" -- 宮だみーのエントリー状況確認")
print(f" SELECT ")
print(f" en.id, en.zekken_number, t.team_name, ")
print(f" en.is_in_rog, en.rogaining_counted, en.start_time")
print(f" FROM rog_entry en")
print(f" JOIN rog_team t ON en.team_id = t.id")
print(f" JOIN rog_newevent2 e ON en.event_id = e.id")
print(f" WHERE en.zekken_number = 9999 AND e.event_code = 'ogaki2509';")
print(f"")
print(f" -- 宮だみーのGpsLogエントリー確認")
print(f" SELECT * FROM rog_gpslog ")
print(f" WHERE zekken_number = 9999 AND event_code = '大垣2509'")
print(f" ORDER BY serial_number;")
def main():
"""メイン処理"""
print("🔧 自動スタート機能テストツール")
print("対象: 大垣2509 - 宮だみーゼッケン9999")
print("=" * 60)
# テスト実行
success = test_auto_start_checkin()
# データベース確認方法を表示
check_database_status()
# 結果サマリー
print(f"\n📋 テスト結果サマリー:")
print(f" 自動スタート機能: {'✅ 成功' if success else '❌ 失敗'}")
if success:
print(f"\n🎉 自動スタート機能が正常に動作しています!")
print(f" これで、スマホアプリユーザーがスタート処理を忘れても、")
print(f" 最初のチェックイン時に自動的にスタートされます。")
else:
print(f"\n⚠️ 自動スタート機能に問題があります。")
print(f" ログを確認して問題を調査してください。")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,3 @@
cp_number,cp_name,latitude,longitude,cp_point,photo_point,buy_point,address,phone,description,sub_loc_id,subcategory,photos,videos,tags,evaluation_value,remark,hidden_location
9999,テスト用チェックポイント,35.4091,136.7581,50,20,5,岐阜県岐阜市テスト町1-1,058-999-9999,CSVアップロードテスト用のチェックポイントです,#9999(75),テスト,TEST_IMG.JPG,TEST_VID.MP4,テスト用撮影タグ,95,これはCSVアップロード機能のテスト用データです。新しいフィールドがすべて正しくインポートされることを確認します。,false
9998,隠しテストポイント,35.4000,136.7500,100,50,0,岐阜県岐阜市隠れ町2-2,058-888-8888,隠しロケーションのテスト,#9998(150),秘密,SECRET_IMG.JPG,,秘密の撮影,100,隠しロケーションとして設定されたテスト用チェックポイント,true
1 cp_number cp_name latitude longitude cp_point photo_point buy_point address phone description sub_loc_id subcategory photos videos tags evaluation_value remark hidden_location
2 9999 テスト用チェックポイント 35.4091 136.7581 50 20 5 岐阜県岐阜市テスト町1-1 058-999-9999 CSVアップロードテスト用のチェックポイントです #9999(75) テスト TEST_IMG.JPG TEST_VID.MP4 テスト用撮影タグ 95 これはCSVアップロード機能のテスト用データです。新しいフィールドがすべて正しくインポートされることを確認します。 false
3 9998 隠しテストポイント 35.4000 136.7500 100 50 0 岐阜県岐阜市隠れ町2-2 058-888-8888 隠しロケーションのテスト #9998(150) 秘密 SECRET_IMG.JPG 秘密の撮影 100 隠しロケーションとして設定されたテスト用チェックポイント true

3
test_email.csv Normal file
View File

@ -0,0 +1,3 @@
チーム名,部門,時間,氏名1,メール,電話番号
テストチーム1,一般,4,テスト 太郎,test@example.com,090-1234-5678
テストチーム2,ファミリー,3,テスト 花子,test2@example.com,080-9876-5432
1 チーム名 部門 時間 氏名1 メール 電話番号
2 テストチーム1 一般 4 テスト 太郎 test@example.com 090-1234-5678
3 テストチーム2 ファミリー 3 テスト 花子 test2@example.com 080-9876-5432

5
test_ogaki.csv Normal file
View File

@ -0,0 +1,5 @@
loc_id,sub_loc_id,cp,loc_name,category,subcategory,zip,address,prefecture,area,city,latitude,longitude,photos,videos,webcontent,status,portal,loc_group,phone,fax,email,facility,remark,tags,hidden_location,auto_checkin,checkin_radius,checkin_point,buy_point,evaluation_value,shop_closed,shop_shutdown,opening_hours_mon,opening_hours_tue,opening_hours_wed,opening_hours_thu,opening_hours_fri,opening_hours_sat,opening_hours_sun,createdate,createuserid,lastupdatedate,lastupdateuserid,check_flag
"122,200,001",#-2(0),-2,大垣公園,スタート,,〒503-0887,郭町2丁目53,岐阜県,西濃,大垣市,35.3603304,136.6152146,KOJ09707.jpg,,https://www.aeon.jp/sc/ogaki/,CP,,大垣2025,080-9578-2796,,,,大垣公園に設置するゲートをスタート・ゴールポイントとします。,,FALSE,FALSE,-1,0,0,0,,,,,,,,,,2023-10-29 14:51:35 +0900,2,2023-10-29 14:51:35 +0900,2,
"122,200,001",#-1(0),-1,大垣公園,ゴール,,〒503-0887,郭町2丁目53,岐阜県,西濃,大垣市,35.3603326,136.61592,KOJ09707.jpg,,https://www.aeon.jp/sc/ogaki/,CP,,大垣2025,080-9578-2796,,,,大垣公園に設置するゲートをスタート・ゴールポイントとします。,,FALSE,FALSE,-1,0,0,0,,,,,,,,,,2023-10-29 14:51:35 +0900,2,2023-10-29 14:51:35 +0900,2,
"122,200,079",#1(5),1,大垣公園遊具,聲の形聖地,,〒503-0887,郭町2丁目53,岐阜県,西濃,大垣市,35.361568,136.6153282,IMG_6429.JPG,,https://www.ogakikanko.jp/koenokatati/,CP,,大垣2025,,,,,アニメ「聲の形」聖地の1つです。石田がマリアを遊ばせにこの公園に行ったとき、遊具の中で結紘を見つけました。,遊具を背景に撮影,FALSE,FALSE,-1,5,0,0,,,,,,,,,,2023-11-11 23:46:15 +0900,2,2023-11-11 23:46:15 +0900,2,
"122,200,014",#2(10),2,大垣城,史跡,歴史(室町),〒503-0887,郭町2丁目52,岐阜県,西濃,大垣市,35.3619211,136.6155542,IMG_1413.JPG,,http://www.city.ogaki.lg.jp/0000000577.html,CP,,大垣2025,0584-74-7875,,,大垣商業高校,美濃守護・土岐一族の宮川吉左衛門尉安定により、天文4(1535)年に創建されたと伝えられています。関ケ原の戦いでは、西軍・石田三成の本拠地となりました。その後、戸田氏が十万石の城主となりました。,騎馬像と大垣城を背景に撮影,FALSE,FALSE,-1,10,0,0,,,,9時00分17時00分,定休日,9時00分17時00分,9時00分17時00分,9時00分17時00分,9時00分17時00分,9時00分17時00分,2,2023-10-29 11:56:13 +0900,2,
1 loc_id sub_loc_id cp loc_name category subcategory zip address prefecture area city latitude longitude photos videos webcontent status portal loc_group phone fax email facility remark tags hidden_location auto_checkin checkin_radius checkin_point buy_point evaluation_value shop_closed shop_shutdown opening_hours_mon opening_hours_tue opening_hours_wed opening_hours_thu opening_hours_fri opening_hours_sat opening_hours_sun createdate createuserid lastupdatedate lastupdateuserid check_flag
2 122,200,001 #-2(0) -2 大垣公園 スタート 〒503-0887 郭町2丁目53 岐阜県 西濃 大垣市 35.3603304 136.6152146 KOJ09707.jpg https://www.aeon.jp/sc/ogaki/ CP 大垣2025 080-9578-2796 大垣公園に設置するゲートをスタート・ゴールポイントとします。 FALSE FALSE -1 0 0 0 2023-10-29 14:51:35 +0900 2 2023-10-29 14:51:35 +0900 2
3 122,200,001 #-1(0) -1 大垣公園 ゴール 〒503-0887 郭町2丁目53 岐阜県 西濃 大垣市 35.3603326 136.61592 KOJ09707.jpg https://www.aeon.jp/sc/ogaki/ CP 大垣2025 080-9578-2796 大垣公園に設置するゲートをスタート・ゴールポイントとします。 FALSE FALSE -1 0 0 0 2023-10-29 14:51:35 +0900 2 2023-10-29 14:51:35 +0900 2
4 122,200,079 #1(5) 1 大垣公園遊具 聲の形聖地 〒503-0887 郭町2丁目53 岐阜県 西濃 大垣市 35.361568 136.6153282 IMG_6429.JPG https://www.ogakikanko.jp/koenokatati/ CP 大垣2025 アニメ「聲の形」聖地の1つです。石田がマリアを遊ばせにこの公園に行ったとき、遊具の中で結紘を見つけました。 遊具を背景に撮影 FALSE FALSE -1 5 0 0 2023-11-11 23:46:15 +0900 2 2023-11-11 23:46:15 +0900 2
5 122,200,014 #2(10) 2 大垣城 史跡 歴史(室町) 〒503-0887 郭町2丁目52 岐阜県 西濃 大垣市 35.3619211 136.6155542 IMG_1413.JPG http://www.city.ogaki.lg.jp/0000000577.html CP 大垣2025 0584-74-7875 大垣商業高校 美濃守護・土岐一族の宮川吉左衛門尉安定により、天文4(1535)年に創建されたと伝えられています。関ケ原の戦いでは、西軍・石田三成の本拠地となりました。その後、戸田氏が十万石の城主となりました。 騎馬像と大垣城を背景に撮影 FALSE FALSE -1 10 0 0 9時00分~17時00分 定休日 9時00分~17時00分 9時00分~17時00分 9時00分~17時00分 9時00分~17時00分 9時00分~17時00分 2 2023-10-29 11:56:13 +0900 2

7
test_simple_route.py Normal file
View File

@ -0,0 +1,7 @@
@api_view(['GET'])
def get_route_simple(request):
"""テスト用の簡単なルート取得関数"""
return Response({
"status": "OK",
"message": "Simple route function is working"
})

View File

@ -0,0 +1,115 @@
#!/usr/bin/env python
"""
Location2025テーブルの新しいフィールドを旧Locationテーブルから更新するスクリプト
"""
import os
import sys
import django
# Django設定
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rogaining_srv.settings')
django.setup()
from rog.models import Location2025, Location
from django.db import transaction
def update_location2025_from_old_location():
"""
旧Locationテーブルから Location2025 の新しいフィールドを更新
"""
print("=== Location2025フィールド更新開始 ===")
# 更新対象のフィールドマッピング
field_mapping = {
'category': 'category',
'zip_code': 'zip', # Location2025.zip_code <- Location.zip
'prefecture': 'prefecture',
'area': 'area',
'city': 'city',
'facility': 'facility'
}
updated_count = 0
not_found_count = 0
try:
with transaction.atomic():
location2025_records = Location2025.objects.all()
total_records = location2025_records.count()
print(f"処理対象レコード数: {total_records}")
for i, loc2025 in enumerate(location2025_records, 1):
if i % 100 == 0:
print(f"進行状況: {i}/{total_records}")
# location_idまたはcp_nameで旧Locationテーブルから対応するレコードを検索
old_location = None
# まずlocation_idで検索sub_loc_idがlocation_idの可能性
if loc2025.sub_loc_id:
try:
location_id = int(loc2025.sub_loc_id)
old_location = Location.objects.filter(location_id=location_id).first()
except (ValueError, TypeError):
pass
# location_idで見つからない場合、location_nameで検索
if not old_location:
old_location = Location.objects.filter(location_name=loc2025.cp_name).first()
# 部分一致で検索
if not old_location and loc2025.cp_name:
old_location = Location.objects.filter(location_name__icontains=loc2025.cp_name).first()
if old_location:
# フィールドを更新
updated = False
for loc2025_field, location_field in field_mapping.items():
if hasattr(old_location, location_field):
old_value = getattr(old_location, location_field)
if old_value: # 値が存在する場合のみ更新
# zip_codeの場合、〒記号を除去
if loc2025_field == 'zip_code' and old_value.startswith(''):
old_value = old_value[1:]
setattr(loc2025, loc2025_field, old_value)
updated = True
if updated:
loc2025.save()
updated_count += 1
if i <= 5: # 最初の5件の詳細を表示
print(f" 更新: CP{loc2025.cp_number} {loc2025.cp_name}")
print(f" Category: {loc2025.category}")
print(f" Area: {loc2025.area}, Prefecture: {loc2025.prefecture}")
print(f" City: {loc2025.city}, Zip: {loc2025.zip_code}")
print(f" Facility: {loc2025.facility}")
else:
not_found_count += 1
if not_found_count <= 5: # 最初の5件のエラーを表示
print(f" 見つからない: CP{loc2025.cp_number} {loc2025.cp_name} (sub_loc_id: {loc2025.sub_loc_id})")
print(f"\n=== 更新完了 ===")
print(f"更新されたレコード数: {updated_count}")
print(f"対応するLocationが見つからないレコード数: {not_found_count}")
# 更新結果のサンプルを表示
print(f"\n=== 更新後サンプル ===")
updated_samples = Location2025.objects.exclude(category__isnull=True).exclude(category='')[:3]
for sample in updated_samples:
print(f"CP{sample.cp_number}: {sample.cp_name}")
print(f" Category: {sample.category}")
print(f" Location: {sample.prefecture} {sample.area} {sample.city}")
print(f" Zip: {sample.zip_code}, Facility: {sample.facility}")
print()
except Exception as e:
print(f"エラーが発生しました: {e}")
raise
if __name__ == "__main__":
update_location2025_from_old_location()

View File

@ -0,0 +1,42 @@
外部システムAPI仕様書.md を前提に、ユーザーデータcsvから、
各ユーザーごとにユーザー登録、チーム登録、エントリー登録、イベント参加を行う
docker compose で実施するPythonスクリプトを作成しなさい。
ユーザーデータのCSVは以下の項目を持つ。
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
1. 起動パラメータで、event_code=大垣2509 を指定する.
2. CSV(CPLIST/input/team2025.csv)を読みこみ、各行ごとに下記の処理を行う。
2-1. カスタムユーザー登録 API
# メールアドレスをキーに既存ユーザーを取得
検索がヒットしなければ、ユーザー登録する。
検索がヒットすれば、パスワードを更新する。
event_codeに指定event_codeを設定
zekken_number に zekken を入力
team_name に team名を入れる
2-2. チーム登録、メンバー登録
# 部門・時間・チーム名でチーム登録
# メンバーを1名ずつ7名まで登録
## それぞれダミーメアドと名前と生年月日でメンバー登録
2-3. エントリー登録
# 指定されたイベントにチームを9/6で登録する。
2-4. イベント参加
# 登録したエントリーでイベント参加する。

35
エントリー.md Normal file
View File

@ -0,0 +1,35 @@
CPLIST/input/teams2025.csv から、以下の手順でデータベーステーブルに書き込むプログラムを作成しなさい。docker compose 環境
CSVは以下の項目を持つ。
部門別数,時間,部門,チーム名,メール,パスワード,電話番号,氏名1,誕生日1,氏名2,誕生日2,氏名3,誕生日3,氏名4,誕生日4,氏名5,誕生日5,氏名6,誕生日6,氏名7,誕生日7,,
1. 起動パラメータで、event_code を指定する. DBはrogdbを使用。user は admin パスワードは admin123456 ホストは localhost を指定
2. CSVを読みこみ、各行ごとに下記の処理を行う。
2-1. カスタムユーザー
# メールアドレスをキーに既存ユーザーを取得
 検索がヒットしなければ、ユーザー登録する。
検索がヒットすれば、パスワードを更新し、
  event_codeに指定event_codeを設定
  zekken_number に zekken を入力
  team_name に team名を入れる
2-2. チーム登録
# 部門・時間・チーム名でチーム登録
# メンバーを1名ずつ7名まで登録
## それぞれダミーメアドと名前と生年月日でメンバー登録
2-3. エントリー登録
# 指定されたイベントにチームを登録する。
2-4. イベント参加
# 登録したエントリーでイベント参加する。

View File

@ -0,0 +1,258 @@
# サーバーAPI変更要求書
**文書番号:** API-REQ-20250901
**作成日:** 2025年9月1日
**作成者:** アプリ開発チーム
**対象システム:** 岐阜ロゲイニングアプリ サーバーAPI
## 概要
岐阜ロゲイニングアプリにおいて、ユーザーのエントリー参加後にアプリ側とサーバー側のデータ同期に問題が発生しています。具体的には、ユーザー情報に含まれる`event_code`とEntryControllerで管理されている実際のエントリー情報が一致しないため、競技開始時にValidationErrorが発生している状況です。
## 問題の詳細
### 現在の問題
1. **データ同期の不整合**
- ユーザー情報: `event_code: "TestEvent"` (Event ID: 140)
- 実際のエントリー: `event: "揖斐川"` (Event ID: 128, Entry ID: 747)
2. **競技開始時のエラー**
- API: `PATCH /api/entries/747/update-status/`
- エラー: `ValidationError at /api/entries/747/update-status/`
- ステータス: HTTP 500
3. **ユーザー情報の未更新**
- エントリー参加後にユーザー情報(`currentUser`)が最新のエントリー情報で更新されない
## 要求される変更
### 1. 新規API追加
#### API 1: ユーザーの最新エントリー情報取得API
**エンドポイント:** `GET /api/user/current-entry-info/`
**目的:** ユーザーの最新エントリー情報を取得し、アプリ側とサーバー側のデータ同期を確保
**認証:** Token認証必須
**リクエスト**
```http
GET /api/user/current-entry-info/
Authorization: Token <user_token>
Content-Type: application/json; charset=UTF-8
```
**成功レスポンス (HTTP 200)**
```json
{
"user": {
"id": 1765,
"email": "akira.miyata@gifuai.net",
"firstname": "明",
"lastname": "宮田",
"is_staff": true,
"event_code": "揖斐川"
},
"current_entry": {
"id": 747,
"team": {
"id": 155,
"team_name": "狸の宮家_v2",
"category": {
"id": 1,
"category_name": "一般-5時間"
}
},
"event": {
"id": 128,
"event_name": "揖斐川",
"start_datetime": "2025-01-25T10:00:00+09:00",
"end_datetime": "2025-01-25T16:00:00+09:00"
},
"date": "2025-01-25",
"has_participated": false,
"has_goaled": false
},
"entries_count": 6,
"latest_entry_date": "2025-01-25T10:00:00+09:00"
}
```
**エラーレスポンス (HTTP 404)**
```json
{
"error": "No active entry found for user",
"detail": "User has no current entries"
}
```
**実装要件:**
- ユーザーの最新エントリーを`team__owner`で検索
- エントリーは`created_at`の降順でソート
- ユーザー情報の`event_code`は最新エントリーのイベント名を反映
- Team, Event, Category情報を含む完全な情報を返却
### 2. 既存API修正
#### API 2: エントリーステータス更新API修正
**エンドポイント:** `PATCH /api/entries/{entry_id}/update-status/`
**問題:** 現在ValidationErrorが発生
**現在のリクエストボディ:**
```json
{
"hasParticipated": true,
"hasGoaled": false
}
```
**期待されるフィールド名 (要確認):**
```json
{
"has_participated": true,
"has_goaled": false
}
```
**対応要求:**
1. フィールド名の整合性確認
2. バリデーションエラーの原因調査
3. 適切なエラーレスポンスの実装
## 実装方針
### Django REST Framework実装例
```python
# views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.contrib.auth.decorators import login_required
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def current_entry_info(request):
user = request.user
# ユーザーの最新エントリーを取得
latest_entry = Entry.objects.filter(
team__owner=user
).order_by('-created_at').first()
if not latest_entry:
return Response({
'error': 'No active entry found for user',
'detail': 'User has no current entries'
}, status=404)
# レスポンスデータを構築
response_data = {
'user': {
'id': user.id,
'email': user.email,
'firstname': user.first_name,
'lastname': user.last_name,
'is_staff': user.is_staff,
'event_code': latest_entry.event.event_name # 最新エントリーのイベント名
},
'current_entry': {
'id': latest_entry.id,
'team': {
'id': latest_entry.team.id,
'team_name': latest_entry.team.team_name,
'category': {
'id': latest_entry.category.id,
'category_name': latest_entry.category.category_name
}
},
'event': {
'id': latest_entry.event.id,
'event_name': latest_entry.event.event_name,
'start_datetime': latest_entry.event.start_datetime,
'end_datetime': latest_entry.event.end_datetime
},
'date': latest_entry.date,
'has_participated': latest_entry.has_participated,
'has_goaled': latest_entry.has_goaled
},
'entries_count': Entry.objects.filter(team__owner=user).count(),
'latest_entry_date': latest_entry.created_at
}
return Response(response_data)
```
```python
# urls.py
urlpatterns = [
path('api/user/current-entry-info/', views.current_entry_info, name='current_entry_info'),
# ... 他のURL
]
```
## 期待される効果
1. **データ整合性の確保**
- アプリ側とサーバー側のエントリー情報が常に同期
- 正しいentry_idとevent_codeを使用した通信
2. **エラーの解消**
- 競技開始時のValidationErrorの解決
- 適切なAPI呼び出しの実現
3. **ユーザビリティの向上**
- 競技開始がスムーズに行える
- データの不整合によるアプリクラッシュの防止
## 実装スケジュール
| 項目 | 期限 | 担当 |
|------|------|------|
| API設計レビュー | 2025年9月3日 | サーバーチーム |
| 新規API実装 | 2025年9月5日 | サーバーチーム |
| 既存API修正 | 2025年9月5日 | サーバーチーム |
| 結合テスト | 2025年9月6日 | 全チーム |
| 本番環境デプロイ | 2025年9月8日 | インフラチーム |
## テスト要件
### 1. 単体テスト
- [ ] 新規API: 正常系レスポンス確認
- [ ] 新規API: エラーケース確認
- [ ] 既存API: バリデーションエラー修正確認
### 2. 結合テスト
- [ ] アプリからの新規API呼び出し
- [ ] データ同期確認
- [ ] 競技開始フロー確認
### 3. 性能テスト
- [ ] 新規APIのレスポンス時間測定
- [ ] 同時アクセス時の動作確認
## リスク評価
| リスク | 影響度 | 対策 |
|--------|--------|------|
| 既存API変更による影響 | 中 | 下位互換性の維持 |
| データ移行の必要性 | 低 | 新規APIのため影響なし |
| 性能劣化 | 低 | インデックス最適化 |
## 承認
| 役割 | 氏名 | 承認日 | 署名 |
|------|------|--------|------|
| アプリ開発リーダー | | | |
| サーバー開発リーダー | | | |
| プロジェクトマネージャー | | | |
---
**注意事項:**
- 本変更要求は岐阜ロゲイニングアプリの安定運用のために必要な修正です
- 実装前に必ずステージング環境での検証を行ってください
- 本番環境デプロイ時はユーザーへの事前通知を行ってください

View File

@ -0,0 +1,161 @@
# サーバーAPI変更要求書20250904.md
## 概要
アプリの状態管理において、競技開始状態とゴール状態の保存・復元に関する問題を解決するため、以下のサーバーAPI修正・追加が必要です。
## 問題点の分析
### 1. 競技状態管理の不足
現在のアプリでは以下4つの重要な状態を管理していますが、サーバー側で対応する状態管理機能が不足している可能性があります
- `isInRog` (ロゲイニング中=スタートしたらTrue)
- `rogainingCounted` (ロゲイニングチェックイン履歴あり=一度でもスタート・ゴール以外でチェックインしたら True)
- `ready_for_goal` (ゴール準備完了=スタートから遠くに移動した際にTrueになる)
- `isAtGoal` (ゴール状態=ゴールしたらTrue)
### 2. チェックイン状態の不整合
チェックポイント詳細画面でスタート処理完了後も「未」のまま表示される問題が発生しています。
## 必要なAPI修正・追加要求
### 1. 競技状態取得API
**エンドポイント**: `GET /api/competition_status/`
**パラメータ**:
- `event_code`: イベントコード
- `zekken_number`: ゼッケン番号
**レスポンス**:
```json
{
"status": "OK",
"data": {
"is_in_rog": true,
"rogaining_counted": false,
"ready_for_goal": false,
"is_at_goal": false,
"start_time": "2025-09-04T09:00:00+09:00",
"goal_time": null,
"last_checkin_time": "2025-09-04T09:30:00+09:00"
}
}
```
### 2. 競技状態更新API
**エンドポイント**: `POST /api/competition_status/update/`
**パラメータ**:
```json
{
"event_code": "EVENT2025",
"zekken_number": "001",
"is_in_rog": true,
"rogaining_counted": false,
"ready_for_goal": false,
"is_at_goal": false
}
```
**レスポンス**:
```json
{
"status": "OK",
"message": "Competition status updated successfully",
"updated_at": "2025-09-04T10:00:00+09:00"
}
```
### 3. スタート処理API拡張
**エンドポイント**: `POST /api/start_rogaining/`
**既存機能に以下を追加**:
- 競技状態の確実な更新is_in_rog = true
- 開始時刻の記録
- レスポンスで更新後の競技状態を返す
**レスポンス拡張**:
```json
{
"status": "OK",
"message": "Rogaining started successfully",
"competition_status": {
"is_in_rog": true,
"rogaining_counted": false,
"ready_for_goal": false,
"is_at_goal": false,
"start_time": "2025-09-04T09:00:00+09:00"
},
"checkin_record": {
"id": 123,
"cp_number": -2,
"checkin_time": "2025-09-04T09:00:00+09:00"
}
}
```
### 4. ゴール処理API拡張
**エンドポイント**: `POST /api/goal_rogaining/`
**既存機能に以下を追加**:
- 競技状態の確実な更新is_at_goal = true
- ゴール時刻の記録
- 最終得点の計算
### 5. チェックイン処理API拡張
**エンドポイント**: `POST /api/checkin/`
**既存機能に以下を追加**:
- スタート・ゴール以外のチェックイン時に rogaining_counted = true に設定
- ready_for_goal フラグの管理GPS位置情報に基づく
### 6. チェックポイント状態取得API
**エンドポイント**: `GET /api/checkpoint_status/`
**パラメータ**:
- `event_code`: イベントコード
- `zekken_number`: ゼッケン番号
- `cp_number`: チェックポイント番号
**レスポンス**:
```json
{
"status": "OK",
"data": {
"cp_number": -2,
"is_checked_in": true,
"checkin_time": "2025-09-04T09:00:00+09:00",
"status": "競技中", // "未", "競技中", "競技終了"
"points_earned": 0
}
}
```
## 実装上の注意点
### 1. 状態の整合性保証
- 競技状態の変更は必ずトランザクション内で実行
- 状態変更時は必ず関連する全てのテーブルを更新
### 2. エラーハンドリング
- ネットワーク切断時でも状態が失われないような設計
- 重複チェックイン防止機能の実装
### 3. パフォーマンス考慮
- 状態取得APIは高頻度でアクセスされるため、適切なキャッシュ機能を実装
- レスポンス時間は500ms以内を目標
### 4. ログ記録
- 競技状態の変更履歴を詳細にログ記録
- デバッグ用のタイムスタンプ情報を含める
## タイムライン
- **緊急度**: 高
- **実装期限**: 2025年9月10日
- **テスト期間**: 2025年9月11日〜13日
- **本番適用**: 2025年9月14日
## 備考
これらの修正により、アプリ側で以下の問題が解決されます:
1. ✅ 競技中にアプリを終了・再開しても正しい状態が復元される
2. ✅ スタート処理完了後、チェックポイント詳細画面で「競技中」と表示される
3. ✅ 4つの重要な状態isInRog, rogainingCounted, ready_for_goal, isAtGoalが確実に保存・復元される
4. ✅ サーバー・クライアント間の状態整合性が保たれる
---
作成日: 2025年9月4日
作成者: システム開発チーム

View File

@ -0,0 +1,104 @@
# サーバーAPI変更要求書
**日付**: 2025年9月5日
**要求者**: システム開発チーム
**対象API**: 通過履歴承認API
## 変更概要
通過履歴の承認処理において、従来の「承認」に加えて「要訂正仮発行」機能を追加するため、APIの拡張を要求します。
## 対象API
### エンドポイント
```
POST /api/approve_checkin_history
```
## 変更内容
### 現在のリクエストパラメータ
```json
{
"event_code": "string",
"zekken_number": "string",
"checkin_ids": [int],
"approval_comment": "string" (optional)
}
```
### 変更後のリクエストパラメータ
```json
{
"event_code": "string",
"zekken_number": "string",
"checkin_ids": [int],
"approval_comment": "string" (optional),
"acc_flag": boolean (optional, new)
}
```
### 新規追加パラメータ詳細
| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|---|-----|------------|-----|
| acc_flag | boolean | No | true | 承認フラグ<br>- `true`: 通常の承認処理(自動印刷実行)<br>- `false`: 要訂正(仮発行処理) |
## 処理仕様
### acc_flag = true承認の場合
- 従来通りの承認処理を実行
- サーバー側で自動印刷を実行
- 承認ステータスを「承認済み」に更新
### acc_flag = false要訂正/仮発行)の場合
- 仮発行処理を実行
- 通過訂正依頼の自動印刷を行う。
- 承認ステータスを「要訂正」または適切なステータスに更新
- 後で再度承認処理が可能な状態を維持
### acc_flagが省略された場合
- デフォルト値 `true` として処理(従来の承認処理)
- 既存のクライアントとの互換性を保持
## レスポンス仕様
現在のレスポンス形式を維持し、処理結果に応じてメッセージを調整:
```json
{
"status": "OK",
"approved_count": int,
"message": "string"
}
```
### メッセージ例
- 承認時: "通過履歴の承認が完了しました"
- 仮発行時: "通過履歴の仮発行が完了しました"
## 実装優先度
**高**: クライアント側実装完了済み、サーバー側対応待ち
## 補足事項
- 既存のAPIとの互換性を保持すること
- acc_flagパラメータは省略可能とし、省略時は従来の承認処理を実行
- エラーハンドリングは既存の仕様を継承
- ログ出力にacc_flagの値を含めることを推奨
## 影響範囲
- 通過履歴承認API/api/approve_checkin_history
- 承認処理ロジック
- 印刷処理の制御
- 承認ステータス管理
## テスト項目
1. acc_flag=trueでの承認処理従来通り
2. acc_flag=falseでの仮発行処理
3. acc_flag省略時のデフォルト動作
4. 不正なacc_flag値のエラーハンドリング
5. 既存クライアントとの互換性確認

View 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

Some files were not shown because too many files have changed in this diff Show More