Compare commits

...

10 Commits

Author SHA1 Message Date
Cal Corum
40c512c665 Add PostgreSQL compatibility fixes for query ordering
- Add explicit ORDER BY id to all queries for consistent results across SQLite and PostgreSQL
- PostgreSQL does not guarantee row order without ORDER BY, unlike SQLite
- Skip table creation when DATABASE_TYPE=postgresql (production tables already exist)
- Fix datetime handling in notifications (PostgreSQL native datetime vs SQLite timestamp)
- Fix grouped query count() calls that don't work in PostgreSQL
- Update .gitignore to include storage/templates/ directory

This completes the PostgreSQL migration compatibility layer while maintaining
backwards compatibility with SQLite for local development.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 10:39:14 -06:00
Cal Corum
0437eab92a Add missing card templates for PostgreSQL deployment
- Recovered player_card.html and related templates from old server
- Required for card generation endpoints (/players/{id}/battingcard)
- Fixes TemplateNotFound errors in production
2026-02-01 19:18:04 -06:00
Cal Corum
b6743f704b Bump version to 1.5.4 2026-01-31 16:07:06 -06:00
Cal Corum
985a6ed2b0 Add default ORDER BY id to BaseModel.select() for PostgreSQL compatibility
PostgreSQL does not guarantee row order without ORDER BY, unlike SQLite
which implicitly returned rows by rowid. This caused bugs where queries
returned results in unexpected order (e.g., get_team_by_owner returning
gauntlet team instead of main team).

Override select() in BaseModel to add default ordering by id. Explicit
.order_by() calls will override this default.

Also mark legacy db_engine.py as deprecated.
2026-01-31 16:06:44 -06:00
Cal Corum
7574e488d9 Bump version to 1.5.3 2026-01-31 15:56:51 -06:00
Cal Corum
8c039dedf8 Fix DateTimeField defaults for PostgreSQL compatibility
Paperdex and GauntletRun models used int timestamps as defaults which
worked in SQLite but fail in PostgreSQL. Changed to datetime.now.
2026-01-31 15:56:33 -06:00
Cal Corum
ab1a25aabc Bump version to 1.5.2 - PostgreSQL timestamp fixes
Fixed 17 timestamp conversion issues where Discord bot sends
milliseconds but PostgreSQL uses DateTimeField.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 23:02:31 -06:00
Cal Corum
5120963e18 Add .dockerignore
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:47:07 -06:00
Cal Corum
be7212032f Add local utility files to gitignore
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:46:56 -06:00
Cal Corum
af94a78ccf Bump version to 1.5.1
Preparing for bugfix release with PostgreSQL timestamp fixes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:46:10 -06:00
39 changed files with 462 additions and 136 deletions

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
postgres_data/
storage/
logs/
__pycache__/
*.pyc
.git/
*.db
*.db-shm
*.db-wal
.env
venv/
.venv/

5
.gitignore vendored
View File

@ -54,6 +54,7 @@ Include/
Lib/ Lib/
Scripts/ Scripts/
storage/ storage/
!storage/templates/
pyenv.cfg pyenv.cfg
pyvenv.cfg pyvenv.cfg
docker-compose.override.yml docker-compose.override.yml
@ -78,3 +79,7 @@ postgres_data/
# PostgreSQL credentials # PostgreSQL credentials
.secrets/ .secrets/
# Local utility files
README_GAUNTLET_CLEANUP.md
wipe_gauntlet_team.py

View File

@ -1 +1 @@
1.5 1.5.4

View File

@ -11,6 +11,8 @@ from playhouse.shortcuts import model_to_dict
# Database configuration - supports both SQLite and PostgreSQL # Database configuration - supports both SQLite and PostgreSQL
DATABASE_TYPE = os.environ.get("DATABASE_TYPE", "sqlite") DATABASE_TYPE = os.environ.get("DATABASE_TYPE", "sqlite")
# Skip table creation for PostgreSQL (tables already exist in production)
SKIP_TABLE_CREATION = DATABASE_TYPE.lower() == "postgresql"
if DATABASE_TYPE.lower() == "postgresql": if DATABASE_TYPE.lower() == "postgresql":
from playhouse.pool import PooledPostgresqlDatabase from playhouse.pool import PooledPostgresqlDatabase
@ -69,7 +71,15 @@ CARDSETS = {
}, },
"hall-of-fame": {"primary": [x for x in range(1, 30)], "human": ranked_cardsets}, "hall-of-fame": {"primary": [x for x in range(1, 30)], "human": ranked_cardsets},
"flashback": { "flashback": {
"primary": [13, 14, 5, 1, 3, 4, 8], # 2018 + Promos, 2019, 2021, 2022 + Promos, Mario "primary": [
13,
14,
5,
1,
3,
4,
8,
], # 2018 + Promos, 2019, 2021, 2022 + Promos, Mario
"secondary": [24], # 2025 "secondary": [24], # 2025
"human": [13, 14, 5, 1, 3, 4, 8], # 2018 + Promos, 2019, 2021, 2022 + Promos "human": [13, 14, 5, 1, 3, 4, 8], # 2018 + Promos, 2019, 2021, 2022 + Promos
}, },
@ -188,7 +198,8 @@ class Current(BaseModel):
return latest_current return latest_current
db.create_tables([Current]) if not SKIP_TABLE_CREATION:
db.create_tables([Current], safe=True)
class Rarity(BaseModel): class Rarity(BaseModel):
@ -204,7 +215,8 @@ class Rarity(BaseModel):
return self.name return self.name
db.create_tables([Rarity]) if not SKIP_TABLE_CREATION:
db.create_tables([Rarity], safe=True)
class Event(BaseModel): class Event(BaseModel):
@ -220,7 +232,8 @@ class Event(BaseModel):
table_name = "event" table_name = "event"
db.create_tables([Event]) if not SKIP_TABLE_CREATION:
db.create_tables([Event], safe=True)
class Cardset(BaseModel): class Cardset(BaseModel):
@ -240,7 +253,8 @@ class Cardset(BaseModel):
return self.name return self.name
db.create_tables([Cardset]) if not SKIP_TABLE_CREATION:
db.create_tables([Cardset], safe=True)
class MlbPlayer(BaseModel): class MlbPlayer(BaseModel):
@ -257,7 +271,8 @@ class MlbPlayer(BaseModel):
table_name = "mlbplayer" table_name = "mlbplayer"
db.create_tables([MlbPlayer]) if not SKIP_TABLE_CREATION:
db.create_tables([MlbPlayer], safe=True)
class Player(BaseModel): class Player(BaseModel):
@ -358,7 +373,8 @@ class Player(BaseModel):
table_name = "player" table_name = "player"
db.create_tables([Player]) if not SKIP_TABLE_CREATION:
db.create_tables([Player], safe=True)
class Team(BaseModel): class Team(BaseModel):
@ -416,7 +432,8 @@ class Team(BaseModel):
table_name = "team" table_name = "team"
db.create_tables([Team]) if not SKIP_TABLE_CREATION:
db.create_tables([Team], safe=True)
class PackType(BaseModel): class PackType(BaseModel):
@ -431,7 +448,8 @@ class PackType(BaseModel):
table_name = "packtype" table_name = "packtype"
db.create_tables([PackType]) if not SKIP_TABLE_CREATION:
db.create_tables([PackType], safe=True)
class Pack(BaseModel): class Pack(BaseModel):
@ -446,7 +464,8 @@ class Pack(BaseModel):
table_name = "pack" table_name = "pack"
db.create_tables([Pack]) if not SKIP_TABLE_CREATION:
db.create_tables([Pack], safe=True)
class Card(BaseModel): class Card(BaseModel):
@ -470,7 +489,8 @@ class Card(BaseModel):
table_name = "card" table_name = "card"
db.create_tables([Card]) if not SKIP_TABLE_CREATION:
db.create_tables([Card], safe=True)
class Roster(BaseModel): class Roster(BaseModel):
@ -642,7 +662,7 @@ class Award(BaseModel):
class Paperdex(BaseModel): class Paperdex(BaseModel):
team = ForeignKeyField(Team) team = ForeignKeyField(Team)
player = ForeignKeyField(Player) player = ForeignKeyField(Player)
created = DateTimeField(default=int(datetime.timestamp(datetime.now()) * 1000)) created = DateTimeField(default=datetime.now)
class Meta: class Meta:
database = db database = db
@ -711,7 +731,7 @@ class GauntletRun(BaseModel):
wins = IntegerField(default=0) wins = IntegerField(default=0)
losses = IntegerField(default=0) losses = IntegerField(default=0)
gsheet = CharField(null=True) gsheet = CharField(null=True)
created = DateTimeField(default=int(datetime.timestamp(datetime.now()) * 1000)) created = DateTimeField(default=datetime.now)
ended = DateTimeField(null=True) # NULL means run not yet ended ended = DateTimeField(null=True) # NULL means run not yet ended
class Meta: class Meta:
@ -719,21 +739,23 @@ class GauntletRun(BaseModel):
table_name = "gauntletrun" table_name = "gauntletrun"
db.create_tables( if not SKIP_TABLE_CREATION:
[ db.create_tables(
Roster, [
BattingStat, Roster,
PitchingStat, BattingStat,
Result, PitchingStat,
Award, Result,
Paperdex, Award,
Reward, Paperdex,
GameRewards, Reward,
Notification, GameRewards,
GauntletReward, Notification,
GauntletRun, GauntletReward,
] GauntletRun,
) ],
safe=True,
)
class BattingCard(BaseModel): class BattingCard(BaseModel):
@ -900,9 +922,11 @@ pos_index = ModelIndex(
CardPosition.add_index(pos_index) CardPosition.add_index(pos_index)
db.create_tables( if not SKIP_TABLE_CREATION:
[BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition] db.create_tables(
) [BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition],
safe=True,
)
class StratGame(BaseModel): class StratGame(BaseModel):
@ -1035,7 +1059,8 @@ decision_index = ModelIndex(Decision, (Decision.game, Decision.pitcher), unique=
Decision.add_index(decision_index) Decision.add_index(decision_index)
db.create_tables([StratGame, StratPlay, Decision]) if not SKIP_TABLE_CREATION:
db.create_tables([StratGame, StratPlay, Decision], safe=True)
db.close() db.close()
@ -1069,7 +1094,7 @@ db.close()
# hand = CharField(default='R') # hand = CharField(default='R')
# #
# #
# scout_db.create_tables([ScoutCardset, ScoutPlayer]) # scout_db.create_tables([ScoutCardset, ScoutPlayer], safe=True)
# #
# #
# class BatterRatings(BaseModelScout): # class BatterRatings(BaseModelScout):
@ -1142,7 +1167,7 @@ db.close()
# slg = FloatField(null=True) # slg = FloatField(null=True)
# #
# #
# # scout_db.create_tables([BatterRatings, PitcherRatings]) # # scout_db.create_tables([BatterRatings, PitcherRatings], safe=True)
# #
# #
# class CardColumns(BaseModelScout): # class CardColumns(BaseModelScout):
@ -1199,7 +1224,7 @@ db.close()
# batting = CharField(null=True) # batting = CharField(null=True)
# #
# #
# scout_db.create_tables([CardColumns, Position, BatterData, PitcherData]) # scout_db.create_tables([CardColumns, Position, BatterData, PitcherData], safe=True)
# #
# #
# class CardOutput(BaseModelScout): # class CardOutput(BaseModelScout):

View File

@ -38,7 +38,7 @@ async def get_awards(
name: Optional[str] = None, season: Optional[int] = None, timing: Optional[str] = None, name: Optional[str] = None, season: Optional[int] = None, timing: Optional[str] = None,
card_id: Optional[int] = None, team_id: Optional[int] = None, image: Optional[str] = None, card_id: Optional[int] = None, team_id: Optional[int] = None, image: Optional[str] = None,
csv: Optional[bool] = None): csv: Optional[bool] = None):
all_awards = Award.select() all_awards = Award.select().order_by(Award.id)
if all_awards.count() == 0: if all_awards.count() == 0:
db.close() db.close()

View File

@ -72,7 +72,7 @@ class BatStatReturnList(pydantic.BaseModel):
async def get_batstats( async def get_batstats(
card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None, card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None,
season: int = None, week_start: int = None, week_end: int = None, created: int = None, csv: bool = None): season: int = None, week_start: int = None, week_end: int = None, created: int = None, csv: bool = None):
all_stats = BattingStat.select().join(Card).join(Player) all_stats = BattingStat.select().join(Card).join(Player).order_by(BattingStat.id)
if season is not None: if season is not None:
all_stats = all_stats.where(BattingStat.season == season) all_stats = all_stats.where(BattingStat.season == season)

View File

@ -170,7 +170,7 @@ async def get_card_ratings(
# detail='You are not authorized to pull card ratings.' # detail='You are not authorized to pull card ratings.'
# ) # )
all_ratings = BattingCardRatings.select() all_ratings = BattingCardRatings.select().order_by(BattingCardRatings.id)
if battingcard_id is not None: if battingcard_id is not None:
all_ratings = all_ratings.where( all_ratings = all_ratings.where(
@ -212,7 +212,7 @@ async def get_card_ratings(
def get_scouting_dfs(cardset_id: list = None): def get_scouting_dfs(cardset_id: list = None):
all_ratings = BattingCardRatings.select() all_ratings = BattingCardRatings.select().order_by(BattingCardRatings.id)
if cardset_id is not None: if cardset_id is not None:
set_players = Player.select(Player.player_id).where( set_players = Player.select(Player.player_id).where(
Player.cardset_id << cardset_id Player.cardset_id << cardset_id
@ -688,7 +688,7 @@ async def get_player_ratings(
all_ratings = BattingCardRatings.select().where( all_ratings = BattingCardRatings.select().where(
BattingCardRatings.battingcard << all_cards BattingCardRatings.battingcard << all_cards
) ).order_by(BattingCardRatings.id)
return_val = { return_val = {
"count": all_ratings.count(), "count": all_ratings.count(),

View File

@ -45,7 +45,7 @@ async def get_batting_cards(
limit: Optional[int] = None, limit: Optional[int] = None,
variant: list = Query(default=None), variant: list = Query(default=None),
): ):
all_cards = BattingCard.select() all_cards = BattingCard.select().order_by(BattingCard.id)
if player_id is not None: if player_id is not None:
all_cards = all_cards.where(BattingCard.player_id << player_id) all_cards = all_cards.where(BattingCard.player_id << player_id)
if cardset_id is not None: if cardset_id is not None:

View File

@ -83,13 +83,13 @@ async def get_card_positions(
all_pos = all_pos.where(CardPosition.player << all_players) all_pos = all_pos.where(CardPosition.player << all_players)
if sort == "innings-desc": if sort == "innings-desc":
all_pos = all_pos.order_by(CardPosition.innings.desc()) all_pos = all_pos.order_by(CardPosition.innings.desc(), CardPosition.id)
elif sort == "innings-asc": elif sort == "innings-asc":
all_pos = all_pos.order_by(CardPosition.innings) all_pos = all_pos.order_by(CardPosition.innings, CardPosition.id)
elif sort == "range-desc": elif sort == "range-desc":
all_pos = all_pos.order_by(CardPosition.range.desc()) all_pos = all_pos.order_by(CardPosition.range.desc(), CardPosition.id)
elif sort == "range-asc": elif sort == "range-asc":
all_pos = all_pos.order_by(CardPosition.range) all_pos = all_pos.order_by(CardPosition.range, CardPosition.id)
return_val = { return_val = {
"count": all_pos.count(), "count": all_pos.count(),

View File

@ -75,6 +75,8 @@ async def get_cards(
if order_by is not None: if order_by is not None:
if order_by.lower() == 'new': if order_by.lower() == 'new':
all_cards = all_cards.order_by(-Card.id) all_cards = all_cards.order_by(-Card.id)
else:
all_cards = all_cards.order_by(Card.id)
if limit is not None: if limit is not None:
all_cards = all_cards.limit(limit) all_cards = all_cards.limit(limit)
if dupes: if dupes:

View File

@ -33,7 +33,7 @@ class CardsetModel(pydantic.BaseModel):
async def get_cardsets( async def get_cardsets(
name: Optional[str] = None, in_desc: Optional[str] = None, event_id: Optional[int] = None, name: Optional[str] = None, in_desc: Optional[str] = None, event_id: Optional[int] = None,
in_packs: Optional[bool] = None, ranked_legal: Optional[bool] = None, csv: Optional[bool] = None): in_packs: Optional[bool] = None, ranked_legal: Optional[bool] = None, csv: Optional[bool] = None):
all_cardsets = Cardset.select() all_cardsets = Cardset.select().order_by(Cardset.id)
if all_cardsets.count() == 0: if all_cardsets.count() == 0:
db.close() db.close()
@ -97,7 +97,7 @@ async def search_cardsets(
Returns cardsets matching the query with exact matches prioritized over partial matches. Returns cardsets matching the query with exact matches prioritized over partial matches.
""" """
# Start with all cardsets # Start with all cardsets
all_cardsets = Cardset.select() all_cardsets = Cardset.select().order_by(Cardset.id)
# Apply name filter (partial match) # Apply name filter (partial match)
all_cardsets = all_cardsets.where(fn.Lower(Cardset.name).contains(q.lower())) all_cardsets = all_cardsets.where(fn.Lower(Cardset.name).contains(q.lower()))

View File

@ -179,8 +179,7 @@ async def get_decisions_for_rest(
.where((StratPlay.game == x.game) & (StratPlay.pitcher == x.pitcher)) .where((StratPlay.game == x.game) & (StratPlay.pitcher == x.pitcher))
.group_by(StratPlay.pitcher, StratPlay.game) .group_by(StratPlay.pitcher, StratPlay.game)
) )
logging.info(f"this_line: {this_line[0]}") if this_line.count() == 0 or this_line[0].sum_outs is None:
if this_line[0].sum_outs is None:
this_val.append(0.0) this_val.append(0.0)
else: else:
this_val.append( this_val.append(

View File

@ -32,7 +32,7 @@ class EventModel(pydantic.BaseModel):
async def v1_events_get( async def v1_events_get(
name: Optional[str] = None, in_desc: Optional[str] = None, active: Optional[bool] = None, name: Optional[str] = None, in_desc: Optional[str] = None, active: Optional[bool] = None,
csv: Optional[bool] = None): csv: Optional[bool] = None):
all_events = Event.select() all_events = Event.select().order_by(Event.id)
if name is not None: if name is not None:
all_events = all_events.where(fn.Lower(Event.name) == name.lower()) all_events = all_events.where(fn.Lower(Event.name) == name.lower())

View File

@ -30,7 +30,7 @@ class GameRewardModel(pydantic.BaseModel):
async def v1_gamerewards_get( async def v1_gamerewards_get(
name: Optional[str] = None, pack_type_id: Optional[int] = None, player_id: Optional[int] = None, name: Optional[str] = None, pack_type_id: Optional[int] = None, player_id: Optional[int] = None,
money: Optional[int] = None, csv: Optional[bool] = None): money: Optional[int] = None, csv: Optional[bool] = None):
all_rewards = GameRewards.select() all_rewards = GameRewards.select().order_by(GameRewards.id)
# if all_rewards.count() == 0: # if all_rewards.count() == 0:
# db.close() # db.close()

View File

@ -36,7 +36,7 @@ async def v1_gauntletreward_get(
win_num: Optional[int] = None, win_num: Optional[int] = None,
loss_max: Optional[int] = None, loss_max: Optional[int] = None,
): ):
all_rewards = GauntletReward.select() all_rewards = GauntletReward.select().order_by(GauntletReward.id)
if name is not None: if name is not None:
all_rewards = all_rewards.where(GauntletReward.name == name) all_rewards = all_rewards.where(GauntletReward.name == name)

View File

@ -36,7 +36,7 @@ async def get_gauntletruns(
losses_max: Optional[int] = None, gsheet: Optional[str] = None, created_after: Optional[int] = None, losses_max: Optional[int] = None, gsheet: Optional[str] = None, created_after: Optional[int] = None,
created_before: Optional[int] = None, ended_after: Optional[int] = None, ended_before: Optional[int] = None, created_before: Optional[int] = None, ended_after: Optional[int] = None, ended_before: Optional[int] = None,
is_active: Optional[bool] = None, gauntlet_id: list = Query(default=None), season: list = Query(default=None)): is_active: Optional[bool] = None, gauntlet_id: list = Query(default=None), season: list = Query(default=None)):
all_gauntlets = GauntletRun.select() all_gauntlets = GauntletRun.select().order_by(GauntletRun.id)
if team_id is not None: if team_id is not None:
all_gauntlets = all_gauntlets.where(GauntletRun.team_id << team_id) all_gauntlets = all_gauntlets.where(GauntletRun.team_id << team_id)

View File

@ -82,7 +82,7 @@ async def get_players(
offense_col: list = Query(default=None), offense_col: list = Query(default=None),
csv: Optional[bool] = False, csv: Optional[bool] = False,
): ):
all_players = MlbPlayer.select() all_players = MlbPlayer.select().order_by(MlbPlayer.id)
if full_name is not None: if full_name is not None:
name_list = [x.lower() for x in full_name] name_list = [x.lower() for x in full_name]

View File

@ -35,7 +35,7 @@ async def get_notifs(
created_after: Optional[int] = None, title: Optional[str] = None, desc: Optional[str] = None, created_after: Optional[int] = None, title: Optional[str] = None, desc: Optional[str] = None,
field_name: Optional[str] = None, in_desc: Optional[str] = None, about: Optional[str] = None, field_name: Optional[str] = None, in_desc: Optional[str] = None, about: Optional[str] = None,
ack: Optional[bool] = None, csv: Optional[bool] = None): ack: Optional[bool] = None, csv: Optional[bool] = None):
all_notif = Notification.select() all_notif = Notification.select().order_by(Notification.id)
if all_notif.count() == 0: if all_notif.count() == 0:
db.close() db.close()

View File

@ -83,8 +83,10 @@ async def get_packs(
all_packs = all_packs.where(Pack.open_time.is_null(not opened)) all_packs = all_packs.where(Pack.open_time.is_null(not opened))
if limit is not None: if limit is not None:
all_packs = all_packs.limit(limit) all_packs = all_packs.limit(limit)
if new_to_old is not None: if new_to_old:
all_packs = all_packs.order_by(-Pack.id) all_packs = all_packs.order_by(-Pack.id)
else:
all_packs = all_packs.order_by(Pack.id)
# if all_packs.count() == 0: # if all_packs.count() == 0:
# db.close() # db.close()
@ -96,7 +98,7 @@ async def get_packs(
data_list.append( data_list.append(
[ [
line.id, line.team.abbrev, line.pack_type.name, line.id, line.team.abbrev, line.pack_type.name,
datetime.fromtimestamp(line.open_time) if line.open_time else None line.open_time # Already datetime in PostgreSQL
] ]
) )
return_val = DataFrame(data_list).to_csv(header=False, index=False) return_val = DataFrame(data_list).to_csv(header=False, index=False)
@ -125,7 +127,7 @@ async def get_one_pack(pack_id, csv: Optional[bool] = False):
data_list = [ data_list = [
['id', 'team', 'pack_type', 'open_time'], ['id', 'team', 'pack_type', 'open_time'],
[this_pack.id, this_pack.team.abbrev, this_pack.pack_type.name, [this_pack.id, this_pack.team.abbrev, this_pack.pack_type.name,
datetime.fromtimestamp(this_pack.open_time) if this_pack.open_time else None] this_pack.open_time] # Already datetime in PostgreSQL
] ]
return_val = DataFrame(data_list).to_csv(header=False, index=False) return_val = DataFrame(data_list).to_csv(header=False, index=False)

View File

@ -31,7 +31,7 @@ class PacktypeModel(pydantic.BaseModel):
async def get_packtypes( async def get_packtypes(
name: Optional[str] = None, card_count: Optional[int] = None, in_desc: Optional[str] = None, name: Optional[str] = None, card_count: Optional[int] = None, in_desc: Optional[str] = None,
available: Optional[bool] = None, csv: Optional[bool] = None): available: Optional[bool] = None, csv: Optional[bool] = None):
all_packtypes = PackType.select() all_packtypes = PackType.select().order_by(PackType.id)
if all_packtypes.count() == 0: if all_packtypes.count() == 0:
db.close() db.close()

View File

@ -31,7 +31,7 @@ async def get_paperdex(
team_id: Optional[int] = None, player_id: Optional[int] = None, created_after: Optional[int] = None, team_id: Optional[int] = None, player_id: Optional[int] = None, created_after: Optional[int] = None,
cardset_id: Optional[int] = None, created_before: Optional[int] = None, flat: Optional[bool] = False, cardset_id: Optional[int] = None, created_before: Optional[int] = None, flat: Optional[bool] = False,
csv: Optional[bool] = None): csv: Optional[bool] = None):
all_dex = Paperdex.select().join(Player).join(Cardset) all_dex = Paperdex.select().join(Player).join(Cardset).order_by(Paperdex.id)
if all_dex.count() == 0: if all_dex.count() == 0:
db.close() db.close()

View File

@ -158,7 +158,7 @@ async def get_card_ratings(
status_code=401, detail="You are not authorized to pull card ratings." status_code=401, detail="You are not authorized to pull card ratings."
) )
all_ratings = PitchingCardRatings.select() all_ratings = PitchingCardRatings.select().order_by(PitchingCardRatings.id)
if pitchingcard_id is not None: if pitchingcard_id is not None:
all_ratings = all_ratings.where( all_ratings = all_ratings.where(
@ -192,7 +192,7 @@ async def get_card_ratings(
def get_scouting_dfs(cardset_id: list = None): def get_scouting_dfs(cardset_id: list = None):
all_ratings = PitchingCardRatings.select() all_ratings = PitchingCardRatings.select().order_by(PitchingCardRatings.id)
if cardset_id is not None: if cardset_id is not None:
set_players = Player.select(Player.player_id).where( set_players = Player.select(Player.player_id).where(
Player.cardset_id << cardset_id Player.cardset_id << cardset_id

View File

@ -44,7 +44,7 @@ async def get_pitching_cards(
short_output: bool = False, short_output: bool = False,
limit: Optional[int] = None, limit: Optional[int] = None,
): ):
all_cards = PitchingCard.select() all_cards = PitchingCard.select().order_by(PitchingCard.id)
if player_id is not None: if player_id is not None:
all_cards = all_cards.where(PitchingCard.player_id << player_id) all_cards = all_cards.where(PitchingCard.player_id << player_id)
if cardset_id is not None: if cardset_id is not None:

View File

@ -58,7 +58,7 @@ async def get_pit_stats(
card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None, card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None,
season: int = None, week_start: int = None, week_end: int = None, created: int = None, gs: bool = None, season: int = None, week_start: int = None, week_end: int = None, created: int = None, gs: bool = None,
csv: bool = None): csv: bool = None):
all_stats = PitchingStat.select().join(Card).join(Player) all_stats = PitchingStat.select().join(Card).join(Player).order_by(PitchingStat.id)
logging.debug(f'pit query:\n\n{all_stats}') logging.debug(f'pit query:\n\n{all_stats}')
if season is not None: if season is not None:

View File

@ -217,6 +217,8 @@ async def get_players(
all_players = all_players.order_by(Player.rarity) all_players = all_players.order_by(Player.rarity)
elif sort_by == "rarity-asc": elif sort_by == "rarity-asc":
all_players = all_players.order_by(-Player.rarity) all_players = all_players.order_by(-Player.rarity)
else:
all_players = all_players.order_by(Player.player_id)
final_players = [] final_players = []
# logging.info(f'pos_exclude: {type(pos_exclude)} - {pos_exclude} - is None: {pos_exclude is None}') # logging.info(f'pos_exclude: {type(pos_exclude)} - {pos_exclude} - is None: {pos_exclude is None}')

View File

@ -28,7 +28,7 @@ class RarityModel(pydantic.BaseModel):
@router.get('') @router.get('')
async def get_rarities(value: Optional[int] = None, name: Optional[str] = None, min_value: Optional[int] = None, async def get_rarities(value: Optional[int] = None, name: Optional[str] = None, min_value: Optional[int] = None,
max_value: Optional[int] = None, csv: Optional[bool] = None): max_value: Optional[int] = None, csv: Optional[bool] = None):
all_rarities = Rarity.select() all_rarities = Rarity.select().order_by(Rarity.id)
if all_rarities.count() == 0: if all_rarities.count() == 0:
db.close() db.close()

View File

@ -196,7 +196,7 @@ async def get_one_results(result_id, csv: Optional[bool] = None):
@router.get('/team/{team_id}') @router.get('/team/{team_id}')
async def get_team_results( async def get_team_results(
team_id: int, season: Optional[int] = None, week: Optional[int] = None, csv: Optional[bool] = False): team_id: int, season: Optional[int] = None, week: Optional[int] = None, csv: Optional[bool] = False):
all_results = Result.select().where((Result.away_team_id == team_id) | (Result.home_team_id == team_id)) all_results = Result.select().where((Result.away_team_id == team_id) | (Result.home_team_id == team_id)).order_by(Result.id)
try: try:
this_team = Team.get_by_id(team_id) this_team = Team.get_by_id(team_id)
except Exception as e: except Exception as e:

View File

@ -33,7 +33,7 @@ async def get_rewards(
name: Optional[str] = None, in_name: Optional[str] = None, team_id: Optional[int] = None, name: Optional[str] = None, in_name: Optional[str] = None, team_id: Optional[int] = None,
season: Optional[int] = None, week: Optional[int] = None, created_after: Optional[int] = None, season: Optional[int] = None, week: Optional[int] = None, created_after: Optional[int] = None,
flat: Optional[bool] = False, csv: Optional[bool] = None): flat: Optional[bool] = False, csv: Optional[bool] = None):
all_rewards = Reward.select() all_rewards = Reward.select().order_by(Reward.id)
if all_rewards.count() == 0: if all_rewards.count() == 0:
db.close() db.close()

View File

@ -48,7 +48,7 @@ async def get_games(
team2_id: list = Query(default=None), game_type: list = Query(default=None), ranked: Optional[bool] = 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, short_game: Optional[bool] = None, csv: Optional[bool] = False, short_output: bool = False,
gauntlet_id: Optional[int] = None): gauntlet_id: Optional[int] = None):
all_games = StratGame.select() all_games = StratGame.select().order_by(StratGame.id)
if season is not None: if season is not None:
all_games = all_games.where(StratGame.season << season) all_games = all_games.where(StratGame.season << season)

View File

@ -177,7 +177,7 @@ async def get_plays(
limit: Optional[int] = 200, limit: Optional[int] = 200,
page_num: Optional[int] = 1, page_num: Optional[int] = 1,
): ):
all_plays = StratPlay.select() all_plays = StratPlay.select().order_by(StratPlay.id)
if season is not None: if season is not None:
s_games = StratGame.select().where(StratGame.season << season) s_games = StratGame.select().where(StratGame.season << season)
@ -648,9 +648,11 @@ async def get_batting_totals(
logging.debug(f"bat_plays query: {bat_plays}") logging.debug(f"bat_plays query: {bat_plays}")
logging.debug(f"run_plays query: {run_plays}") logging.debug(f"run_plays query: {run_plays}")
return_stats = {"count": bat_plays.count(), "stats": []} # Convert to list first - .count() doesn't work on grouped queries in PostgreSQL
bat_plays_list = list(bat_plays)
return_stats = {"count": len(bat_plays_list), "stats": []}
for x in bat_plays: for x in bat_plays_list:
# NOTE: Removed .order_by(StratPlay.id) - not valid with GROUP BY in PostgreSQL # NOTE: Removed .order_by(StratPlay.id) - not valid with GROUP BY in PostgreSQL
# and not meaningful for aggregated results anyway # and not meaningful for aggregated results anyway
this_run = run_plays this_run = run_plays
@ -1066,9 +1068,11 @@ async def get_pitching_totals(
limit = 500 limit = 500
pit_plays = pit_plays.paginate(page_num, limit) pit_plays = pit_plays.paginate(page_num, limit)
return_stats = {"count": pit_plays.count(), "stats": []} # Convert to list first - .count() doesn't work on grouped queries in PostgreSQL
pit_plays_list = list(pit_plays)
return_stats = {"count": len(pit_plays_list), "stats": []}
for x in pit_plays: for x in pit_plays_list:
this_dec = all_dec.where(Decision.pitcher == x.pitcher) this_dec = all_dec.where(Decision.pitcher == x.pitcher)
if game_type is not None: if game_type is not None:
all_types = [x.lower() for x in game_type] all_types = [x.lower() for x in game_type]

View File

@ -155,6 +155,9 @@ async def get_teams(
if event_id is not None: if event_id is not None:
all_teams = all_teams.where(Team.event_id == event_id) all_teams = all_teams.where(Team.event_id == event_id)
# Default ordering for PostgreSQL compatibility
all_teams = all_teams.order_by(Team.id)
if limit is not None: if limit is not None:
all_teams = all_teams.limit(limit) all_teams = all_teams.limit(limit)
@ -1098,7 +1101,7 @@ async def team_buy_players(team_id: int, ids: str, ts: str):
# Post a notification # Post a notification
if this_player.rarity.value >= 2: if this_player.rarity.value >= 2:
new_notif = Notification( new_notif = Notification(
created=int_timestamp(datetime.now()), created=datetime.now(),
title=f"Price Change", title=f"Price Change",
desc="Modified by buying and selling", desc="Modified by buying and selling",
field_name=f"{this_player.description} " field_name=f"{this_player.description} "
@ -1250,7 +1253,7 @@ async def team_sell_cards(team_id: int, ids: str, ts: str):
# post a notification # post a notification
if this_player.rarity.value >= 2: if this_player.rarity.value >= 2:
new_notif = Notification( new_notif = Notification(
created=int_timestamp(datetime.now()), created=datetime.now(),
title=f"Price Change", title=f"Price Change",
desc="Modified by buying and selling", desc="Modified by buying and selling",
field_name=f"{this_player.description} " field_name=f"{this_player.description} "

View File

@ -1,4 +1,11 @@
import math import math
"""
DEPRECATED: This file is a legacy implementation from before the deployment
of /database/app/. The active codebase is now in /database/app/db_engine.py.
This file is kept for reference only and should not be used.
"""
from datetime import datetime from datetime import datetime
from typing import List from typing import List
import logging import logging
@ -10,20 +17,16 @@ from peewee import ModelSelect
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
db = SqliteDatabase( db = SqliteDatabase(
'storage/pd_master.db', "storage/pd_master.db",
pragmas={ pragmas={"journal_mode": "wal", "cache_size": -1 * 64000, "synchronous": 0},
'journal_mode': 'wal',
'cache_size': -1 * 64000,
'synchronous': 0
}
) )
date = f'{datetime.now().year}-{datetime.now().month}-{datetime.now().day}' date = f"{datetime.now().year}-{datetime.now().month}-{datetime.now().day}"
log_level = logging.INFO if os.environ.get('LOG_LEVEL') == 'INFO' else 'WARN' log_level = logging.INFO if os.environ.get("LOG_LEVEL") == "INFO" else "WARN"
logging.basicConfig( logging.basicConfig(
filename=f'logs/database/{date}.log', filename=f"logs/database/{date}.log",
format='%(asctime)s - database - %(levelname)s - %(message)s', format="%(asctime)s - database - %(levelname)s - %(message)s",
level=log_level level=log_level,
) )
@ -39,7 +42,7 @@ def model_to_csv(this_obj, exclude=None) -> List:
def query_to_csv(all_items: ModelSelect, exclude=None): def query_to_csv(all_items: ModelSelect, exclude=None):
if all_items.count() == 0: if all_items.count() == 0:
data_list = [['No data found']] data_list = [["No data found"]]
else: else:
data_list = [model_csv_headers(all_items[0], exclude=exclude)] data_list = [model_csv_headers(all_items[0], exclude=exclude)]
for x in all_items: for x in all_items:
@ -50,29 +53,29 @@ def query_to_csv(all_items: ModelSelect, exclude=None):
def complex_data_to_csv(complex_data: List): def complex_data_to_csv(complex_data: List):
if len(complex_data) == 0: if len(complex_data) == 0:
data_list = [['No data found']] data_list = [["No data found"]]
else: else:
data_list = [[x for x in complex_data[0].keys()]] data_list = [[x for x in complex_data[0].keys()]]
for line in complex_data: for line in complex_data:
logging.debug(f'line: {line}') logging.debug(f"line: {line}")
this_row = [] this_row = []
for key in line: for key in line:
logging.debug(f'key: {key}') logging.debug(f"key: {key}")
if line[key] is None: if line[key] is None:
this_row.append('') this_row.append("")
elif isinstance(line[key], dict): elif isinstance(line[key], dict):
if 'name' in line[key]: if "name" in line[key]:
this_row.append(line[key]['name']) this_row.append(line[key]["name"])
elif 'abbrev' in line[key]: elif "abbrev" in line[key]:
this_row.append(line[key]['abbrev']) this_row.append(line[key]["abbrev"])
else: else:
this_row.append(line[key]['id']) this_row.append(line[key]["id"])
elif isinstance(line[key], int) and line[key] > 100000000: elif isinstance(line[key], int) and line[key] > 100000000:
this_row.append(f"'{line[key]}") this_row.append(f"'{line[key]}")
elif isinstance(line[key], str) and ',' in line[key]: elif isinstance(line[key], str) and "," in line[key]:
this_row.append(line[key].replace(",", "-_-")) this_row.append(line[key].replace(",", "-_-"))
else: else:
@ -186,7 +189,7 @@ class Player(BaseModel):
mlbplayer = ForeignKeyField(MlbPlayer, null=True) mlbplayer = ForeignKeyField(MlbPlayer, null=True)
def __str__(self): def __str__(self):
return f'{self.cardset} {self.p_name} ({self.rarity.name})' return f"{self.cardset} {self.p_name} ({self.rarity.name})"
# def __eq__(self, other): # def __eq__(self, other):
# if self.cardset.id == other.cardset.id and self.name == other.name: # if self.cardset.id == other.cardset.id and self.name == other.name:
@ -207,21 +210,21 @@ class Player(BaseModel):
def get_all_pos(self): def get_all_pos(self):
all_pos = [] all_pos = []
if self.pos_1 and self.pos_1 != 'CP': if self.pos_1 and self.pos_1 != "CP":
all_pos.append(self.pos_1) all_pos.append(self.pos_1)
if self.pos_2 and self.pos_2 != 'CP': if self.pos_2 and self.pos_2 != "CP":
all_pos.append(self.pos_2) all_pos.append(self.pos_2)
if self.pos_3 and self.pos_3 != 'CP': if self.pos_3 and self.pos_3 != "CP":
all_pos.append(self.pos_3) all_pos.append(self.pos_3)
if self.pos_4 and self.pos_4 != 'CP': if self.pos_4 and self.pos_4 != "CP":
all_pos.append(self.pos_4) all_pos.append(self.pos_4)
if self.pos_5 and self.pos_5 != 'CP': if self.pos_5 and self.pos_5 != "CP":
all_pos.append(self.pos_5) all_pos.append(self.pos_5)
if self.pos_6 and self.pos_6 != 'CP': if self.pos_6 and self.pos_6 != "CP":
all_pos.append(self.pos_6) all_pos.append(self.pos_6)
if self.pos_7 and self.pos_7 != 'CP': if self.pos_7 and self.pos_7 != "CP":
all_pos.append(self.pos_7) all_pos.append(self.pos_7)
if self.pos_8 and self.pos_8 != 'CP': if self.pos_8 and self.pos_8 != "CP":
all_pos.append(self.pos_8) all_pos.append(self.pos_8)
return all_pos return all_pos
@ -235,19 +238,19 @@ class Player(BaseModel):
# 'mvp': 2500, # 'mvp': 2500,
# 'hof': 999999999 # 'hof': 999999999
# } # }
logging.info(f'{self.p_name} cost changing from: {self.cost}') logging.info(f"{self.p_name} cost changing from: {self.cost}")
self.cost = max(math.floor(self.cost * .95), 1) self.cost = max(math.floor(self.cost * 0.95), 1)
# if self.quantity != 999: # if self.quantity != 999:
# self.quantity += 1 # self.quantity += 1
logging.info(f'{self.p_name} cost now: {self.cost}') logging.info(f"{self.p_name} cost now: {self.cost}")
self.save() self.save()
def change_on_buy(self): def change_on_buy(self):
logging.info(f'{self.p_name} cost changing from: {self.cost}') logging.info(f"{self.p_name} cost changing from: {self.cost}")
self.cost = math.ceil(self.cost * 1.1) self.cost = math.ceil(self.cost * 1.1)
# if self.quantity != 999: # if self.quantity != 999:
# self.quantity -= 1 # self.quantity -= 1
logging.info(f'{self.p_name} cost now: {self.cost}') logging.info(f"{self.p_name} cost now: {self.cost}")
self.save() self.save()
@ -274,7 +277,7 @@ class Team(BaseModel):
is_ai = IntegerField(null=True) is_ai = IntegerField(null=True)
def __str__(self): def __str__(self):
return f'S{self.season} {self.lname}' return f"S{self.season} {self.lname}"
@staticmethod @staticmethod
def get_by_owner(gmid, season=None): def get_by_owner(gmid, season=None):
@ -300,8 +303,8 @@ class Team(BaseModel):
return Team.get_or_none(Team.season == season, Team.abbrev == abbrev.upper()) return Team.get_or_none(Team.season == season, Team.abbrev == abbrev.upper())
def team_hash(self): def team_hash(self):
hash_string = f'{self.sname[-1]}{self.gmid / 6950123:.0f}{self.sname[-2]}{self.gmid / 42069123:.0f}' hash_string = f"{self.sname[-1]}{self.gmid / 6950123:.0f}{self.sname[-2]}{self.gmid / 42069123:.0f}"
logging.info(f'string: {hash_string}') logging.info(f"string: {hash_string}")
return hash_string return hash_string
@ -338,9 +341,9 @@ class Card(BaseModel):
def __str__(self): def __str__(self):
if self.player: if self.player:
return f'{self.player} - {self.team.sname}' return f"{self.player} - {self.team.sname}"
else: else:
return f'Blank - {self.team.sname}' return f"Blank - {self.team.sname}"
@staticmethod @staticmethod
def select_season(season): def select_season(season):
@ -382,7 +385,7 @@ class Roster(BaseModel):
card_26 = ForeignKeyField(Card) card_26 = ForeignKeyField(Card)
def __str__(self): def __str__(self):
return f'{self.team} Roster' return f"{self.team} Roster"
# def get_cards(self, team): # def get_cards(self, team):
# all_cards = Card.select().where(Card.roster == self) # all_cards = Card.select().where(Card.roster == self)
@ -499,7 +502,7 @@ class Award(BaseModel):
class Paperdex(BaseModel): class Paperdex(BaseModel):
team = ForeignKeyField(Team) team = ForeignKeyField(Team)
player = ForeignKeyField(Player) player = ForeignKeyField(Player)
created = DateTimeField(default=int(datetime.timestamp(datetime.now())*1000)) created = DateTimeField(default=datetime.now)
# def add_to_paperdex(self, team, cards: list): # def add_to_paperdex(self, team, cards: list):
# for x in players: # for x in players:
@ -548,14 +551,25 @@ class GauntletRun(BaseModel):
wins = IntegerField(default=0) wins = IntegerField(default=0)
losses = IntegerField(default=0) losses = IntegerField(default=0)
gsheet = CharField(null=True) gsheet = CharField(null=True)
created = DateTimeField(default=int(datetime.timestamp(datetime.now())*1000)) created = DateTimeField(default=datetime.now)
ended = DateTimeField(default=0) ended = DateTimeField(null=True)
db.create_tables([ db.create_tables(
Roster, BattingStat, PitchingStat, Result, Award, Paperdex, Reward, GameRewards, Notification, GauntletReward, [
GauntletRun Roster,
]) BattingStat,
PitchingStat,
Result,
Award,
Paperdex,
Reward,
GameRewards,
Notification,
GauntletReward,
GauntletRun,
]
)
class BattingCard(BaseModel): class BattingCard(BaseModel):
@ -569,16 +583,18 @@ class BattingCard(BaseModel):
hit_and_run = CharField() hit_and_run = CharField()
running = IntegerField() running = IntegerField()
offense_col = IntegerField() offense_col = IntegerField()
hand = CharField(default='R') hand = CharField(default="R")
bc_index = ModelIndex(BattingCard, (BattingCard.player, BattingCard.variant), unique=True) bc_index = ModelIndex(
BattingCard, (BattingCard.player, BattingCard.variant), unique=True
)
BattingCard.add_index(bc_index) BattingCard.add_index(bc_index)
class BattingCardRatings(BaseModel): class BattingCardRatings(BaseModel):
battingcard = ForeignKeyField(BattingCard) battingcard = ForeignKeyField(BattingCard)
vs_hand = CharField(default='R') vs_hand = CharField(default="R")
pull_rate = FloatField() pull_rate = FloatField()
center_rate = FloatField() center_rate = FloatField()
slap_rate = FloatField() slap_rate = FloatField()
@ -610,7 +626,9 @@ class BattingCardRatings(BaseModel):
bcr_index = ModelIndex( bcr_index = ModelIndex(
BattingCardRatings, (BattingCardRatings.battingcard, BattingCardRatings.vs_hand), unique=True BattingCardRatings,
(BattingCardRatings.battingcard, BattingCardRatings.vs_hand),
unique=True,
) )
BattingCardRatings.add_index(bcr_index) BattingCardRatings.add_index(bcr_index)
@ -626,16 +644,18 @@ class PitchingCard(BaseModel):
closer_rating = IntegerField(null=True) closer_rating = IntegerField(null=True)
batting = CharField(null=True) batting = CharField(null=True)
offense_col = IntegerField() offense_col = IntegerField()
hand = CharField(default='R') hand = CharField(default="R")
pc_index = ModelIndex(PitchingCard, (PitchingCard.player, PitchingCard.variant), unique=True) pc_index = ModelIndex(
PitchingCard, (PitchingCard.player, PitchingCard.variant), unique=True
)
PitchingCard.add_index(pc_index) PitchingCard.add_index(pc_index)
class PitchingCardRatings(BaseModel): class PitchingCardRatings(BaseModel):
pitchingcard = ForeignKeyField(PitchingCard) pitchingcard = ForeignKeyField(PitchingCard)
vs_hand = CharField(default='R') vs_hand = CharField(default="R")
homerun = FloatField() homerun = FloatField()
bp_homerun = FloatField() bp_homerun = FloatField()
triple = FloatField() triple = FloatField()
@ -669,7 +689,9 @@ class PitchingCardRatings(BaseModel):
pcr_index = ModelIndex( pcr_index = ModelIndex(
PitchingCardRatings, (PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand), unique=True PitchingCardRatings,
(PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand),
unique=True,
) )
PitchingCardRatings.add_index(pcr_index) PitchingCardRatings.add_index(pcr_index)
@ -687,12 +709,16 @@ class CardPosition(BaseModel):
pos_index = ModelIndex( pos_index = ModelIndex(
CardPosition, (CardPosition.player, CardPosition.variant, CardPosition.position), unique=True CardPosition,
(CardPosition.player, CardPosition.variant, CardPosition.position),
unique=True,
) )
CardPosition.add_index(pos_index) CardPosition.add_index(pos_index)
db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition]) db.create_tables(
[BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition]
)
db.close() db.close()
@ -870,4 +896,3 @@ db.close()
# #
# #
# scout_db.close() # scout_db.close()

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% include 'style.html' %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;700&family=Source+Sans+3:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="fullCard" style="width: 1200px; height: 600px;">
<div id="header" class="row-wrapper header-text border-bot" style="height: 65px">
<!-- <div id="headerLeft" style="flex-grow: 3; height: auto">-->
<div id="headerLeft" style="width: 477px; height: auto">
<div class="row-wrapper" style="height: 100%">
<div style="width: 29px; height: auto; font-size: 30px; margin-left: 6px">
<b>{{ hand }}</b>
</div>
<div class="vline"></div>
<div class="header-text" style="padding-left: 5px; width: 462px">
<div style="height: 50%; font-variant: small-caps; font-size: 27px"><b>{{ player.p_name }}</b></div>
<div style="height: 50%; padding-left: 18px; margin-top: {{ position_margin }}px; font-size: {{ position_font }}px;">{{ position_string }}</div>
</div>
</div>
</div>
<div id="headerMiddle" style="width: 246px; height: auto; ">
<img src="data:image/png;base64,{{ rarity_file|safe }}" class="center" style="height: {% if player.rarity.name == 'Hall of Fame' %}62.{% else %}6{% endif %}5px; width: auto;"/>
</div>
{% if card_type == 'batter' %}
{% include 'topright_batter.html' %}
{% else %}
{% include 'topright_pitcher.html' %}
{% endif %}
</div>
<div id="allResults" class="result">
<div id="resultHeader" class="row-wrapper border-bot" style="height: 30px">
<div class="row-wrapper border-right-thick" style="width: 600px; ">
<div id="leftResultHeaderOne" class="column-num border-right-thin blue-gradient" style="width: 200px">
<b>{{ 1 if card_type == 'batter' else 4 }}</b>
</div>
<div id="leftResultHeaderTwo" class="column-num border-right-thin blue-gradient" style="width: 200px">
<b>{{ 2 if card_type == 'batter' else 5 }}</b>
</div>
<div id="leftResultHeaderThree" class="column-num blue-gradient" style="width: 200px">
<b>{{ 3 if card_type == 'batter' else 6 }}</b>
</div>
</div>
<div class="row-wrapper" style="width: 600px; ">
<div id="rightResultHeaderOne" class="column-num border-right-thin red-gradient" style="width: 200px">
<b>{{ 1 if card_type == 'batter' else 4 }}</b>
</div>
<div id="rightResultHeaderTwo" class="column-num border-right-thin red-gradient" style="width: 200px">
<b>{{ 2 if card_type == 'batter' else 5 }}</b>
</div>
<div id="rightResultHeaderThree" class="column-num red-gradient" style="width: 200px">
<b>{{ 3 if card_type == 'batter' else 6 }}</b>
</div>
</div>
</div>
<div id="resultWrapper" class="row-wrapper" style="height: 505px">
{% include 'result_columns.html' %}
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,37 @@
<div id="resultWrapper" class="row-wrapper" style="height: 100%">
<div class="row-wrapper border-right-thick" style="width: 600px; background-color: #ACE6FF">
<div class="row-wrapper border-right-thin" style="width: 200px; position: relative;">
<div class="result-2d6">{{ vl_one_2d6|safe }}</div>
<div class="result-col">{{ vl_one_results|safe }}</div>
<div class="result-d20">{{ vl_one_d20|safe }}</div>
</div>
<div class="border-right-thin" style="width: 200px; position: relative;">
<div class="result-2d6">{{ vl_two_2d6|safe }}</div>
<div class="result-col">{{ vl_two_results|safe }}</div>
<div class="result-d20">{{ vl_two_d20|safe }}</div>
</div>
<div style="width: 200px; position: relative;">
<div class="result-2d6">{{ vl_three_2d6|safe }}</div>
<div class="result-col">{{ vl_three_results|safe }}</div>
<div class="result-d20">{{ vl_three_d20|safe }}</div>
</div>
</div>
<div class="row-wrapper" style="width: 600px; background-color: #EAA49C">
<div class="row-wrapper border-right-thin" style="width: 200px; position: relative;">
<div class="result-2d6">{{ vr_one_2d6|safe }}</div>
<div class="result-col">{{ vr_one_results|safe }}</div>
<div class="result-d20">{{ vr_one_d20|safe }}</div>
</div>
<div class="border-right-thin" style="width: 200px; position: relative;">
<div class="result-2d6">{{ vr_two_2d6|safe }}</div>
<div class="result-col">{{ vr_two_results|safe }}</div>
<div class="result-d20">{{ vr_two_d20|safe }}</div>
</div>
<div style="width: 200px; position: relative;">
<div class="result-2d6">{{ vr_three_2d6|safe }}</div>
<div class="result-col">{{ vr_three_results|safe }}</div>
<div class="result-d20">{{ vr_three_d20|safe }}</div>
</div>
</div>
</div>

View File

@ -0,0 +1,15 @@
<div class="row-wrapper border-right-thick" style="width: 600px; background-color: #ACE6FF">
<div class="row-wrapper border-right-thin" style="width: 200px; position: relative;">
<div class="result-2d6"><b>1-</b></div>
<div class="result-col"><b>{{ results_vl_one }}</b></div>
<div class="result-d20"><b>1-20</b></div>
</div>
<div class="border-right-thin" style="width: 200px; position: relative;">
<div class="result-2d6"><b>1-</b></div>
<div class="result-col"><b>{{ results_vl_two }}</b></div>
<div class="result-d20"><b>1-20</b></div>
</div>
<div style="width: 200px">
{{ results_vl_three }}
</div>
</div>

View File

@ -0,0 +1,11 @@
<div class="row-wrapper" style="width: 600px; background-color: #EAA49C">
<div class="border-right-thin" style="width: 200px">
{{ results_vr_one }}
</div>
<div class="border-right-thin" style="width: 200px">
{{ results_vr_two }}
</div>
<div style="width: 200px">
{{ results_vr_three }}
</div>
</div>

View File

@ -0,0 +1,103 @@
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Open Sans", sans-serif;
font-weight: 400;
<!-- margin-top: 5px;-->
<!-- margin-bottom: 5px;-->
justify-content: center;
overflow: hidden;
<!-- background-color: red;-->
}
.row-wrapper {
width: 100%;
flex-grow: 1;
display: flex;
flex-direction: row;
}
.vline {
border-left: 3px solid black;
height: 100%;
}
.header-text {
font-size: 25px;
text-align: left;
}
.column-num {
display: flex;
height: 100%;
justify-content: center;
align-items: center
}
.border-bot {
border-bottom: 3px solid black;
}
.border-right-thick {
border-right: 5px solid black
}
.border-right-thin {
border-right: 3px solid black
}
.red-gradient {
background-image: linear-gradient(to right, rgba(211, 49, 21, 1), rgba(211, 49, 21, 0.5), rgba(211, 49, 21, 1))
}
.blue-gradient {
background-image: linear-gradient(to right, rgba(0, 156, 224, 1), rgba(0, 156, 224, 0.5), rgba(0, 156, 224, 1))
}
.result {
font-family: 'Source Sans 3', sans-serif;
font-size: 26px;
line-height: 1.3;
}
.result-2d6 {
position: absolute;
left: 1px;
width: 35px;
text-align: right;
}
.result-col {
position: absolute;
left: 36px;
width: 170
text-align: left;
align-items: left
}
.result-d20 {
position: absolute;
left: 124px;
width: 70px;
text-align: right;
}
.center {
display: block;
margin-left: auto;
margin-right: auto;
}
<!-- .neg-margin {-->
<!-- line-height: 1.3;-->
<!-- }-->
<!-- b {-->
<!-- font-weight: 800-->
<!-- }-->
</style>

View File

@ -0,0 +1,7 @@
<div id="headerRight" style="width: 477px; height: auto; text-align: right">
<div style="position: absolute; left: 705px; width: 320px;">stealing <b>{{ stealing_string }}</b></div>
<div style="position: absolute; left: 1040px; width: 150px;">running <b>{{ bat_card.running }}</b></div>
<div style="position: absolute; left: 920px; top: 29px; width: 120px; ">bunting <b>{{ bat_card.bunting }}</b></div>
<div style="position: absolute; left: 1060px; top: 29px; width: 130px;">hit & run <b>{{ bat_card.hit_and_run }}</b></div>
<div style="position: absolute; left: 760px; top: 40px; width: 140px; font-size: 14px">{{ cardset_name }}</div>
</div>

View File

@ -0,0 +1,9 @@
<div id="headerRight" style="width: 477px; height: auto; text-align: right">
<div style="position: absolute; left: 785px; width: 160px;">bat <b>{{ pit_card.batting[1:] if pit_card.batting.startswith('#') else pit_card.batting }}</b></div>
<div style="position: absolute; left: 970px; width: 90px;">hold <b>{% if pit_card.hold >= 0 %}+{% endif %}{{ pit_card.hold }}</b></div>
<div style="position: absolute; left: 1060px; width: 130px;">starter({{ pit_card.starter_rating }})</div>
<div style="position: absolute; left: 725px; top: 40px; width: 160px; font-size: 14px">{{ cardset_name }}</div>
<div style="position: absolute; left: 900px; top: 29px; width: 80px; ">wp <b>{{ pit_card.wild_pitch }}</b></div>
<div style="position: absolute; left: 995px; top: 29px; width: 65px; ">bk <b>{{ pit_card.balk }}</b></div>
<div style="position: absolute; left: 1070px; top: 29px; width: 120px;">relief({{ pit_card.relief_rating }})/{{ pit_card.closer_rating if pit_card.closer_rating is not none else 'N' }}</div>
</div>