diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index fa2a250..5ea8888 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -748,8 +748,8 @@ python dbutils.py --game SDGT upgrade | Course ID | Course Name | Direction | | --------- | ------------------------- | ------------------------ | -| 0 | Akina Lake(秋名湖) | CounterClockwise(左周り) | -| 2 | Akina Lake(秋名湖) | Clockwise(右周り) | +| 0 | Lake Akina(秋名湖) | CounterClockwise(左周り) | +| 2 | Lake Akina(秋名湖) | Clockwise(右周り) | | 52 | Hakone(箱根) | Downhill(下り) | | 54 | Hakone(箱根) | Hillclimb(上り) | | 36 | Usui(碓氷) | CounterClockwise(左周り) | @@ -762,10 +762,10 @@ python dbutils.py --game SDGT upgrade | 14 | Akina(秋名) | Hillclimb(上り) | | 16 | Irohazaka(いろは坂) | Downhill(下り) | | 18 | Irohazaka(いろは坂) | Reverse(逆走) | -| 56 | Momiji Line(もみじライン) | Downhill(下り) | -| 58 | Momiji Line(もみじライン) | Hillclimb(上り) | | 20 | Tsukuba(筑波) | Outbound(往路) | | 22 | Tsukuba(筑波) | Inbound(復路) | +| 56 | Momiji Line(もみじライン) | Downhill(下り) | +| 58 | Momiji Line(もみじライン) | Hillclimb(上り) | | 24 | Happogahara(八方ヶ原) | Outbound(往路) | | 26 | Happogahara(八方ヶ原) | Inbound(復路) | | 40 | Sadamine(定峰) | Downhill(下り) | diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py index 78abae8..19c51ff 100644 --- a/titles/idac/frontend.py +++ b/titles/idac/frontend.py @@ -1,7 +1,10 @@ import json import yaml import jinja2 + from os import path +from typing import Any, Type +from twisted.web import resource from twisted.web.util import redirectTo from twisted.web.http import Request from twisted.web.server import Session @@ -15,12 +18,109 @@ from titles.idac.config import IDACConfig 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): + isLeaf = False + children: Dict[str, Any] = {} + def __init__( self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str ) -> None: super().__init__(cfg, environment) self.data = IDACData(cfg) + self.core_cfg = cfg self.game_cfg = IDACConfig() if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): self.game_cfg.update( @@ -30,13 +130,159 @@ class IDACFrontend(FE_Base): # TODO: Add version list 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 = { 3: "car_dressup_points", 5: "avatar_points", 25: "full_tune_tickets", 34: "full_tune_fragments", } - + def generate_all_tables_json(self, user_id: int): json_export = {} @@ -60,7 +306,7 @@ class IDACFrontend(FE_Base): theory_running, vs_info, stamp, - timetrial_event + timetrial_event, } 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) + def render_GET(self, request: Request) -> bytes: uri: str = request.uri.decode() template = self.environment.get_template( - "titles/idac/frontend/idac_index.jinja" + "titles/idac/frontend/profile/index.jinja" ) sesh: Session = request.getSession() usr_sesh = IUserSession(sesh) @@ -98,7 +345,7 @@ class IDACFrontend(FE_Base): # user_id = usr_sesh.user_id # profile export - if uri.startswith("/game/idac/export"): + if uri.startswith("/game/idac/profile/export.get"): if user_id == 0: return redirectTo(b"/game/idac", request) @@ -136,7 +383,5 @@ class IDACFrontend(FE_Base): rank=rank, sesh=vars(usr_sesh), active_page="idac", + active_tab="profile", ).encode("utf-16") - - def render_POST(self, request: Request) -> bytes: - pass diff --git a/titles/idac/frontend/const.json b/titles/idac/frontend/const.json new file mode 100644 index 0000000..339d382 --- /dev/null +++ b/titles/idac/frontend/const.json @@ -0,0 +1 @@ +{"car":[{"style_car_id":0,"style_name":"SPRINTER TRUENO GT-APEX (AE86) [DH]","route_style_name":"DH"},{"style_car_id":131072,"style_name":"SPRINTER TRUENO GT-APEX (AE86) [HC]","route_style_name":"HC"},{"style_car_id":9,"style_name":"SPRINTER TRUENO 2door GT-APEX (AE86) [DH]","route_style_name":"DH"},{"style_car_id":131073,"style_name":"COROLLA LEVIN GT-APEX (AE86) [HC]","route_style_name":"HC"},{"style_car_id":65537,"style_name":"COROLLA LEVIN GT-APEX (AE86) [AR]","route_style_name":"AR"},{"style_car_id":131074,"style_name":"COROLLA LEVIN SR (AE85) [HC]","route_style_name":"HC"},{"style_car_id":65538,"style_name":"COROLLA LEVIN SR (AE85) [AR]","route_style_name":"AR"},{"style_car_id":7,"style_name":"86 GT (ZN6) [DH]","route_style_name":"DH"},{"style_car_id":131079,"style_name":"86 GT (ZN6) [HC]","route_style_name":"HC"},{"style_car_id":65543,"style_name":"86 GT (ZN6) [AR]","route_style_name":"AR"},{"style_car_id":3,"style_name":"MR2 G-Limited (SW20) [DH]","route_style_name":"DH"},{"style_car_id":65539,"style_name":"MR2 G-Limited (SW20) [AR]","route_style_name":"AR"},{"style_car_id":5,"style_name":"MR-S S EDITION (ZZW30) [DH]","route_style_name":"DH"},{"style_car_id":4,"style_name":"ALTEZZA RS200 Z EDITION (SXE10) [DH]","route_style_name":"DH"},{"style_car_id":131082,"style_name":"CELICA GT-FOUR (ST205) [HC]","route_style_name":"HC"},{"style_car_id":65546,"style_name":"CELICA GT-FOUR (ST205) [AR]","route_style_name":"AR"},{"style_car_id":131078,"style_name":"SUPRA RZ (JZA80) [HC]","route_style_name":"HC"},{"style_car_id":65542,"style_name":"SUPRA RZ (JZA80) [AR]","route_style_name":"AR"},{"style_car_id":65547,"style_name":"GR YARIS 1st Edition RZ “High performance” (GXPA16) [AR]","route_style_name":"AR"},{"style_car_id":12,"style_name":"GR SUPRA RZ (DB42) [DH]","route_style_name":"DH"},{"style_car_id":65548,"style_name":"GR SUPRA RZ (DB42) [AR]","route_style_name":"AR"},{"style_car_id":131328,"style_name":"SKYLINE GT-R V・specⅡ (BNR32) [HC]","route_style_name":"HC"},{"style_car_id":65792,"style_name":"SKYLINE GT-R V・specⅡ (BNR32) [AR]","route_style_name":"AR"},{"style_car_id":131329,"style_name":"SKYLINE GT-R V・specⅡ Nür (BNR34) [HC]","route_style_name":"HC"},{"style_car_id":65793,"style_name":"SKYLINE GT-R V・specⅡ Nür (BNR34) [AR]","route_style_name":"AR"},{"style_car_id":131330,"style_name":"SILVIA K's (S13) [HC]","route_style_name":"HC"},{"style_car_id":65794,"style_name":"SILVIA K's (S13) [AR]","route_style_name":"AR"},{"style_car_id":259,"style_name":"Silvia Q's (S14) [DH]","route_style_name":"DH"},{"style_car_id":131331,"style_name":"Silvia Q's (S14) [HC]","route_style_name":"HC"},{"style_car_id":131332,"style_name":"Silvia spec-R (S15) [HC]","route_style_name":"HC"},{"style_car_id":65796,"style_name":"Silvia spec-R (S15) [AR]","route_style_name":"AR"},{"style_car_id":261,"style_name":"180SX TYPE Ⅱ (RPS13) [DH]","route_style_name":"DH"},{"style_car_id":65797,"style_name":"180SX TYPE Ⅱ (RPS13) [AR]","route_style_name":"AR"},{"style_car_id":131334,"style_name":"FAIRLADY Z Version S (Z33) [HC]","route_style_name":"HC"},{"style_car_id":131335,"style_name":"NISSAN GT-R NISMO (R35) [HC]","route_style_name":"HC"},{"style_car_id":264,"style_name":"SKYLINE 25GT TURBO (ER34) [DH]","route_style_name":"DH"},{"style_car_id":131336,"style_name":"SKYLINE 25GT TURBO (ER34) [HC]","route_style_name":"HC"},{"style_car_id":265,"style_name":"Fairlady Z (S30) [DH]","route_style_name":"DH"},{"style_car_id":131337,"style_name":"Fairlady Z (S30) [HC]","route_style_name":"HC"},{"style_car_id":512,"style_name":"Civic SiR・Ⅱ (EG6) [DH]","route_style_name":"DH"},{"style_car_id":131584,"style_name":"Civic SiR・Ⅱ (EG6) [HC]","route_style_name":"HC"},{"style_car_id":513,"style_name":"CIVIC TYPE R (EK9) [DH]","route_style_name":"DH"},{"style_car_id":66049,"style_name":"CIVIC TYPE R (EK9) [AR]","route_style_name":"AR"},{"style_car_id":514,"style_name":"INTEGRA TYPE R (DC2) [DH]","route_style_name":"DH"},{"style_car_id":131586,"style_name":"INTEGRA TYPE R (DC2) [HC]","route_style_name":"HC"},{"style_car_id":515,"style_name":"S2000 (AP1) [DH]","route_style_name":"DH"},{"style_car_id":66051,"style_name":"S2000 (AP1) [AR]","route_style_name":"AR"},{"style_car_id":131588,"style_name":"NSX (NA1) [HC]","route_style_name":"HC"},{"style_car_id":768,"style_name":"SAVANNA RX-7 ∞Ⅲ (FC3S) [DH]","route_style_name":"DH"},{"style_car_id":66304,"style_name":"SAVANNA RX-7 ∞Ⅲ (FC3S) [AR]","route_style_name":"AR"},{"style_car_id":131841,"style_name":"ε~fini RX-7 Type R (FD3S) [HC]","route_style_name":"HC"},{"style_car_id":66305,"style_name":"ε~fini RX-7 Type R (FD3S) [AR]","route_style_name":"AR"},{"style_car_id":770,"style_name":"RX-8 Type S (SE3P) [DH]","route_style_name":"DH"},{"style_car_id":771,"style_name":"EUNOS ROADSTER (NA6CE) [DH]","route_style_name":"DH"},{"style_car_id":772,"style_name":"ROADSTER RS (NB8C) [DH]","route_style_name":"DH"},{"style_car_id":131845,"style_name":"RX-7 Type RS (FD3S) [HC]","route_style_name":"HC"},{"style_car_id":66309,"style_name":"RX-7 Type RS (FD3S) [AR]","route_style_name":"AR"},{"style_car_id":132096,"style_name":"IMPREZA WRX type R STi Version Ⅴ (GC8) [HC]","route_style_name":"HC"},{"style_car_id":66560,"style_name":"IMPREZA WRX type R STi Version Ⅴ (GC8) [AR]","route_style_name":"AR"},{"style_car_id":1027,"style_name":"SUBARU BRZ S (ZC6) [DH]","route_style_name":"DH"},{"style_car_id":66563,"style_name":"SUBARU BRZ S (ZC6) [AR]","route_style_name":"AR"},{"style_car_id":132100,"style_name":"STI S207 NBR CHALLENGE PACKAGE (VAB) [HC]","route_style_name":"HC"},{"style_car_id":1280,"style_name":"LANCER GSR Evolution Ⅲ (CE9A) [DH]","route_style_name":"DH"},{"style_car_id":66816,"style_name":"LANCER GSR Evolution Ⅲ (CE9A) [AR]","route_style_name":"AR"},{"style_car_id":132353,"style_name":"LANCER RS EVOLUTION Ⅳ (CN9A) [HC]","route_style_name":"HC"},{"style_car_id":66817,"style_name":"LANCER RS EVOLUTION Ⅳ (CN9A) [AR]","route_style_name":"AR"},{"style_car_id":132355,"style_name":"LANCER EVOLUTION Ⅶ GSR (CT9A) [HC]","route_style_name":"HC"},{"style_car_id":66821,"style_name":"LANCER RS EVOLUTION Ⅴ (CP9A) [AR]","route_style_name":"AR"},{"style_car_id":66822,"style_name":"LANCER GSR EVOLUTION Ⅵ T.M.EDITION (CP9A) [AR]","route_style_name":"AR"},{"style_car_id":1536,"style_name":"Cappuccino (EA11R) [DH]","route_style_name":"DH"},{"style_car_id":67073,"style_name":"SWIFT Sport (ZC33S) [AR]","route_style_name":"AR"},{"style_car_id":132864,"style_name":"SILEIGHTY [HC]","route_style_name":"HC"},{"style_car_id":67328,"style_name":"SILEIGHTY [AR]","route_style_name":"AR"},{"style_car_id":133376,"style_name":"911Turbo3.6 (964) [HC]","route_style_name":"HC"},{"style_car_id":67840,"style_name":"911Turbo3.6 (964) [AR]","route_style_name":"AR"},{"style_car_id":2305,"style_name":"718Cayman GTS (982) [DH]","route_style_name":"DH"}],"course":[{"course_id":0,"course_name":"Lake Akina/CCW"},{"course_id":2,"course_name":"Lake Akina/CW"},{"course_id":52,"course_name":"Hakone/DH"},{"course_id":54,"course_name":"Hakone/HC"},{"course_id":36,"course_name":"Usui/CCW"},{"course_id":38,"course_name":"Usui/CW"},{"course_id":4,"course_name":"Myogi/DH"},{"course_id":6,"course_name":"Myogi/HC"},{"course_id":8,"course_name":"Akagi/DH"},{"course_id":10,"course_name":"Akagi/HC"},{"course_id":12,"course_name":"Akina/DH"},{"course_id":14,"course_name":"Akina/HC"},{"course_id":16,"course_name":"Irohazaka/DH"},{"course_id":18,"course_name":"Irohazaka/HC"},{"course_id":20,"course_name":"Tsukuba/OB"},{"course_id":22,"course_name":"Tsukuba/IB"},{"course_id":56,"course_name":"Momiji Line/DH"},{"course_id":58,"course_name":"Momiji Line/HC"},{"course_id":24,"course_name":"Happogahara/OB"},{"course_id":26,"course_name":"Happogahara/IB"},{"course_id":40,"course_name":"Sadamine/DH"},{"course_id":42,"course_name":"Sadamine/HC"},{"course_id":44,"course_name":"Tsuchisaka/OB"},{"course_id":46,"course_name":"Tsuchisaka/IB"},{"course_id":48,"course_name":"Akina Snow/DH"},{"course_id":50,"course_name":"Akina Snow/HC"},{"course_id":68,"course_name":"Odawara/F"},{"course_id":70,"course_name":"Odawara/R"}]} \ No newline at end of file diff --git a/titles/idac/frontend/idac_index.jinja b/titles/idac/frontend/idac_index.jinja index eeecc65..0d71533 100644 --- a/titles/idac/frontend/idac_index.jinja +++ b/titles/idac/frontend/idac_index.jinja @@ -2,130 +2,20 @@ {% block content %}

頭文字D THE ARCADE

-{% if sesh is defined and sesh["userId"] > 0 %} -
-
-
-
-

{{ sesh["username"] }}'s Profile

-
-
- - -
-
-
-
- - {% if profile is defined and profile is not none %} -
-
-
-
-
Information
-
-
Username
-

{{ profile.username }}

-
Cash
-

{{ profile.cash }} D

-
Grade
-

- {% 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 %} -

-
-
-
-
-
+ -
-
Statistics
-
-
-
-
Total Plays
-

{{ profile.total_play }}

-
-
-
Last Played
-

{{ profile.last_play_date }}

-
-
-
Mileage
-

{{ profile.mileage / 1000}} km

-
-
- {% if tickets is defined and tickets|length > 0 %} -
Tokens/Tickets
-
-
-
-
Avatar Tokens
-

{{ tickets.avatar_points }}/30

-
-
-
Car Dressup Tokens
-

{{ tickets.car_dressup_points }}/30

-
-
-
FullTune Tickets
-

{{ tickets.full_tune_tickets }}/99

-
-
-
FullTune Fragments
-

{{ tickets.full_tune_fragments }}/10

-
-
- {% endif %} -
-
-
-
- {% else %} - - {% endif %} - -
-
-{% else %} - -{% endif %} +{% block tab %} - +{% endblock tab %} + +{% endblock tab %} \ No newline at end of file diff --git a/titles/idac/frontend/profile/js/scripts.js b/titles/idac/frontend/profile/js/scripts.js new file mode 100644 index 0000000..7805456 --- /dev/null +++ b/titles/idac/frontend/profile/js/scripts.js @@ -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'); + }); +}); diff --git a/titles/idac/frontend/ranking/index.jinja b/titles/idac/frontend/ranking/index.jinja new file mode 100644 index 0000000..a5bbc7a --- /dev/null +++ b/titles/idac/frontend/ranking/index.jinja @@ -0,0 +1,30 @@ +{% extends "titles/idac/frontend/idac_index.jinja" %} +{% block tab %} + + + + + +{% endblock tab %} diff --git a/titles/idac/frontend/ranking/js/scripts.js b/titles/idac/frontend/ranking/js/scripts.js new file mode 100644 index 0000000..297d79e --- /dev/null +++ b/titles/idac/frontend/ranking/js/scripts.js @@ -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("
" + data.error + "
"); + console.error("Error: " + data.error); + return; + } + + // get the total number of pages + var total_pages = data.total_pages; + + // Generate the HTML table + var tableHtml = '
'; + $.each(data.ranking, function (i, ranking) { + tableHtml += ''; + tableHtml += ''; + tableHtml += ''; + tableHtml += ''; + tableHtml += ''; + // Ignore the Store and Date columns on small screens + tableHtml += ''; + tableHtml += ''; + tableHtml += ''; + }); + tableHtml += '
#NameCarTimeStoreDate
' + ranking.rank + '' + ranking.name + '' + getCarName(ranking.style_car_id) + '' + formatGoalTime(ranking.record) + '' + ranking.store + '' + ranking.update_date + '
'; + + // Inject the table into the container + $("#table-ranking").html(tableHtml); + + // Generate the Pagination HTML + var paginationHtml = ''; + + // Inject the pagination into the container + $("#pagination-ranking").html(paginationHtml); + }, + error: function (jqXHR, textStatus, errorThrown) { + // Inject the table into the container + $("#table-ranking").html("
" + textStatus + "
"); + 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); + } + }); +}); diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py index 80ee7ba..d8574bc 100644 --- a/titles/idac/schema/item.py +++ b/titles/idac/schema/item.py @@ -499,23 +499,20 @@ class IDACItemData(BaseData): def get_time_trial_best_cars_by_course( self, version: int, course_id: int, aime_id: Optional[int] = None ) -> Optional[List[Row]]: - subquery = ( - select( - trial.c.version, - func.min(trial.c.goal_time).label("min_goal_time"), - trial.c.style_car_id, - ) - .where( - and_( - trial.c.version == version, - trial.c.course_id == course_id, - ) + subquery = select( + trial.c.version, + func.min(trial.c.goal_time).label("min_goal_time"), + trial.c.style_car_id, + ).where( + and_( + trial.c.version == version, + trial.c.course_id == course_id, ) ) if aime_id is not None: subquery = subquery.where(trial.c.user == aime_id) - + subquery = subquery.group_by(trial.c.style_car_id).subquery() sql = select(trial).where( @@ -532,12 +529,45 @@ class IDACItemData(BaseData): return None 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( self, version: int, course_id: int, style_car_id: Optional[int] = None, limit: Optional[int] = 10, + offset: Optional[int] = 0, ) -> Optional[List[Row]]: # get the top 10 ranking by goal_time for a given course which is grouped by user subquery = select( @@ -546,7 +576,7 @@ class IDACItemData(BaseData): func.min(trial.c.goal_time).label("min_goal_time"), ).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: subquery = subquery.where(trial.c.style_car_id == style_car_id) @@ -568,6 +598,10 @@ class IDACItemData(BaseData): if limit is not None: sql = sql.limit(limit) + # offset the result if needed + if offset is not None: + sql = sql.offset(offset) + result = self.execute(sql) if result is None: return None @@ -750,7 +784,9 @@ class IDACItemData(BaseData): return None 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( and_( timetrial_event.c.user == aime_id, @@ -946,9 +982,7 @@ class IDACItemData(BaseData): return None return result.lastrowid - def put_stamp( - self, aime_id: int, stamp_data: Dict - ) -> Optional[int]: + def put_stamp(self, aime_id: int, stamp_data: Dict) -> Optional[int]: stamp_data["user"] = aime_id sql = insert(stamp).values(**stamp_data) @@ -956,9 +990,7 @@ class IDACItemData(BaseData): result = self.execute(conflict) if result is None: - self.logger.warn( - f"putstamp: Failed to update! aime_id: {aime_id}" - ) + self.logger.warn(f"putstamp: Failed to update! aime_id: {aime_id}") return None return result.lastrowid