diff --git a/register_teams_from_csv.py b/register_teams_from_csv.py new file mode 100644 index 0000000..491eaea --- /dev/null +++ b/register_teams_from_csv.py @@ -0,0 +1,590 @@ +#!/usr/bin/env python +""" +CSVファイルからチーム情報をデータベースに登録するスクリプト +CPLIST/input/teams2025.csv から以下の手順でデータベーステーブルに書き込む + +実行方法: +python register_teams_from_csv.py --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() diff --git a/rog/management/commands/register_teams_from_csv.py b/rog/management/commands/register_teams_from_csv.py new file mode 100644 index 0000000..5254ae4 --- /dev/null +++ b/rog/management/commands/register_teams_from_csv.py @@ -0,0 +1,619 @@ +""" +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): + 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 + + 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): + 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: + 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 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=member.is_temporary + ) + 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') diff --git a/エントリー.md b/エントリー.md index 69dacd7..da012dd 100644 --- a/エントリー.md +++ b/エントリー.md @@ -13,6 +13,10 @@ CSVは以下の項目を持つ。 # メールアドレスをキーに既存ユーザーを取得  検索がヒットしなければ、ユーザー登録する。 + 検索がヒットすれば、パスワードを更新し、 +   event_codeに指定event_codeを設定 +   zekken_number に zekken を入力 +   team_name に team名を入れる 2-2. チーム登録 @@ -26,9 +30,6 @@ CSVは以下の項目を持つ。 2-4. イベント参加 -# 登録したエントリーでイベント登録する。 +# 登録したエントリーでイベント参加する。 -2-5. カスタムユーザーの参加イベント記録 - -# 関連なデータをカスタムユーザーに書き込む