Compare commits
No commits in common. "main" and "feature/refractor-phase2-integration" have entirely different histories.
main
...
feature/re
@ -1,31 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Install git hooks for this repository
|
|
||||||
#
|
|
||||||
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
||||||
|
|
||||||
if [ -z "$REPO_ROOT" ]; then
|
|
||||||
echo "Error: Not in a git repository"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
HOOKS_DIR="$REPO_ROOT/.githooks"
|
|
||||||
GIT_HOOKS_DIR="$REPO_ROOT/.git/hooks"
|
|
||||||
|
|
||||||
echo "Installing git hooks..."
|
|
||||||
|
|
||||||
if [ -f "$HOOKS_DIR/pre-commit" ]; then
|
|
||||||
cp "$HOOKS_DIR/pre-commit" "$GIT_HOOKS_DIR/pre-commit"
|
|
||||||
chmod +x "$GIT_HOOKS_DIR/pre-commit"
|
|
||||||
echo "Installed pre-commit hook"
|
|
||||||
else
|
|
||||||
echo "pre-commit hook not found in $HOOKS_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "The pre-commit hook will:"
|
|
||||||
echo " - Auto-fix ruff lint violations (unused imports, formatting, etc.)"
|
|
||||||
echo " - Block commits only on truly unfixable issues"
|
|
||||||
echo ""
|
|
||||||
echo "To bypass in emergency: git commit --no-verify"
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Pre-commit hook: ruff lint check on staged Python files.
|
|
||||||
# Catches syntax errors, unused imports, and basic issues before commit.
|
|
||||||
# To bypass in emergency: git commit --no-verify
|
|
||||||
#
|
|
||||||
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
|
|
||||||
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM -z -- '*.py')
|
|
||||||
if [ -z "$STAGED_PY" ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "ruff check on staged files..."
|
|
||||||
|
|
||||||
# Stash unstaged changes so ruff only operates on staged content.
|
|
||||||
# Without this, ruff --fix runs on the full working tree file (staged +
|
|
||||||
# unstaged), and the subsequent git add would silently include unstaged
|
|
||||||
# changes in the commit — breaking git add -p workflows.
|
|
||||||
STASHED=0
|
|
||||||
if git stash --keep-index -q 2>/dev/null; then
|
|
||||||
STASHED=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Auto-fix what we can, then re-stage the fixed files
|
|
||||||
printf '%s' "$STAGED_PY" | xargs -0 ruff check --fix --exit-zero
|
|
||||||
printf '%s' "$STAGED_PY" | xargs -0 git add
|
|
||||||
|
|
||||||
# Restore unstaged changes
|
|
||||||
if [ $STASHED -eq 1 ]; then
|
|
||||||
git stash pop -q
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Now check for remaining unfixable issues
|
|
||||||
printf '%s' "$STAGED_PY" | xargs -0 ruff check
|
|
||||||
RUFF_EXIT=$?
|
|
||||||
|
|
||||||
if [ $RUFF_EXIT -ne 0 ]; then
|
|
||||||
echo ""
|
|
||||||
echo -e "${RED}Pre-commit checks failed (unfixable issues). Commit blocked.${NC}"
|
|
||||||
echo -e "${YELLOW}To bypass (not recommended): git commit --no-verify${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}All checks passed.${NC}"
|
|
||||||
exit 0
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -59,8 +59,6 @@ pyenv.cfg
|
|||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
docker-compose.*.yml
|
docker-compose.*.yml
|
||||||
.run-local.pid
|
|
||||||
.env.local
|
|
||||||
*.db
|
*.db
|
||||||
venv
|
venv
|
||||||
.claude/
|
.claude/
|
||||||
|
|||||||
@ -1257,15 +1257,50 @@ refractor_card_state_team_index = ModelIndex(
|
|||||||
RefractorCardState.add_index(refractor_card_state_team_index)
|
RefractorCardState.add_index(refractor_card_state_team_index)
|
||||||
|
|
||||||
|
|
||||||
|
class RefractorTierBoost(BaseModel):
|
||||||
|
track = ForeignKeyField(RefractorTrack)
|
||||||
|
tier = IntegerField() # 1-4
|
||||||
|
boost_type = CharField() # e.g. 'rating', 'stat'
|
||||||
|
boost_target = CharField() # e.g. 'contact_vl', 'power_vr'
|
||||||
|
boost_value = FloatField(default=0.0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
table_name = "refractor_tier_boost"
|
||||||
|
|
||||||
|
|
||||||
|
refractor_tier_boost_index = ModelIndex(
|
||||||
|
RefractorTierBoost,
|
||||||
|
(
|
||||||
|
RefractorTierBoost.track,
|
||||||
|
RefractorTierBoost.tier,
|
||||||
|
RefractorTierBoost.boost_type,
|
||||||
|
RefractorTierBoost.boost_target,
|
||||||
|
),
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
RefractorTierBoost.add_index(refractor_tier_boost_index)
|
||||||
|
|
||||||
|
|
||||||
|
class RefractorCosmetic(BaseModel):
|
||||||
|
name = CharField(unique=True)
|
||||||
|
tier_required = IntegerField(default=0)
|
||||||
|
cosmetic_type = CharField() # 'frame', 'badge', 'theme'
|
||||||
|
css_class = CharField(null=True)
|
||||||
|
asset_url = CharField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
table_name = "refractor_cosmetic"
|
||||||
|
|
||||||
|
|
||||||
class RefractorBoostAudit(BaseModel):
|
class RefractorBoostAudit(BaseModel):
|
||||||
card_state = ForeignKeyField(RefractorCardState, on_delete="CASCADE")
|
card_state = ForeignKeyField(RefractorCardState, on_delete="CASCADE")
|
||||||
tier = IntegerField() # 1-4
|
tier = IntegerField() # 1-4
|
||||||
battingcard = ForeignKeyField(BattingCard, null=True)
|
battingcard = ForeignKeyField(BattingCard, null=True)
|
||||||
pitchingcard = ForeignKeyField(PitchingCard, null=True)
|
pitchingcard = ForeignKeyField(PitchingCard, null=True)
|
||||||
variant_created = IntegerField()
|
variant_created = IntegerField()
|
||||||
boost_delta_json = (
|
boost_delta_json = TextField() # JSONB in PostgreSQL; TextField for SQLite test compat
|
||||||
TextField()
|
|
||||||
) # JSONB in PostgreSQL; TextField for SQLite test compat
|
|
||||||
applied_at = DateTimeField(default=datetime.now)
|
applied_at = DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1278,6 +1313,8 @@ if not SKIP_TABLE_CREATION:
|
|||||||
[
|
[
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorTierBoost,
|
||||||
|
RefractorCosmetic,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
],
|
],
|
||||||
safe=True,
|
safe=True,
|
||||||
|
|||||||
@ -32,7 +32,6 @@ from ..db_engine import (
|
|||||||
)
|
)
|
||||||
from ..db_helpers import upsert_players
|
from ..db_helpers import upsert_players
|
||||||
from ..dependencies import oauth2_scheme, valid_token
|
from ..dependencies import oauth2_scheme, valid_token
|
||||||
from ..services.refractor_boost import compute_variant_hash
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Persistent browser instance (WP-02)
|
# Persistent browser instance (WP-02)
|
||||||
@ -133,19 +132,6 @@ def normalize_franchise(franchise: str) -> str:
|
|||||||
return FRANCHISE_NORMALIZE.get(titled, titled)
|
return FRANCHISE_NORMALIZE.get(titled, titled)
|
||||||
|
|
||||||
|
|
||||||
def resolve_refractor_tier(player_id: int, variant: int) -> int:
|
|
||||||
"""Determine the refractor tier (0-4) from a player's variant hash.
|
|
||||||
|
|
||||||
Pure math — no DB query needed. Returns 0 for base cards or unknown variants.
|
|
||||||
"""
|
|
||||||
if variant == 0:
|
|
||||||
return 0
|
|
||||||
for tier in range(1, 5):
|
|
||||||
if compute_variant_hash(player_id, tier) == variant:
|
|
||||||
return tier
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v2/players", tags=["players"])
|
router = APIRouter(prefix="/api/v2/players", tags=["players"])
|
||||||
|
|
||||||
|
|
||||||
@ -737,9 +723,6 @@ async def get_batter_card(
|
|||||||
variant: int = 0,
|
variant: int = 0,
|
||||||
d: str = None,
|
d: str = None,
|
||||||
html: Optional[bool] = False,
|
html: Optional[bool] = False,
|
||||||
tier: Optional[int] = Query(
|
|
||||||
None, ge=0, le=4, description="Override refractor tier for preview (dev only)"
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
this_player = Player.get_by_id(player_id)
|
this_player = Player.get_by_id(player_id)
|
||||||
@ -757,7 +740,6 @@ async def get_batter_card(
|
|||||||
f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
||||||
)
|
)
|
||||||
and html is False
|
and html is False
|
||||||
and tier is None
|
|
||||||
):
|
):
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png",
|
path=f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png",
|
||||||
@ -804,9 +786,6 @@ async def get_batter_card(
|
|||||||
card_data["cardset_name"] = this_player.cardset.name
|
card_data["cardset_name"] = this_player.cardset.name
|
||||||
else:
|
else:
|
||||||
card_data["cardset_name"] = this_player.description
|
card_data["cardset_name"] = this_player.description
|
||||||
card_data["refractor_tier"] = (
|
|
||||||
tier if tier is not None else resolve_refractor_tier(player_id, variant)
|
|
||||||
)
|
|
||||||
card_data["request"] = request
|
card_data["request"] = request
|
||||||
html_response = templates.TemplateResponse("player_card.html", card_data)
|
html_response = templates.TemplateResponse("player_card.html", card_data)
|
||||||
|
|
||||||
@ -844,9 +823,6 @@ async def get_batter_card(
|
|||||||
card_data["cardset_name"] = this_player.cardset.name
|
card_data["cardset_name"] = this_player.cardset.name
|
||||||
else:
|
else:
|
||||||
card_data["cardset_name"] = this_player.description
|
card_data["cardset_name"] = this_player.description
|
||||||
card_data["refractor_tier"] = (
|
|
||||||
tier if tier is not None else resolve_refractor_tier(player_id, variant)
|
|
||||||
)
|
|
||||||
card_data["request"] = request
|
card_data["request"] = request
|
||||||
html_response = templates.TemplateResponse("player_card.html", card_data)
|
html_response = templates.TemplateResponse("player_card.html", card_data)
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ from typing import Optional
|
|||||||
|
|
||||||
from ..db_engine import model_to_dict
|
from ..db_engine import model_to_dict
|
||||||
from ..dependencies import oauth2_scheme, valid_token
|
from ..dependencies import oauth2_scheme, valid_token
|
||||||
from ..services.refractor_init import initialize_card_refractor, _determine_card_type
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -303,9 +302,8 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
|||||||
|
|
||||||
Finds all unique (player_id, team_id) pairs from the game's StratPlay rows,
|
Finds all unique (player_id, team_id) pairs from the game's StratPlay rows,
|
||||||
then for each pair that has a RefractorCardState, re-computes the refractor
|
then for each pair that has a RefractorCardState, re-computes the refractor
|
||||||
tier. Pairs without a state row are auto-initialized on-the-fly via
|
tier. Pairs without a state row are silently skipped. Per-player errors are
|
||||||
initialize_card_refractor (idempotent). Per-player errors are logged but
|
logged but do not abort the batch.
|
||||||
do not abort the batch.
|
|
||||||
"""
|
"""
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning("Bad Token: [REDACTED]")
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
@ -335,16 +333,6 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
|||||||
(RefractorCardState.player_id == player_id)
|
(RefractorCardState.player_id == player_id)
|
||||||
& (RefractorCardState.team_id == team_id)
|
& (RefractorCardState.team_id == team_id)
|
||||||
)
|
)
|
||||||
if state is None:
|
|
||||||
try:
|
|
||||||
player = Player.get_by_id(player_id)
|
|
||||||
card_type = _determine_card_type(player)
|
|
||||||
state = initialize_card_refractor(player_id, team_id, card_type)
|
|
||||||
except Exception:
|
|
||||||
logger.warning(
|
|
||||||
f"Refractor auto-init failed for player={player_id} "
|
|
||||||
f"team={team_id} — skipping"
|
|
||||||
)
|
|
||||||
if state is None:
|
if state is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
-- Drop orphaned RefractorTierBoost and RefractorCosmetic tables.
|
|
||||||
-- These were speculative schema from the initial Refractor design that were
|
|
||||||
-- never used — boosts are hardcoded in refractor_boost.py and tier visuals
|
|
||||||
-- are embedded in CSS templates. Both tables have zero rows on dev and prod.
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS refractor_tier_boost;
|
|
||||||
DROP TABLE IF EXISTS refractor_cosmetic;
|
|
||||||
132
run-local.sh
132
run-local.sh
@ -1,132 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# run-local.sh — Spin up the Paper Dynasty Database API locally for testing.
|
|
||||||
#
|
|
||||||
# Connects to the dev PostgreSQL on the homelab (10.10.0.42) so you get real
|
|
||||||
# card data for rendering. Playwright Chromium must be installed locally
|
|
||||||
# (it already is on this workstation).
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./run-local.sh # start on default port 8000
|
|
||||||
# ./run-local.sh 8001 # start on custom port
|
|
||||||
# ./run-local.sh --stop # kill a running instance
|
|
||||||
#
|
|
||||||
# Card rendering test URLs (after startup):
|
|
||||||
# HTML preview: http://localhost:8000/api/v2/players/{id}/battingcard/{date}/{variant}?html=True
|
|
||||||
# PNG render: http://localhost:8000/api/v2/players/{id}/battingcard/{date}/{variant}
|
|
||||||
# API docs: http://localhost:8000/api/docs
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
PORT="${1:-8000}"
|
|
||||||
PIDFILE=".run-local.pid"
|
|
||||||
LOGFILE="logs/database/run-local.log"
|
|
||||||
|
|
||||||
# ── Stop mode ────────────────────────────────────────────────────────────────
|
|
||||||
if [[ "${1:-}" == "--stop" ]]; then
|
|
||||||
if [[ -f "$PIDFILE" ]]; then
|
|
||||||
pid=$(cat "$PIDFILE")
|
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
|
||||||
kill "$pid"
|
|
||||||
echo "Stopped local API (PID $pid)"
|
|
||||||
else
|
|
||||||
echo "PID $pid not running (stale pidfile)"
|
|
||||||
fi
|
|
||||||
rm -f "$PIDFILE"
|
|
||||||
else
|
|
||||||
echo "No pidfile found — nothing to stop"
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Pre-flight checks ───────────────────────────────────────────────────────
|
|
||||||
if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
||||||
echo "Already running (PID $(cat "$PIDFILE")). Use './run-local.sh --stop' first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Python deps are importable
|
|
||||||
python -c "import fastapi, peewee, playwright" 2>/dev/null || {
|
|
||||||
echo "Missing Python dependencies. Install with: pip install -r requirements.txt"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check Playwright Chromium is available
|
|
||||||
python -c "
|
|
||||||
from playwright.sync_api import sync_playwright
|
|
||||||
p = sync_playwright().start()
|
|
||||||
print(p.chromium.executable_path)
|
|
||||||
p.stop()
|
|
||||||
" >/dev/null 2>&1 || {
|
|
||||||
echo "Playwright Chromium not installed. Run: playwright install chromium"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check dev DB is reachable
|
|
||||||
DB_HOST="${POSTGRES_HOST_LOCAL:-10.10.0.42}"
|
|
||||||
python -c "
|
|
||||||
import socket, sys
|
|
||||||
s = socket.create_connection((sys.argv[1], 5432), timeout=3)
|
|
||||||
s.close()
|
|
||||||
" "$DB_HOST" 2>/dev/null || {
|
|
||||||
echo "Cannot reach dev PostgreSQL at ${DB_HOST}:5432 — is the homelab up?"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Ensure directories exist ────────────────────────────────────────────────
|
|
||||||
mkdir -p logs/database
|
|
||||||
mkdir -p storage/cards
|
|
||||||
|
|
||||||
# ── Launch ───────────────────────────────────────────────────────────────────
|
|
||||||
echo "Starting Paper Dynasty Database API on http://localhost:${PORT}"
|
|
||||||
echo " DB: paperdynasty_dev @ 10.10.0.42"
|
|
||||||
echo " Logs: ${LOGFILE}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Load .env, then .env.local overrides (for passwords not in version control)
|
|
||||||
set -a
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
[[ -f .env ]] && source .env
|
|
||||||
[[ -f .env.local ]] && source .env.local
|
|
||||||
set +a
|
|
||||||
|
|
||||||
# Override DB host to point at the dev server's IP (not Docker network name)
|
|
||||||
export DATABASE_TYPE=postgresql
|
|
||||||
export POSTGRES_HOST="$DB_HOST"
|
|
||||||
export POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
|
||||||
export POSTGRES_DB="${POSTGRES_DB:-paperdynasty_dev}"
|
|
||||||
export POSTGRES_USER="${POSTGRES_USER:-sba_admin}"
|
|
||||||
export LOG_LEVEL=INFO
|
|
||||||
export TESTING=True
|
|
||||||
|
|
||||||
if [[ -z "${POSTGRES_PASSWORD:-}" || "$POSTGRES_PASSWORD" == "your_production_password" ]]; then
|
|
||||||
echo "ERROR: POSTGRES_PASSWORD not set or is the placeholder value."
|
|
||||||
echo "Create .env.local with: POSTGRES_PASSWORD=<actual password>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
uvicorn app.main:app \
|
|
||||||
--host 0.0.0.0 \
|
|
||||||
--port "$PORT" \
|
|
||||||
--reload \
|
|
||||||
--reload-dir app \
|
|
||||||
--reload-dir storage/templates \
|
|
||||||
2>&1 | tee "$LOGFILE" &
|
|
||||||
|
|
||||||
echo $! >"$PIDFILE"
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
if kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
||||||
echo ""
|
|
||||||
echo "API running (PID $(cat "$PIDFILE"))."
|
|
||||||
echo ""
|
|
||||||
echo "Quick test URLs:"
|
|
||||||
echo " API docs: http://localhost:${PORT}/api/docs"
|
|
||||||
echo " Health: curl -s http://localhost:${PORT}/api/v2/players/1/battingcard?html=True"
|
|
||||||
echo ""
|
|
||||||
echo "Stop with: ./run-local.sh --stop"
|
|
||||||
else
|
|
||||||
echo "Failed to start — check ${LOGFILE}"
|
|
||||||
rm -f "$PIDFILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@ -2,26 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% include 'style.html' %}
|
{% include 'style.html' %}
|
||||||
{% include 'tier_style.html' %}
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="fullCard" style="width: 1200px; height: 600px;">
|
<div id="fullCard" style="width: 1200px; height: 600px;">
|
||||||
{% if refractor_tier is defined and refractor_tier > 0 %}
|
|
||||||
{%- set diamond_colors = {
|
|
||||||
1: {'color': '#1a6b1a', 'highlight': '#40b040'},
|
|
||||||
2: {'color': '#2070b0', 'highlight': '#50a0e8'},
|
|
||||||
3: {'color': '#a82020', 'highlight': '#e85050'},
|
|
||||||
4: {'color': '#6b2d8e', 'highlight': '#a060d0'},
|
|
||||||
} -%}
|
|
||||||
{%- set dc = diamond_colors[refractor_tier] -%}
|
|
||||||
{%- set filled_bg = 'linear-gradient(135deg, ' ~ dc.highlight ~ ' 0%, ' ~ dc.color ~ ' 50%, ' ~ dc.color ~ ' 100%)' -%}
|
|
||||||
<div class="tier-diamond{% if refractor_tier == 4 %} diamond-glow{% endif %}">
|
|
||||||
<div class="diamond-quad{% if refractor_tier >= 2 %} filled{% endif %}" {% if refractor_tier >= 2 %}style="background: {{ filled_bg }};"{% endif %}></div>
|
|
||||||
<div class="diamond-quad{% if refractor_tier >= 1 %} filled{% endif %}" {% if refractor_tier >= 1 %}style="background: {{ filled_bg }};"{% endif %}></div>
|
|
||||||
<div class="diamond-quad{% if refractor_tier >= 3 %} filled{% endif %}" {% if refractor_tier >= 3 %}style="background: {{ filled_bg }};"{% endif %}></div>
|
|
||||||
<div class="diamond-quad{% if refractor_tier >= 4 %} filled{% endif %}" {% if refractor_tier >= 4 %}style="background: {{ filled_bg }};"{% endif %}></div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div id="header" class="row-wrapper header-text border-bot" style="height: 65px">
|
<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="flex-grow: 3; height: auto">-->
|
||||||
<div id="headerLeft" style="width: 477px; height: auto">
|
<div id="headerLeft" style="width: 477px; height: auto">
|
||||||
|
|||||||
@ -1,216 +0,0 @@
|
|||||||
<style>
|
|
||||||
#fullCard {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% if refractor_tier is defined and refractor_tier > 0 %}
|
|
||||||
<style>
|
|
||||||
.tier-diamond {
|
|
||||||
position: absolute;
|
|
||||||
left: 597px;
|
|
||||||
top: 78.5px;
|
|
||||||
transform: translate(-50%, -50%) rotate(45deg);
|
|
||||||
display: grid;
|
|
||||||
grid-template: 1fr 1fr / 1fr 1fr;
|
|
||||||
gap: 2px;
|
|
||||||
z-index: 20;
|
|
||||||
pointer-events: none;
|
|
||||||
background: rgba(0,0,0,0.75);
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 0 0 1.5px rgba(0,0,0,0.7), 0 2px 5px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.diamond-quad {
|
|
||||||
width: 19px;
|
|
||||||
height: 19px;
|
|
||||||
background: rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.diamond-quad.filled {
|
|
||||||
box-shadow: inset 0 1px 2px rgba(255,255,255,0.45),
|
|
||||||
inset 0 -1px 2px rgba(0,0,0,0.35),
|
|
||||||
inset 1px 0 2px rgba(255,255,255,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{% if refractor_tier == 1 %}
|
|
||||||
/* T1 — Base Chrome */
|
|
||||||
#header {
|
|
||||||
background: linear-gradient(135deg, rgba(185,195,210,0.25) 0%, rgba(210,218,228,0.35) 50%, rgba(185,195,210,0.25) 100%), #ffffff;
|
|
||||||
}
|
|
||||||
.border-bot {
|
|
||||||
border-bottom-color: #8e9baf;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
}
|
|
||||||
#resultHeader.border-bot {
|
|
||||||
border-bottom-width: 3px;
|
|
||||||
}
|
|
||||||
.border-right-thick {
|
|
||||||
border-right-color: #8e9baf;
|
|
||||||
}
|
|
||||||
.border-right-thin {
|
|
||||||
border-right-color: #8e9baf;
|
|
||||||
}
|
|
||||||
.vline {
|
|
||||||
border-left-color: #8e9baf;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% elif refractor_tier == 2 %}
|
|
||||||
/* T2 — Refractor */
|
|
||||||
#header {
|
|
||||||
background: linear-gradient(135deg, rgba(100,155,230,0.28) 0%, rgba(155,90,220,0.18) 25%, rgba(90,200,210,0.24) 50%, rgba(185,80,170,0.16) 75%, rgba(100,155,230,0.28) 100%), #ffffff;
|
|
||||||
}
|
|
||||||
#fullCard {
|
|
||||||
box-shadow: inset 0 0 14px 3px rgba(90,143,207,0.22);
|
|
||||||
}
|
|
||||||
.border-bot {
|
|
||||||
border-bottom-color: #7a9cc4;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
}
|
|
||||||
#resultHeader .border-right-thick {
|
|
||||||
border-right-width: 6px;
|
|
||||||
}
|
|
||||||
.border-right-thick {
|
|
||||||
border-right-color: #7a9cc4;
|
|
||||||
}
|
|
||||||
.border-right-thin {
|
|
||||||
border-right-color: #7a9cc4;
|
|
||||||
border-right-width: 3px;
|
|
||||||
}
|
|
||||||
.vline {
|
|
||||||
border-left-color: #7a9cc4;
|
|
||||||
}
|
|
||||||
.blue-gradient {
|
|
||||||
background-image: linear-gradient(to right, rgba(60,110,200,1), rgba(100,55,185,0.55), rgba(60,110,200,1));
|
|
||||||
}
|
|
||||||
.red-gradient {
|
|
||||||
background-image: linear-gradient(to right, rgba(190,35,80,1), rgba(165,25,100,0.55), rgba(190,35,80,1));
|
|
||||||
}
|
|
||||||
|
|
||||||
{% elif refractor_tier == 3 %}
|
|
||||||
/* T3 — Gold Refractor */
|
|
||||||
#header {
|
|
||||||
background: linear-gradient(135deg, rgba(195,155,35,0.26) 0%, rgba(235,200,70,0.2) 50%, rgba(195,155,35,0.26) 100%), #ffffff;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
#fullCard {
|
|
||||||
box-shadow: inset 0 0 16px 4px rgba(200,165,48,0.22);
|
|
||||||
}
|
|
||||||
.border-bot {
|
|
||||||
border-bottom-color: #c9a94e;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
}
|
|
||||||
.border-right-thick {
|
|
||||||
border-right-color: #c9a94e;
|
|
||||||
}
|
|
||||||
.border-right-thin {
|
|
||||||
border-right-color: #c9a94e;
|
|
||||||
border-right-width: 3px;
|
|
||||||
}
|
|
||||||
.vline {
|
|
||||||
border-left-color: #c9a94e;
|
|
||||||
}
|
|
||||||
.blue-gradient {
|
|
||||||
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
|
||||||
}
|
|
||||||
.red-gradient {
|
|
||||||
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* T3 shimmer animation — paused for static PNG capture */
|
|
||||||
@keyframes t3-shimmer {
|
|
||||||
0% { transform: translateX(-130%); }
|
|
||||||
100% { transform: translateX(230%); }
|
|
||||||
}
|
|
||||||
#header::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background: linear-gradient(
|
|
||||||
105deg,
|
|
||||||
transparent 38%,
|
|
||||||
rgba(255,240,140,0.18) 44%,
|
|
||||||
rgba(255,220,80,0.38) 50%,
|
|
||||||
rgba(255,200,60,0.30) 53%,
|
|
||||||
rgba(255,240,140,0.14) 58%,
|
|
||||||
transparent 64%
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 5;
|
|
||||||
animation: t3-shimmer 2.5s ease-in-out infinite;
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% elif refractor_tier == 4 %}
|
|
||||||
/* T4 — Superfractor */
|
|
||||||
#header {
|
|
||||||
background: #ffffff;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
#fullCard {
|
|
||||||
box-shadow: inset 0 0 22px 6px rgba(45,212,191,0.28), inset 0 0 39px 9px rgba(200,165,48,0.15);
|
|
||||||
}
|
|
||||||
.border-bot {
|
|
||||||
border-bottom-color: #c9a94e;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
}
|
|
||||||
.border-right-thick {
|
|
||||||
border-right-color: #c9a94e;
|
|
||||||
}
|
|
||||||
.border-right-thin {
|
|
||||||
border-right-color: #c9a94e;
|
|
||||||
}
|
|
||||||
.vline {
|
|
||||||
border-left-color: #c9a94e;
|
|
||||||
}
|
|
||||||
.blue-gradient {
|
|
||||||
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
|
||||||
}
|
|
||||||
.red-gradient {
|
|
||||||
background-image: linear-gradient(to right, rgba(195,160,40,1), rgba(220,185,60,0.55), rgba(195,160,40,1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* T4 prismatic header sweep — paused for static PNG capture */
|
|
||||||
@keyframes t4-prismatic-sweep {
|
|
||||||
0% { transform: translateX(0%); }
|
|
||||||
100% { transform: translateX(-50%); }
|
|
||||||
}
|
|
||||||
#header::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0;
|
|
||||||
width: 200%; height: 100%;
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
transparent 2%, rgba(255,100,100,0.28) 8%, rgba(255,200,50,0.32) 14%,
|
|
||||||
rgba(100,255,150,0.30) 20%, rgba(50,190,255,0.32) 26%, rgba(140,80,255,0.28) 32%,
|
|
||||||
rgba(255,100,180,0.24) 38%, transparent 44%,
|
|
||||||
transparent 52%, rgba(255,100,100,0.28) 58%, rgba(255,200,50,0.32) 64%,
|
|
||||||
rgba(100,255,150,0.30) 70%, rgba(50,190,255,0.32) 76%, rgba(140,80,255,0.28) 82%,
|
|
||||||
rgba(255,100,180,0.24) 88%, transparent 94%
|
|
||||||
);
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
animation: t4-prismatic-sweep 6s linear infinite;
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
#header > * { z-index: 2; }
|
|
||||||
|
|
||||||
/* T4 diamond glow pulse — paused for static PNG */
|
|
||||||
@keyframes diamond-glow-pulse {
|
|
||||||
0%, 100% { box-shadow: 0 0 0 1.5px rgba(0,0,0,0.7), 0 2px 5px rgba(0,0,0,0.5),
|
|
||||||
0 0 8px 2px rgba(107,45,142,0.6); }
|
|
||||||
50% { box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5), 0 2px 4px rgba(0,0,0,0.3),
|
|
||||||
0 0 14px 5px rgba(107,45,142,0.8),
|
|
||||||
0 0 24px 8px rgba(107,45,142,0.3); }
|
|
||||||
}
|
|
||||||
.tier-diamond.diamond-glow {
|
|
||||||
animation: diamond-glow-pulse 2s ease-in-out infinite;
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</style>
|
|
||||||
{% endif %}
|
|
||||||
@ -48,6 +48,8 @@ from app.db_engine import (
|
|||||||
PitchingCard,
|
PitchingCard,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorTierBoost,
|
||||||
|
RefractorCosmetic,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
ScoutOpportunity,
|
ScoutOpportunity,
|
||||||
ScoutClaim,
|
ScoutClaim,
|
||||||
@ -79,6 +81,8 @@ _TEST_MODELS = [
|
|||||||
ScoutClaim,
|
ScoutClaim,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorTierBoost,
|
||||||
|
RefractorCosmetic,
|
||||||
BattingCard,
|
BattingCard,
|
||||||
PitchingCard,
|
PitchingCard,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
|
|||||||
@ -72,6 +72,8 @@ from app.db_engine import (
|
|||||||
Rarity,
|
Rarity,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorCosmetic,
|
||||||
|
RefractorTierBoost,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -121,6 +123,8 @@ _WP13_MODELS = [
|
|||||||
PitchingCardRatings,
|
PitchingCardRatings,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorTierBoost,
|
||||||
|
RefractorCosmetic,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,6 @@ from app.services.refractor_boost import (
|
|||||||
PITCHER_PRIORITY,
|
PITCHER_PRIORITY,
|
||||||
PITCHER_XCHECK_COLUMNS,
|
PITCHER_XCHECK_COLUMNS,
|
||||||
)
|
)
|
||||||
from app.routers_v2.players import resolve_refractor_tier
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -912,34 +911,6 @@ class TestVariantHash:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Tier resolution (from variant hash)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestResolveTier:
|
|
||||||
"""Verify resolve_refractor_tier correctly reverse-maps variant hashes to
|
|
||||||
tier numbers using compute_variant_hash as the source of truth.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("tier", [1, 2, 3, 4])
|
|
||||||
def test_known_tier_roundtrips(self, tier):
|
|
||||||
"""resolve_refractor_tier returns the correct tier for a variant hash
|
|
||||||
produced by compute_variant_hash.
|
|
||||||
"""
|
|
||||||
player_id = 42
|
|
||||||
variant = compute_variant_hash(player_id, tier)
|
|
||||||
assert resolve_refractor_tier(player_id, variant) == tier
|
|
||||||
|
|
||||||
def test_base_card_returns_zero(self):
|
|
||||||
"""variant=0 (base card) always returns tier 0."""
|
|
||||||
assert resolve_refractor_tier(999, 0) == 0
|
|
||||||
|
|
||||||
def test_unknown_variant_returns_zero(self):
|
|
||||||
"""An unrecognized variant hash falls back to tier 0."""
|
|
||||||
assert resolve_refractor_tier(1, 99999999) == 0
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Batter display stats
|
# Batter display stats
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -52,6 +52,8 @@ from app.db_engine import (
|
|||||||
Rarity,
|
Rarity,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorCosmetic,
|
||||||
|
RefractorTierBoost,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -109,6 +111,8 @@ _BOOST_INT_MODELS = [
|
|||||||
PitchingCardRatings,
|
PitchingCardRatings,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorTierBoost,
|
||||||
|
RefractorCosmetic,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ Covers WP-01 acceptance criteria:
|
|||||||
- RefractorTrack: CRUD and unique-name constraint
|
- RefractorTrack: CRUD and unique-name constraint
|
||||||
- RefractorCardState: CRUD, defaults, unique-(player,team) constraint,
|
- RefractorCardState: CRUD, defaults, unique-(player,team) constraint,
|
||||||
and FK resolution back to RefractorTrack
|
and FK resolution back to RefractorTrack
|
||||||
|
- RefractorTierBoost: CRUD and unique-(track, tier, boost_type, boost_target)
|
||||||
|
- RefractorCosmetic: CRUD and unique-name constraint
|
||||||
- BattingSeasonStats: CRUD with defaults, unique-(player, team, season),
|
- BattingSeasonStats: CRUD with defaults, unique-(player, team, season),
|
||||||
and in-place stat accumulation
|
and in-place stat accumulation
|
||||||
|
|
||||||
@ -20,6 +22,8 @@ from playhouse.shortcuts import model_to_dict
|
|||||||
from app.db_engine import (
|
from app.db_engine import (
|
||||||
BattingSeasonStats,
|
BattingSeasonStats,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorCosmetic,
|
||||||
|
RefractorTierBoost,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -130,6 +134,115 @@ class TestRefractorCardState:
|
|||||||
assert resolved_track.name == "Batter Track"
|
assert resolved_track.name == "Batter Track"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# RefractorTierBoost
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestRefractorTierBoost:
|
||||||
|
"""Tests for RefractorTierBoost, the per-tier stat/rating bonus table.
|
||||||
|
|
||||||
|
Each row maps a (track, tier) combination to a single boost — the
|
||||||
|
specific stat or rating column to buff and by how much. The four-
|
||||||
|
column unique constraint prevents double-booking the same boost slot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_create_tier_boost(self, track):
|
||||||
|
"""Creating a boost row persists all fields accurately.
|
||||||
|
|
||||||
|
Verifies boost_type, boost_target, and boost_value are stored
|
||||||
|
and retrieved without modification.
|
||||||
|
"""
|
||||||
|
boost = RefractorTierBoost.create(
|
||||||
|
track=track,
|
||||||
|
tier=1,
|
||||||
|
boost_type="rating",
|
||||||
|
boost_target="contact_vl",
|
||||||
|
boost_value=1.5,
|
||||||
|
)
|
||||||
|
fetched = RefractorTierBoost.get_by_id(boost.id)
|
||||||
|
assert fetched.track_id == track.id
|
||||||
|
assert fetched.tier == 1
|
||||||
|
assert fetched.boost_type == "rating"
|
||||||
|
assert fetched.boost_target == "contact_vl"
|
||||||
|
assert fetched.boost_value == 1.5
|
||||||
|
|
||||||
|
def test_tier_boost_unique_constraint(self, track):
|
||||||
|
"""Duplicate (track, tier, boost_type, boost_target) raises IntegrityError.
|
||||||
|
|
||||||
|
The four-column unique index ensures that a single boost slot
|
||||||
|
(e.g. Tier-1 contact_vl rating) cannot be defined twice for the
|
||||||
|
same track, which would create ambiguity during evolution evaluation.
|
||||||
|
"""
|
||||||
|
RefractorTierBoost.create(
|
||||||
|
track=track,
|
||||||
|
tier=2,
|
||||||
|
boost_type="rating",
|
||||||
|
boost_target="power_vr",
|
||||||
|
boost_value=2.0,
|
||||||
|
)
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
RefractorTierBoost.create(
|
||||||
|
track=track,
|
||||||
|
tier=2,
|
||||||
|
boost_type="rating",
|
||||||
|
boost_target="power_vr",
|
||||||
|
boost_value=3.0, # different value, same identity columns
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# RefractorCosmetic
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestRefractorCosmetic:
|
||||||
|
"""Tests for RefractorCosmetic, decorative unlocks tied to evolution tiers.
|
||||||
|
|
||||||
|
Cosmetics are purely visual rewards (frames, badges, themes) that a
|
||||||
|
card unlocks when it reaches a required tier. The name column is
|
||||||
|
the stable identifier and carries a UNIQUE constraint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_create_cosmetic(self):
|
||||||
|
"""Creating a cosmetic persists all fields correctly.
|
||||||
|
|
||||||
|
Verifies all columns including optional ones (css_class, asset_url)
|
||||||
|
are stored and retrieved.
|
||||||
|
"""
|
||||||
|
cosmetic = RefractorCosmetic.create(
|
||||||
|
name="Gold Frame",
|
||||||
|
tier_required=2,
|
||||||
|
cosmetic_type="frame",
|
||||||
|
css_class="evo-frame-gold",
|
||||||
|
asset_url="https://cdn.example.com/frames/gold.png",
|
||||||
|
)
|
||||||
|
fetched = RefractorCosmetic.get_by_id(cosmetic.id)
|
||||||
|
assert fetched.name == "Gold Frame"
|
||||||
|
assert fetched.tier_required == 2
|
||||||
|
assert fetched.cosmetic_type == "frame"
|
||||||
|
assert fetched.css_class == "evo-frame-gold"
|
||||||
|
assert fetched.asset_url == "https://cdn.example.com/frames/gold.png"
|
||||||
|
|
||||||
|
def test_cosmetic_unique_name(self):
|
||||||
|
"""Inserting a second cosmetic with the same name raises IntegrityError.
|
||||||
|
|
||||||
|
The UNIQUE constraint on RefractorCosmetic.name prevents duplicate
|
||||||
|
cosmetic definitions that could cause ambiguous tier unlock lookups.
|
||||||
|
"""
|
||||||
|
RefractorCosmetic.create(
|
||||||
|
name="Silver Badge",
|
||||||
|
tier_required=1,
|
||||||
|
cosmetic_type="badge",
|
||||||
|
)
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
RefractorCosmetic.create(
|
||||||
|
name="Silver Badge", # duplicate
|
||||||
|
tier_required=3,
|
||||||
|
cosmetic_type="badge",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# BattingSeasonStats
|
# BattingSeasonStats
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -64,6 +64,8 @@ from app.db_engine import (
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
Rarity,
|
Rarity,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorCosmetic,
|
||||||
|
RefractorTierBoost,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -679,6 +681,8 @@ _STATE_API_MODELS = [
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorTierBoost,
|
||||||
|
RefractorCosmetic,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,8 @@ from app.db_engine import ( # noqa: E402
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
Rarity,
|
Rarity,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorCosmetic,
|
||||||
|
RefractorTierBoost,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -202,6 +204,8 @@ _TRACK_API_MODELS = [
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
|
RefractorTierBoost,
|
||||||
|
RefractorCosmetic,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user