StratPlays added
This commit is contained in:
parent
0b3c4b2f55
commit
edde7a1b82
@ -721,7 +721,6 @@ class StratGame(BaseModel):
|
||||
game_type = CharField()
|
||||
away_team = ForeignKeyField(Team)
|
||||
home_team = ForeignKeyField(Team)
|
||||
|
||||
week = IntegerField(default=1)
|
||||
away_score = IntegerField(default=0)
|
||||
home_score = IntegerField(default=0)
|
||||
@ -731,6 +730,7 @@ class StratGame(BaseModel):
|
||||
home_team_ranking = IntegerField(null=True)
|
||||
ranked = BooleanField(default=False)
|
||||
short_game = BooleanField(default=False)
|
||||
forfeit = BooleanField(default=False)
|
||||
|
||||
|
||||
class StratPlay(BaseModel):
|
||||
@ -780,7 +780,8 @@ class StratPlay(BaseModel):
|
||||
sb = IntegerField(default=0)
|
||||
cs = IntegerField(default=0)
|
||||
outs = IntegerField(default=0)
|
||||
wpa = FloatField(default=0)
|
||||
wpa = FloatField(default=0.0)
|
||||
re24 = FloatField(default=0.0)
|
||||
|
||||
# These <position> fields are only required if the play is an x-check or baserunning play
|
||||
catcher = ForeignKeyField(Player, null=True)
|
||||
|
||||
@ -7,7 +7,7 @@ from fastapi import FastAPI
|
||||
from .routers_v2 import (
|
||||
current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards,
|
||||
batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards,
|
||||
battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame)
|
||||
battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame, stratplays)
|
||||
|
||||
app = FastAPI(
|
||||
responses={404: {'description': 'Not found'}}
|
||||
@ -42,3 +42,4 @@ app.include_router(cardpositions.router)
|
||||
app.include_router(scouting.router)
|
||||
app.include_router(mlbplayers.router)
|
||||
app.include_router(stratgame.router)
|
||||
app.include_router(stratplays.router)
|
||||
|
||||
@ -15,8 +15,8 @@ logging.basicConfig(
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/stratgames',
|
||||
tags=['stratgames']
|
||||
prefix='/api/v2/games',
|
||||
tags=['games']
|
||||
)
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ class GameModel(pydantic.BaseModel):
|
||||
home_team_ranking: int = None
|
||||
ranked: bool = False
|
||||
short_game: bool = False
|
||||
forfeit: bool = False
|
||||
|
||||
|
||||
class GameList(pydantic.BaseModel):
|
||||
@ -42,7 +43,7 @@ class GameList(pydantic.BaseModel):
|
||||
|
||||
@router.get('')
|
||||
async def get_games(
|
||||
season: list = Query(default=None), away_team_id: list = Query(default=None),
|
||||
season: list = Query(default=None), forfeit: Optional[bool] = None, away_team_id: list = Query(default=None),
|
||||
home_team_id: list = Query(default=None), team1_id: list = Query(default=None),
|
||||
team2_id: list = Query(default=None), game_type: list = Query(default=None), ranked: Optional[bool] = None,
|
||||
short_game: Optional[bool] = None, csv: Optional[bool] = False, short_output: bool = False):
|
||||
@ -50,6 +51,8 @@ async def get_games(
|
||||
|
||||
if season is not None:
|
||||
all_games = all_games.where(StratGame.season << season)
|
||||
if forfeit is not None:
|
||||
all_games = all_games.where(StratGame.forfeit == forfeit)
|
||||
if away_team_id is not None:
|
||||
all_games = all_games.where(StratGame.away_team_id << away_team_id)
|
||||
if home_team_id is not None:
|
||||
@ -129,26 +132,23 @@ async def patch_game(
|
||||
|
||||
|
||||
@router.post('')
|
||||
async def post_games(game_list: GameList, token: str = Depends(oauth2_scheme)):
|
||||
async def post_game(this_game: GameModel, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning(f'post_games - Bad Token: {token}')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
|
||||
new_games = []
|
||||
for x in game_list.games:
|
||||
if Team.get_or_none(Team.id == x.away_team_id) is None:
|
||||
raise HTTPException(status_code=404, detail=f'Team ID {x.away_team_id} not found')
|
||||
if Team.get_or_none(Team.id == x.home_team_id) is None:
|
||||
raise HTTPException(status_code=404, detail=f'Team ID {x.home_team_id} not found')
|
||||
this_game = StratGame(**this_game.dict())
|
||||
|
||||
new_games.append(x.dict())
|
||||
|
||||
with db.atomic():
|
||||
for batch in chunked(new_games, 16):
|
||||
StratGame.insert_many(batch).on_conflict_replace().execute()
|
||||
db.close()
|
||||
|
||||
return f'Inserted {len(new_games)} games'
|
||||
saved = this_game.save()
|
||||
if saved == 1:
|
||||
return_val = model_to_dict(this_game)
|
||||
db.close()
|
||||
return return_val
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=418,
|
||||
detail='Well slap my ass and call me a teapot; I could not save that game'
|
||||
)
|
||||
|
||||
|
||||
@router.delete('/{game_id}')
|
||||
|
||||
369
app/routers_v2/stratplays.py
Normal file
369
app/routers_v2/stratplays.py
Normal file
@ -0,0 +1,369 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
from typing import List, Optional, Literal
|
||||
import logging
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
from ..db_engine import db, StratPlay, StratGame, Team, Player, model_to_dict, chunked, fn, SQL, \
|
||||
complex_data_to_csv
|
||||
from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
|
||||
|
||||
logging.basicConfig(
|
||||
filename=LOG_DATA['filename'],
|
||||
format=LOG_DATA['format'],
|
||||
level=LOG_DATA['log_level']
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v2/plays',
|
||||
tags=['plays']
|
||||
)
|
||||
|
||||
POS_LIST = Literal['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P', 'DH', 'PH', 'PR', 'GHOST']
|
||||
|
||||
|
||||
class PlayModel(BaseModel):
|
||||
game_id: int
|
||||
play_num: int
|
||||
batter_id: int = None
|
||||
batter_team_id: int = None
|
||||
pitcher_id: int
|
||||
pitcher_team_id: int = None
|
||||
on_base_code: str
|
||||
inning_half: Literal['top', 'bot', 'Top', 'Bot']
|
||||
inning_num: int
|
||||
batting_order: int
|
||||
starting_outs: int
|
||||
away_score: int
|
||||
home_score: int
|
||||
batter_pos: POS_LIST = None
|
||||
|
||||
on_first_id: int = None
|
||||
on_first_final: int = None
|
||||
on_second_id: int = None
|
||||
on_second_final: int = None
|
||||
on_third_id: int = None
|
||||
on_third_final: int = None
|
||||
batter_final: int = None
|
||||
|
||||
pa: int = 0
|
||||
ab: int = 0
|
||||
e_run: int = 0
|
||||
run: int = 0
|
||||
hit: int = 0
|
||||
rbi: int = 0
|
||||
double: int = 0
|
||||
triple: int = 0
|
||||
homerun: int = 0
|
||||
bb: int = 0
|
||||
so: int = 0
|
||||
hbp: int = 0
|
||||
sac: int = 0
|
||||
ibb: int = 0
|
||||
gidp: int = 0
|
||||
bphr: int = 0
|
||||
bpfo: int = 0
|
||||
bp1b: int = 0
|
||||
bplo: int = 0
|
||||
sb: int = 0
|
||||
cs: int = 0
|
||||
outs: int = 0
|
||||
wpa: float = 0.0
|
||||
re24: float = 0.0
|
||||
|
||||
catcher_id: int = None
|
||||
catcher_team_id: int = None
|
||||
defender_id: int = None
|
||||
defender_team_id: int = None
|
||||
runner_id: int = None
|
||||
runner_team_id: int = None
|
||||
|
||||
check_pos: POS_LIST = None
|
||||
error: int = 0
|
||||
wild_pitch: int = 0
|
||||
passed_ball: int = 0
|
||||
pick_off: int = 0
|
||||
balk: int = 0
|
||||
is_go_ahead: bool = False
|
||||
is_tied: bool = False
|
||||
is_new_inning: bool = False
|
||||
|
||||
@validator('on_first_final')
|
||||
def no_final_if_no_runner_one(cls, v, values):
|
||||
if values['on_first_id'] is None:
|
||||
return None
|
||||
return v
|
||||
|
||||
@validator('on_second_final')
|
||||
def no_final_if_no_runner_two(cls, v, values):
|
||||
if values['on_second_id'] is None:
|
||||
return None
|
||||
return v
|
||||
|
||||
@validator('on_third_final')
|
||||
def no_final_if_no_runner_three(cls, v, values):
|
||||
if values['on_third_id'] is None:
|
||||
return None
|
||||
return v
|
||||
|
||||
@validator('batter_final')
|
||||
def no_final_if_no_batter(cls, v, values):
|
||||
if values['batter_id'] is None:
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
class PlayList(BaseModel):
|
||||
plays: List[PlayModel]
|
||||
|
||||
|
||||
@router.get('')
|
||||
async def get_plays(
|
||||
game_id: list = Query(default=None), batter_id: list = Query(default=None), season: list = Query(default=None),
|
||||
week: list = Query(default=None), has_defender: Optional[bool] = None, has_catcher: Optional[bool] = None,
|
||||
has_defender_or_catcher: Optional[bool] = None, is_scoring_play: Optional[bool] = None,
|
||||
pitcher_id: list = Query(default=None), obc: list = Query(default=None), inning: list = Query(default=None),
|
||||
batting_order: list = Query(default=None), starting_outs: list = Query(default=None),
|
||||
batter_pos: list = Query(default=None), catcher_id: list = Query(default=None),
|
||||
defender_id: list = Query(default=None), runner_id: list = Query(default=None),
|
||||
offense_team_id: list = Query(default=None), defense_team_id: list = Query(default=None),
|
||||
hit: Optional[int] = None, double: Optional[int] = None, triple: Optional[int] = None,
|
||||
homerun: Optional[int] = None, play_num: list = Query(default=None), game_type: list = Query(default=None),
|
||||
sb: Optional[int] = None, cs: Optional[int] = None, manager_id: list = Query(default=None),
|
||||
run: Optional[int] = None, e_run: Optional[int] = None, rbi: list = Query(default=None),
|
||||
outs: list = Query(default=None), wild_pitch: Optional[int] = None, is_final_out: Optional[bool] = None,
|
||||
is_go_ahead: Optional[bool] = None, is_tied: Optional[bool] = None, is_new_inning: Optional[bool] = None,
|
||||
min_wpa: Optional[float] = None, max_wpa: Optional[float] = None, sort: Optional[str] = None,
|
||||
short_output: Optional[bool] = False, limit: Optional[int] = 200, page_num: Optional[int] = 1):
|
||||
all_plays = StratPlay.select()
|
||||
|
||||
if season is not None:
|
||||
s_games = StratGame.select().where(StratGame.season << season)
|
||||
all_plays = all_plays.where(StratPlay.game << s_games)
|
||||
if week is not None:
|
||||
w_games = StratGame.select().where(StratGame.week << week)
|
||||
all_plays = all_plays.where(StratPlay.game << w_games)
|
||||
if has_defender is not None:
|
||||
all_plays = all_plays.where(StratPlay.defender.is_null(False))
|
||||
if has_catcher is not None:
|
||||
all_plays = all_plays.where(StratPlay.catcher.is_null(False))
|
||||
if has_defender_or_catcher is not None:
|
||||
all_plays = all_plays.where(
|
||||
(StratPlay.catcher.is_null(False)) | (StratPlay.defender.is_null(False))
|
||||
)
|
||||
if game_id is not None:
|
||||
all_plays = all_plays.where(StratPlay.game_id << game_id)
|
||||
if batter_id is not None:
|
||||
all_plays = all_plays.where(StratPlay.batter_id << batter_id)
|
||||
if pitcher_id is not None:
|
||||
all_plays = all_plays.where(StratPlay.pitcher_id << pitcher_id)
|
||||
if obc is not None:
|
||||
all_plays = all_plays.where(StratPlay.on_base_code << obc)
|
||||
if inning is not None:
|
||||
all_plays = all_plays.where(StratPlay.inning_num << inning)
|
||||
if batting_order is not None:
|
||||
all_plays = all_plays.where(StratPlay.batting_order << batting_order)
|
||||
if starting_outs is not None:
|
||||
all_plays = all_plays.where(StratPlay.starting_outs << starting_outs)
|
||||
if batter_pos is not None:
|
||||
all_plays = all_plays.where(StratPlay.batter_pos << batter_pos)
|
||||
if catcher_id is not None:
|
||||
all_plays = all_plays.where(StratPlay.catcher_id << catcher_id)
|
||||
if defender_id is not None:
|
||||
all_plays = all_plays.where(StratPlay.defender_id << defender_id)
|
||||
if runner_id is not None:
|
||||
all_plays = all_plays.where(StratPlay.runner_id << runner_id)
|
||||
if offense_team_id is not None:
|
||||
all_teams = Team.select().where(Team.id << offense_team_id)
|
||||
all_plays = all_plays.where(
|
||||
(StratPlay.batter_team << all_teams) | (StratPlay.runner_team << all_teams)
|
||||
)
|
||||
if defense_team_id is not None:
|
||||
all_teams = Team.select().where(Team.id << defense_team_id)
|
||||
all_plays = all_plays.where(
|
||||
(StratPlay.catcher_team << all_teams) | (StratPlay.defender_team << all_teams)
|
||||
)
|
||||
if hit is not None:
|
||||
all_plays = all_plays.where(StratPlay.hit == hit)
|
||||
if double is not None:
|
||||
all_plays = all_plays.where(StratPlay.double == double)
|
||||
if triple is not None:
|
||||
all_plays = all_plays.where(StratPlay.triple == triple)
|
||||
if homerun is not None:
|
||||
all_plays = all_plays.where(StratPlay.homerun == homerun)
|
||||
if sb is not None:
|
||||
all_plays = all_plays.where(StratPlay.sb == sb)
|
||||
if cs is not None:
|
||||
all_plays = all_plays.where(StratPlay.cs == cs)
|
||||
if wild_pitch is not None:
|
||||
all_plays = all_plays.where(StratPlay.wild_pitch == wild_pitch)
|
||||
if run is not None:
|
||||
all_plays = all_plays.where(StratPlay.run == run)
|
||||
if e_run is not None:
|
||||
all_plays = all_plays.where(StratPlay.e_run == e_run)
|
||||
if rbi is not None:
|
||||
all_plays = all_plays.where(StratPlay.rbi << rbi)
|
||||
if outs is not None:
|
||||
all_plays = all_plays.where(StratPlay.outs << outs)
|
||||
if manager_id is not None:
|
||||
all_games = StratGame.select().where(
|
||||
(StratGame.away_manager_id << manager_id) | (StratGame.home_manager_id << manager_id)
|
||||
)
|
||||
all_plays = all_plays.where(StratPlay.game << all_games)
|
||||
if is_final_out is not None:
|
||||
all_plays = all_plays.where(StratPlay.starting_outs + StratPlay.outs == 3)
|
||||
if is_go_ahead is not None:
|
||||
all_plays = all_plays.where(StratPlay.is_go_ahead == is_go_ahead)
|
||||
if is_tied is not None:
|
||||
all_plays = all_plays.where(StratPlay.is_tied == is_tied)
|
||||
if is_new_inning is not None:
|
||||
all_plays = all_plays.where(StratPlay.is_new_inning == is_new_inning)
|
||||
if is_scoring_play is not None:
|
||||
all_plays = all_plays.where(
|
||||
(StratPlay.on_first_final == 4) | (StratPlay.on_second_final == 4) | (StratPlay.on_third_final == 4) |
|
||||
(StratPlay.batter_final == 4)
|
||||
)
|
||||
if min_wpa is not None:
|
||||
all_plays = all_plays.where(StratPlay.wpa >= min_wpa)
|
||||
if max_wpa is not None:
|
||||
all_plays = all_plays.where(StratPlay.wpa <= max_wpa)
|
||||
if play_num is not None:
|
||||
all_plays = all_plays.where(StratPlay.play_num << play_num)
|
||||
if game_type is not None:
|
||||
all_types = [x.lower() for x in game_type]
|
||||
all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types)
|
||||
all_plays = all_plays.where(StratPlay.game << all_games)
|
||||
|
||||
if limit > 5000:
|
||||
limit = 5000
|
||||
elif limit < 1:
|
||||
limit = 1
|
||||
if page_num < 1:
|
||||
page_num = 1
|
||||
|
||||
if sort == 'wpa-desc':
|
||||
all_plays = all_plays.order_by(-fn.ABS(StratPlay.wpa))
|
||||
elif sort == 'wpa-asc':
|
||||
all_plays = all_plays.order_by(fn.ABS(StratPlay.wpa))
|
||||
elif sort == 're24-desc':
|
||||
all_plays = all_plays.order_by(-fn.ABS(StratPlay.re24))
|
||||
elif sort == 're24-asc':
|
||||
all_plays = all_plays.order_by(fn.ABS(StratPlay.re24))
|
||||
elif sort == 'newest':
|
||||
all_plays = all_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc())
|
||||
elif sort == 'oldest':
|
||||
all_plays = all_plays.order_by(StratPlay.game_id, StratPlay.play_num)
|
||||
|
||||
all_plays = all_plays.paginate(page_num, limit)
|
||||
|
||||
return_plays = {
|
||||
'count': all_plays.count(),
|
||||
'plays': [model_to_dict(x, recurse=not short_output) for x in all_plays]
|
||||
}
|
||||
db.close()
|
||||
return return_plays
|
||||
|
||||
|
||||
@router.get('/{play_id}')
|
||||
async def get_one_play(play_id: int):
|
||||
if StratPlay.get_or_none(StratPlay.id == play_id) is None:
|
||||
db.close()
|
||||
raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found')
|
||||
r_play = model_to_dict(StratPlay.get_by_id(play_id))
|
||||
db.close()
|
||||
return r_play
|
||||
|
||||
|
||||
@router.patch('/{play_id}')
|
||||
async def patch_play(play_id: int, new_play: PlayModel, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning(f'patch_play - Bad Token: {token}')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
|
||||
if StratPlay.get_or_none(StratPlay.id == play_id) is None:
|
||||
db.close()
|
||||
raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found')
|
||||
|
||||
StratPlay.update(**new_play.dict()).where(StratPlay.id == play_id).execute()
|
||||
r_play = model_to_dict(StratPlay.get_by_id(play_id))
|
||||
db.close()
|
||||
return r_play
|
||||
|
||||
|
||||
@router.post('')
|
||||
async def post_plays(p_list: PlayList, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning(f'post_plays - Bad Token: {token}')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
|
||||
new_plays = []
|
||||
this_game = StratGame.get_or_none(StratGame.id == p_list.plays[0].game_id)
|
||||
if this_game is None:
|
||||
raise HTTPException(status_code=404, detail=f'Game ID {p_list.plays[0].game_id} not found')
|
||||
|
||||
for play in p_list.plays:
|
||||
this_play = play
|
||||
this_play.inning_half = this_play.inning_half.lower()
|
||||
top_half = this_play.inning_half == 'top'
|
||||
|
||||
if this_play.batter_team_id is None and this_play.batter_id is not None:
|
||||
this_play.batter_team_id = this_game.away_team.id if top_half else this_game.home_team.id
|
||||
if this_play.pitcher_team_id is None:
|
||||
this_play.pitcher_team_id = this_game.home_team.id if top_half else this_game.away_team.id
|
||||
if this_play.catcher_id is not None:
|
||||
this_play.catcher_team_id = this_game.home_team.id if top_half else this_game.away_team.id
|
||||
if this_play.defender_id is not None:
|
||||
this_play.defender_team_id = this_game.home_team.id if top_half else this_game.away_team.id
|
||||
if this_play.runner_id is not None:
|
||||
this_play.runner_team_id = this_game.away_team.id if top_half else this_game.home_team.id
|
||||
if this_play.pa == 0:
|
||||
this_play.batter_final = None
|
||||
|
||||
new_plays.append(this_play.dict())
|
||||
|
||||
with db.atomic():
|
||||
for batch in chunked(new_plays, 20):
|
||||
StratPlay.insert_many(batch).on_conflict_replace().execute()
|
||||
db.close()
|
||||
|
||||
return f'Inserted {len(new_plays)} plays'
|
||||
|
||||
|
||||
@router.delete('/{play_id}')
|
||||
async def delete_play(play_id: int, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning(f'delete_play - Bad Token: {token}')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
|
||||
this_play = StratPlay.get_or_none(StratPlay.id == play_id)
|
||||
if not this_play:
|
||||
db.close()
|
||||
raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found')
|
||||
|
||||
count = this_play.delete_instance()
|
||||
db.close()
|
||||
|
||||
if count == 1:
|
||||
return f'Play {play_id} has been deleted'
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'Play {play_id} could not be deleted')
|
||||
|
||||
|
||||
@router.delete('/game/{game_id}')
|
||||
async def delete_plays_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
||||
if not valid_token(token):
|
||||
logging.warning(f'delete_plays_game - Bad Token: {token}')
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
|
||||
this_game = StratGame.get_or_none(StratGame.id == game_id)
|
||||
if not this_game:
|
||||
db.close()
|
||||
raise HTTPException(status_code=404, detail=f'Game ID {game_id} not found')
|
||||
|
||||
count = StratPlay.delete().where(StratPlay.game == this_game).execute()
|
||||
db.close()
|
||||
|
||||
if count > 0:
|
||||
return f'Deleted {count} plays matching Game ID {game_id}'
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f'No plays matching Game ID {game_id} were deleted')
|
||||
|
||||
@ -134,7 +134,7 @@ async def get_teams(
|
||||
|
||||
|
||||
@router.get('/{team_id}')
|
||||
async def get_one_team(team_id, csv: Optional[bool] = False):
|
||||
async def get_one_team(team_id, inc_packs: bool = True, csv: Optional[bool] = False):
|
||||
try:
|
||||
this_team = Team.get_by_id(team_id)
|
||||
except Exception:
|
||||
@ -148,7 +148,8 @@ async def get_one_team(team_id, csv: Optional[bool] = False):
|
||||
return_val = complex_data_to_csv([data])
|
||||
else:
|
||||
return_val = model_to_dict(this_team)
|
||||
return_val['sealed_packs'] = [model_to_dict(x) for x in p_query]
|
||||
if inc_packs:
|
||||
return_val['sealed_packs'] = [model_to_dict(x) for x in p_query]
|
||||
|
||||
db.close()
|
||||
return return_val
|
||||
|
||||
Loading…
Reference in New Issue
Block a user