forked from Dniel97/artemis
idac: added "simple" ranking to frontend
This commit is contained in:
parent
d1a7b898a7
commit
6ea8cca1a2
@ -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(下り) |
|
||||||
|
@ -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
|
|
||||||
|
1
titles/idac/frontend/const.json
Normal file
1
titles/idac/frontend/const.json
Normal file
File diff suppressed because one or more lines are too long
@ -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" %}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
129
titles/idac/frontend/profile/index.jinja
Normal file
129
titles/idac/frontend/profile/index.jinja
Normal 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 %}
|
10
titles/idac/frontend/profile/js/scripts.js
Normal file
10
titles/idac/frontend/profile/js/scripts.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
30
titles/idac/frontend/ranking/index.jinja
Normal file
30
titles/idac/frontend/ranking/index.jinja
Normal 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 %}
|
95
titles/idac/frontend/ranking/js/scripts.js
Normal file
95
titles/idac/frontend/ranking/js/scripts.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -499,17 +499,14 @@ 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(
|
||||||
)
|
and_(
|
||||||
.where(
|
trial.c.version == version,
|
||||||
and_(
|
trial.c.course_id == course_id,
|
||||||
trial.c.version == version,
|
|
||||||
trial.c.course_id == course_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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user