Update projektu

This commit is contained in:
lachtan
2026-07-22 12:32:02 +02:00
parent 19014ed3d9
commit 8e66d6b92a
22 changed files with 1995 additions and 503 deletions

View File

@@ -94,6 +94,35 @@ def _schedule_lines(conn, reminder_id: int) -> list[str]:
return lines
def _reject_duplicate_text(conn, text: str, exclude_id: int | None = None) -> bool:
"""Print an error and return True if an active reminder already has this text.
A single reminder text never needs more than one record: multiple times/days
belong on one reminder via repeated --at/--cron. This guard turns an
accidental split (the agent issuing several `add`s) into a hard error.
"""
dupes = store.find_active_by_exact_text(conn, text, exclude_id=exclude_id)
if not dupes:
return False
display_id = store.active_display_order(conn).index(dupes[0]["id"]) + 1
print(
json.dumps(
{
"error": "duplicate text",
"display_id": display_id,
"hint": (
"a reminder with this text already exists; to fire one message at "
"several times use a single add with repeated --at/--cron, or run: "
f"edit --id {display_id} --replace-schedules --at <t1> --at <t2> ..."
),
},
ensure_ascii=False,
),
file=sys.stderr,
)
return True
def _resolve_one(conn, args: argparse.Namespace) -> dict | None:
"""Resolve exactly one active reminder by --id (display ID) or --keyword (substring).
@@ -198,6 +227,10 @@ def cmd_add(args: argparse.Namespace) -> int:
)
return 1
with store.connection(DB_PATH) as conn:
if _reject_duplicate_text(conn, text):
return 1
try:
with store.transaction(DB_PATH) as conn:
now = _now()
@@ -271,6 +304,8 @@ def cmd_edit(args: argparse.Namespace) -> int:
if target is None:
return 1
rid = target["id"]
if new_text is not None and _reject_duplicate_text(conn, new_text, exclude_id=rid):
return 1
with store.transaction(DB_PATH) as conn:
now = _now()

View File

@@ -204,6 +204,27 @@ def find_active_by_keyword(conn: sqlite3.Connection, keyword: str) -> list[dict]
return [dict(r) for r in rows]
def find_active_by_exact_text(
conn: sqlite3.Connection, text: str, exclude_id: int | None = None
) -> list[dict]:
"""Active reminders whose text equals `text` (trimmed, case-insensitive).
Comparison happens in Python via ``casefold`` — SQLite's ``lower()`` only
folds ASCII, so Czech diacritics ("Čaj"/"čaj") would slip through. Pass
``exclude_id`` to ignore the reminder currently being edited.
"""
target = text.strip().casefold()
rows = conn.execute(
"SELECT id, text, enabled, timezone, created_at, updated_at, deleted_at "
"FROM reminders WHERE deleted_at IS NULL"
).fetchall()
return [
dict(r)
for r in rows
if r["id"] != exclude_id and r["text"].strip().casefold() == target
]
def active_display_order(conn: sqlite3.Connection) -> list[int]:
"""Internal ids of active reminders in display order (ascending by id)."""
rows = conn.execute(