639 lines
26 KiB
Python
639 lines
26 KiB
Python
"""
|
||
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')
|