Implement admin tool to list profiles on admin user page, including ability to delete a profile.

This commit is contained in:
Jennifer Taylor 2026-07-18 21:05:23 +00:00
parent 552f64af0a
commit 35b939bc64
6 changed files with 195 additions and 16 deletions

View File

@ -21,6 +21,21 @@ class GameConstants(Enum):
REFLEC_BEAT = "reflec"
SDVX = "sdvx"
@classmethod
def LUT(cls) -> Dict[str, str]:
return {
cls.BISHI_BASHI.value: "BishiBashi",
cls.DANCE_EVOLUTION.value: "Dance Evolution",
cls.DDR.value: "DDR",
cls.IIDX.value: "IIDX",
cls.JUBEAT.value: "Jubeat",
cls.MGA.value: "Metal Gear Arcade",
cls.MUSECA.value: "MÚSECA",
cls.POPN_MUSIC.value: "Pop'n Music",
cls.REFLEC_BEAT.value: "Reflec Beat",
cls.SDVX.value: "SDVX",
}
class VersionConstants:
"""
@ -476,7 +491,7 @@ class _RegionConstants:
raise Exception(f"Unexpected value {region} for game region!")
@property
@classmethod
def LUT(cls) -> Dict[int, str]:
return {
cls.HOKKAIDO: "北海道 (Hokkaido)",

View File

@ -529,6 +529,38 @@ class UserData(BaseData):
self.deserialize(result["data"]),
)
def get_profiles(self, userid: UserID) -> List[Profile]:
"""
Given a user ID, look up all associated profiles across all games and versions. If the user
does not have any profiles then returns an empty list.
Parameters:
userid - Integer user ID, as looked up by one of the above functions.
Returns:
A list of valid profiles that were pulled from the DB.
"""
sql = """
SELECT refid.refid AS refid, extid.extid AS extid, refid.game AS game, refid.version AS version, profile.data AS data
FROM refid, extid, profile
WHERE
refid.userid = :userid AND
extid.userid = refid.userid AND
extid.game = refid.game AND
profile.refid = refid.refid
"""
cursor = self.execute(sql, {"userid": userid})
return [
Profile(
GameConstants(result["game"]),
result["version"],
result["refid"],
result["extid"],
self.deserialize(result["data"]),
)
for result in cursor.mappings()
]
def get_any_profile(self, game: GameConstants, version: int, userid: UserID) -> Optional[Profile]:
"""
Given a game/version/userid, look up the associated profile. If the profile for that version
@ -780,6 +812,12 @@ class UserData(BaseData):
sql = "DELETE FROM profile WHERE refid = :refid LIMIT 1"
self.execute(sql, {"refid": refid})
# Now delete any associated data items that went with that profile.
sql = "DELETE FROM achievement WHERE refid = :refid"
self.execute(sql, {"refid": refid})
sql = "DELETE FROM time_based_achievement WHERE refid = :refid"
self.execute(sql, {"refid": refid})
def get_achievement(
self,
game: GameConstants,

View File

@ -9,6 +9,8 @@ from bemani.common import (
GameConstants,
RegionConstants,
ValidatedDict,
Profile,
ID,
)
from bemani.data import Arcade, Machine, User, UserID, News, Event, Server, Client
from bemani.data.api.client import APIClient, NotAuthorizedAPIException, APIException
@ -98,6 +100,16 @@ def format_user(user: User) -> Dict[str, Any]:
}
def format_profile(profile: Profile) -> Dict[str, object]:
return {
"game": profile.game.value,
"version": profile.version,
"extid": ID.format_extid(profile.extid),
"refid": profile.refid,
"name": profile.get_str("name", ""),
}
def format_news(news: News) -> Dict[str, Any]:
return {
"id": news.id,
@ -145,7 +157,7 @@ def viewsettings() -> Response:
**{
"title": "Network Settings",
"config": g.config,
"region": RegionConstants.LUT,
"region": RegionConstants.LUT(),
},
)
)
@ -240,7 +252,7 @@ def viewarcades() -> Response:
"admin/arcades.react.js",
{
"arcades": [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()],
"regions": RegionConstants.LUT,
"regions": RegionConstants.LUT(),
"usernames": g.data.local.user.get_all_usernames(),
"paseli_enabled": g.config.paseli.enabled,
"paseli_infinite": g.config.paseli.infinite,
@ -271,17 +283,7 @@ def viewmachines() -> Response:
{
"machines": [format_machine(machine) for machine in g.data.local.machine.get_all_machines()],
"arcades": {arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()},
"series": {
GameConstants.BISHI_BASHI.value: "BishiBashi",
GameConstants.DDR.value: "DDR",
GameConstants.IIDX.value: "IIDX",
GameConstants.JUBEAT.value: "Jubeat",
GameConstants.MGA.value: "Metal Gear Arcade",
GameConstants.MUSECA.value: "MÚSECA",
GameConstants.POPN_MUSIC.value: "Pop'n Music",
GameConstants.REFLEC_BEAT.value: "Reflec Beat",
GameConstants.SDVX.value: "SDVX",
},
"series": GameConstants.LUT(),
"games": games,
"enforcing": g.config.server.enforce_pcbid,
},
@ -374,7 +376,15 @@ def viewuser(userid: int) -> Response:
except CardCipherException:
return "????????????????"
# Get list of all game names and versions.
games: Dict[str, Dict[int, str]] = {}
for game, version, name in Base.all_games():
if game.value not in games:
games[game.value] = {}
games[game.value][version] = name
cards = [__format_card(card) for card in g.data.local.user.get_cards(userid)]
profiles = [format_profile(profile) for profile in g.data.local.user.get_profiles(userid)]
arcades = g.data.local.machine.get_all_arcades()
return render_react(
"User",
@ -384,7 +394,9 @@ def viewuser(userid: int) -> Response:
"email": user.email,
"username": user.username,
},
"games": games,
"cards": cards,
"profiles": profiles,
"arcades": {arcade.id: arcade.name for arcade in arcades},
"balances": {arcade.id: g.data.local.user.get_balance(userid, arcade.id) for arcade in arcades},
"events": [
@ -395,6 +407,7 @@ def viewuser(userid: int) -> Response:
{
"refresh": url_for("admin_pages.listuser", userid=userid),
"removeusercard": url_for("admin_pages.removeusercard", userid=userid),
"removeuserprofile": url_for("admin_pages.removeuserprofile", userid=userid),
"addusercard": url_for("admin_pages.addusercard", userid=userid),
"updatebalance": url_for("admin_pages.updatebalance", userid=userid),
"updateusername": url_for("admin_pages.updateusername", userid=userid),
@ -1078,6 +1091,34 @@ def removeusercard(userid: int) -> Dict[str, Any]:
}
@admin_pages.route("/users/<int:userid>/profiles/remove", methods=["POST"])
@jsonify
@adminrequired
def removeuserprofile(userid: int) -> Dict[str, Any]:
# Cast the userID.
userid = UserID(userid)
# Grab refid, see if it exists.
refid = request.get_json()["refid"]
user = g.data.local.user.get_user(userid)
# Make sure the user ID is valid
if user is None:
raise Exception("Cannot find user to update!")
# Make sure the profile is valid.
profiles = [p for p in g.data.local.user.get_profiles(userid) if p.refid == refid]
if len(profiles) != 1:
raise Exception("Cannot find profile to delete!")
profile = profiles[0]
# Remove it from the user's account
g.data.local.user.delete_profile(profile.game, profile.version, userid)
# Return new profile list
return {"profiles": [format_profile(profile) for profile in g.data.local.user.get_profiles(userid)]}
@admin_pages.route("/users/<int:userid>/cards/add", methods=["POST"])
@jsonify
@adminrequired

View File

@ -105,7 +105,7 @@ def viewarcade(arcadeid: int) -> Response:
"arcade/arcade.react.js",
{
"arcade": format_arcade(arcade),
"regions": RegionConstants.LUT,
"regions": RegionConstants.LUT(),
"machines": machines,
"game_settings": get_game_settings(g.data, arcadeid),
"balances": {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)},

View File

@ -324,7 +324,7 @@ def viewsettings() -> Response:
"iidx/settings.react.js",
{
"player": djinfo,
"regions": RegionConstants.LUT,
"regions": RegionConstants.LUT(),
"versions": {version: name for (game, version, name) in frontend.all_games()},
"qpros": frontend.get_all_items(versions),
},

View File

@ -19,6 +19,7 @@ var user_management = createReactClass({
new_password1: '',
new_password2: '',
cards: window.cards,
profiles: window.profiles,
new_card: '',
balances: window.balances,
credits: credits,
@ -392,6 +393,49 @@ var user_management = createReactClass({
);
},
deleteExistingProfile: function(event, refid) {
$.confirm({
escapeKey: 'Cancel',
animation: 'none',
closeAnimation: 'none',
title: 'Delete Profile',
content: 'Are you sure you want to delete this profile?',
buttons: {
Delete: {
btnClass: 'delete',
action: function() {
AJAX.post(
Link.get('removeuserprofile'),
{refid: refid},
function(response) {
// Kill the entry we no longer need
var profiles = this.state.profiles;
this.setState({
profiles: profiles.filter((prof) => prof.refid != refid),
});
}.bind(this)
);
}.bind(this),
},
Cancel: function() {
},
}
});
event.preventDefault();
},
renderDeleteProfileButton: function(profile) {
return (
<>
<Delete
onClick={function(event) {
this.deleteExistingProfile(event, profile.refid);
}.bind(this)}
/>
</>
);
},
render: function() {
return (
<div>
@ -430,6 +474,47 @@ var user_management = createReactClass({
<input type="submit" value="add card" />
</form>
</div>
<div className="section">
<h3>Profiles</h3>
{ this.state.profiles.length == 0 ?
<div>
<span className="placeholder">No profiles present!</span>
</div> :
<Table
className="list profile"
columns={[
{
name: 'Game',
render: function(profile) {
return window.games[profile.game][profile.version]
}.bind(this),
sort: function(a, b) {
return a.game.localeCompare(b.game);
}.bind(this),
},
{
name: 'Game ID',
render: function(profile) {
return profile.extid;
}.bind(this),
},
{
name: 'Name',
render: function(profile) {
return profile.name;
}.bind(this),
},
{
name: '',
render: this.renderDeleteProfileButton,
action: true,
},
]}
rows={this.state.profiles}
emptymessage="There are no profiles associated with this user."
/>
}
</div>
<div className="section">
<h3>PASELI Balance</h3>
{ Object.keys(this.state.arcades).length == 0 ?