forked from Hay1tsme/artemis
idac: fixed frontend
This commit is contained in:
parent
a0fba8c3a4
commit
88b3cfc750
@ -1,11 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
from typing import List
|
|
||||||
from starlette.routing import Route
|
|
||||||
from starlette.responses import Response, RedirectResponse
|
|
||||||
import yaml
|
import yaml
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
|
from typing import List, Any, Type
|
||||||
|
from starlette.routing import Route, Mount
|
||||||
|
from starlette.responses import Response, RedirectResponse, JSONResponse
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from core.frontend import FE_Base, UserSession
|
from core.frontend import FE_Base, UserSession
|
||||||
@ -54,7 +54,7 @@ class RequestValidator:
|
|||||||
required: bool = True,
|
required: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Check if the parameter is missing
|
# Check if the parameter is missing
|
||||||
if param_name.encode() not in request_args:
|
if param_name not in request_args:
|
||||||
if required:
|
if required:
|
||||||
self.success = False
|
self.success = False
|
||||||
self.error += f"Missing parameter: '{param_name}'. "
|
self.error += f"Missing parameter: '{param_name}'. "
|
||||||
@ -64,7 +64,7 @@ class RequestValidator:
|
|||||||
return default
|
return default
|
||||||
return None
|
return None
|
||||||
|
|
||||||
param_value = request_args[param_name.encode()][0].decode()
|
param_value = request_args[param_name]
|
||||||
|
|
||||||
# Check if the parameter type is not empty
|
# Check if the parameter type is not empty
|
||||||
if param_type:
|
if param_type:
|
||||||
@ -108,10 +108,6 @@ class RankingResponse:
|
|||||||
|
|
||||||
return ret
|
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
|
isLeaf = False
|
||||||
children: Dict[str, Any] = {}
|
children: Dict[str, Any] = {}
|
||||||
@ -127,36 +123,52 @@ class IDACFrontend(FE_Base):
|
|||||||
self.game_cfg.update(
|
self.game_cfg.update(
|
||||||
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
|
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
|
||||||
)
|
)
|
||||||
#self.nav_name = "頭文字D THE ARCADE"
|
self.nav_name = "頭文字D THE ARCADE"
|
||||||
self.nav_name = "IDAC"
|
# self.nav_name = "IDAC"
|
||||||
# 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.profile = IDACProfileFrontend(cfg, self.environment)
|
||||||
self.putChild(b"ranking", IDACRankingFrontend(cfg, self.environment))
|
self.ranking = IDACRankingFrontend(cfg, self.environment)
|
||||||
|
|
||||||
|
def get_routes(self) -> List[Route]:
|
||||||
|
return [
|
||||||
|
Route("/", self.render_GET),
|
||||||
|
Mount("/profile", routes=[
|
||||||
|
Route("/", self.profile.render_GET),
|
||||||
|
# dirty hack
|
||||||
|
Route("/export.get", self.profile.render_GET),
|
||||||
|
]),
|
||||||
|
Mount("/ranking", routes=[
|
||||||
|
Route("/", self.ranking.render_GET),
|
||||||
|
# dirty hack
|
||||||
|
Route("/const.get", self.ranking.render_GET),
|
||||||
|
Route("/ranking.get", self.ranking.render_GET),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
def render_GET(self, request: Request) -> bytes:
|
async def render_GET(self, request: Request) -> bytes:
|
||||||
uri: str = request.uri.decode()
|
uri: str = request.url.path
|
||||||
|
|
||||||
template = self.environment.get_template(
|
template = self.environment.get_template(
|
||||||
"titles/idac/frontend/idac_index.jinja"
|
"titles/idac/templates/idac_index.jinja"
|
||||||
)
|
)
|
||||||
sesh: Session = request.getSession()
|
usr_sesh = self.validate_session(request)
|
||||||
usr_sesh = IUserSession(sesh)
|
if not usr_sesh:
|
||||||
|
usr_sesh = UserSession()
|
||||||
|
|
||||||
# redirect to the ranking page
|
# redirect to the ranking page
|
||||||
if uri.startswith("/game/idac"):
|
if uri.startswith("/game/idac"):
|
||||||
return redirectTo(b"/game/idac/ranking", request)
|
return RedirectResponse("/game/idac/ranking", 303)
|
||||||
|
|
||||||
return template.render(
|
return Response(template.render(
|
||||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
game_list=self.environment.globals["game_list"],
|
game_list=self.environment.globals["game_list"],
|
||||||
sesh=vars(usr_sesh),
|
sesh=vars(usr_sesh),
|
||||||
active_page="idac",
|
active_page="idac",
|
||||||
).encode("utf-16")
|
), media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
def render_POST(self, request: Request) -> bytes:
|
async def render_POST(self, request: Request) -> bytes:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -170,48 +182,42 @@ class IDACRankingFrontend(FE_Base):
|
|||||||
# TODO: Add version list
|
# TODO: Add version list
|
||||||
self.version = IDACConstants.VER_IDAC_SEASON_2
|
self.version = IDACConstants.VER_IDAC_SEASON_2
|
||||||
|
|
||||||
def render_GET(self, request: Request) -> bytes:
|
async def render_GET(self, request: Request) -> bytes:
|
||||||
uri: str = request.uri.decode()
|
uri: str = request.url.path
|
||||||
|
|
||||||
template = self.environment.get_template(
|
template = self.environment.get_template(
|
||||||
"titles/idac/frontend/ranking/index.jinja"
|
"titles/idac/templates/ranking/index.jinja"
|
||||||
)
|
)
|
||||||
sesh: Session = request.getSession()
|
usr_sesh = self.validate_session(request)
|
||||||
usr_sesh = IUserSession(sesh)
|
if not usr_sesh:
|
||||||
user_id = usr_sesh.userId
|
usr_sesh = UserSession()
|
||||||
# user_id = usr_sesh.user_id
|
user_id = usr_sesh.user_id
|
||||||
|
|
||||||
# IDAC constants
|
# IDAC constants
|
||||||
if uri.startswith("/game/idac/ranking/const.get"):
|
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
|
# get the constants
|
||||||
with open("titles/idac/frontend/const.json", "r", encoding="utf-8") as f:
|
with open("titles/idac/templates/const.json", "r", encoding="utf-8") as f:
|
||||||
constants = json.load(f)
|
constants = json.load(f)
|
||||||
|
|
||||||
return json.dumps(constants, ensure_ascii=False).encode("utf-8")
|
return JSONResponse(constants)
|
||||||
|
|
||||||
# leaderboard ranking
|
# leaderboard ranking
|
||||||
elif uri.startswith("/game/idac/ranking/ranking.get"):
|
elif uri.startswith("/game/idac/ranking/ranking.get"):
|
||||||
# set the content type to json
|
req = RankingRequest(request.query_params._dict)
|
||||||
request.responseHeaders.addRawHeader(b"content-type", b"application/json")
|
|
||||||
|
|
||||||
req = RankingRequest(request.args)
|
|
||||||
resp = RankingResponse()
|
resp = RankingResponse()
|
||||||
|
|
||||||
if not req.success:
|
if not req.success:
|
||||||
resp.error = req.error
|
resp.error = req.error
|
||||||
return resp.to_json()
|
return JSONResponse(resp.make())
|
||||||
|
|
||||||
# get the total number of records
|
# get the total number of records
|
||||||
total_records = self.data.item.get_time_trial_ranking_by_course_total(
|
total_records = await self.data.item.get_time_trial_ranking_by_course_total(
|
||||||
self.version, req.course_id
|
self.version, req.course_id
|
||||||
)
|
)
|
||||||
# return an error if there are no records
|
# return an error if there are no records
|
||||||
if total_records is None or total_records == 0:
|
if total_records is None or total_records == 0:
|
||||||
resp.error = "No records found."
|
resp.error = "No records found."
|
||||||
return resp.to_json()
|
return JSONResponse(resp.make())
|
||||||
|
|
||||||
# get the total number of records
|
# get the total number of records
|
||||||
total = total_records["count"]
|
total = total_records["count"]
|
||||||
@ -219,7 +225,7 @@ class IDACRankingFrontend(FE_Base):
|
|||||||
limit = 50
|
limit = 50
|
||||||
offset = (req.page_number - 1) * limit
|
offset = (req.page_number - 1) * limit
|
||||||
|
|
||||||
ranking = self.data.item.get_time_trial_ranking_by_course(
|
ranking = await self.data.item.get_time_trial_ranking_by_course(
|
||||||
self.version,
|
self.version,
|
||||||
req.course_id,
|
req.course_id,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
@ -230,8 +236,8 @@ class IDACRankingFrontend(FE_Base):
|
|||||||
user_id = rank["user"]
|
user_id = rank["user"]
|
||||||
|
|
||||||
# get the username, country and store from the profile
|
# get the username, country and store from the profile
|
||||||
profile = self.data.profile.get_profile(user_id, self.version)
|
profile = await self.data.profile.get_profile(user_id, self.version)
|
||||||
arcade = self.data.arcade.get_arcade(profile["store"])
|
arcade = await self.data.arcade.get_arcade(profile["store"])
|
||||||
|
|
||||||
if arcade is None:
|
if arcade is None:
|
||||||
arcade = {}
|
arcade = {}
|
||||||
@ -258,15 +264,15 @@ class IDACRankingFrontend(FE_Base):
|
|||||||
resp.success = True
|
resp.success = True
|
||||||
resp.total_pages = (total // limit) + 1
|
resp.total_pages = (total // limit) + 1
|
||||||
resp.total_records = total
|
resp.total_records = total
|
||||||
return resp.to_json()
|
return JSONResponse(resp.make())
|
||||||
|
|
||||||
return template.render(
|
return Response(template.render(
|
||||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
game_list=self.environment.globals["game_list"],
|
game_list=self.environment.globals["game_list"],
|
||||||
sesh=vars(usr_sesh),
|
sesh=vars(usr_sesh),
|
||||||
active_page="idac",
|
active_page="idac",
|
||||||
active_tab="ranking",
|
active_tab="ranking",
|
||||||
).encode("utf-16")
|
), media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
class IDACProfileFrontend(FE_Base):
|
class IDACProfileFrontend(FE_Base):
|
||||||
@ -286,11 +292,6 @@ class IDACProfileFrontend(FE_Base):
|
|||||||
34: "full_tune_fragments",
|
34: "full_tune_fragments",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_routes(self) -> List[Route]:
|
|
||||||
return [
|
|
||||||
Route("/", self.render_GET)
|
|
||||||
]
|
|
||||||
|
|
||||||
async def generate_all_tables_json(self, user_id: int):
|
async def generate_all_tables_json(self, user_id: int):
|
||||||
json_export = {}
|
json_export = {}
|
||||||
|
|
||||||
@ -344,25 +345,29 @@ class IDACProfileFrontend(FE_Base):
|
|||||||
uri: str = request.url.path
|
uri: str = request.url.path
|
||||||
|
|
||||||
template = self.environment.get_template(
|
template = self.environment.get_template(
|
||||||
"titles/idac/templates/idac_index.jinja"
|
"titles/idac/templates/profile/index.jinja"
|
||||||
)
|
)
|
||||||
usr_sesh = self.validate_session(request)
|
usr_sesh = self.validate_session(request)
|
||||||
if not usr_sesh:
|
if not usr_sesh:
|
||||||
usr_sesh = UserSession()
|
usr_sesh = UserSession()
|
||||||
user_id = usr_sesh.user_id
|
user_id = usr_sesh.user_id
|
||||||
# user_id = usr_sesh.user_id
|
|
||||||
|
user = await self.data.user.get_user(user_id)
|
||||||
|
if user is None:
|
||||||
|
self.logger.debug(f"User {user_id} not found")
|
||||||
|
return RedirectResponse("/user/", 303)
|
||||||
|
|
||||||
# profile export
|
# profile export
|
||||||
if uri.startswith("/game/idac/profile/export.get"):
|
if uri.startswith("/game/idac/profile/export.get"):
|
||||||
if user_id == 0:
|
if user_id == 0:
|
||||||
return RedirectResponse(b"/game/idac", request)
|
return RedirectResponse("/game/idac", 303)
|
||||||
|
|
||||||
# set the file name, content type and size to download the json
|
# set the file name, content type and size to download the json
|
||||||
content = await self.generate_all_tables_json(user_id).encode("utf-8")
|
content = await self.generate_all_tables_json(user_id)
|
||||||
|
|
||||||
self.logger.info(f"User {user_id} exported their IDAC data")
|
self.logger.info(f"User {user_id} exported their IDAC data")
|
||||||
return Response(
|
return Response(
|
||||||
content,
|
content.encode("utf-8"),
|
||||||
200,
|
200,
|
||||||
{'content-disposition': 'attachment; filename=idac_profile.json'},
|
{'content-disposition': 'attachment; filename=idac_profile.json'},
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
@ -387,5 +392,7 @@ class IDACProfileFrontend(FE_Base):
|
|||||||
tickets=tickets,
|
tickets=tickets,
|
||||||
rank=rank,
|
rank=rank,
|
||||||
sesh=vars(usr_sesh),
|
sesh=vars(usr_sesh),
|
||||||
|
username=user["username"],
|
||||||
active_page="idac",
|
active_page="idac",
|
||||||
|
active_tab="profile",
|
||||||
), media_type="text/html; charset=utf-8")
|
), media_type="text/html; charset=utf-8")
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
from starlette.routing import Route
|
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
import yaml
|
import yaml
|
||||||
import logging
|
import logging
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
import asyncio
|
from starlette.routing import Route
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.title import BaseServlet, JSONResponseNoASCII
|
from core.title import BaseServlet, JSONResponseNoASCII
|
||||||
|
@ -555,7 +555,7 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def get_time_trial_ranking_by_course_total(
|
async def get_time_trial_ranking_by_course_total(
|
||||||
self,
|
self,
|
||||||
version: int,
|
version: int,
|
||||||
course_id: int,
|
course_id: int,
|
||||||
@ -582,7 +582,7 @@ class IDACItemData(BaseData):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
@ -798,23 +798,23 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def get_vs_info_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
|
async def get_vs_info_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
|
||||||
sql = select(vs_info).where(
|
sql = select(vs_info).where(
|
||||||
and_(vs_info.c.user == aime_id, vs_info.c.battle_mode == battle_mode)
|
and_(vs_info.c.user == aime_id, vs_info.c.battle_mode == battle_mode)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
# This method returns a list of course_info
|
# This method returns a list of course_info
|
||||||
def get_vs_course_infos_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
|
async def get_vs_course_infos_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
|
||||||
sql = select(vs_course_info).where(
|
sql = select(vs_course_info).where(
|
||||||
and_(vs_course_info.c.user == aime_id, vs_course_info.c.battle_mode == battle_mode)
|
and_(vs_course_info.c.user == aime_id, vs_course_info.c.battle_mode == battle_mode)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
@ -1015,7 +1015,7 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def put_vs_info(self, aime_id: int, battle_mode: int, vs_info_data: Dict) -> Optional[int]:
|
async def put_vs_info(self, aime_id: int, battle_mode: int, vs_info_data: Dict) -> Optional[int]:
|
||||||
vs_info_data["user"] = aime_id
|
vs_info_data["user"] = aime_id
|
||||||
vs_info_data["battle_mode"] = battle_mode
|
vs_info_data["battle_mode"] = battle_mode
|
||||||
|
|
||||||
@ -1028,13 +1028,13 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def put_vs_course_info(self, aime_id: int, battle_mode: int, course_info_data: Dict) -> Optional[int]:
|
async def put_vs_course_info(self, aime_id: int, battle_mode: int, course_info_data: Dict) -> Optional[int]:
|
||||||
course_info_data["user"] = aime_id
|
course_info_data["user"] = aime_id
|
||||||
course_info_data["battle_mode"] = battle_mode
|
course_info_data["battle_mode"] = battle_mode
|
||||||
|
|
||||||
sql = insert(vs_course_info).values(**course_info_data)
|
sql = insert(vs_course_info).values(**course_info_data)
|
||||||
conflict = sql.on_duplicate_key_update(**course_info_data)
|
conflict = sql.on_duplicate_key_update(**course_info_data)
|
||||||
result = self.execute(conflict)
|
result = await self.execute(conflict)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.warn(f"put_vs_course_info: Failed to update! aime_id: {aime_id}")
|
self.logger.warn(f"put_vs_course_info: Failed to update! aime_id: {aime_id}")
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
<nav class="mb-3">
|
<nav class="mb-3">
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if active_tab == 'ranking' %}active{% endif %}" aria-current="page" href="ranking">Ranking</a>
|
<a class="nav-link {% if active_tab == 'ranking' %}active{% endif %}" aria-current="page" href="/game/idac/ranking">Ranking</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if active_tab == 'profile' %}active{% endif %}" href="profile">Profile</a>
|
<a class="nav-link {% if active_tab == 'profile' %}active{% endif %}" href="/game/idac/profile">Profile</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
{% extends "titles/idac/frontend/idac_index.jinja" %}
|
{% extends "titles/idac/templates/idac_index.jinja" %}
|
||||||
{% block tab %}
|
{% block tab %}
|
||||||
|
|
||||||
{% if sesh is defined and sesh["userId"] > 0 %}
|
{% if sesh is defined and sesh["user_id"] > 0 %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center">
|
||||||
<h3>{{ sesh["username"] }}'s Profile</h3>
|
<h3>{{ username }}'s Profile</h3>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<!--<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>-->
|
<!--<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>-->
|
||||||
@ -123,7 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% include "titles/idac/frontend/profile/js/scripts.js" %}
|
{% include "titles/idac/templates/profile/js/scripts.js" %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock tab %}
|
{% endblock tab %}
|
@ -1,4 +1,4 @@
|
|||||||
{% extends "titles/idac/frontend/idac_index.jinja" %}
|
{% extends "titles/idac/templates/idac_index.jinja" %}
|
||||||
{% block tab %}
|
{% block tab %}
|
||||||
|
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% include "titles/idac/frontend/ranking/js/scripts.js" %}
|
{% include "titles/idac/templates/ranking/js/scripts.js" %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock tab %}
|
{% endblock tab %}
|
||||||
|
Loading…
Reference in New Issue
Block a user