Fix Ranking code step1

This commit is contained in:
2024-11-12 07:19:18 +09:00
parent 19f12652b9
commit fccc55cf18
9 changed files with 4094 additions and 4 deletions

View File

@ -27,6 +27,7 @@ from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
from datetime import datetime, date from datetime import datetime, date
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LocationCatSerializer(serializers.ModelSerializer): class LocationCatSerializer(serializers.ModelSerializer):
@ -876,3 +877,37 @@ class UserLastGoalTimeSerializer(serializers.Serializer):
user_email = serializers.EmailField() user_email = serializers.EmailField()
last_goal_time = serializers.DateTimeField() 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('認証情報が正しくありません。')

View File

@ -1,7 +1,7 @@
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 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 from django.urls import path, include
@ -128,6 +128,8 @@ urlpatterns += [
path('get-goalimage/', views.get_goalimage, name='get-goalimage'), path('get-goalimage/', views.get_goalimage, name='get-goalimage'),
path('get-photolist/', views.get_photo_list, name='get-photolist'), path('get-photolist/', views.get_photo_list, name='get-photolist'),
path('api/rankings/<str:event_code>/<str:category_name>/', get_ranking, name='get_ranking'),
path('api/rankings/top3/<str:event_code>/', all_ranking_top3, name='all_ranking_top3'),
] ]

View File

@ -23,7 +23,7 @@ import uuid
from rest_framework.exceptions import ValidationError as DRFValidationError from rest_framework.exceptions import ValidationError as DRFValidationError
from django.db import transaction 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 import viewsets, permissions, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
@ -853,6 +853,48 @@ class LoginAPI(generics.GenericAPIView):
serializer_class = LoginUserSerializer serializer_class = LoginUserSerializer
def post(self, request, *args, **kwargs): 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.info(f"Login attempt for user: {request.data.get('email', 'email not provided')}")
logger.debug(f"Request data: {request.data}") logger.debug(f"Request data: {request.data}")
@ -3428,3 +3470,259 @@ def update_goal_time(request):
status=status.HTTP_500_INTERNAL_SERVER_ERROR 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 # ゴール時間がない場合は失格としない(競技中の可能性)
# durationtimedeltaに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
)

View File

@ -10,9 +10,40 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
</head> </head>
<body class="bg-gray-50"> <body class="bg-gray-50">
<div class="container mx-auto p-4"> <!-- ログインフォーム -->
<div class="bg-white rounded-lg shadow-lg p-6 mb-6"> <div id="loginForm" class="fixed inset-0 bg-gray-800 bg-opacity-50 flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-lg w-96">
<h2 class="text-2xl font-bold mb-6 text-center">ログイン</h2>
<form onsubmit="return login(event)">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
ユーザー名
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username" type="text" placeholder="ユーザー名">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
パスワード
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password" type="password" placeholder="パスワード">
</div>
<div class="flex items-center justify-center">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit">
ログイン
</button>
</div>
</form>
</div>
</div>
<div id="mainContent" class="container mx-auto p-4">
<div class="bg-white rounded-lg shadow-lg p-6 mb-6" style="display: none;">
<h1 class="text-2xl font-bold mb-6">通過審査管理画面</h1> <h1 class="text-2xl font-bold mb-6">通過審査管理画面</h1>
<button onclick="logout()" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
ログアウト
</button>
<!-- 選択フォーム --> <!-- 選択フォーム -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
@ -125,8 +156,62 @@
let original_goal_time = ''; let original_goal_time = '';
let selected_event_code = ''; let selected_event_code = '';
// ユーザー認証用の定数(実際の運用ではサーバーサイドで管理すべき)
const VALID_USERNAME = 'admin';
const VALID_PASSWORD = 'password123';
// セッション管理
function checkAuth() {
const isAuthenticated = sessionStorage.getItem('isAuthenticated');
if (isAuthenticated) {
document.getElementById('loginForm').style.display = 'none';
document.getElementById('mainContent').style.display = 'block';
} else {
document.getElementById('loginForm').style.display = 'flex';
document.getElementById('mainContent').style.display = 'none';
}
}
// ログイン処理
async function login(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const response = await fetch('/api/login/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identifier: username, // メールアドレス
password: password
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'ログインに失敗しました');
}
// SuperVisor User のみを許可する。
console.info('Login successful:', data);
sessionStorage.setItem('isAuthenticated', 'true');
checkAuth();
return false;
}
// ログアウト処理
function logout() {
sessionStorage.removeItem('isAuthenticated');
checkAuth();
}
// イベントリスナーの設定 // イベントリスナーの設定
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
checkAuth();
// Sortable初期化これで、通過順序を変更できる // Sortable初期化これで、通過順序を変更できる
const checkinList = document.getElementById('checkinList'); const checkinList = document.getElementById('checkinList');
new Sortable(checkinList, { new Sortable(checkinList, {

View File

@ -0,0 +1,295 @@
<!DOCTYPE html>
<html>
<head>
<title>ランキング</title>
<style>
.box2 { margin: 10px; padding: 10px; border: 1px solid #ccc; }
.best3 { margin: 5px 0; padding: 5px; }
.span2 { margin-left: 20px; }
.span3 { font-weight: bold; }
.span6 { display: inline-block; width: 30px; }
.black { background-color: #f0f0f0; padding: 10px; }
.arrow { margin: 10px 0; }
.arrow2 { margin: 10px 0; }
.disqualified { color: #999; }
.status {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
margin-left: 8px;
}
.status-retired {
background-color: #ffebee;
color: #c62828;
}
.status-finished {
background-color: #e8f5e9;
color: #2e7d32;
}
.status-running {
background-color: #e3f2fd;
color: #1565c0;
}
.disqualified-header {
margin-top: 20px;
padding: 5px;
background-color: #f0f0f0;
border-left: 4px solid #999;
}
</style>
</head>
<body>
<div id="ranking">
<div class="black">
<span class="span3"></span>
<span style="font-size: 24px">ランキング</span>
</div>
<div class="arrow">
<select id="eventSelect">
<option value="">イベントを選択してください</option>
</select>
</div>
<div class="arrow2">
<select id="classSelect" disabled>
<option value="">クラスを選択してください</option>
</select>
</div>
<button id="toggleButton" onclick="toggleView()">TOP3表示</button>
<!-- 通常のランキング表示 -->
<div id="normalRanking">
<div id="teamList"></div>
</div>
<!-- TOP3のランキング表示 -->
<div id="top3Ranking" style="display: none;">
<div id="top3List"></div>
</div>
</div>
<script>
let showTop3 = false;
// ページ読み込み時にイベント一覧を取得
document.addEventListener('DOMContentLoaded', async () => {
await loadEvents();
setupEventListeners();
});
// 日時のフォーマット
function formatDateTime(dateStr) {
if (!dateStr) return '未ゴール';
const date = new Date(dateStr);
return date.toLocaleString('ja-JP', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// イベントリスナーの設定
function setupEventListeners() {
document.getElementById('eventSelect').addEventListener('change', async function() {
const eventCode = this.value;
if (eventCode) {
await loadClasses(eventCode);
await updateRankings();
}
});
document.getElementById('classSelect').addEventListener('change', async function() {
await updateRankings();
});
}
// イベント一覧の取得と表示
async function loadEvents() {
try {
const response = await fetch('/api/newevent2/');
const events = await response.json();
const select = document.getElementById('eventSelect');
events.filter(event => event.public).forEach(event => {
const option = document.createElement('option');
option.value = event.event_name;
option.textContent = event.event_name;
select.appendChild(option);
});
} catch (error) {
console.error('イベント一覧の取得に失敗:', error);
}
}
// クラス一覧の取得と表示
async function loadClasses(eventCode) {
try {
const response = await fetch(`/api/categories/${eventCode}/`);
const classes = await response.json();
const select = document.getElementById('classSelect');
// 既存のオプションをクリア
select.innerHTML = '<option value="">クラスを選択してください</option>';
select.disabled = false;
classes.forEach(cls => {
const option = document.createElement('option');
option.value = cls.category_name;
option.textContent = cls.category_name;
select.appendChild(option);
});
} catch (error) {
console.error('クラス一覧の取得に失敗:', error);
}
}
// ランキングの更新
async function updateRankings() {
const eventCode = document.getElementById('eventSelect').value;
if (!eventCode) return;
try {
if (showTop3) {
await loadTop3Rankings(eventCode);
} else {
const classCode = document.getElementById('classSelect').value;
if (classCode) {
await loadClassRankings(eventCode, classCode);
}
}
} catch (error) {
console.error('ランキングの取得に失敗:', error);
}
}
// クラス別ランキングの表示
async function loadClassRankings(eventCode, classCode) {
const response = await fetch(`/api/rankings/${eventCode}/${classCode}/`);
const rankingData = await response.json();
displayNormalRankings(rankingData);
}
// TOP3ランキングの表示
async function loadTop3Rankings(eventCode) {
const response = await fetch(`/api/rankings/top3/${eventCode}/`);
const rankingData = await response.json();
displayTop3Rankings(rankingData);
}
// 通常ランキングの表示処理
function displayNormalRankings(rankingData) {
const container = document.getElementById('teamList');
container.innerHTML = '';
// 有効なランキングの表示
rankingData.rankings.forEach((team, index) => {
const div = document.createElement('div');
div.className = 'best';
const statusClass = team.status === '棄権' ? 'status-retired' :
team.status === 'ゴール' ? 'status-finished' : 'status-running';
div.innerHTML = `
${index + 1}. ${team.team_name} (${team.zekken_number})
<span class="span2">合計得点:${team.final_point}</span>
<span class="span2">獲得ポイント:${team.point}</span>
<span class="span2">遅刻減点: ${team.late_point}</span>
<span class="span2">最終更新: ${formatDateTime(team.last_checkin)}</span>
<span class="status ${statusClass}">(${team.status})</span>
`;
container.appendChild(div);
});
// 失格チームの表示(見出し)
if (rankingData.disqualified && rankingData.disqualified.length > 0) {
const disqHeader = document.createElement('div');
disqHeader.className = 'disqualified-header';
disqHeader.innerHTML = '<h3>失格チーム</h3>';
container.appendChild(disqHeader);
// 失格チームのリスト
rankingData.disqualified.forEach(team => {
const div = document.createElement('div');
div.className = 'best disqualified';
div.innerHTML = `
${team.team_name} (${team.zekken_number})
<span class="span2">獲得ポイント:${team.point}</span>
<span class="span2">理由: ${team.reason}</span>
<span class="span2">ゴール: ${formatDateTime(team.goal_time)}</span>
`;
container.appendChild(div);
});
}
}
// TOP3ランキングの表示処理
function displayTop3Rankings(rankingData) {
const container = document.getElementById('top3List');
container.innerHTML = '';
Object.entries(rankingData).forEach(([category, data]) => {
const categoryDiv = document.createElement('div');
categoryDiv.className = 'box2';
const categoryHeader = document.createElement('h3');
categoryHeader.textContent = category;
categoryDiv.appendChild(categoryHeader);
// 有効なランキングの表示
data.rankings.forEach((team, index) => {
const teamDiv = document.createElement('div');
teamDiv.className = 'best3';
const statusClass = team.status === '棄権' ? 'status-retired' :
team.status === 'ゴール' ? 'status-finished' : 'status-running';
teamDiv.innerHTML = `
<span class="span6">${index + 1}</span>
${team.team_name} (${team.zekken_number}) <span class="status ${statusClass}">(${team.status})</span><br>
合計得点:${team.final_point} (獲得:${team.point} 減点:${team.late_point})<br>
最終更新: ${formatDateTime(team.last_checkin)}
`;
categoryDiv.appendChild(teamDiv);
});
// 失格チームの表示
if (data.disqualified && data.disqualified.length > 0) {
const disqHeader = document.createElement('div');
disqHeader.className = 'disqualified-header';
disqHeader.innerHTML = '<h4>失格チーム</h4>';
categoryDiv.appendChild(disqHeader);
data.disqualified.forEach(team => {
const teamDiv = document.createElement('div');
teamDiv.className = 'best3 disqualified';
teamDiv.innerHTML = `
${team.team_name} (${team.zekken_number})<br>
獲得ポイント:${team.point} 理由:${team.reason}<br>
ゴール: ${formatDateTime(team.goal_time)}
`;
categoryDiv.appendChild(teamDiv);
});
}
container.appendChild(categoryDiv);
});
}
// 表示モードの切り替え
function toggleView() {
showTop3 = !showTop3;
const button = document.getElementById('toggleButton');
const normalRanking = document.getElementById('normalRanking');
const top3Ranking = document.getElementById('top3Ranking');
const classSelect = document.getElementById('classSelect');
button.textContent = showTop3 ? 'クラス別ランキング' : 'TOP3表示';
normalRanking.style.display = showTop3 ? 'none' : 'block';
top3Ranking.style.display = showTop3 ? 'block' : 'none';
classSelect.disabled = showTop3;
updateRankings();
}
</script>
</body>
</html>

View File

@ -0,0 +1,486 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ランキング</title>
<style>
.flex-slave {
margin: 10px;
}
#map {
width: 800px;
height: 600px;
}
#best{
position: relative;
line-height: 1.5em;
background: #fff;
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .5);
margin: 0 auto;
margin-top: 20px;
border-radius:8px;
counter-increment: count-ex1-5;
content: counter(number);
padding: 0px 10px 10px 30px;
margin:0px 0px 0px 5px;
}
#best::marker{
font-weight:bold;
color: #ff0801;
}
#best li{
counter-increment: count-ex1-5;
content: counter(number);
padding: 8px 10px 0px 0px;
margin:8px 0px 0px 8px;
line-height: 2em;
}
li::marker{
font-weight:bold;
color: #ff0801;
line-height: 1;
}
#best::before {
position: absolute;
background: #ff0801;
color: #FFF;
font-size: 15px;
border-radius: 50%;
left: 0;
width: 23px;
height: 23px;
line-height: 22px;
text-align: center;
top: 5px;
font-family: "Font Awesome 5 Free";
content: "\f521";
font-weight: 900;
margin:6px 0px 0px 10px;
}
.black{
background-color: #000;
}
h5 {
position: relative;
padding: 0px 0px 0px 70px;
background-image: linear-gradient(0deg, #b8751e 0%, #ffce08 37%, #fefeb2 47%, #fafad6 50%, #fefeb2 53%, #e1ce08 63%, #b8751e 100%);
-webkit-background-clip: text;
color: transparent;
display:flex;
align-items: center;
height:60px;
}
h5 .span3 {
position: absolute;
top: -10px;
left: 0px;
display: inline-block;
width: 52px;
height: px;
text-align: center;
background: #fa4141;
}
h5 .span3:before,
h5 .span3:after {
position: absolute;
content: '';
}
h5 .span3:before {
right: -10px;
width: 0;
height: 0;
border-right: 10px solid transparent;
border-bottom: 10px solid #d90606;
}
h5 .span3:after {
top: 50%;
left: 0;
display: block;
height: %;
border: 26px solid #fa4141;
border-bottom-width: 15px;
border-bottom-color: transparent;
}
h5 .span3 i {
position: relative;
z-index: 1;
color: #fff100;
padding-top: 10px;
font-size: 24px;
}
.best
select.name{
display: block;
margin-bottom: -10px;
}
.best2 li{
position: relative;
overflow: hidden;
padding: 1.5rem 2rem 1.5rem 130px;
word-break: break-all;
border-top: 3px solid #000;
border-radius: 12px 0 0 0;
margin: 10px 0px 0px 0px;
}
.best2 span{
font-size: 40px;
font-size: 4rem;
position: absolute;
top: 0;
left: 0;
display: block;
padding: 3px 20px;
color: #fff;
border-radius: 10px 0 20px 10px;
background: #000;
}
.button3{
color: #fff;
border: 2px solid #fff;
border-radius: 0;
background-image: -webkit-linear-gradient(left, #fa709a 0%, #fee140 100%);
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
-webkit-box-shadow: 0 5px 5px rgba(0, 0, 0, .1);
box-shadow: 0 3px 5px rgba(0, 0, 0, .1);
border-radius: 100vh;
font-family: "Arial", "メイリオ";
/*letter-spacing: 0.1em;*/
padding: 7px 25px 7px 25px;
}
.button3:hover{
-webkit-transform: translate(0, -2px);
transform: translate(0, -2px);
color: #fff;
-webkit-box-shadow: 0 8px 15px rgba(0, 0, 0, .2);
box-shadow: 0 8px 15px rgba(0, 0, 0, .2);
font-family: "Arial", "メイリオ";
}
#best::before {
position: absolute;
background: #ff0801;
color: #FFF;
font-size: 15px;
border-radius: 50%;
left: 0;
width: 23px;
height: 23px;
line-height: 22px;
text-align: center;
top: 5px;
font-family: "Font Awesome 5 Free";
content: "\f521";
font-weight: 900;
margin:6px 0px 0px 10px;
}
select {
display: block;
border-left: solid 10px #27acd9;
padding: 0.75rem 1.5rem;
border-color: transparent transparent transparent blue;
font-weight: bold;
box-shadow: 3px 5px 3px -2px #aaaaaa,3px 3px 2px 0px #ffffff inset;
margin-bottom: 10px;
width: 250px;
}
.box2{
position: relative;
line-height: 1.5em;
background: #fff;
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .5);
margin: 0 auto;
margin-top: 20px;
border-radius:8px;
counter-increment: count-ex1-5;
content: counter(number);
padding: 10px 10px 10px 30px;
margin:10px 0px 0px 5px;
}
.best2::before {
position: absolute;
background: #ff0801;
color: #FFF;
font-size: 15px;
border-radius: 50%;
left: 0;
width: 23px;
height: 23px;
line-height: 22px;
text-align: center;
top: 5px;
font-family: "Font Awesome 5 Free";
content: "\f521";
font-weight: 900;
margin: 10px 0px 0px 10px;
}
h3{
margin: 3px 0px 0px 10px;
}
@media screen and (max-width: 767px) {
#best{
position: relative;
line-height: 1.5em;
background: #fff;
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .5);
margin: 0 auto;
margin-top: 20px;
border-radius:8px;
counter-increment: count-ex1-5;
content: counter(number);
padding: 0px 10px 10px 30px;
margin:0px 0px 0px 5px;
}
#best::marker{
font-weight:bold;
color: #ff0801;
}
#best li{
counter-increment: count-ex1-5;
content: counter(number);
padding: 8px 10px 0px 0px;
margin:8px 0px 0px 8px;
line-height: 2em;
margin: 10px 0px 0px 0px;
}
li::marker{
font-weight:bold;
color: #ff0801;
line-height: 1;
}
select{
width:100%;
}
.button3{
width: 100%;
text-align: center;
font-size: 24px;
}
select.name{
margin-bottom: -20px;
margin-top: -10px;
}
.arrow{
position: relative;
}
.arrow::after {
color: #828282;
position: absolute;
top:18px; /* 矢印の位置 */
right: 25px; /* 矢印の位置 */
width: 13px; /* 矢印の大きさ */
height: 13px; /* 矢印の大きさ */
border-top: 3px solid #58504A; /* 矢印の線 */
border-right: 3px solid #58504A; /* 矢印の線 */
-webkit-transform: rotate(135deg); /* 矢印の傾き */
transform: rotate(135deg); /* 矢印の傾き */
pointer-events: none; /* 矢印部分もクリック可能にする */
content: "";
border-color: #828282;
}
.arrow2{
position: relative;
}
.arrow2::after {
color: #828282;
position: absolute;
top:20px; /* 矢印の位置 */
right: 25px; /* 矢印の位置 */
width: 13px; /* 矢印の大きさ */
height: 13px; /* 矢印の大きさ */
border-top: 3px solid #58504A; /* 矢印の線 */
border-right: 3px solid #58504A; /* 矢印の線 */
-webkit-transform: rotate(135deg); /* 矢印の傾き */
transform: rotate(135deg); /* 矢印の傾き */
pointer-events: none; /* 矢印部分もクリック可能にする */
content: "";
border-color: #828282;
}
.score {
text-align: right;
}
</style>
</head>
<body>
<div id="ranking">
<div class="black">
<h5><span class="span3"><i class="fas fa-crown"></i></span>
<span style="font-size: 24px">ランキング</span></h5>
</div>
<form @submit.prevent="ranking_view">
<div class="arrow">
<select class="name" v-model="selectedEvent">
<option disabled value="">イベント一覧</option>
<option selected value="FC岐阜">with FC岐阜</option>
<!--
<option value="関ケ原2410">関ケ原-2024年10月</option>
<option value="養老2410">養老-2024年10月</option>
<option value="大垣2410">大垣-2024年10月</option>
<option value="各務原2410">各務原-2024年10月</option>
<option value="多治見2410">多治見-2024年10月</option>
<option value="美濃加茂2410">美濃加茂-2024年10月</option>
<option value="下呂2410">下呂-2024年10月</option>
<option value="郡上2410">郡上-2024年10月</option>
<option value="高山2410">高山-2024年10月</option>
<option value="関ケ原2409">関ケ原-2024年9月</option>
<option value="養老2409">養老-2024年9月</option>
<option value="大垣2409">大垣-2024年9月</option>
<option value="各務原2409">各務原-2024年9月</option>
<option value="多治見2409">多治見-2024年9月</option>
<option value="美濃加茂2409">美濃加茂-2024年9月</option>
<option value="下呂2409">下呂-2024年9月</option>
<option value="郡上2409">郡上-2024年9月</option>
<option value="高山2409">高山-2024年9月</option>
-->
<option value="美濃加茂">岐阜ロゲin美濃加茂</option>
<option value="養老ロゲ">養老町</option>
<option value="岐阜市">岐阜市</option>
<option value="大垣2">岐阜ロゲin大垣@イオンモール大垣</option>
<option value="大垣">岐阜ロゲin大垣</option>
<option value="多治見">岐阜ロゲin多治見</option>
<option value="各務原">岐阜ロゲin各務原</option>
<option value="下呂">岐阜ロゲin下呂温泉</option>
<option value="郡上">岐阜ロゲin郡上</option>
<option value="高山">岐阜ロゲin高山</option>
</select>
<div class="arrow2">
<select v-model="selectedClass">
<option selected value="top3">top3</option>
<option value="3時間一般">3時間一般</option>
<option value="3時間ファミリー">3時間ファミリー</option>
<option value="3時間自転車">3時間自転車</option>
<option value="3時間ソロ男子">3時間ソロ男子</option>
<option value="3時間ソロ女子">3時間ソロ女子</option>
<option value="3時間パラロゲ">3時間パラロゲ</option>
<option value="5時間一般">5時間一般</option>
<option value="5時間ファミリー">5時間ファミリー</option>
<option value="5時間自転車">5時間自転車</option>
<option value="5時間ソロ男子">5時間ソロ男子</option>
<option value="5時間ソロ女子">5時間ソロ女子</option>
</div>
</select>
<button class="button3" type="submit">CLICK</button>
</div>
</form>
<ol v-if="top_three_flag == false" >
<div id="best" v-for="team in team_list">
<li>
{{ team.team_name }}({{ team.zekken_number }})<br/><p class="score"><span class="span2">合計得点:{{ team.point }}</span> <span class="span2">内減点: {{ team.late_point }}</span></p>
</li>
</div>
</ol>
<div v-if="top_three_flag == true">
<div class="box2" v-for="(teams, index) in team_list">
<h3> {{ index }} </h3>
<ol>
<div class="best3" v-for="team in teams">
<span class="span6"><li class="best2"></span>
{{ team.team_name }}({{ team.zekken_number }})<br/><p class="score">合計得点:{{ team.point }} 内減点: {{ team.late_point }}</p>
</li>
</ol>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.1/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
axios.defaults.baseURL = 'https://rogaining.sumasen.net/gifuroge';
axios.default.withCridentials = true;
(function() {
'use strict';
var vm = new Vue({
el: '#ranking',
data: {
selectedEvent: "FC岐阜",
selectedClass: "top3",
team_list: [],
top_three_flag: false,
three_pop: [],
interval: 1
},
mounted : function(){
var int = this.interval * 60 * 1000
setInterval(function() {this.ranking_view()}.bind(this), int);
},
methods: {
ranking_view: function(){
if (this.selectedClass == 'top3'){
this.top_three_flag = true
var url = "/all_ranking_top3?event=" + this.selectedEvent
axios
.get(url)
.then(response => ( this.team_list = response.data))}
else {
this.top_three_flag = false
var url = "/get_ranking?class=" + this.selectedClass + '&event=' + this.selectedEvent
axios
.get(url)
.then(response => ( this.team_list = response.data ))}
}
}
});
})();
</script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>岐阜ロゲwith FC岐阜 Myアルバム|岐阜aiネットワーク</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="./css/reset.css">
<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
<script src="https://kit.fontawesome.com/94e0c17dd1.js" crossorigin="anonymous"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap" rel="stylesheet">
<!-- drawer.css -->
<link rel="stylesheet" href="./css/drawer.min.css">
<!-- Meta -->
<meta name="description" content="岐阜ロゲin岐阜市のMyアルバムページです。">
<meta name="keywords" content="FC岐阜ロゲイニング map 岐阜aiネットワーク ran">
<meta name="robot" content="index,follow,noarchive">
<meta name="author" content="岐阜aiネットワーク">
<meta name="language" content="ja">
<!-- Favicon -->
<link rel="shortcut icon" href="favicon.png">
<style>
/* ここにCSSスタイルを記述 */
.view {
max-width: 1000px;
margin: 50px auto 20px;
padding: 0 20px;
}
section.view input,
button,
select {
font-size: 16px;
}
select,input{
padding: 4px;
}
div#photoList {
display: flex;
flex-wrap: wrap;
max-width: 1200px;
/* margin: 0 10px; */
margin: 0 auto;
}
.event-photo {
width: 100%;
max-width: calc(32.33% - 2px);
margin: 5px;
/* その他のスタイル */
}
.viewtop{
min-height: calc(50vh - 50px);
}
@media screen and (max-width:768px) {
.event-photo {
width: 100%;
max-width: calc(31% - 2px);
margin: 5px;
/* その他のスタイル */
}
.viewtop{
min-height: auto;
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<header>
<div id="headerContent">
<div id="sp_headerContent">
<h1><a href="https://www.gifuai.net/"><img src="./img/rogo.png"></a></h1>
<a id="headerMenuBtn" href="#"><img src="./img/menu_open.svg" alt="メニューを開く"></a>
</div><!--sp_headerContent-->
<div id="headerMenu">
<h2>メニュー<a id="headerMenuClose" href="#"><img class="close" src="./img/btn_close_02.svg" alt="閉じる"></a>
</h2>
<!--ナビバー左側-->
<div class="left">
<ul class="utility">
<h1><a href="https://www.gifuai.net/"><img src="./img/rogo.png"></a></h1>
</ul>
</div><!--left-->
<!--ナビバー右側-->
<div class="right">
<ul class="utility">
<li><a href="https://www.gifuai.net/">ホーム</a></li>
<li><a href="https://www.gifuai.net/?page_id=60043">岐阜ロゲ</a></li>
<li><a href="https://www.gifuai.net/?page_id=4427">自治会SNS</a></li>
<li><a href="https://www.gifuai.net/?page_id=9370">会員・寄付金募集</a></li>
<li><a href="https://www.gifuai.net/?page_id=12434">フォトギャラリー</a></li>
<li><a href="https://www.gifuai.net/?page_id=52511">プレスリリース</a></li>
</ul>
</div><!--right-->
</div><!--headerMenu-->
</div><!--headerContent-->
</header>
<div class="to_classification">
<div class="to_class_box">
<div class="to_class_tebox">
<div class="to_class_text">
<h1>Myアルバム</h1>
</div>
</div>
<div class="to_class_img">
<img src="./img/title_event.png">
</div>
</div>
</div>
<section class="viewtop">
<section class="view">
<!-- イベント選択 -->
<select id="eventSelect">
<option disabled="disabled" value="">イベント一覧</option>
<option selected="selected" value="FC岐阜">FC岐阜</option>
<!-- 他のイベントオプションを追加 -->
</select>
<!-- ゼッケン番号入力 -->
<input type="text" id="zekkenInput" placeholder="ゼッケン番号">
<!-- パスワード入力 -->
<input type="password" id="passwordInput" placeholder="パスワード">
<!-- 検索ボタン -->
<button onclick="searchPhotos()">写真リストを検索</button>
</section>
<!-- 結果表示エリア -->
<div id="photoList"></div>
</section>
<footer class="gifu_fotter">
<div class="footer_menubox">
<div><a href="https://www.gifuai.net/"><img src="./img/rogo.png"></a></div>
<div class="footer_menu">
<ul class="footer_menulink">
<li><a href="https://www.gifuai.net/">ホーム</a></li>
<li><a href="https://www.gifuai.net/?page_id=4806">information</a></li>
<li><a
href="https://docs.google.com/forms/d/e/1FAIpQLScEXBGEZroAR6F8z2OKhjXn74PhZ5bcSheZVlGlGjz12Iu1JA/viewform">お問い合わせ</a>
</li>
</ul>
<ul class="footer_menulogo">
<li><a href="https://twitter.com/GifuK7"><img src="./img/Xlogo.svg" alt="Xロゴ"></a></li>
<li><a href="https://www.facebook.com/gifu.ai.network/"><img src="./img/facebook_logo.svg"
alt="Facebookロゴ"></a></li>
<li><a href="https://www.instagram.com/gifuainetwork/?igshid=MzMyNGUyNmU2YQ%3D%3D"><img
src="./img/Instagram_logo.svg" alt="instagramロゴ"></a></li>
<li><a href=""><img></a></li>
</ul>
</div>
</div>
<div class="f_copy">Copyright©NPO岐阜aiネットワーク</div>
</footer>
<script>
function searchPhotos() {
var selectedEvent = document.getElementById('eventSelect').value;
var selectedZekken = document.getElementById('zekkenInput').value;
var inputedPassword = document.getElementById('passwordInput').value;
// login関数を実行して写真リストを取得
login(selectedEvent, selectedZekken, inputedPassword);
}
async function Login(selectedEvent, selectedZekken, inputedPassword) {
const event = selectedEvent;
const identifier = selectedZekken;
const password = inputedPassword;
try {
const response = await fetch('/api/login/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identifier: identifier, // メールアドレスまたはゼッケン番号
password: password
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'ログインに失敗しました');
}
// ログイン成功時の処理
localStorage.setItem('authToken', data.token);
localStorage.setItem('userData', JSON.stringify(data.user));
var URL = "https://rogaining.sumasen.net/api/get-photolist?event=" + selectedEvent + "&zekken=" + selectedZekken + "&pw=" + inputedPassword;
axios.get(URL)
.then(function (response) {
displayPhotos(response.data); // 写真リストを表示する関数にレスポンスオブジェクトを渡す
})
.catch(function (error) {
console.error("login function error: ", error);
});
} catch (error) {
// エラーメッセージを表示
errorMessage.textContent = error.message;
errorMessage.style.display = 'block';
} finally {
// 送信ボタンを再度有効化
submitButton.disabled = false;
submitButton.textContent = 'ログイン';
}
}
// login関数内で写真リストをDOMに表示する処理を追加
function login_old(selectedEvent, selectedZekken, inputedPassword) {
var URL = "https://rogaining.sumasen.net/api/get-photolist?event=" + selectedEvent + "&zekken=" + selectedZekken + "&pw=" + inputedPassword;
axios.get(URL)
.then(function (response) {
displayPhotos(response.data); // 写真リストを表示する関数にレスポンスオブジェクトを渡す
})
.catch(function (error) {
console.error("login function error: ", error);
});
}
// 写真リストを表示する関数
function displayPhotos(response) {
var photoListDiv = document.getElementById('photoList');
photoListDiv.innerHTML = ''; // 既存の内容をクリア
// レスポンス全体をログに出力
console.log('Response object:', response);
// レスポンスオブジェクトからphoto_list配列を取得
var photos = response.photo_list;
// photo_listの内容をログに出力
console.log('Photo list array:', photos);
// 'photos'が配列であることを確認
if (Array.isArray(photos)) {
photos.forEach(function (photodata,index) {
// 各写真のURLをインデックス付きでログに出力
console.log(`Photo ${index + 1} data:`, photodata);
// photodataのプロパティの存在確認とcp_numberの条件チェック
if (!photodata.hasOwnProperty('photo_url') ||
!photodata.hasOwnProperty('cp_number') ||
photodata.cp_number <= 0) { // cp_numberが0以下の場合はスキップ
console.log(`Skipping photo at index ${index}. cp_number: ${photodata.cp_number}`);
return; // この写真をスキップ
}
// img要素を作成
var img = document.createElement('img');
img.src = photodata.photo_url; // 写真のURLをsrc属性に設定
img.className = 'event-photo'; // クラス名を設定
img.alt = 'Photo cp=${photodata.cp_number}'; // 代替テキストを設定
// 画像の読み込みエラーをキャッチ
img.onerror = function() {
console.error(`Failed to load image ${index + 1}:`, photodata.photorl);
};
// 画像を表示エリアに追加
photoListDiv.appendChild(img);
});
} else {
photoListDiv.innerHTML = 'ゼッケン番号とパスワードが一致していません。もう一度入力をお願いします。'
console.error('Expected photos to be an array, but received:', photos);
}
}
</script>
<script src="jquery-2.1.3.min.js"></script>
<script src="./js/main.js"></script>
<link rel="stylesheet" href="./js/drawer.min.js">
</body>
</html>