diff --git a/rog/serializers.py b/rog/serializers.py index 8d5ae3e..87c8371 100644 --- a/rog/serializers.py +++ b/rog/serializers.py @@ -27,6 +27,7 @@ from django.shortcuts import get_object_or_404 from django.utils import timezone from datetime import datetime, date + logger = logging.getLogger(__name__) class LocationCatSerializer(serializers.ModelSerializer): @@ -876,3 +877,37 @@ class UserLastGoalTimeSerializer(serializers.Serializer): user_email = serializers.EmailField() last_goal_time = serializers.DateTimeField() +class LoginUserSerializer(serializers.Serializer): + identifier = serializers.CharField(required=True) # メールアドレスまたはゼッケン番号 + password = serializers.CharField(required=True) + + def validate(self, data): + identifier = data.get('identifier') + password = data.get('password') + + if not identifier or not password: + raise serializers.ValidationError('認証情報を入力してください。') + + # ゼッケン番号かメールアドレスかを判定 + if '@' in identifier: + # メールアドレスの場合 + user = authenticate(username=identifier, password=password) + else: + # ゼッケン番号の場合 + try: + # ゼッケン番号からユーザーを検索 + user = CustomUser.objects.filter(zekken_number=identifier).first() + if user: + # パスワード認証 + if not user.check_password(password): + user = None + except ValueError: + user = None + + if user and user.is_active: + return user + elif user and not user.is_active: + raise serializers.ValidationError('アカウントが有効化されていません。') + else: + raise serializers.ValidationError('認証情報が正しくありません。') + diff --git a/rog/urls.py b/rog/urls.py index 796f312..d7a0f39 100644 --- a/rog/urls.py +++ b/rog/urls.py @@ -1,7 +1,7 @@ from sys import prefix from rest_framework import urlpatterns from rest_framework.routers import DefaultRouter -from .views import 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 +from .views import 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 django.urls import path, include @@ -128,6 +128,8 @@ urlpatterns += [ path('get-goalimage/', views.get_goalimage, name='get-goalimage'), path('get-photolist/', views.get_photo_list, name='get-photolist'), + path('api/rankings///', get_ranking, name='get_ranking'), + path('api/rankings/top3//', all_ranking_top3, name='all_ranking_top3'), ] diff --git a/rog/views.py b/rog/views.py index 1f1834f..5db87ba 100644 --- a/rog/views.py +++ b/rog/views.py @@ -23,7 +23,7 @@ import uuid from rest_framework.exceptions import ValidationError as DRFValidationError from django.db import transaction -from django.db.models import F +from django.db.models import F,Sum from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response @@ -853,6 +853,48 @@ class LoginAPI(generics.GenericAPIView): serializer_class = LoginUserSerializer def post(self, request, *args, **kwargs): + logger.info(f"Login attempt for identifier: {request.data.get('identifier', 'identifier not provided')}") + logger.debug(f"Request data: {request.data}") + + serializer = self.get_serializer(data=request.data) + try: + serializer.is_valid(raise_exception=True) + user = serializer.validated_data + logger.info(f"User {user.email} logged in successfully") + + # ユーザー情報をシリアライズ + user_data = UserSerializer(user, context=self.get_serializer_context()).data + + # 認証トークンを生成 + token = AuthToken.objects.create(user)[1] + + return Response({ + "user": user_data, + "token": token + }) + + except serializers.ValidationError as e: + logger.error(f"Login failed for identifier {request.data.get('identifier', 'identifier not provided')}: {str(e)}") + logger.error(f"Serializer errors: {serializer.errors}") + + error_msg = serializer.errors.get('non_field_errors', ['ログインに失敗しました。'])[0] + return Response({ + "error": error_msg, + "details": serializer.errors + }, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error(f"Unexpected error during login for identifier {request.data.get('identifier', 'identifier not provided')}: {str(e)}") + logger.error(f"Traceback: {traceback.format_exc()}") + + return Response({ + "error": "予期せぬエラーが発生しました。", + "details": str(e) + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + + def post_old(self, request, *args, **kwargs): logger.info(f"Login attempt for user: {request.data.get('email', 'email not provided')}") logger.debug(f"Request data: {request.data}") @@ -3428,3 +3470,259 @@ def update_goal_time(request): status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + + +def get_team_status(last_checkin_time, goal_time, event_end_time): + """ + チームの状態を判定する + """ + now = timezone.now() + + if goal_time: + return "ゴール" + + if not last_checkin_time: + if now > event_end_time + timedelta(minutes=30): + return "棄権" + return "競技中" + + # 最終チェックインから30分以上経過 + if now > last_checkin_time + timedelta(minutes=30): + return "棄権" + + return "競技中" + +def calculate_late_points(goal_time, event_end_time): + """遅刻による減点を計算する""" + if not goal_time or not event_end_time: + return 0 + + minutes_late = max(0, int((goal_time - event_end_time).total_seconds() / 60)) + return minutes_late * -50 + +def is_disqualified(start_time, goal_time, duration): + """失格判定を行う""" + if not goal_time or not start_time or not duration: + return False # ゴール時間がない場合は失格としない(競技中の可能性) + + # duration(timedelta)に15分を加算 + max_time = start_time + duration + timedelta(minutes=15) + return goal_time > max_time + +@api_view(['GET']) +def get_ranking(request, event_code, category_name): + """特定のイベントとクラスのランキングを取得する""" + try: + # イベントの情報を取得 + event = NewEvent2.objects.get(event_name=event_code) + + # 有効なエントリーを取得 + entries = Entry.objects.filter( + event=event, + category__category_name=category_name, + is_active=True + ).select_related('team', 'category') + + rankings = [] + disqualified = [] # 失格チームのリスト + + for entry in entries: + # チェックインポイントを集計 + checkins = GpsCheckin.objects.filter( + zekken_number=entry.zekken_number, + event_code=event_code + ).aggregate( + total_points=Sum('points') + ) + + # 最後のチェックイン時刻を取得 + last_checkin = GpsCheckin.objects.filter( + zekken_number=entry.zekken_number, + event_code=event_code + ).order_by('-create_at').first() + + last_checkin_time = last_checkin.create_at if last_checkin else None + + # ゴール時間を取得 (最も早いゴール時間) + goal_record = GoalImages.objects.filter( + zekken_number=entry.zekken_number, + event_code=event_code + ).order_by('goaltime').first() + + goal_time = goal_record.goaltime if goal_record else None + total_points = checkins['total_points'] or 0 + + # イベントの終了予定時刻を計算 + expected_end_time = event.start_datetime + entry.category.duration + + # チーム状態の判定 + team_status = get_team_status(last_checkin_time, goal_time, expected_end_time) + + # 失格判定 + if is_disqualified(event.start_datetime, goal_time, entry.category.duration): + disqualified.append({ + 'team_name': entry.team.team_name, + 'zekken_number': entry.zekken_number, + 'point': total_points, + 'late_point': 0, + 'goal_time': goal_time, + 'reason': '制限時間超過', + 'status': team_status + }) + continue + + # 遅刻減点を計算 + late_points = calculate_late_points(goal_time, expected_end_time) + + rankings.append({ + 'team_name': entry.team.team_name, + 'zekken_number': entry.zekken_number, + 'point': total_points, + 'late_point': abs(late_points), + 'final_point': total_points + late_points, + 'goal_time': goal_time, + 'status': team_status, + 'last_checkin': last_checkin_time + }) + + # ポイントの高い順(同点の場合はゴール時間が早い順)にソート + # 棄権チームを最後に + rankings.sort(key=lambda x: ( + -1 if x['status'] != '棄権' else 0, # 棄権でないチームを優先 + -x['final_point'], # 得点の高い順 + x['goal_time'] or datetime.max # ゴール時間の早い順 + )) + + # 有効なランキングと失格チームを結合 + final_rankings = { + 'rankings': rankings, + 'disqualified': disqualified + } + + return Response(final_rankings) + + except NewEvent2.DoesNotExist: + return Response( + {"error": "Specified event not found"}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + logger.error(f"Error in get_ranking: {str(e)}", exc_info=True) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET']) +def all_ranking_top3(request, event_code): + """特定のイベントの全クラスのTOP3を取得する""" + try: + # イベントの情報を取得 + event = NewEvent2.objects.get(event_name=event_code) + + # イベントの有効なカテゴリーを取得 + categories = NewCategory.objects.filter( + entry__event=event, + entry__is_active=True + ).distinct() + + rankings = {} + for category in categories: + # カテゴリーごとのエントリーを取得 + entries = Entry.objects.filter( + event=event, + category=category, + is_active=True + ).select_related('team') + + category_rankings = [] + disqualified = [] # カテゴリーごとの失格チーム + + for entry in entries: + # チェックインポイントを集計 + checkins = GpsCheckin.objects.filter( + zekken_number=entry.zekken_number, + event_code=event_code + ).aggregate( + total_points=Sum('points') + ) + + # 最後のチェックイン時刻を取得 + last_checkin = GpsCheckin.objects.filter( + zekken_number=entry.zekken_number, + event_code=event_code + ).order_by('-create_at').first() + + last_checkin_time = last_checkin.create_at if last_checkin else None + + # ゴール時間を取得 + goal_record = GoalImages.objects.filter( + zekken_number=entry.zekken_number, + event_code=event_code + ).order_by('goaltime').first() + + goal_time = goal_record.goaltime if goal_record else None + total_points = checkins['total_points'] or 0 + + # イベントの終了予定時刻を計算 + expected_end_time = event.start_datetime + category.duration + + # チーム状態の判定 + team_status = get_team_status(last_checkin_time, goal_time, expected_end_time) + + # 失格判定 + if is_disqualified(event.start_datetime, goal_time, entry.category.duration): + disqualified.append({ + 'team_name': entry.team.team_name, + 'zekken_number': entry.zekken_number, + 'point': total_points, + 'late_point': 0, + 'goal_time': goal_time, + 'reason': '制限時間超過', + 'status': team_status + }) + continue + + # 遅刻減点を計算 + late_points = calculate_late_points(goal_time, expected_end_time) + + category_rankings.append({ + 'team_name': entry.team.team_name, + 'zekken_number': entry.zekken_number, + 'point': total_points, + 'late_point': abs(late_points), + 'final_point': total_points + late_points, + 'goal_time': goal_time, + 'status': team_status, + 'last_checkin': last_checkin_time + }) + + # ポイントの高い順(同点の場合はゴール時間が早い順)にソート + # 棄権チームを最後に + category_rankings.sort(key=lambda x: ( + -1 if x['status'] != '棄権' else 0, # 棄権でないチームを優先 + -x['final_point'], # 得点の高い順 + x['goal_time'] or datetime.max # ゴール時間の早い順 + )) + + # TOP3のみを保持(棄権を除く) + top_rankings = [r for r in category_rankings if r['status'] != '棄権'][:3] + + rankings[category.category_name] = { + 'rankings': top_rankings, + 'disqualified': disqualified + } + + return Response(rankings) + + except NewEvent2.DoesNotExist: + return Response( + {"error": "Specified event not found"}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + logger.error(f"Error in all_ranking_top3: {str(e)}", exc_info=True) + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) \ No newline at end of file diff --git a/supervisor/html/index.html b/supervisor/html/index.html index fcfebb7..22aafab 100755 --- a/supervisor/html/index.html +++ b/supervisor/html/index.html @@ -10,9 +10,40 @@ -
-
+ +
+
+

ログイン

+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+