05/23 Phase 11
This commit is contained in:
@@ -325,6 +325,21 @@ All actions are logged to console and optionally to a log file:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### ✅ Phase 11 — Saved Predictions Viewer
|
||||||
|
- [x] Add `delete_prediction(pred_id)` and `delete_all_predictions(game_id=None)` to `db/models.py`
|
||||||
|
- [x] Refactor `ui/predictor_ui.py` to ttk.Notebook with two tabs
|
||||||
|
- [x] Generate tab — existing UI unchanged
|
||||||
|
- [x] Saved tab — treeview of all DB predictions (ID, Game, Strategy, Numbers, Bonus, Matches, Saved)
|
||||||
|
- [x] Match column — shows "X/5" comparing prediction vs last real draw (green if > 0, grey if zero)
|
||||||
|
- [x] Game filter dropdown — show all games or filter to one
|
||||||
|
- [x] Delete Selected — removes checked rows from DB + refreshes
|
||||||
|
- [x] Clear All — confirm dialog, then wipes all (or game-filtered) predictions
|
||||||
|
- [x] Auto-refreshes Saved tab after Save to DB
|
||||||
|
- [x] `_count_matches()` helper — set intersection between prediction and last draw numbers
|
||||||
|
- [x] `tests/test_saved_predictions.py` — 17 tests (214/214 total passing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### ✅ Phase 10 — Complete Analysis Screen (7 Charts)
|
### ✅ Phase 10 — Complete Analysis Screen (7 Charts)
|
||||||
- [x] Extend `ui/analysis.py` with 4 new chart tabs (was 3, now 7)
|
- [x] Extend `ui/analysis.py` with 4 new chart tabs (was 3, now 7)
|
||||||
- [x] Pairs — horizontal bar chart, top-20 most common number pairs
|
- [x] Pairs — horizontal bar chart, top-20 most common number pairs
|
||||||
|
|||||||
Binary file not shown.
@@ -356,6 +356,41 @@ def get_predictions(game_id=None, limit=50):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_prediction(pred_id: int):
|
||||||
|
"""Delete a single saved prediction by id."""
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DELETE FROM predictions WHERE id = ?", (pred_id,))
|
||||||
|
conn.commit()
|
||||||
|
logger.info("[PREDICT] Deleted prediction id=%d", pred_id)
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logger.error("[ERROR] delete_prediction failed: %s", e, exc_info=True)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_all_predictions(game_id=None):
|
||||||
|
"""Delete all saved predictions, optionally filtered by game."""
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
if game_id is not None:
|
||||||
|
cursor.execute("DELETE FROM predictions WHERE game_id = ?", (game_id,))
|
||||||
|
else:
|
||||||
|
cursor.execute("DELETE FROM predictions")
|
||||||
|
conn.commit()
|
||||||
|
logger.info("[PREDICT] Cleared all predictions (game_id=%s)", game_id)
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logger.error("[ERROR] delete_all_predictions failed: %s", e, exc_info=True)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
# FETCH LOG
|
# FETCH LOG
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"""
|
||||||
|
tests/test_saved_predictions.py
|
||||||
|
--------------------------------
|
||||||
|
Tests for the saved-prediction model functions (delete_prediction,
|
||||||
|
delete_all_predictions) and the _count_matches helper in predictor_ui.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from db.models import (
|
||||||
|
get_game_by_name, insert_draw, insert_prediction,
|
||||||
|
get_predictions, delete_prediction, delete_all_predictions,
|
||||||
|
get_last_draw,
|
||||||
|
)
|
||||||
|
from ui.predictor_ui import _count_matches
|
||||||
|
|
||||||
|
|
||||||
|
# ── Fixtures ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pb(tmp_db):
|
||||||
|
return get_game_by_name("Powerball")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mm(tmp_db):
|
||||||
|
return get_game_by_name("Mega Millions")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def with_predictions(pb, mm):
|
||||||
|
insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7)
|
||||||
|
p1 = insert_prediction(pb["id"], "Hot Numbers", [1, 13, 36, 61, 69], bonus=7)
|
||||||
|
p2 = insert_prediction(pb["id"], "Due Numbers", [5, 10, 20, 30, 40], bonus=15)
|
||||||
|
p3 = insert_prediction(mm["id"], "Monte Carlo", [2, 14, 37, 62, 70], bonus=5)
|
||||||
|
return p1, p2, p3
|
||||||
|
|
||||||
|
|
||||||
|
# ── delete_prediction ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_delete_prediction_removes_row(pb, with_predictions):
|
||||||
|
p1, p2, p3 = with_predictions
|
||||||
|
delete_prediction(p1)
|
||||||
|
ids = [p["id"] for p in get_predictions(limit=100_000)]
|
||||||
|
assert p1 not in ids
|
||||||
|
assert p2 in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_prediction_leaves_others_intact(pb, with_predictions):
|
||||||
|
p1, p2, p3 = with_predictions
|
||||||
|
delete_prediction(p1)
|
||||||
|
remaining = get_predictions(limit=100_000)
|
||||||
|
assert len(remaining) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_nonexistent_prediction_no_error(tmp_db):
|
||||||
|
delete_prediction(99999) # should not raise
|
||||||
|
|
||||||
|
|
||||||
|
# ── delete_all_predictions ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_delete_all_clears_everything(pb, with_predictions):
|
||||||
|
delete_all_predictions()
|
||||||
|
assert get_predictions(limit=100_000) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_all_with_game_id_only_removes_that_game(pb, mm, with_predictions):
|
||||||
|
p1, p2, p3 = with_predictions
|
||||||
|
delete_all_predictions(game_id=pb["id"])
|
||||||
|
remaining = get_predictions(limit=100_000)
|
||||||
|
assert len(remaining) == 1
|
||||||
|
assert remaining[0]["id"] == p3
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_all_empty_db_no_error(tmp_db):
|
||||||
|
delete_all_predictions() # should not raise
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_all_game_id_no_match_no_error(pb, tmp_db):
|
||||||
|
delete_all_predictions(game_id=pb["id"]) # nothing to delete
|
||||||
|
|
||||||
|
|
||||||
|
# ── _count_matches ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_count_matches_full_match(pb):
|
||||||
|
insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7)
|
||||||
|
last = get_last_draw(pb["id"])
|
||||||
|
assert _count_matches("1,13,36,61,69", last) == "5/5"
|
||||||
|
|
||||||
|
|
||||||
|
def test_count_matches_partial(pb):
|
||||||
|
insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7)
|
||||||
|
last = get_last_draw(pb["id"])
|
||||||
|
assert _count_matches("1,13,2,3,4", last) == "2/5"
|
||||||
|
|
||||||
|
|
||||||
|
def test_count_matches_zero(pb):
|
||||||
|
insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7)
|
||||||
|
last = get_last_draw(pb["id"])
|
||||||
|
assert _count_matches("2,3,4,5,6", last) == "0/5"
|
||||||
|
|
||||||
|
|
||||||
|
def test_count_matches_no_last_draw(tmp_db):
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
assert _count_matches("1,2,3,4,5", None) == "—"
|
||||||
|
|
||||||
|
|
||||||
|
# ── UI smoke tests ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _has_display():
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
r = tk.Tk(); r.withdraw(); r.destroy()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _has_display(), reason="no display available")
|
||||||
|
def test_predictor_screen_saved_tab_loads(tmp_db):
|
||||||
|
import tkinter as tk
|
||||||
|
from ui.predictor_ui import PredictorScreen
|
||||||
|
|
||||||
|
root = tk.Tk(); root.withdraw()
|
||||||
|
try:
|
||||||
|
screen = PredictorScreen(root)
|
||||||
|
screen.refresh()
|
||||||
|
assert screen.winfo_exists()
|
||||||
|
finally:
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _has_display(), reason="no display available")
|
||||||
|
def test_saved_tab_shows_predictions(tmp_db):
|
||||||
|
import tkinter as tk
|
||||||
|
from ui.predictor_ui import PredictorScreen
|
||||||
|
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
insert_prediction(pb["id"], "Hot Numbers", [1, 13, 36, 61, 69], bonus=7)
|
||||||
|
|
||||||
|
root = tk.Tk(); root.withdraw()
|
||||||
|
try:
|
||||||
|
screen = PredictorScreen(root)
|
||||||
|
screen.refresh()
|
||||||
|
children = screen._saved_tree.get_children()
|
||||||
|
assert len(children) == 1
|
||||||
|
finally:
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _has_display(), reason="no display available")
|
||||||
|
def test_saved_tab_delete_selected(tmp_db):
|
||||||
|
import tkinter as tk
|
||||||
|
from ui.predictor_ui import PredictorScreen
|
||||||
|
|
||||||
|
pb = get_game_by_name("Powerball")
|
||||||
|
pid = insert_prediction(pb["id"], "Hot Numbers", [1, 13, 36, 61, 69], bonus=7)
|
||||||
|
|
||||||
|
root = tk.Tk(); root.withdraw()
|
||||||
|
try:
|
||||||
|
screen = PredictorScreen(root)
|
||||||
|
screen.refresh()
|
||||||
|
iid = screen._saved_tree.get_children()[0]
|
||||||
|
screen._saved_tree.selection_set(iid)
|
||||||
|
screen._delete_selected()
|
||||||
|
assert len(screen._saved_tree.get_children()) == 0
|
||||||
|
assert get_predictions(limit=100_000) == []
|
||||||
|
finally:
|
||||||
|
root.destroy()
|
||||||
+190
-23
@@ -1,8 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
ui/predictor_ui.py
|
ui/predictor_ui.py
|
||||||
------------------
|
------------------
|
||||||
Prediction generator screen.
|
Prediction generator screen — two tabs inside a ttk.Notebook.
|
||||||
Pick a strategy + game + ticket count → generate → save to DB.
|
|
||||||
|
Generate tab — pick strategy + game + count → generate tickets → save to DB
|
||||||
|
Saved tab — browse all saved predictions, see match count vs last real
|
||||||
|
draw, delete individual rows or clear all
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -10,7 +13,11 @@ import tkinter as tk
|
|||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from db.models import get_all_games, get_game_by_name, insert_prediction
|
from db.models import (
|
||||||
|
get_all_games, get_game_by_name, get_last_draw,
|
||||||
|
insert_prediction, get_predictions,
|
||||||
|
delete_prediction, delete_all_predictions,
|
||||||
|
)
|
||||||
from core.predictor import (
|
from core.predictor import (
|
||||||
hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick,
|
hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick,
|
||||||
)
|
)
|
||||||
@@ -37,19 +44,45 @@ _DESCRIPTIONS = {
|
|||||||
_TICKET_COUNTS = [str(n) for n in range(1, 11)]
|
_TICKET_COUNTS = [str(n) for n in range(1, 11)]
|
||||||
|
|
||||||
|
|
||||||
|
def _count_matches(pred_numbers: str, last_draw) -> str:
|
||||||
|
"""Compare prediction numbers to last draw, return 'X/5' string."""
|
||||||
|
if last_draw is None:
|
||||||
|
return "—"
|
||||||
|
try:
|
||||||
|
pred_set = {int(n) for n in pred_numbers.split(",")}
|
||||||
|
draw_set = {int(n) for n in last_draw["numbers"].split(",")}
|
||||||
|
matched = len(pred_set & draw_set)
|
||||||
|
return f"{matched}/{len(pred_set)}"
|
||||||
|
except Exception:
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
|
||||||
class PredictorScreen(ttk.Frame):
|
class PredictorScreen(ttk.Frame):
|
||||||
def __init__(self, parent, **kwargs):
|
def __init__(self, parent, **kwargs):
|
||||||
super().__init__(parent, **kwargs)
|
super().__init__(parent, **kwargs)
|
||||||
self._game_id: int | None = None
|
self._game_id: int | None = None
|
||||||
self._tickets: list[dict] = [] # [{"numbers": [...], "bonus": int|None}]
|
self._tickets: list[dict] = []
|
||||||
self._strategy_name: str = "Hot Numbers"
|
self._strategy_name: str = "Hot Numbers"
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
# ── UI construction ───────────────────────────────────────────────────────
|
# ── UI construction ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
# ── Controls bar ──────────────────────────────────────────────────────
|
nb = ttk.Notebook(self)
|
||||||
bar = ttk.Frame(self, padding=(6, 6, 6, 4))
|
nb.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
gen_frame = ttk.Frame(nb)
|
||||||
|
saved_frame = ttk.Frame(nb)
|
||||||
|
nb.add(gen_frame, text="Generate")
|
||||||
|
nb.add(saved_frame, text="Saved")
|
||||||
|
|
||||||
|
self._build_generate_tab(gen_frame)
|
||||||
|
self._build_saved_tab(saved_frame)
|
||||||
|
|
||||||
|
# ── Generate tab ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _build_generate_tab(self, parent):
|
||||||
|
bar = ttk.Frame(parent, padding=(6, 6, 6, 4))
|
||||||
bar.pack(fill="x")
|
bar.pack(fill="x")
|
||||||
|
|
||||||
ttk.Label(bar, text="Game:").pack(side="left")
|
ttk.Label(bar, text="Game:").pack(side="left")
|
||||||
@@ -80,8 +113,8 @@ class PredictorScreen(ttk.Frame):
|
|||||||
self._gen_btn = ttk.Button(bar, text="Generate", command=self._generate)
|
self._gen_btn = ttk.Button(bar, text="Generate", command=self._generate)
|
||||||
self._gen_btn.pack(side="left", padx=(14, 0))
|
self._gen_btn.pack(side="left", padx=(14, 0))
|
||||||
|
|
||||||
# ── Results Treeview ──────────────────────────────────────────────────
|
# Results treeview
|
||||||
tree_frame = ttk.Frame(self)
|
tree_frame = ttk.Frame(parent)
|
||||||
tree_frame.pack(fill="both", expand=True, padx=6, pady=(4, 0))
|
tree_frame.pack(fill="both", expand=True, padx=6, pady=(4, 0))
|
||||||
|
|
||||||
cols = ("#", "numbers", "bonus")
|
cols = ("#", "numbers", "bonus")
|
||||||
@@ -100,8 +133,8 @@ class PredictorScreen(ttk.Frame):
|
|||||||
self._tree.pack(side="left", fill="both", expand=True)
|
self._tree.pack(side="left", fill="both", expand=True)
|
||||||
vsb.pack(side="right", fill="y")
|
vsb.pack(side="right", fill="y")
|
||||||
|
|
||||||
# ── Bottom bar ────────────────────────────────────────────────────────
|
# Bottom bar
|
||||||
bottom = ttk.Frame(self, padding=(6, 4))
|
bottom = ttk.Frame(parent, padding=(6, 4))
|
||||||
bottom.pack(fill="x")
|
bottom.pack(fill="x")
|
||||||
|
|
||||||
self._desc_var = tk.StringVar(value=_DESCRIPTIONS["Hot Numbers"])
|
self._desc_var = tk.StringVar(value=_DESCRIPTIONS["Hot Numbers"])
|
||||||
@@ -119,22 +152,79 @@ class PredictorScreen(ttk.Frame):
|
|||||||
)
|
)
|
||||||
self._save_btn.pack(side="right")
|
self._save_btn.pack(side="right")
|
||||||
|
|
||||||
ttk.Button(
|
ttk.Button(bottom, text="Clear", command=self._clear
|
||||||
bottom, text="Clear", command=self._clear
|
).pack(side="right", padx=(0, 4))
|
||||||
|
ttk.Button(bottom, text="Export CSV", command=self._export_csv
|
||||||
|
).pack(side="right", padx=(0, 4))
|
||||||
|
ttk.Button(bottom, text="Export Excel", command=self._export_excel
|
||||||
).pack(side="right", padx=(0, 4))
|
).pack(side="right", padx=(0, 4))
|
||||||
|
|
||||||
ttk.Button(
|
# ── Saved tab ─────────────────────────────────────────────────────────────
|
||||||
bottom, text="Export CSV", command=self._export_csv, state="normal"
|
|
||||||
).pack(side="right", padx=(0, 4))
|
|
||||||
|
|
||||||
ttk.Button(
|
def _build_saved_tab(self, parent):
|
||||||
bottom, text="Export Excel", command=self._export_excel, state="normal"
|
# Filter + action bar
|
||||||
).pack(side="right", padx=(0, 4))
|
bar = ttk.Frame(parent, padding=(6, 6, 6, 4))
|
||||||
|
bar.pack(fill="x")
|
||||||
|
|
||||||
# ── Callbacks ─────────────────────────────────────────────────────────────
|
ttk.Label(bar, text="Game:").pack(side="left")
|
||||||
|
self._saved_game_var = tk.StringVar(value="All Games")
|
||||||
|
self._saved_game_cb = ttk.Combobox(
|
||||||
|
bar, textvariable=self._saved_game_var, state="readonly", width=15
|
||||||
|
)
|
||||||
|
self._saved_game_cb.pack(side="left", padx=(4, 14))
|
||||||
|
self._saved_game_cb.bind("<<ComboboxSelected>>", lambda _: self._refresh_saved())
|
||||||
|
|
||||||
|
ttk.Button(bar, text="↻ Refresh", command=self._refresh_saved).pack(side="left", padx=(0, 4))
|
||||||
|
ttk.Button(bar, text="Delete Selected", command=self._delete_selected).pack(side="left", padx=(0, 4))
|
||||||
|
ttk.Button(bar, text="Clear All", command=self._clear_all_saved).pack(side="left")
|
||||||
|
|
||||||
|
self._saved_count_var = tk.StringVar(value="0 saved")
|
||||||
|
ttk.Label(bar, textvariable=self._saved_count_var,
|
||||||
|
foreground="#777777").pack(side="right")
|
||||||
|
|
||||||
|
# Treeview
|
||||||
|
tree_frame = ttk.Frame(parent)
|
||||||
|
tree_frame.pack(fill="both", expand=True, padx=6, pady=(2, 6))
|
||||||
|
|
||||||
|
s_cols = ("id", "game", "strategy", "numbers", "bonus", "matches", "saved")
|
||||||
|
self._saved_tree = ttk.Treeview(
|
||||||
|
tree_frame, columns=s_cols, show="headings", selectmode="extended"
|
||||||
|
)
|
||||||
|
self._saved_tree.heading("id", text="ID")
|
||||||
|
self._saved_tree.heading("game", text="Game")
|
||||||
|
self._saved_tree.heading("strategy", text="Strategy")
|
||||||
|
self._saved_tree.heading("numbers", text="Numbers")
|
||||||
|
self._saved_tree.heading("bonus", text="Bonus")
|
||||||
|
self._saved_tree.heading("matches", text="Matches")
|
||||||
|
self._saved_tree.heading("saved", text="Saved")
|
||||||
|
|
||||||
|
self._saved_tree.column("id", width=45, anchor="center", stretch=False)
|
||||||
|
self._saved_tree.column("game", width=120, anchor="w", stretch=False)
|
||||||
|
self._saved_tree.column("strategy", width=120, anchor="w", stretch=False)
|
||||||
|
self._saved_tree.column("numbers", width=220, anchor="w")
|
||||||
|
self._saved_tree.column("bonus", width=55, anchor="center", stretch=False)
|
||||||
|
self._saved_tree.column("matches", width=65, anchor="center", stretch=False)
|
||||||
|
self._saved_tree.column("saved", width=130, anchor="center", stretch=False)
|
||||||
|
|
||||||
|
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self._saved_tree.yview)
|
||||||
|
hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self._saved_tree.xview)
|
||||||
|
self._saved_tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
|
||||||
|
|
||||||
|
self._saved_tree.grid(row=0, column=0, sticky="nsew")
|
||||||
|
vsb.grid(row=0, column=1, sticky="ns")
|
||||||
|
hsb.grid(row=1, column=0, sticky="ew")
|
||||||
|
tree_frame.rowconfigure(0, weight=1)
|
||||||
|
tree_frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Colour rows by match count
|
||||||
|
self._saved_tree.tag_configure("match_good", foreground="#1e8449")
|
||||||
|
self._saved_tree.tag_configure("match_zero", foreground="#aaaaaa")
|
||||||
|
|
||||||
|
# ── Refresh / data ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self._load_games()
|
self._load_games()
|
||||||
|
self._refresh_saved()
|
||||||
|
|
||||||
def _load_games(self):
|
def _load_games(self):
|
||||||
games = get_all_games(active_only=True)
|
games = get_all_games(active_only=True)
|
||||||
@@ -146,6 +236,59 @@ class PredictorScreen(ttk.Frame):
|
|||||||
game = get_game_by_name(self._game_var.get())
|
game = get_game_by_name(self._game_var.get())
|
||||||
self._game_id = game["id"] if game else None
|
self._game_id = game["id"] if game else None
|
||||||
|
|
||||||
|
saved_names = ["All Games"] + names
|
||||||
|
self._saved_game_cb["values"] = saved_names
|
||||||
|
if self._saved_game_var.get() not in saved_names:
|
||||||
|
self._saved_game_var.set("All Games")
|
||||||
|
|
||||||
|
def _refresh_saved(self):
|
||||||
|
name = self._saved_game_var.get()
|
||||||
|
game_id = None
|
||||||
|
if name != "All Games":
|
||||||
|
g = get_game_by_name(name)
|
||||||
|
game_id = g["id"] if g else None
|
||||||
|
|
||||||
|
preds = get_predictions(game_id=game_id, limit=100_000)
|
||||||
|
|
||||||
|
# Cache last draw per game to avoid repeated DB hits
|
||||||
|
last_draw_cache: dict[int, object] = {}
|
||||||
|
|
||||||
|
self._saved_tree.delete(*self._saved_tree.get_children())
|
||||||
|
for p in preds:
|
||||||
|
gid = p["game_id"]
|
||||||
|
if gid not in last_draw_cache:
|
||||||
|
last_draw_cache[gid] = get_last_draw(gid)
|
||||||
|
last = last_draw_cache[gid]
|
||||||
|
|
||||||
|
match_str = _count_matches(p["numbers"], last)
|
||||||
|
nums_fmt = " ".join(f"{int(n):02d}" for n in p["numbers"].split(","))
|
||||||
|
saved_ts = p["created_at"][:16].replace("T", " ")
|
||||||
|
|
||||||
|
try:
|
||||||
|
matched_n = int(match_str.split("/")[0])
|
||||||
|
tag = "match_good" if matched_n > 0 else "match_zero"
|
||||||
|
except Exception:
|
||||||
|
tag = ""
|
||||||
|
|
||||||
|
self._saved_tree.insert("", "end",
|
||||||
|
iid=str(p["id"]),
|
||||||
|
values=(
|
||||||
|
p["id"],
|
||||||
|
p["game_name"],
|
||||||
|
p["strategy"],
|
||||||
|
nums_fmt,
|
||||||
|
p["bonus"] or "—",
|
||||||
|
match_str,
|
||||||
|
saved_ts,
|
||||||
|
),
|
||||||
|
tags=(tag,),
|
||||||
|
)
|
||||||
|
|
||||||
|
count = len(preds)
|
||||||
|
self._saved_count_var.set(f"{count} saved prediction{'s' if count != 1 else ''}")
|
||||||
|
|
||||||
|
# ── Generate tab callbacks ────────────────────────────────────────────────
|
||||||
|
|
||||||
def _on_game_change(self):
|
def _on_game_change(self):
|
||||||
game = get_game_by_name(self._game_var.get())
|
game = get_game_by_name(self._game_var.get())
|
||||||
self._game_id = game["id"] if game else None
|
self._game_id = game["id"] if game else None
|
||||||
@@ -201,6 +344,7 @@ class PredictorScreen(ttk.Frame):
|
|||||||
saved += 1
|
saved += 1
|
||||||
self._status_var.set(f"Saved {saved} prediction{'s' if saved != 1 else ''} to DB.")
|
self._status_var.set(f"Saved {saved} prediction{'s' if saved != 1 else ''} to DB.")
|
||||||
self._save_btn.config(state="disabled")
|
self._save_btn.config(state="disabled")
|
||||||
|
self._refresh_saved()
|
||||||
logger.info("[PREDICT] Saved %d prediction(s) to DB", saved)
|
logger.info("[PREDICT] Saved %d prediction(s) to DB", saved)
|
||||||
|
|
||||||
def _clear(self):
|
def _clear(self):
|
||||||
@@ -220,8 +364,7 @@ class PredictorScreen(ttk.Frame):
|
|||||||
if not fp:
|
if not fp:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
game_id = self._game_id
|
export_predictions_excel(fp, game_id=self._game_id)
|
||||||
export_predictions_excel(fp, game_id=game_id)
|
|
||||||
self._status_var.set(f"Exported → {os.path.basename(fp)}")
|
self._status_var.set(f"Exported → {os.path.basename(fp)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Export failed", str(e))
|
messagebox.showerror("Export failed", str(e))
|
||||||
@@ -237,8 +380,32 @@ class PredictorScreen(ttk.Frame):
|
|||||||
if not fp:
|
if not fp:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
game_id = self._game_id
|
export_predictions_csv(fp, game_id=self._game_id)
|
||||||
export_predictions_csv(fp, game_id=game_id)
|
|
||||||
self._status_var.set(f"Exported → {os.path.basename(fp)}")
|
self._status_var.set(f"Exported → {os.path.basename(fp)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Export failed", str(e))
|
messagebox.showerror("Export failed", str(e))
|
||||||
|
|
||||||
|
# ── Saved tab callbacks ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _delete_selected(self):
|
||||||
|
selected = self._saved_tree.selection()
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
for iid in selected:
|
||||||
|
pred_id = int(self._saved_tree.item(iid, "values")[0])
|
||||||
|
delete_prediction(pred_id)
|
||||||
|
self._refresh_saved()
|
||||||
|
|
||||||
|
def _clear_all_saved(self):
|
||||||
|
name = self._saved_game_var.get()
|
||||||
|
game_id = None
|
||||||
|
if name != "All Games":
|
||||||
|
g = get_game_by_name(name)
|
||||||
|
game_id = g["id"] if g else None
|
||||||
|
|
||||||
|
scope = f" for {name}" if game_id else ""
|
||||||
|
if not messagebox.askyesno("Clear All",
|
||||||
|
f"Delete all saved predictions{scope}?"):
|
||||||
|
return
|
||||||
|
delete_all_predictions(game_id=game_id)
|
||||||
|
self._refresh_saved()
|
||||||
|
|||||||
Reference in New Issue
Block a user