Merge pull request 'develop' (#10) from Hay1tsme/artemis:develop into develop

Reviewed-on: ThatzOkay/artemis#10
This commit is contained in:
ThatzOkay 2024-07-09 22:48:20 +00:00
commit 00d3b6e69a
19 changed files with 526 additions and 58 deletions

View File

@ -1,6 +1,10 @@
# Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20240630
### DIVA
+ Added configurable festa options'
## 20240629
### CHUNITHM
+ Add team points

View File

@ -1,16 +1,18 @@
from enum import Enum
class MainboardPlatformCodes:
RINGEDGE = "AALE"
RINGWIDE = "AAML"
NU = "AAVE"
NUSX = "AAWE"
ALLS_UX = "ACAE"
ALLS_HX = "ACAX"
class MainboardPlatformCodes(Enum):
RINGEDGE = "AAL"
RINGEDGE2 = "AAS"
RINGWIDE = "AAM"
NU = "AAV"
NUSX = "AAW"
ALLS = "ACA"
#ALLS_UX = "ACAE"
#ALLS_HX = "ACAX"
class MainboardRevisions:
class MainboardRevisions(Enum):
RINGEDGE = 1
RINGEDGE2 = 2
@ -29,11 +31,10 @@ class MainboardRevisions:
ALLS_HX2 = 12
class KeychipPlatformsCodes:
RING = "A72E"
NU = ("A60E", "A60E", "A60E")
NUSX = ("A61X", "A69X")
ALLS = "A63E"
class KeychipPlatformsCodes(Enum):
RING = "72"
NU = ("60", "61", "69")
ALLS = "63"
class AllnetCountryCode(Enum):

View File

@ -0,0 +1,28 @@
"""card_add_memo
Revision ID: 5ea73f89d982
Revises: 745448d83696
Create Date: 2024-07-06 22:46:56.992152
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '5ea73f89d982'
down_revision = '745448d83696'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('aime_card', sa.Column('memo', sa.VARCHAR(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('aime_card', 'memo')
# ### end Alembic commands ###

View File

@ -206,17 +206,6 @@ class ArcadeData(BaseData):
return None
return result.lastrowid
def format_serial( # TODO: Actual serial stuff
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 8888
) -> str:
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
return False
return True
async def get_arcade_by_name(self, name: str) -> Optional[List[Row]]:
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
result = await self.execute(sql)
@ -230,3 +219,53 @@ class ArcadeData(BaseData):
if result is None:
return None
return result.fetchall()
async def get_num_generated_keychips(self) -> Optional[int]:
result = await self.execute(select(func.count("serial LIKE 'A69A%'")).select_from(machine))
if result:
return result.fetchone()['count_1']
self.logger.error("Failed to count machine serials that start with A69A!")
def format_serial(
self, platform_code: str, platform_rev: int, serial_letter: str, serial_num: int, append: int, dash: bool = False
) -> str:
return f"{platform_code}{'-' if dash else ''}{platform_rev:02d}{serial_letter}{serial_num:04d}{append:04d}"
def validate_keychip_format(self, serial: str) -> bool:
# For the 2nd letter, E and X are the only "real" values that have been observed
if re.fullmatch(r"^A[0-9]{2}[A-Z][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
return False
return True
# Thanks bottersnike!
def get_keychip_suffix(self, year: int, month: int) -> str:
assert year > 1957
assert 1 <= month <= 12
year -= 1957
# Jan/Feb/Mar are from the previous tax year
if month < 4:
year -= 1
assert year >= 1 and year <= 99
month = ((month - 1) + 9) % 12 # Offset so April=0
return f"{year:02}{month // 6:01}{month % 6 + 1:01}"
def parse_keychip_suffix(self, suffix: str) -> tuple[int, int]:
year = int(suffix[0:2])
half = int(suffix[2])
assert half in (0, 1)
period = int(suffix[3])
assert period in (1, 2, 3, 4, 5, 6)
month = half * 6 + (period - 1)
month = ((month + 3) % 12) + 1 # Offset so Jan=1
# Jan/Feb/Mar are from the previous tax year
if month < 4:
year += 1
year += 1957
return (year, month)

View File

@ -1,6 +1,6 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT, VARCHAR
from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.engine import Row
@ -19,6 +19,7 @@ aime_card = Table(
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("is_locked", Boolean, server_default="0"),
Column("is_banned", Boolean, server_default="0"),
Column("memo", VARCHAR(16)),
UniqueConstraint("user", "access_code", name="aime_card_uk"),
mysql_charset="utf8mb4",
)
@ -148,6 +149,11 @@ class CardData(BaseData):
if not result:
self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}")
async def set_memo_by_access_code(self, access_code: str, memo: str) -> None:
result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(memo=memo))
if not result:
self.logger.error(f"Failed to add memo to card {access_code}")
def to_access_code(self, luid: str) -> str:
"""
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string

View File

@ -476,10 +476,10 @@ class FE_User(FE_Base):
card_data.append({
'access_code': ac,
'status': status,
'chip_id': "", #None if c['chip_id'] is None else f"{c['chip_id']:X}",
'idm': "",
'chip_id': c['chip_id'],
'idm': c['idm'],
'type': c_type,
"memo": ""
"memo": c['memo']
})
if "e" in request.query_params:
@ -516,7 +516,42 @@ class FE_User(FE_Base):
return resp
async def edit_card(self, request: Request) -> RedirectResponse:
return RedirectResponse("/user/", 303)
frm = await request.form()
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD):
return RedirectResponse("/gate/", 303)
frm = await request.form()
ac = frm.get("add_access_code", None)
if not ac:
return RedirectResponse("/user/?e=999", 303)
card = await self.data.card.get_card_by_access_code(ac)
if not card:
return RedirectResponse("/user/?e=2", 303)
if card['user'] != usr_sesh.user_id and not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD):
return RedirectResponse("/user/?e=11", 303)
if frm.get("add_memo", None):
memo = frm.get("add_memo")
if len(memo) > 16 or len(memo) == 0:
return RedirectResponse("/user/?e=4", 303)
await self.data.card.set_memo_by_access_code(ac, memo)
if frm.get("add_felica_idm", None):
idm = frm.get('add_felica_idm')
if not all(c in string.hexdigits for c in idm):
return RedirectResponse("/user/?e=4", 303)
await self.data.card.set_idm_by_access_code(ac, idm)
if frm.get("add_mifare_chip_id", None):
chip_id: str = frm.get('add_mifare_chip_id')
if not all(c in string.hexdigits for c in idm):
return RedirectResponse("/user/?e=4", 303)
await self.data.card.set_chip_id_by_access_code(ac, int(chip_id, 16))
return RedirectResponse("/user/?s=4", 303)
async def add_card(self, request: Request) -> RedirectResponse:
return RedirectResponse("/user/", 303)
@ -791,14 +826,19 @@ class FE_System(FE_Base):
return RedirectResponse("/sys/?e=4", 303)
if not serial:
serial = self.data.arcade.format_serial("A69E", 1, random.randint(1, 9999))
append = self.data.arcade.get_keychip_suffix(datetime.now().year, datetime.now().month)
generated = await self.data.arcade.get_num_generated_keychips()
if not generated:
generated = 0
serial = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append))
serial_dash = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append), True)
cab_id = await self.data.arcade.create_machine(int(shopid), serial, None, game_code if game_code else None)
return Response(template.render(
title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh),
cabadd={"id": cab_id, "serial": serial},
cabadd={"id": cab_id, "serial": serial_dash},
), media_type="text/html; charset=utf-8")
async def render_logs(self, request: Request):

View File

@ -46,7 +46,11 @@ var per_page = 0;
var page = 0;
function update_tbl() {
if (TBL_DATA.length == 0) { return; }
if (TBL_DATA.length == 0) {
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = true;
return;
}
var tbl = document.getElementById("tbl_events");
for (var i = 0; i < per_page; i++) {
@ -183,7 +187,7 @@ function chg_page(num) {
document.getElementById("btn_prev").disabled = true;
return;
} else if (page == 0) {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_next").disabled = TBL_DATA.length == 0;
document.getElementById("btn_prev").disabled = true;
} else {
document.getElementById("btn_next").disabled = false;

View File

@ -49,11 +49,14 @@ function prep_edit_form(access_code, chip_id, idm, card_type, u_memo) {
fidm.value = idm;
memo.value = u_memo;
if (card_type == "AmusementIC") {
if (access_code.startsWith("3") || access_code.startsWith("010")) {
cid.disabled = false;
fidm.disabled = true;
} else if (access_code.startsWith("5")) {
cid.disabled = true;
fidm.disabled = false;
} else {
cid.disabled = false;
cid.disabled = true;
fidm.disabled = true;
}
}
@ -91,9 +94,14 @@ Card added successfully
</form>
<br>
</div>
{% if success is defined and success == 4 %}
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
Update successful
</div>
{% endif %}
<ul style="font-size: 20px;">
{% for c in cards %}
<li>{{ c.access_code }} ({{ c.type}}): {{ c.status }}&nbsp;<button onclick="prep_edit_form('{{ c.access_code }}', '{{ c.chip_id}}', '{{ c.idm }}', '{{ c.type }}', '{{ c.memo }}')" data-bs-toggle="modal" data-bs-target="#card_edit" class="btn btn-secondary" id="btn_edit_card_{{ c.access_code }}">Edit</button>&nbsp;{% if c.status == 'Active'%}<button class="btn-warning btn">Lock</button>{% elif c.status == 'Locked' %}<button class="btn-warning btn">Unlock</button>{% endif %}&nbsp;<button class="btn-danger btn">Delete</button></li>
<li>{{ c.access_code }} ({{ c.type if c.memo is none or not c.memo else c.memo }}): {{ c.status }}&nbsp;<button onclick="prep_edit_form('{{ c.access_code }}', '{{ c.chip_id}}', '{{ c.idm }}', '{{ c.type }}', '{{ c.memo }}')" data-bs-toggle="modal" data-bs-target="#card_edit" class="btn btn-secondary" id="btn_edit_card_{{ c.access_code }}">Edit</button>&nbsp;{% if c.status == 'Active'%}<button class="btn-warning btn">Lock</button>{% elif c.status == 'Locked' %}<button class="btn-warning btn">Unlock</button>{% endif %}&nbsp;<button class="btn-danger btn" {{ "disabled" if cards|length == 1 else ""}}>Delete</button></li>
{% endfor %}
</ul>

View File

@ -42,6 +42,8 @@ if __name__ == "__main__":
data = Data(cfg)
loop = asyncio.get_event_loop()
if args.action == "create":
data.create_database()
@ -55,19 +57,15 @@ if __name__ == "__main__":
data.schema_downgrade(args.version)
elif args.action == "create-owner":
loop = asyncio.get_event_loop()
loop.run_until_complete(data.create_owner(args.email, args.access_code))
elif args.action == "migrate":
loop = asyncio.get_event_loop()
loop.run_until_complete(data.migrate())
elif args.action == "create-revision":
loop = asyncio.get_event_loop()
loop.run_until_complete(data.create_revision(args.message))
elif args.action == "create-autorevision":
loop = asyncio.get_event_loop()
loop.run_until_complete(data.create_revision_auto(args.message))
else:

View File

@ -269,10 +269,14 @@ the Shop, Modules and Customizations.
Config file is located in `config/diva.yaml`.
| Option | Info |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
| Option | Info |
| -------------------- | ------------------------------------------------------------------------------------------------ |
| `festa_enable` | Enable or disable the ingame festa |
| `festa_add_VP` | Set the extra VP you get when clearing a song, if festa is not enabled no extra VP will be given |
| `festa_multiply_VP` | Multiplier for festa add VP |
| `festa_end_time` | Set the date time for when festa will end and not show up in game anymore |
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
### Custom PV Lists (databanks)

View File

@ -1,6 +1,10 @@
server:
enable: True
loglevel: "info"
festa_enable: True
festa_add_VP: "20,5"
festa_multiply_VP: "1,2"
festa_end_time: "2029-01-01 00:00:00.0"
mods:
unlock_all_modules: True

View File

@ -234,11 +234,11 @@ class ChuniFrontend(FE_Base):
if usr_sesh.user_id > 0:
form_data = await request.form()
chunithm_version = form_data.get("version")
self.logger.info(f"version change to: {chunithm_version}")
self.logger.debug(f"version change to: {chunithm_version}")
if(chunithm_version.isdigit()):
usr_sesh.chunithm_version=int(chunithm_version)
encoded_sesh = self.encode_session(usr_sesh)
self.logger.info(f"Created session with JWT {encoded_sesh}")
self.logger.debug(f"Created session with JWT {encoded_sesh}")
resp = RedirectResponse("/game/chuni/", 303)
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp

View File

@ -264,6 +264,11 @@ class DivaBase:
return response
async def handle_festa_info_request(self, data: Dict) -> Dict:
if self.game_config.server.festa_enable:
festa_end_time = self.game_config.server.festa_end_time
else:
festa_end_time = (datetime.datetime.now() - datetime.timedelta(days=365)).strftime("%Y-%m-%d %H:%M:%S") + ".0"
encoded = "&"
params = {
"fi_id": "1,2",
@ -273,10 +278,10 @@ class DivaBase:
"fi_difficulty": "-1,-1",
"fi_pv_id_lst": "ALL,ALL",
"fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"fi_add_vp": "20,5",
"fi_mul_vp": "1,2",
"fi_add_vp": f"{self.game_config.server.festa_add_VP}",
"fi_mul_vp": f"{self.game_config.server.festa_multiply_VP}",
"fi_st": "2019-01-01 00:00:00.0,2019-01-01 00:00:00.0",
"fi_et": "2029-01-01 00:00:00.0,2029-01-01 00:00:00.0",
"fi_et": f"{festa_end_time},{festa_end_time}",
"fi_lut": "{self.time_lut}",
}

View File

@ -19,6 +19,29 @@ class DivaServerConfig:
)
)
@property
def festa_enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_enable", default=True
)
@property
def festa_add_VP(self) -> str:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_add_VP", default="20,5"
)
@property
def festa_multiply_VP(self) -> str:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_multiply_VP", default="1,2"
)
@property
def festa_end_time(self) -> str:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_end_time", default="2029-01-01 00:00:00.0"
)
class DivaModsConfig:
def __init__(self, parent_config: "DivaConfig") -> None:

View File

@ -5,8 +5,9 @@ from starlette.responses import Response, RedirectResponse
from os import path
import yaml
import jinja2
from datetime import datetime
from core.frontend import FE_Base, UserSession
from core.frontend import FE_Base, UserSession, PermissionOffset
from core.config import CoreConfig
from .database import Mai2Data
from .config import Mai2Config
@ -32,6 +33,12 @@ class Mai2Frontend(FE_Base):
Route("/", self.render_GET_playlog, methods=['GET']),
Route("/{index}", self.render_GET_playlog, methods=['GET']),
]),
Mount("/events", routes=[
Route("/", self.render_events, methods=['GET']),
Route("/{event_id:int}", self.render_event_edit, methods=['GET']),
Route("/update", self.update_event, methods=['POST']),
Route("/version.change", self.version_change, methods=['POST']),
]),
Route("/update.name", self.update_name, methods=['POST']),
Route("/version.change", self.version_change, methods=['POST']),
]
@ -43,13 +50,15 @@ class Mai2Frontend(FE_Base):
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
incoming_ver = usr_sesh.maimai_version
if usr_sesh.user_id > 0:
versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id)
profile = []
if versions:
# maimai_version is -1 means it is not initialized yet, select a default version from existing.
if usr_sesh.maimai_version < 0:
if incoming_ver < 0:
usr_sesh.maimai_version = versions[0]['version']
profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version)
versions = [x['version'] for x in versions]
@ -65,7 +74,7 @@ class Mai2Frontend(FE_Base):
cur_version=usr_sesh.maimai_version
), media_type="text/html; charset=utf-8")
if usr_sesh.maimai_version >= 0:
if incoming_ver < 0:
encoded_sesh = self.encode_session(usr_sesh)
resp.delete_cookie("ARTEMIS_SESH")
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
@ -80,12 +89,10 @@ class Mai2Frontend(FE_Base):
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
print("wtf")
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
if usr_sesh.maimai_version < 0:
print(usr_sesh.maimai_version)
return RedirectResponse("/game/mai2/", 303)
path_index = request.path_params.get('index')
if not path_index or int(path_index) < 1:
@ -175,6 +182,11 @@ class Mai2Frontend(FE_Base):
if not usr_sesh:
usr_sesh = UserSession()
if "/events/" in request.url.path:
resp = RedirectResponse("/game/mai2/events/", 303)
else:
resp = RedirectResponse("/game/mai2/", 303)
if usr_sesh.user_id > 0:
form_data = await request.form()
maimai_version = form_data.get("version")
@ -182,9 +194,108 @@ class Mai2Frontend(FE_Base):
if(maimai_version.isdigit()):
usr_sesh.maimai_version=int(maimai_version)
encoded_sesh = self.encode_session(usr_sesh)
self.logger.info(f"Created session with JWT {encoded_sesh}")
resp = RedirectResponse("/game/mai2/", 303)
self.logger.debug(f"Created session with JWT {encoded_sesh}")
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
return RedirectResponse("/gate/", 303)
return RedirectResponse("/gate/", 303)
async def render_events(self, request: Request) -> Response:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate/", 303)
if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN):
return RedirectResponse("/game/mai2/", 303)
template = self.environment.get_template(
"titles/mai2/templates/events/mai2_events.jinja"
)
incoming_ver = usr_sesh.maimai_version
evts = []
if incoming_ver < 0:
usr_sesh.maimai_version = Mai2Constants.VER_MAIMAI_DX
event_list = await self.data.static.get_game_events(usr_sesh.maimai_version)
self.logger.info(f"Get events for v{usr_sesh.maimai_version}")
for event in event_list:
evts.append({
"id": event['id'],
"version": event['version'],
"eventId": event['eventId'],
"eventType": event['type'],
"name": event['name'],
"startDate": event['startDate'].strftime("%x %X"),
"enabled": "true" if event['enabled'] else "false",
})
resp = Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name} Events",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
version_list=Mai2Constants.VERSION_STRING,
events=evts
), media_type="text/html; charset=utf-8")
if incoming_ver < 0:
encoded_sesh = self.encode_session(usr_sesh)
resp.delete_cookie("ARTEMIS_SESH")
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
async def render_event_edit(self, request: Request) -> Response:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate/", 303)
if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN):
return RedirectResponse("/game/mai2/", 303)
template = self.environment.get_template(
"titles/mai2/templates/events/mai2_event_edit.jinja"
)
evt_id = request.path_params.get("event_id")
event_id = await self.data.static.get_event_by_id(evt_id)
if not event_id:
return RedirectResponse("/game/mai2/events/", 303)
return Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name} Edit Event {evt_id}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
user_id=usr_sesh.user_id,
version_list=Mai2Constants.VERSION_STRING,
cur_version=usr_sesh.maimai_version,
event=event_id._asdict()
), media_type="text/html; charset=utf-8")
async def update_event(self, request: Request) -> RedirectResponse:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate/", 303)
if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN):
return RedirectResponse("/game/mai2/", 303)
form_data = await request.form()
print(form_data)
event_id: int = form_data.get("evtId", None)
new_enabled: bool = bool(form_data.get("evtEnabled", False))
try:
new_start_date: datetime = datetime.strptime(form_data.get("evtStart", None), "%Y-%m-%dT%H:%M:%S")
except:
new_start_date = None
print(f"{event_id} {new_enabled} {new_start_date}")
if event_id is None or new_start_date is None:
return RedirectResponse("/game/mai2/events/?e=4", 303)
await self.data.static.update_event_by_id(int(event_id), new_enabled, new_start_date)
return RedirectResponse("/game/mai2/events/?s=1", 303)

View File

@ -7,6 +7,7 @@ from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from datetime import datetime
event = Table(
"mai2_static_event",
@ -248,3 +249,18 @@ class Mai2StaticData(BaseData):
if result is None:
return None
return result.fetchall()
async def get_event_by_id(self, table_id: int) -> Optional[Row]:
result = await self.execute(event.select(event.c.id == table_id))
if result:
return result.fetchone()
async def get_events_by_event_id(self, event_id: int) -> Optional[List[Row]]:
result = await self.execute(event.select(event.c.eventId == event_id))
if result:
return result.fetchall()
async def update_event_by_id(self, table_id: int, is_enable: bool, start_date: datetime) -> None:
result = await self.execute(event.update(event.c.id == table_id).values(enabled=is_enable, startDate = start_date))
if not result:
self.logger.error(f"Failed to update event {table_id} - {is_enable} {start_date}")

View File

@ -0,0 +1,16 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<!-- TODO: This can probably just be a modal on the main event page -->
<form id="frmEvent" method="post" action="/game/mai2/events/update">
<h3>Event {{ event.eventId }} for {{ version_list[event.version] }}: {{ event.name }}</h3>
<input type="hidden" readonly value="{{event.id}}" id="evtId" name="evtId">
<label for="evtEnabled" class="form-label">Enabled</label><br>
<input class="form-check-input" type="checkbox" {{ 'checked' if event.enabled else ''}} id="evtEnabled" name="evtEnabled"><br><br>
<label for="evtStart" class="form-label">Start Date</label><br>
<input class="form-input" type="datetime-local" id="evtStart" name="evtStart" value="{{ event.startDate }}"><br><br>
<button type="submit" class="btn btn-primary">Update</button>&nbsp;
<button type="cancel" class="btn btn-danger">Delete</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,156 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<h1>Events</h1>
<form id="verForm" method="POST" action="/game/mai2/events/version.change">
<select id="version" name="version" onchange="updateVer()" form="verForm">
{% for ver in range(version_list|length) %}
<option value="{{ver}}">{{ version_list[ver] }}</option>
{% endfor %}
</select>
</form>
<table class="table table-dark table-striped-columns" id="tbl_events">
<caption>Viewing all events</caption>
<thead>
<tr>
<th>ID</th>
<th>Version</th>
<th>Event ID</th>
<th>Event Type</th>
<th>Name</th>
<th>Start Date</th>
<th>Enabled</th>
<th>Actions</th>
</tr>
</thead>
{% if events is not defined or events|length == 0 %}
<tr>
<td colspan="11" style="text-align:center"><i>No Events</i></td>
</tr>
{% endif %}
</table>
<div id="div_tbl_ctrl">
<select id="sel_per_page" onchange="update_tbl()">
<option value="10" selected>10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
&nbsp;
<button class="btn btn-primary" id="btn_prev" disabled onclick="chg_page(-1)"><<</button>
<button class="btn btn-primary" id="btn_next" onclick="chg_page(1)">>></button>
</div>
<script type="text/javascript">
{% if events is defined %}
const TBL_DATA = {{events}};
{% else %}
const TBL_DATA = [];
{% endif %}
document.getElementById("version").value = {{ sesh.maimai_version }};
var per_page = 0;
var page = 0;
function updateVer() {
var sel = document.getElementById("version");
var frm = document.getElementById("verForm");
if (sel.value == {{ sesh.maimai_version }}) {
return;
}
frm.submit();
}
function update_tbl() {
if (TBL_DATA.length == 0) { return; }
var tbl = document.getElementById("tbl_events");
for (var i = 0; i < per_page; i++) {
try{
tbl.deleteRow(1);
} catch {
break;
}
}
per_page = document.getElementById("sel_per_page").value;
if (per_page >= TBL_DATA.length) {
page = 0;
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = true;
}
for (var i = 0; i < per_page; i++) {
let off = (page * per_page) + i;
if (off >= TBL_DATA.length) {
if (page != 0) {
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = false;
}
break;
}
var data = TBL_DATA[off];
var row = tbl.insertRow(i + 1);
var cell_id = row.insertCell(0);
cell_id.innerHTML = data.id;
var cell_ver = row.insertCell(1);
cell_ver.innerHTML = data.version;
var cell_evtid = row.insertCell(2);
cell_evtid.innerHTML = data.eventId;
var cell_evttype = row.insertCell(3);
cell_evttype.innerHTML = data.eventType;
var cell_name = row.insertCell(4);
cell_name.innerHTML = data.name;
var cell_date = row.insertCell(5);
cell_date.innerHTML = data.startDate;
var call_enabled = row.insertCell(6);
if (data.enabled === "true")
call_enabled.innerHTML = "✔"
else
call_enabled.innerHTML = "✖"
var cell_action = row.insertCell(7);
cell_action.innerHTML = "<a href=/game/mai2/events/" + data.id +"><button class='btn btn-primary'>🖉</button></a>"
}
}
function chg_page(num) {
var max_page = TBL_DATA.length / per_page;
console.log(max_page);
page = page + num;
if (page > max_page && max_page >= 1) {
page = max_page;
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = false;
return;
} else if (page < 0) {
page = 0;
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = true;
return;
} else if (page == 0) {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = true;
} else {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = false;
}
update_tbl();
}
update_tbl();
</script>
{% endblock content %}

View File

@ -3,6 +3,9 @@
<ul class="mai2-navi">
<li><a class="nav-link" href="/game/mai2/">PROFILE</a></li>
<li><a class="nav-link" href="/game/mai2/playlog/">RECORD</a></li>
{% if sesh is defined and sesh is not none and "{:08b}".format(sesh.permissions)[4] == "1" %}
<li><a class="nav-link" href="/game/mai2/events/">EVENTS</a></li>
{% endif %}
</ul>
</div>
<script>
@ -12,6 +15,8 @@
$('.nav-link[href="/game/mai2/"]').addClass('active');
} else if (currentPath.startsWith('/game/mai2/playlog/')) {
$('.nav-link[href="/game/mai2/playlog/"]').addClass('active');
}
} {% if sesh is defined and sesh is not none and "{:08b}".format(sesh.permissions)[4] == "1" %}else if (currentPath.startsWith('/game/mai2/events/')) {
$('.nav-link[href="/game/mai2/events/"]').addClass('active');
} {% endif %}
});
</script>