idac: added "simple" ranking to frontend

This commit is contained in:
Dniel97 2023-11-21 22:51:10 +01:00
parent d1a7b898a7
commit 6ea8cca1a2
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
10 changed files with 641 additions and 160 deletions

View File

@ -748,8 +748,8 @@ python dbutils.py --game SDGT upgrade
| Course ID | Course Name | Direction | | Course ID | Course Name | Direction |
| --------- | ------------------------- | ------------------------ | | --------- | ------------------------- | ------------------------ |
| 0 | Akina Lake(秋名湖) | CounterClockwise(左周り) | | 0 | Lake Akina(秋名湖) | CounterClockwise(左周り) |
| 2 | Akina Lake(秋名湖) | Clockwise(右周り) | | 2 | Lake Akina(秋名湖) | Clockwise(右周り) |
| 52 | Hakone(箱根) | Downhill(下り) | | 52 | Hakone(箱根) | Downhill(下り) |
| 54 | Hakone(箱根) | Hillclimb(上り) | | 54 | Hakone(箱根) | Hillclimb(上り) |
| 36 | Usui(碓氷) | CounterClockwise(左周り) | | 36 | Usui(碓氷) | CounterClockwise(左周り) |
@ -762,10 +762,10 @@ python dbutils.py --game SDGT upgrade
| 14 | Akina(秋名) | Hillclimb(上り) | | 14 | Akina(秋名) | Hillclimb(上り) |
| 16 | Irohazaka(いろは坂) | Downhill(下り) | | 16 | Irohazaka(いろは坂) | Downhill(下り) |
| 18 | Irohazaka(いろは坂) | Reverse(逆走) | | 18 | Irohazaka(いろは坂) | Reverse(逆走) |
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
| 20 | Tsukuba(筑波) | Outbound(往路) | | 20 | Tsukuba(筑波) | Outbound(往路) |
| 22 | Tsukuba(筑波) | Inbound(復路) | | 22 | Tsukuba(筑波) | Inbound(復路) |
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
| 24 | Happogahara(八方ヶ原) | Outbound(往路) | | 24 | Happogahara(八方ヶ原) | Outbound(往路) |
| 26 | Happogahara(八方ヶ原) | Inbound(復路) | | 26 | Happogahara(八方ヶ原) | Inbound(復路) |
| 40 | Sadamine(定峰) | Downhill(下り) | | 40 | Sadamine(定峰) | Downhill(下り) |

View File

@ -1,7 +1,10 @@
import json import json
import yaml import yaml
import jinja2 import jinja2
from os import path from os import path
from typing import Any, Type
from twisted.web import resource
from twisted.web.util import redirectTo from twisted.web.util import redirectTo
from twisted.web.http import Request from twisted.web.http import Request
from twisted.web.server import Session from twisted.web.server import Session
@ -15,12 +18,109 @@ from titles.idac.config import IDACConfig
from titles.idac.const import IDACConstants from titles.idac.const import IDACConstants
class RankingData:
def __init__(
self,
rank: int,
name: str,
record: int,
store: str,
style_car_id: int,
update_date: str,
) -> None:
self.rank: int = rank
self.name: str = name
self.record: str = record
self.store: str = store
self.style_car_id: int = style_car_id
self.update_date: str = update_date
def make(self):
return vars(self)
class RequestValidator:
def __init__(self) -> None:
self.success: bool = True
self.error: str = ""
def validate_param(
self,
request_args: Dict[bytes, bytes],
param_name: str,
param_type: Type[None],
default=None,
required: bool = True,
) -> None:
# Check if the parameter is missing
if param_name.encode() not in request_args:
if required:
self.success = False
self.error += f"Missing parameter: '{param_name}'. "
else:
# If the parameter is not required,
# return the default value if it exists
return default
return None
param_value = request_args[param_name.encode()][0].decode()
# Check if the parameter type is not empty
if param_type:
try:
# Attempt to convert the parameter value to the specified type
param_value = param_type(param_value)
except ValueError:
# If the conversion fails, return an error
self.success = False
self.error += f"Invalid parameter type for '{param_name}'. "
return None
return param_value
class RankingRequest(RequestValidator):
def __init__(self, request_args: Dict[bytes, bytes]) -> None:
super().__init__()
self.course_id: int = self.validate_param(request_args, "courseId", int)
self.page_number: int = self.validate_param(
request_args, "pageNumber", int, default=1, required=False
)
class RankingResponse:
def __init__(self) -> None:
self.success: bool = False
self.error: str = ""
self.total_pages: int = 0
self.total_records: int = 0
self.updated_at: str = ""
self.ranking: list[RankingData] = []
def make(self):
ret = vars(self)
self.error = (
"Unknown error." if not self.success and self.error == "" else self.error
)
ret["ranking"] = [rank.make() for rank in self.ranking]
return ret
def to_json(self):
return json.dumps(self.make(), default=str, ensure_ascii=False).encode("utf-8")
class IDACFrontend(FE_Base): class IDACFrontend(FE_Base):
isLeaf = False
children: Dict[str, Any] = {}
def __init__( def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None: ) -> None:
super().__init__(cfg, environment) super().__init__(cfg, environment)
self.data = IDACData(cfg) self.data = IDACData(cfg)
self.core_cfg = cfg
self.game_cfg = IDACConfig() self.game_cfg = IDACConfig()
if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"):
self.game_cfg.update( self.game_cfg.update(
@ -30,6 +130,152 @@ class IDACFrontend(FE_Base):
# TODO: Add version list # TODO: Add version list
self.version = IDACConstants.VER_IDAC_SEASON_2 self.version = IDACConstants.VER_IDAC_SEASON_2
self.putChild(b"profile", IDACProfileFrontend(cfg, self.environment))
self.putChild(b"ranking", IDACRankingFrontend(cfg, self.environment))
def render_GET(self, request: Request) -> bytes:
uri: str = request.uri.decode()
template = self.environment.get_template(
"titles/idac/frontend/idac_index.jinja"
)
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
# redirect to the ranking page
if uri.startswith("/game/idac"):
return redirectTo(b"/game/idac/ranking", request)
return template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
active_page="idac",
).encode("utf-16")
def render_POST(self, request: Request) -> bytes:
pass
class IDACRankingFrontend(FE_Base):
def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None:
super().__init__(cfg, environment)
self.data = IDACData(cfg)
self.core_cfg = cfg
self.nav_name = "頭文字D THE ARCADE"
# TODO: Add version list
self.version = IDACConstants.VER_IDAC_SEASON_2
def render_GET(self, request: Request) -> bytes:
uri: str = request.uri.decode()
template = self.environment.get_template(
"titles/idac/frontend/ranking/index.jinja"
)
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
user_id = usr_sesh.userId
# user_id = usr_sesh.user_id
# IDAC constants
if uri.startswith("/game/idac/ranking/const.get"):
# set the content type to json
request.responseHeaders.addRawHeader(b"content-type", b"application/json")
# get the constants
with open("titles/idac/frontend/const.json", "r", encoding="utf-8") as f:
constants = json.load(f)
return json.dumps(constants, ensure_ascii=False).encode("utf-8")
# leaderboard ranking
elif uri.startswith("/game/idac/ranking/ranking.get"):
# set the content type to json
request.responseHeaders.addRawHeader(b"content-type", b"application/json")
req = RankingRequest(request.args)
resp = RankingResponse()
if not req.success:
resp.error = req.error
return resp.to_json()
# get the total number of records
total_records = self.data.item.get_time_trial_ranking_by_course_total(
self.version, req.course_id
)
# return an error if there are no records
if total_records is None or total_records == 0:
resp.error = "No records found."
return resp.to_json()
# get the total number of records
total = total_records["count"]
limit = 50
offset = (req.page_number - 1) * limit
ranking = self.data.item.get_time_trial_ranking_by_course(
self.version,
req.course_id,
limit=limit,
offset=offset,
)
for i, rank in enumerate(ranking):
user_id = rank["user"]
# get the username, country and store from the profile
profile = self.data.profile.get_profile(user_id, self.version)
arcade = self.data.arcade.get_arcade(profile["store"])
if arcade is None:
arcade = {}
arcade["name"] = self.core_config.server.name
# should never happen
if profile is None:
continue
resp.ranking.append(
RankingData(
rank=offset + i + 1,
name=profile["username"],
record=rank["goal_time"],
store=arcade["name"],
style_car_id=rank["style_car_id"],
update_date=str(rank["play_dt"]),
)
)
# now return the json data, with the total number of pages and records
# round up the total pages
resp.success = True
resp.total_pages = (total // limit) + 1
resp.total_records = total
return resp.to_json()
return template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
active_page="idac",
active_tab="ranking",
).encode("utf-16")
class IDACProfileFrontend(FE_Base):
def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None:
super().__init__(cfg, environment)
self.data = IDACData(cfg)
self.core_cfg = cfg
self.nav_name = "頭文字D THE ARCADE"
# TODO: Add version list
self.version = IDACConstants.VER_IDAC_SEASON_2
self.ticket_names = { self.ticket_names = {
3: "car_dressup_points", 3: "car_dressup_points",
5: "avatar_points", 5: "avatar_points",
@ -60,7 +306,7 @@ class IDACFrontend(FE_Base):
theory_running, theory_running,
vs_info, vs_info,
stamp, stamp,
timetrial_event timetrial_event,
} }
for table in idac_tables: for table in idac_tables:
@ -86,11 +332,12 @@ class IDACFrontend(FE_Base):
return json.dumps(json_export, indent=4, default=str, ensure_ascii=False) return json.dumps(json_export, indent=4, default=str, ensure_ascii=False)
def render_GET(self, request: Request) -> bytes: def render_GET(self, request: Request) -> bytes:
uri: str = request.uri.decode() uri: str = request.uri.decode()
template = self.environment.get_template( template = self.environment.get_template(
"titles/idac/frontend/idac_index.jinja" "titles/idac/frontend/profile/index.jinja"
) )
sesh: Session = request.getSession() sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh) usr_sesh = IUserSession(sesh)
@ -98,7 +345,7 @@ class IDACFrontend(FE_Base):
# user_id = usr_sesh.user_id # user_id = usr_sesh.user_id
# profile export # profile export
if uri.startswith("/game/idac/export"): if uri.startswith("/game/idac/profile/export.get"):
if user_id == 0: if user_id == 0:
return redirectTo(b"/game/idac", request) return redirectTo(b"/game/idac", request)
@ -136,7 +383,5 @@ class IDACFrontend(FE_Base):
rank=rank, rank=rank,
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
active_page="idac", active_page="idac",
active_tab="profile",
).encode("utf-16") ).encode("utf-16")
def render_POST(self, request: Request) -> bytes:
pass

File diff suppressed because one or more lines are too long

View File

@ -2,130 +2,20 @@
{% block content %} {% block content %}
<h1 class="mb-3">頭文字D THE ARCADE</h1> <h1 class="mb-3">頭文字D THE ARCADE</h1>
{% if sesh is defined and sesh["userId"] > 0 %} <nav class="mb-3">
<div class="card mb-3"> <ul class="nav nav-tabs">
<div class="card-body"> <li class="nav-item">
<div class="card-title"> <a class="nav-link {% if active_tab == 'ranking' %}active{% endif %}" aria-current="page" href="ranking">Ranking</a>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center"> </li>
<h3>{{ sesh["username"] }}'s Profile</h3> <li class="nav-item">
<div class="btn-toolbar mb-2 mb-md-0"> <a class="nav-link {% if active_tab == 'profile' %}active{% endif %}" href="profile">Profile</a>
<div class="btn-group me-2"> </li>
<!--<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>--> </ul>
<button type="button" data-bs-toggle="modal" data-bs-target="#export" </nav>
class="btn btn-sm btn-outline-primary">Export</button>
</div>
</div>
</div>
</div>
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
{% if profile is defined and profile is not none %}
<div class="row d-flex justify-content-center h-100">
<div class="col col-lg-3 col-12">
<div class="card mb-3">
<div class="card-body p-4">
<h5>Information</h5>
<hr class="mt-0 mb-4">
<h6>Username</h6>
<p class="text-muted">{{ profile.username }}</p>
<h6>Cash</h6>
<p class="text-muted">{{ profile.cash }} D</p>
<h6>Grade</h6>
<h4>
{% set grade = rank.grade %}
{% if grade >= 1 and grade <= 72 %}
{% set grade_number = (grade - 1) // 9 %}
{% set grade_letters = ['E', 'D', 'C', 'B', 'A', 'S', 'SS', 'X'] %}
{{ grade_letters[grade_number] }}{{ 9 - ((grade-1) % 9) }}
{% else %}
Unknown
{% endif %}
</h4>
</div>
</div>
</div>
<div class="col col-lg-9 col-12">
<div class="card mb-3">
<div class="card-body p-4"> {% block tab %}
<h5>Statistics</h5>
<hr class="mt-0 mb-4">
<div class="row pt-1">
<div class="col-lg-4 col-md-6 mb-3">
<h6>Total Plays</h6>
<p class="text-muted">{{ profile.total_play }}</p>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<h6>Last Played</h6>
<p class="text-muted">{{ profile.last_play_date }}</p>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<h6>Mileage</h6>
<p class="text-muted">{{ profile.mileage / 1000}} km</p>
</div>
</div>
{% if tickets is defined and tickets|length > 0 %}
<h5>Tokens/Tickets</h5>
<hr class="mt-0 mb-4">
<div class="row pt-1">
<div class="col-lg-3 col-md-6 mb-3">
<h6>Avatar Tokens</h6>
<p class="text-muted">{{ tickets.avatar_points }}/30</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>Car Dressup Tokens</h6>
<p class="text-muted">{{ tickets.car_dressup_points }}/30</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>FullTune Tickets</h6>
<p class="text-muted">{{ tickets.full_tune_tickets }}/99</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>FullTune Fragments</h6>
<p class="text-muted">{{ tickets.full_tune_fragments }}/10</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
You need to play 頭文字D THE ARCADE first to view your profile.
</div>
{% endif %}
<!--<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="card-link">Add Card</a>-->
</div>
</div>
{% else %}
<div class="alert alert-info" role="alert">
You need to be logged in to view this page. <a href="/gate">Login</a></a>
</div>
{% endif %}
<div class="modal fade" id="export" tabindex="-1" aria-labelledby="export-label" aria-hidden="true"> {% endblock tab %}
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exort-label">Export Profile</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Download your profile as a <strong>.json</strong> file in order to import it into your local ARTEMiS
database.
<div class="alert alert-warning mt-3" role="alert">
{% if profile is defined and profile is not none %}
Are you sure you want to export your profile with the username {{ profile.username }}?
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="exportBtn">Download Profile</a>
</div>
</div>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
{% include "titles/idac/frontend/js/idac_scripts.js" %} {% include "titles/idac/frontend/js/idac_scripts.js" %}

View File

@ -1,10 +1,59 @@
// Declare a global variable to store the JSON data
var constData;
function formatGoalTime(milliseconds) {
// Convert the milliseconds to a time string
var minutes = Math.floor(milliseconds / 60000);
var seconds = Math.floor((milliseconds % 60000) / 1000);
milliseconds %= 1000;
return `${parseInt(minutes)}'${seconds.toString().padStart(2, '0')}"${milliseconds.toString().padStart(3, '0')}`;
}
// Function to get style_name for a given style_car_id
function getCarName(style_car_id) {
// Find the car with the matching style_car_id
var foundCar = constData.car.find(function (style) {
return style.style_car_id === style_car_id;
});
// Return the style_name if found, otherwise return Unknown
return foundCar ? foundCar.style_name : "Unknown car";
}
$(document).ready(function () { $(document).ready(function () {
$('#exportBtn').click(function () { // Make an AJAX request to load the JSON file
window.location = "/game/idac/export"; $.ajax({
url: "/game/idac/ranking/const.get",
type: "GET",
dataType: "json",
success: function (data) {
// Check if the 'course' array exists in the JSON data
if (data && data.course) {
// Assign the JSON data to the global variable
constData = data;
// appendAlert('Successfully exported the profile', 'success'); // Get the select element
var selectElement = $("#course-select");
// Close the modal on success // Remove the Loading text
$('#export').modal('hide'); selectElement.empty();
// Loop through the 'course' array and add options to the select
$.each(constData.course, function (index, course) {
var option = '<option value="' + course.course_id + '"' + (index === 0 ? 'selected' : '') + '>' + course.course_name + '</option>';
selectElement.append(option);
});
// Simulate a change event on page load with the default value (0)
$("#course-select").val("0").change();
}
},
error: function (jqXHR, textStatus, errorThrown) {
// Print the error message as an option element
$("#course-select").html("<option value='0' selected disabled>" + textStatus + "</option>");
console.error("Error loading JSON file:", textStatus, errorThrown);
}
}); });
}); });

View File

@ -0,0 +1,129 @@
{% extends "titles/idac/frontend/idac_index.jinja" %}
{% block tab %}
{% if sesh is defined and sesh["userId"] > 0 %}
<div class="card mb-3">
<div class="card-body">
<div class="card-title">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center">
<h3>{{ sesh["username"] }}'s Profile</h3>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<!--<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>-->
<button type="button" data-bs-toggle="modal" data-bs-target="#export"
class="btn btn-sm btn-outline-primary">Export</button>
</div>
</div>
</div>
</div>
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
{% if profile is defined and profile is not none %}
<div class="row d-flex justify-content-center h-100">
<div class="col col-lg-3 col-12">
<div class="card mb-3">
<div class="card-body p-4">
<h5>Information</h5>
<hr class="mt-0 mb-4">
<h6>Username</h6>
<p class="text-muted">{{ profile.username }}</p>
<h6>Cash</h6>
<p class="text-muted">{{ profile.cash }} D</p>
<h6>Grade</h6>
<h4>
{% set grade = rank.grade %}
{% if grade >= 1 and grade <= 72 %} {% set grade_number=(grade - 1) // 9 %} {% set
grade_letters=['E', 'D' , 'C' , 'B' , 'A' , 'S' , 'SS' , 'X' ] %} {{
grade_letters[grade_number] }}{{ 9 - ((grade-1) % 9) }} {% else %} Unknown {% endif %}
</h4>
</div>
</div>
</div>
<div class="col col-lg-9 col-12">
<div class="card mb-3">
<div class="card-body p-4">
<h5>Statistics</h5>
<hr class="mt-0 mb-4">
<div class="row pt-1">
<div class="col-lg-4 col-md-6 mb-3">
<h6>Total Plays</h6>
<p class="text-muted">{{ profile.total_play }}</p>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<h6>Last Played</h6>
<p class="text-muted">{{ profile.last_play_date }}</p>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<h6>Mileage</h6>
<p class="text-muted">{{ profile.mileage / 1000}} km</p>
</div>
</div>
{% if tickets is defined and tickets|length > 0 %}
<h5>Tokens/Tickets</h5>
<hr class="mt-0 mb-4">
<div class="row pt-1">
<div class="col-lg-3 col-md-6 mb-3">
<h6>Avatar Tokens</h6>
<p class="text-muted">{{ tickets.avatar_points }}/30</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>Car Dressup Tokens</h6>
<p class="text-muted">{{ tickets.car_dressup_points }}/30</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>FullTune Tickets</h6>
<p class="text-muted">{{ tickets.full_tune_tickets }}/99</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>FullTune Fragments</h6>
<p class="text-muted">{{ tickets.full_tune_fragments }}/10</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
You need to play 頭文字D THE ARCADE first to view your profile.
</div>
{% endif %}
<!--<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="card-link">Add Card</a>-->
</div>
</div>
{% else %}
<div class="alert alert-info" role="alert">
You need to be logged in to view this page. <a href="/gate">Login</a></a>
</div>
{% endif %}
<div class="modal fade" id="export" tabindex="-1" aria-labelledby="export-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exort-label">Export Profile</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Download your profile as a <strong>.json</strong> file in order to import it into your local ARTEMiS
database.
<div class="alert alert-warning mt-3" role="alert">
{% if profile is defined and profile is not none %}
Are you sure you want to export your profile with the username {{ profile.username }}?
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="exportBtn">Download Profile</a>
</div>
</div>
</div>
</div>
<script type="text/javascript">
{% include "titles/idac/frontend/profile/js/scripts.js" %}
</script>
{% endblock tab %}

View File

@ -0,0 +1,10 @@
$(document).ready(function () {
$('#exportBtn').click(function () {
window.location = "/game/idac/profile/export.get";
// appendAlert('Successfully exported the profile', 'success');
// Close the modal on success
$('#export').modal('hide');
});
});

View File

@ -0,0 +1,30 @@
{% extends "titles/idac/frontend/idac_index.jinja" %}
{% block tab %}
<div class="tab-content" id="nav-tabContent">
<!-- Ranking -->
<div class="tab-pane fade show active" id="nav-ranking" role="tabpanel" aria-labelledby="nav-ranking-tab"
tabindex="0">
<div class="row justify-content-md-center form-signin">
<div class="col col-lg-4">
<select class="form-select mb-3" id="course-select">
<option value="0" selected disabled>Loading Courses...</option>
</select>
</div>
</div>
<div class="card">
<div class="card-body">
<div id="table-ranking">
<div class="text-center">Loading Ranking...</div>
</div>
</div>
</div>
<div id="pagination-ranking"></div>
</div>
</div>
<script type="text/javascript">
{% include "titles/idac/frontend/ranking/js/scripts.js" %}
</script>
{% endblock tab %}

View File

@ -0,0 +1,95 @@
// Function to load data based on the selected value
function loadRanking(courseId, pageNumber = 1) {
// Make a GET request to the server
$.ajax({
url: "/game/idac/ranking/ranking.get",
type: "GET",
data: { courseId: courseId, pageNumber: pageNumber },
dataType: "json",
success: function (data) {
// check if an error inside the json exists
if (!data.success) {
// Inject the table into the container
$("#table-ranking").html("<div class='text-center'>" + data.error + "</div>");
console.error("Error: " + data.error);
return;
}
// get the total number of pages
var total_pages = data.total_pages;
// Generate the HTML table
var tableHtml = '<div class="table-responsive"><table class="table table-hover"><thead><tr><th scope="col">#</th><th scope="col">Name</th><th scope="col">Car</th><th scope="col">Time</th><th scope="col" class="d-none d-lg-table-cell">Store</th><th scope="col" class="d-none d-lg-table-cell">Date</th></tr></thead><tbody>';
$.each(data.ranking, function (i, ranking) {
tableHtml += '<tr class="align-middle">';
tableHtml += '<td>' + ranking.rank + '</td>';
tableHtml += '<td>' + ranking.name + '</td>';
tableHtml += '<td>' + getCarName(ranking.style_car_id) + '</td>';
tableHtml += '<td>' + formatGoalTime(ranking.record) + '</td>';
// Ignore the Store and Date columns on small screens
tableHtml += '<td class="d-none d-lg-table-cell">' + ranking.store + '</td>';
tableHtml += '<td class="d-none d-lg-table-cell">' + ranking.update_date + '</td>';
tableHtml += '</tr>';
});
tableHtml += '</tbody></table></div>';
// Inject the table into the container
$("#table-ranking").html(tableHtml);
// Generate the Pagination HTML
var paginationHtml = '<nav class="mt-3"><ul class="pagination justify-content-center">';
// Deactivate the previous button if the current page is the first page
paginationHtml += '<li class="page-item ' + (pageNumber === 1 ? 'disabled' : '') + '">';
paginationHtml += '<a class="page-link" href="#" data-page="' + (pageNumber - 1) + '">Previous</a>';
paginationHtml += '</li>';
for (var i = 1; i <= total_pages; i++) {
// Set the active class to the current page
paginationHtml += '<li class="page-item ' + (pageNumber === i ? 'active disabled' : '') + '"><a class="page-link" href="#" data-page="' + i + '">' + i + '</a></li>';
}
// Deactivate the next button if the current page is the last page
paginationHtml += '<li class="page-item ' + (pageNumber === total_pages ? 'disabled' : '') + '">';
paginationHtml += '<a class="page-link" href="#" data-page="' + (pageNumber + 1) + '">Next</a>';
paginationHtml += '</li>';
paginationHtml += '</ul></nav>';
// Inject the pagination into the container
$("#pagination-ranking").html(paginationHtml);
},
error: function (jqXHR, textStatus, errorThrown) {
// Inject the table into the container
$("#table-ranking").html("<div class='text-center'>" + textStatus + "</div>");
console.error("Error: " + textStatus, errorThrown);
}
});
}
// Function to handle page changes
function changePage(pageNumber) {
// Get the selected value
var courseId = $("#course-select").val();
// Call the function to load data with the new page number
loadRanking(courseId, pageNumber);
}
$(document).ready(function () {
// Attach an event handler to the select element
$("#course-select").change(function () {
// Get the selected value
var courseId = $(this).val();
// Call the function to load data
loadRanking(courseId);
});
// Event delegation for pagination links
$("#pagination-ranking").on("click", "a.page-link", function (event) {
event.preventDefault(); // Prevent the default behavior of the link
var clickedPage = $(this).data("page");
// Check if the changePage function is not already in progress
if (!$(this).hasClass('disabled')) {
// Handle the page change here
changePage(clickedPage);
}
});
});

View File

@ -499,19 +499,16 @@ class IDACItemData(BaseData):
def get_time_trial_best_cars_by_course( def get_time_trial_best_cars_by_course(
self, version: int, course_id: int, aime_id: Optional[int] = None self, version: int, course_id: int, aime_id: Optional[int] = None
) -> Optional[List[Row]]: ) -> Optional[List[Row]]:
subquery = ( subquery = select(
select(
trial.c.version, trial.c.version,
func.min(trial.c.goal_time).label("min_goal_time"), func.min(trial.c.goal_time).label("min_goal_time"),
trial.c.style_car_id, trial.c.style_car_id,
) ).where(
.where(
and_( and_(
trial.c.version == version, trial.c.version == version,
trial.c.course_id == course_id, trial.c.course_id == course_id,
) )
) )
)
if aime_id is not None: if aime_id is not None:
subquery = subquery.where(trial.c.user == aime_id) subquery = subquery.where(trial.c.user == aime_id)
@ -532,12 +529,45 @@ class IDACItemData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
def get_time_trial_ranking_by_course_total(
self,
version: int,
course_id: int,
) -> Optional[List[Row]]:
# count the number of rows returned by the query
subquery = (
select(
trial.c.version,
trial.c.user,
func.min(trial.c.goal_time).label("min_goal_time"),
)
.where(and_(trial.c.version == version, trial.c.course_id == course_id))
.group_by(trial.c.user)
).subquery()
sql = (
select(func.count().label("count"))
.where(
and_(
trial.c.version == subquery.c.version,
trial.c.user == subquery.c.user,
trial.c.goal_time == subquery.c.min_goal_time,
),
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_time_trial_ranking_by_course( def get_time_trial_ranking_by_course(
self, self,
version: int, version: int,
course_id: int, course_id: int,
style_car_id: Optional[int] = None, style_car_id: Optional[int] = None,
limit: Optional[int] = 10, limit: Optional[int] = 10,
offset: Optional[int] = 0,
) -> Optional[List[Row]]: ) -> Optional[List[Row]]:
# get the top 10 ranking by goal_time for a given course which is grouped by user # get the top 10 ranking by goal_time for a given course which is grouped by user
subquery = select( subquery = select(
@ -546,7 +576,7 @@ class IDACItemData(BaseData):
func.min(trial.c.goal_time).label("min_goal_time"), func.min(trial.c.goal_time).label("min_goal_time"),
).where(and_(trial.c.version == version, trial.c.course_id == course_id)) ).where(and_(trial.c.version == version, trial.c.course_id == course_id))
# if wantd filter only by style_car_id # if wanted filter only by style_car_id
if style_car_id is not None: if style_car_id is not None:
subquery = subquery.where(trial.c.style_car_id == style_car_id) subquery = subquery.where(trial.c.style_car_id == style_car_id)
@ -568,6 +598,10 @@ class IDACItemData(BaseData):
if limit is not None: if limit is not None:
sql = sql.limit(limit) sql = sql.limit(limit)
# offset the result if needed
if offset is not None:
sql = sql.offset(offset)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
return None return None
@ -750,7 +784,9 @@ class IDACItemData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
def get_timetrial_event(self, aime_id: int, timetrial_event_id: int) -> Optional[Row]: def get_timetrial_event(
self, aime_id: int, timetrial_event_id: int
) -> Optional[Row]:
sql = select(timetrial_event).where( sql = select(timetrial_event).where(
and_( and_(
timetrial_event.c.user == aime_id, timetrial_event.c.user == aime_id,
@ -946,9 +982,7 @@ class IDACItemData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
def put_stamp( def put_stamp(self, aime_id: int, stamp_data: Dict) -> Optional[int]:
self, aime_id: int, stamp_data: Dict
) -> Optional[int]:
stamp_data["user"] = aime_id stamp_data["user"] = aime_id
sql = insert(stamp).values(**stamp_data) sql = insert(stamp).values(**stamp_data)
@ -956,9 +990,7 @@ class IDACItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warn(f"putstamp: Failed to update! aime_id: {aime_id}")
f"putstamp: Failed to update! aime_id: {aime_id}"
)
return None return None
return result.lastrowid return result.lastrowid