Add Event user registration

This commit is contained in:
2025-01-22 17:14:56 +09:00
parent acf6e36e71
commit 005de98ecc
6 changed files with 438 additions and 4 deletions

View File

@ -29,6 +29,24 @@ from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import gettext_lazy as _
from .services.csv_processor import EntryCSVProcessor
@admin.register(Entry)
class EntryAdmin(admin.ModelAdmin):
list_display = ['team', 'event', 'category', 'date', 'is_active']
def get_urls(self):
from django.urls import path
urls = super().get_urls()
custom_urls = [
path('upload-csv/', self.upload_csv_view, name='entry_upload_csv'),
]
return custom_urls + urls
def upload_csv_view(self, request):
processor = EntryCSVProcessor()
return processor.process_upload(request)
@admin.register(GifurogeRegister)
class GifurogeRegisterAdmin(admin.ModelAdmin):
list_display = ('event_code', 'time', 'owner_name', 'email', 'team_name', 'department')
@ -912,7 +930,7 @@ class CustomUserAdmin(UserAdmin):
#model = CustomUser
list_display = ('email', 'is_staff', 'is_active', 'is_rogaining', 'zekken_number', 'event_code', 'team_name', 'group', 'firstname', 'lastname')
search_fields = ('email', 'firstname', 'lastname', 'zekken_number')
search_fields = ('egit mail', 'firstname', 'lastname', 'zekken_number')
list_filter = ('is_staff', 'is_active', 'is_rogaining', 'group')
ordering = ('email',)

View File

@ -877,7 +877,7 @@ class UserLastGoalTimeSerializer(serializers.Serializer):
user_email = serializers.EmailField()
last_goal_time = serializers.DateTimeField()
class LoginUserSerializer(serializers.Serializer):
class LoginUserSerializer_old(serializers.Serializer):
identifier = serializers.CharField(required=True) # メールアドレスまたはゼッケン番号
password = serializers.CharField(required=True)

View File

@ -0,0 +1,214 @@
# services/csv_processor.py
from typing import Dict, Any
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.hashers import make_password
from django.core.exceptions import ValidationError
from django.db import transaction
from datetime import timedelta
import csv
from ..models import CustomUser, Team, Member, NewCategory, Entry, NewEvent2
from ..utils.date_converter import DateConverter
from ..utils.name_splitter import NameSplitter
class EntryCSVProcessor:
def __init__(self):
self.date_converter = DateConverter()
self.name_splitter = NameSplitter()
def process_upload(self, request):
"""
CSVファイルのアップロードとデータ処理を行う
"""
if request.method == 'POST':
try:
if 'csv_file' not in request.FILES:
messages.error(request, 'No file was uploaded.')
return redirect('..')
csv_file = request.FILES['csv_file']
if not csv_file.name.endswith('.csv'):
messages.error(request, 'File is not CSV type')
return redirect('..')
# BOMを考慮してファイルを読み込む
file_content = csv_file.read()
if file_content.startswith(b'\xef\xbb\xbf'):
file_content = file_content[3:]
decoded_file = file_content.decode('utf-8').splitlines()
reader = csv.DictReader(decoded_file)
for row in reader:
try:
self.process_csv_row(row)
except Exception as e:
messages.error(request, f'Error in row: {str(e)}')
return redirect('..')
messages.success(request, 'CSV file processed successfully')
return redirect('..')
except Exception as e:
messages.error(request, f'Error processing CSV: {str(e)}')
return redirect('..')
return render(request, 'admin/entry/upload_csv.html')
def process_csv_row(self, row: Dict[str, Any]) -> None:
"""
CSVの1行のデータを処理する
"""
try:
with transaction.atomic():
# 1) ユーザーの作成/取得
user = self._get_or_create_user(row)
if not user:
raise ValidationError("Failed to create/get user")
# 2) チームの作成/取得とカテゴリの設定
team = self._get_or_create_team(row, user)
if not team:
raise ValidationError("Failed to create/get team")
# 3) メンバーの作成/更新
self._process_team_members(row, team, user)
# 4) エントリーの作成
self._create_entry(row, team, user)
except Exception as e:
raise ValidationError(f"Error processing row: {str(e)}")
def _get_or_create_user(self, row: Dict[str, Any]) -> CustomUser:
"""
メールアドレスでユーザーを検索し、存在しない場合は新規作成
"""
user = CustomUser.objects.filter(email=row['email']).first()
if not user:
last_name, first_name = self.name_splitter.split_full_name(row['owner_name'])
birth_date = self.date_converter.convert_date(row['owner_birthday'])
is_female = row['owner_sex'] in ['女性', '', '女子', 'female']
user = CustomUser.objects.create(
email=row['email'],
password=make_password(row['password']),
firstname=first_name,
lastname=last_name,
date_of_birth=birth_date,
female=is_female,
is_active=True
)
return user
def _get_or_create_team(self, row: Dict[str, Any], user: CustomUser) -> Team:
"""
チーム名でチームを検索し、存在しない場合は新規作成
既存チームの場合はメンバー構成を確認し、必要に応じて新バージョンを作成
"""
team_name = row['team_name']
base_team_name = team_name
version = 1
while Team.objects.filter(team_name=team_name).exists():
existing_team = Team.objects.get(team_name=team_name)
if self._check_same_members(existing_team, row, user):
return existing_team
version += 1
team_name = f"{base_team_name}_v{version}"
# 新規チームを作成
category = self._get_or_create_category(row)
team = Team.objects.create(
team_name=team_name,
owner=user,
category=category
)
return team
def _get_or_create_category(self, row: Dict[str, Any]) -> NewCategory:
"""
時間とデパートメントに基づいてカテゴリを取得または作成
"""
category_name = f"{row['department']}_{row['time']}h"
category, _ = NewCategory.objects.get_or_create(
category_name=category_name,
defaults={
'duration': timedelta(hours=int(row['time'])),
'num_of_member': int(row['members_count'])
}
)
return category
def _check_same_members(self, team: Team, row: Dict[str, Any], owner: CustomUser) -> bool:
"""
既存チームと新しいメンバー構成が同じかどうかをチェック
"""
existing_members = set(member.user.email for member in team.members.all())
new_members = {owner.email}
for i in range(2, int(row['members_count']) + 1):
if row.get(f'member{i}'):
new_members.add(f"dummy_{team.team_name}_{i}@example.com")
return existing_members == new_members
def _process_team_members(self, row: Dict[str, Any], team: Team, owner: CustomUser) -> None:
"""
チームメンバーを処理(オーナーとその他のメンバー)
"""
# オーナーをメンバーとして追加
Member.objects.get_or_create(
team=team,
user=owner,
defaults={'is_temporary': False}
)
# 追加メンバーの処理
for i in range(2, int(row['members_count']) + 1):
if row.get(f'member{i}'):
self._create_team_member(row, team, i)
def _create_team_member(self, row: Dict[str, Any], team: Team, member_num: int) -> None:
"""
チームの追加メンバーを作成
"""
last_name, first_name = self.name_splitter.split_full_name(row[f'member{member_num}'])
birth_date = self.date_converter.convert_date(row[f'birthday{member_num}'])
is_female = row.get(f'sex{member_num}', '') in ['女性', '', '女子', 'female']
dummy_email = f"dummy_{team.team_name}_{member_num}@example.com"
dummy_user, _ = CustomUser.objects.get_or_create(
email=dummy_email,
defaults={
'password': make_password('dummy_password'),
'firstname': first_name,
'lastname': last_name,
'date_of_birth': birth_date,
'female': is_female
}
)
Member.objects.get_or_create(
team=team,
user=dummy_user,
defaults={'is_temporary': True}
)
def _create_entry(self, row: Dict[str, Any], team: Team, owner: CustomUser) -> None:
"""
エントリーを作成
"""
try:
event = NewEvent2.objects.get(event_name=row['event_code'])
Entry.objects.create(
team=team,
event=event,
category=team.category,
date=event.start_datetime,
owner=owner,
is_active=False
)
except NewEvent2.DoesNotExist:
raise ValidationError(f"Event with code {row['event_code']} does not exist")

View File

@ -0,0 +1,83 @@
# utils/date_converter.py
from datetime import datetime, date
from typing import Optional
class DateConverter:
"""
日本語の日付文字列を扱うユーティリティクラス
"""
def convert_date(self, date_str: str) -> Optional[date]:
"""
日本語の日付文字列をdateオブジェクトに変換する
Args:
date_str: 変換する日付文字列(例: '1990年1月1日' or '1990-01-01' or '1990/01/01'
Returns:
変換されたdateオブジェクト。変換できない場合はNone
"""
if not date_str or date_str.strip() == '':
return None
try:
# 全角数字を半角数字に変換
date_str = date_str.translate(
str.maketrans('', '0123456789')
)
date_str = date_str.strip()
# 区切り文字の判定と分割
if '' in date_str:
# 年月日形式の場合
date_parts = date_str.replace('', '/').replace('', '/').replace('', '').split('/')
elif '/' in date_str:
# スラッシュ区切りの場合
date_parts = date_str.split('/')
elif '-' in date_str:
# ハイフン区切りの場合
date_parts = date_str.split('-')
else:
return None
# 部分の数を確認
if len(date_parts) != 3:
return None
year = int(date_parts[0])
month = int(date_parts[1])
day = int(date_parts[2])
# 簡単な妥当性チェック
if not (1900 <= year <= 2100):
return None
if not (1 <= month <= 12):
return None
if not (1 <= day <= 31):
return None
return date(year, month, day)
except (ValueError, IndexError, TypeError):
return None
def format_date(self, d: date, format_type: str = 'ja') -> str:
"""
dateオブジェクトを指定された形式の文字列に変換する
Args:
d: 変換するdateオブジェクト
format_type: 出力形式 ('ja': 日本語形式, 'iso': ISO形式)
Returns:
変換された日付文字列
"""
if not isinstance(d, date):
return ''
if format_type == 'ja':
return f"{d.year}{d.month}{d.day}"
elif format_type == 'iso':
return d.isoformat()
else:
return str(d)

119
rog/utils/name_splitter.py Normal file
View File

@ -0,0 +1,119 @@
# utils/name_splitter.py
from typing import Tuple
class NameSplitter:
"""
日本語の氏名を扱うユーティリティクラス
"""
def split_full_name(self, full_name: str) -> Tuple[str, str]:
"""
フルネームを姓と名に分割する
Args:
full_name: 分割する氏名(例: '山田 太郎' or '山田 太郎' or 'Yamada Taro'
Returns:
(姓, 名)のタプル。分割できない場合は(フルネーム, '')を返す
"""
if not full_name:
return ('', '')
try:
# 空白文字で分割(半角スペース、全角スペース、タブなど)
parts = full_name.replace(' ', ' ').split()
if len(parts) >= 2:
last_name = parts[0]
first_name = ' '.join(parts[1:]) # 名が複数単語の場合に対応
return (last_name.strip(), first_name.strip())
else:
# 分割できない場合は全体を姓とする
return (full_name.strip(), '')
except Exception:
return (full_name.strip(), '')
def join_name(self, last_name: str, first_name: str, format_type: str = 'ja') -> str:
"""
姓と名を結合して一つの文字列にする
Args:
last_name: 姓
first_name: 名
format_type: 出力形式 ('ja': 日本語形式, 'en': 英語形式)
Returns:
結合された氏名文字列
"""
last_name = last_name.strip()
first_name = first_name.strip()
if not last_name and not first_name:
return ''
if not first_name:
return last_name
if not last_name:
return first_name
if format_type == 'ja':
return f"{last_name} {first_name}" # 全角スペース
elif format_type == 'en':
return f"{first_name} {last_name}" # 英語形式:名 姓
else:
return f"{last_name} {first_name}" # デフォルト:半角スペース
def normalize_name(self, name: str) -> str:
"""
名前の正規化を行う
Args:
name: 正規化する名前文字列
Returns:
正規化された名前文字列
"""
if not name:
return ''
# 空白文字の正規化
name = ' '.join(name.split()) # 連続する空白を単一の半角スペースに
# 全角英数字を半角に変換
name = name.translate(str.maketrans({
' ': ' ', # 全角スペースを半角に
'': '.',
'': ',',
'': '!',
'': '?',
'': ':',
'': ';',
}))
return name.strip()
def is_valid_name(self, name: str) -> bool:
"""
名前が有効かどうかをチェックする
Args:
name: チェックする名前文字列
Returns:
名前が有効な場合はTrue、そうでない場合はFalse
"""
if not name or not name.strip():
return False
# 最小文字数チェック
if len(name.strip()) < 2:
return False
# 記号のチェック(一般的でない記号が含まれていないか)
invalid_chars = set('!@#$%^&*()_+=<>?/\\|~`')
if any(char in invalid_chars for char in name):
return False
return True

View File

@ -1554,11 +1554,11 @@ class NewCategoryListView(generics.ListAPIView):
"""
GETメソッドは認証不要、その他のメソッドは認証必要
"""
if self.action in ['list', 'retrieve']:
if self.request.method == 'GET':
permission_classes = [permissions.AllowAny]
else:
permission_classes = [permissions.IsAuthenticated]
return [permission() for permission in permission_classes]
return [permission() for permission in permission_classes]
class CategoryViewSet(viewsets.ModelViewSet):