05/23 Phase 12

This commit is contained in:
2026-05-23 12:37:01 -04:00
parent 78e88ae8ab
commit ae0c64f731
7 changed files with 172 additions and 1 deletions
+14
View File
@@ -325,6 +325,20 @@ All actions are logged to console and optionally to a log file:
---
### ✅ Phase 12 — Number Search + Next Draw Schedule
- [x] Number search in History screen
- [x] Add `number` param to `get_draws_with_game()` — SQL: `',' || numbers || ',' LIKE ?` for exact boundary matching
- [x] Add "Number:" entry field to History filter bar; bound to `<Return>` for quick search
- [x] Wire through `_apply_filter()` and `_clear_filter()`
- [x] 7 new tests — exact match, boundary false-positive prevention, combined with game/date filter
- [x] Next draw schedule on Dashboard
- [x] `_next_draw_date(game_name, from_date)` — computes next Powerball (Mon/Wed/Sat) or Mega Millions (Tue/Fri) draw date
- [x] Shown on each Last Draw card as "Next draw: Sat, May 24" in green
- [x] 6 new tests — day-of-week transitions, unknown game, always-in-future guarantee
- [x] 227/227 tests passing
---
### ✅ 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
Binary file not shown.
+7 -1
View File
@@ -224,10 +224,12 @@ def get_draw_count(game_id):
conn.close()
def get_draws_with_game(game_id=None, limit=None, date_from=None, date_to=None, order="DESC"):
def get_draws_with_game(game_id=None, limit=None, date_from=None, date_to=None,
order="DESC", number=None):
"""
Like get_draws() but JOINs games so each row includes game_name.
game_id=None returns draws for all games.
number (int|str): if given, restrict to draws containing that ball number.
Used by the History screen.
"""
conn = get_connection()
@@ -249,6 +251,10 @@ def get_draws_with_game(game_id=None, limit=None, date_from=None, date_to=None,
if date_to:
query += " AND d.draw_date <= ?"
params.append(date_to)
if number is not None:
# Wrap stored CSV in commas so boundary matches are exact
query += " AND ',' || d.numbers || ',' LIKE ?"
params.append(f"%,{int(number)},%")
order_sql = "DESC" if order.upper() == "DESC" else "ASC"
query += f" ORDER BY d.draw_date {order_sql}, g.name ASC"
+45
View File
@@ -4,6 +4,8 @@ tests/test_dashboard.py
Tests for the Dashboard screen data layer and UI smoke tests.
"""
from datetime import date
import pytest
from db.models import (
get_all_games, get_game_by_name, get_last_draw,
@@ -11,6 +13,7 @@ from db.models import (
)
from db.database import get_db_stats
from core.analyzer import frequency_analysis
from ui.dashboard import _next_draw_date
# ── Data-layer checks used by the Dashboard ───────────────────────────────────
@@ -77,6 +80,48 @@ def test_predictions_count_correct(tmp_db):
assert len(get_predictions(limit=100_000)) == 2
# ── _next_draw_date ───────────────────────────────────────────────────────────
def test_next_draw_powerball_from_friday():
# Friday → next Powerball is Saturday
fri = date(2024, 5, 24) # Friday, weekday=4
result = _next_draw_date("Powerball", from_date=fri)
assert "Sat" in result
def test_next_draw_powerball_from_saturday():
# Saturday → next Powerball is Monday
sat = date(2024, 5, 25) # Saturday, weekday=5
result = _next_draw_date("Powerball", from_date=sat)
assert "Mon" in result
def test_next_draw_mega_millions_from_friday():
# Friday → next MM is Tuesday (4 days away)
fri = date(2024, 5, 24)
result = _next_draw_date("Mega Millions", from_date=fri)
assert "Tue" in result
def test_next_draw_mega_millions_from_monday():
# Monday → next MM is Tuesday (1 day away)
mon = date(2024, 5, 20) # Monday, weekday=0
result = _next_draw_date("Mega Millions", from_date=mon)
assert "Tue" in result
def test_next_draw_unknown_game_returns_empty():
assert _next_draw_date("Unknown Game") == ""
def test_next_draw_always_in_future():
today = date.today()
for name in ("Powerball", "Mega Millions"):
result = _next_draw_date(name, from_date=today)
assert result != "" # always finds a future date within 7 days
# ── UI smoke tests ────────────────────────────────────────────────────────────
def _has_display():
+66
View File
@@ -122,6 +122,72 @@ def test_combined_filters(tmp_db):
assert rows[0]["draw_date"] == "2024-03-01"
# ── number filter ─────────────────────────────────────────────────────────────
def test_number_filter_returns_matching_draws(tmp_db):
pb = get_game_by_name("Powerball")
insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7)
insert_draw(pb["id"], "2024-01-03", [2, 14, 37, 55, 68], bonus=10)
rows = get_draws_with_game(number=13)
assert len(rows) == 1
assert rows[0]["draw_date"] == "2024-01-01"
def test_number_filter_no_match_returns_empty(tmp_db):
pb = get_game_by_name("Powerball")
insert_draw(pb["id"], "2024-01-01", [1, 2, 3, 4, 5], bonus=7)
rows = get_draws_with_game(number=99)
assert rows == []
def test_number_filter_does_not_false_match_substring(tmp_db):
pb = get_game_by_name("Powerball")
# numbers 1 and 13 — searching for 1 must NOT return the draw containing 13
insert_draw(pb["id"], "2024-01-01", [13, 23, 33, 43, 53], bonus=7)
rows = get_draws_with_game(number=1)
assert rows == []
def test_number_filter_matches_first_position(tmp_db):
pb = get_game_by_name("Powerball")
insert_draw(pb["id"], "2024-01-01", [1, 2, 3, 4, 5], bonus=7)
rows = get_draws_with_game(number=1)
assert len(rows) == 1
def test_number_filter_matches_last_position(tmp_db):
pb = get_game_by_name("Powerball")
insert_draw(pb["id"], "2024-01-01", [1, 2, 3, 4, 69], bonus=7)
rows = get_draws_with_game(number=69)
assert len(rows) == 1
def test_number_filter_combined_with_game(tmp_db):
pb = get_game_by_name("Powerball")
mm = get_game_by_name("Mega Millions")
insert_draw(pb["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7)
insert_draw(mm["id"], "2024-01-02", [1, 14, 37, 55, 68], bonus=4)
rows = get_draws_with_game(game_id=pb["id"], number=1)
assert len(rows) == 1
assert rows[0]["game_name"] == "Powerball"
def test_number_filter_none_returns_all(tmp_db):
pb = get_game_by_name("Powerball")
insert_draw(pb["id"], "2024-01-01", [1, 2, 3, 4, 5], bonus=7)
insert_draw(pb["id"], "2024-01-03", [6, 7, 8, 9, 10], bonus=2)
rows = get_draws_with_game(number=None)
assert len(rows) == 2
# ── UI smoke test (skipped on headless) ───────────────────────────────────────
def _has_display():
+25
View File
@@ -11,11 +11,31 @@ Sections:
import tkinter as tk
from tkinter import ttk
from datetime import date, timedelta
from db.database import get_db_stats
from db.models import get_all_games, get_last_draw, get_draw_count, get_predictions
from core.analyzer import frequency_analysis
# Weekday indices: Monday=0 … Sunday=6
_DRAW_DAYS: dict[str, list[int]] = {
"Powerball": [0, 2, 5], # Mon, Wed, Sat
"Mega Millions": [1, 4], # Tue, Fri
}
def _next_draw_date(game_name: str, from_date: date | None = None) -> str:
"""Return the next scheduled draw date as 'Day, Mon DD' (e.g. 'Sat, May 24')."""
draw_days = _DRAW_DAYS.get(game_name)
if not draw_days:
return ""
today = from_date or date.today()
for delta in range(1, 8):
candidate = today + timedelta(days=delta)
if candidate.weekday() in draw_days:
return f"{candidate.strftime('%a, %b')} {candidate.day}"
return ""
_HEADER_FONT = ("TkDefaultFont", 10, "bold")
_CARD_FONT = ("TkDefaultFont", 9)
_NUMBER_FONT = ("TkDefaultFont", 12, "bold")
@@ -122,6 +142,11 @@ class DashboardScreen(ttk.Frame):
ttk.Label(card, text=f"Multiplier: {draw['multiplier']}",
foreground="#777777", font=_CARD_FONT).pack(anchor="w")
next_draw = _next_draw_date(game["name"])
if next_draw:
ttk.Label(card, text=f"Next draw: {next_draw}",
foreground="#117a65", font=_CARD_FONT).pack(anchor="w", pady=(6, 0))
def _refresh_db_summary(self):
for w in self._db_body.winfo_children():
w.destroy()
+15
View File
@@ -78,6 +78,12 @@ class HistoryScreen(ttk.Frame):
self._to_entry.bind("<FocusIn>", lambda e: self._clear_hint(self._to_entry, "YYYY-MM-DD"))
self._to_entry.bind("<FocusOut>", lambda e: self._restore_hint(self._to_entry, "YYYY-MM-DD"))
ttk.Label(bar, text="Number:", padding=(8, 0, 0, 0)).pack(side="left")
self._number_var = tk.StringVar()
self._number_entry = ttk.Entry(bar, textvariable=self._number_var, width=5)
self._number_entry.pack(side="left", padx=(4, 14))
self._number_entry.bind("<Return>", lambda e: self._apply_filter())
ttk.Button(bar, text="Search", command=self._apply_filter).pack(side="left", padx=(0, 4))
ttk.Button(bar, text="Clear", command=self._clear_filter).pack(side="left")
@@ -147,6 +153,9 @@ class HistoryScreen(ttk.Frame):
date_from = raw_from if raw_from and raw_from != "YYYY-MM-DD" else None
date_to = raw_to if raw_to and raw_to != "YYYY-MM-DD" else None
raw_num = self._number_var.get().strip()
number = int(raw_num) if raw_num.isdigit() else None
if game_name == "All Games":
game_id = None
else:
@@ -158,6 +167,7 @@ class HistoryScreen(ttk.Frame):
date_from=date_from,
date_to=date_to,
order="DESC" if not self._sort_asc else "ASC",
number=number,
)
self._populate(rows)
@@ -214,6 +224,7 @@ class HistoryScreen(ttk.Frame):
self._from_entry.insert(0, "YYYY-MM-DD")
self._to_entry.delete(0, "end")
self._to_entry.insert(0, "YYYY-MM-DD")
self._number_var.set("")
self._sort_col = "date"
self._sort_asc = False
for c in _COLUMNS:
@@ -237,6 +248,10 @@ class HistoryScreen(ttk.Frame):
date_to = raw_to if raw_to and raw_to != "YYYY-MM-DD" else None
return game_id, date_from, date_to
def _active_number(self):
raw = self._number_var.get().strip()
return int(raw) if raw.isdigit() else None
def _export_excel(self):
ensure_exports_dir()
fp = filedialog.asksaveasfilename(