Add format_date helper to handle datetime objects in templates

The db now returns datetime objects instead of strings in some cases.
Added format_date() helper function that handles both datetime and string
values, and replaced all [:10] date slicing with calls to this helper.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-09 01:20:32 +00:00
parent 647c564c47
commit 2267096271
4 changed files with 621 additions and 7 deletions

136
db.py
View File

@@ -72,7 +72,22 @@ CREATE TABLE IF NOT EXISTS activities (
actor_id TEXT NOT NULL,
object_data JSONB NOT NULL,
published TIMESTAMPTZ NOT NULL,
signature JSONB
signature JSONB,
anchor_root VARCHAR(64) -- Merkle root this activity is anchored to
);
-- Anchors table (Bitcoin timestamps via OpenTimestamps)
CREATE TABLE IF NOT EXISTS anchors (
id SERIAL PRIMARY KEY,
merkle_root VARCHAR(64) NOT NULL UNIQUE,
tree_ipfs_cid VARCHAR(128),
ots_proof_cid VARCHAR(128),
activity_count INTEGER NOT NULL,
first_activity_id UUID,
last_activity_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
confirmed_at TIMESTAMPTZ,
bitcoin_txid VARCHAR(64)
);
-- Followers table
@@ -94,6 +109,8 @@ CREATE INDEX IF NOT EXISTS idx_assets_created_at ON assets(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_assets_tags ON assets USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_activities_actor_id ON activities(actor_id);
CREATE INDEX IF NOT EXISTS idx_activities_published ON activities(published DESC);
CREATE INDEX IF NOT EXISTS idx_activities_anchor ON activities(anchor_root);
CREATE INDEX IF NOT EXISTS idx_anchors_created ON anchors(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_followers_username ON followers(username);
"""
@@ -567,3 +584,120 @@ async def get_stats() -> dict:
activities = await conn.fetchval("SELECT COUNT(*) FROM activities")
users = await conn.fetchval("SELECT COUNT(*) FROM users")
return {"assets": assets, "activities": activities, "users": users}
# ============ Anchors (Bitcoin timestamps) ============
async def get_unanchored_activities() -> list[dict]:
"""Get all activities not yet anchored to Bitcoin."""
async with get_connection() as conn:
rows = await conn.fetch(
"""SELECT activity_id, activity_type, actor_id, object_data, published, signature
FROM activities WHERE anchor_root IS NULL ORDER BY published ASC"""
)
return [_parse_activity_row(row) for row in rows]
async def create_anchor(anchor: dict) -> dict:
"""Create an anchor record."""
async with get_connection() as conn:
row = await conn.fetchrow(
"""INSERT INTO anchors (merkle_root, tree_ipfs_cid, ots_proof_cid,
activity_count, first_activity_id, last_activity_id)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *""",
anchor["merkle_root"],
anchor.get("tree_ipfs_cid"),
anchor.get("ots_proof_cid"),
anchor["activity_count"],
UUID(anchor["first_activity_id"]) if anchor.get("first_activity_id") else None,
UUID(anchor["last_activity_id"]) if anchor.get("last_activity_id") else None
)
return dict(row)
async def mark_activities_anchored(activity_ids: list[str], merkle_root: str) -> int:
"""Mark activities as anchored with the given merkle root."""
async with get_connection() as conn:
result = await conn.execute(
"""UPDATE activities SET anchor_root = $1
WHERE activity_id = ANY($2::uuid[])""",
merkle_root,
[UUID(aid) for aid in activity_ids]
)
# Returns "UPDATE N"
return int(result.split()[1]) if result else 0
async def get_anchor(merkle_root: str) -> Optional[dict]:
"""Get anchor by merkle root."""
async with get_connection() as conn:
row = await conn.fetchrow(
"SELECT * FROM anchors WHERE merkle_root = $1",
merkle_root
)
if row:
result = dict(row)
if result.get("first_activity_id"):
result["first_activity_id"] = str(result["first_activity_id"])
if result.get("last_activity_id"):
result["last_activity_id"] = str(result["last_activity_id"])
if result.get("created_at"):
result["created_at"] = result["created_at"].isoformat()
if result.get("confirmed_at"):
result["confirmed_at"] = result["confirmed_at"].isoformat()
return result
return None
async def get_all_anchors() -> list[dict]:
"""Get all anchors, newest first."""
async with get_connection() as conn:
rows = await conn.fetch(
"SELECT * FROM anchors ORDER BY created_at DESC"
)
results = []
for row in rows:
result = dict(row)
if result.get("first_activity_id"):
result["first_activity_id"] = str(result["first_activity_id"])
if result.get("last_activity_id"):
result["last_activity_id"] = str(result["last_activity_id"])
if result.get("created_at"):
result["created_at"] = result["created_at"].isoformat()
if result.get("confirmed_at"):
result["confirmed_at"] = result["confirmed_at"].isoformat()
results.append(result)
return results
async def update_anchor_confirmed(merkle_root: str, bitcoin_txid: str) -> bool:
"""Mark anchor as confirmed with Bitcoin txid."""
async with get_connection() as conn:
result = await conn.execute(
"""UPDATE anchors SET confirmed_at = NOW(), bitcoin_txid = $1
WHERE merkle_root = $2""",
bitcoin_txid, merkle_root
)
return result == "UPDATE 1"
async def get_anchor_stats() -> dict:
"""Get anchoring statistics."""
async with get_connection() as conn:
total_anchors = await conn.fetchval("SELECT COUNT(*) FROM anchors")
confirmed_anchors = await conn.fetchval(
"SELECT COUNT(*) FROM anchors WHERE confirmed_at IS NOT NULL"
)
anchored_activities = await conn.fetchval(
"SELECT COUNT(*) FROM activities WHERE anchor_root IS NOT NULL"
)
unanchored_activities = await conn.fetchval(
"SELECT COUNT(*) FROM activities WHERE anchor_root IS NULL"
)
return {
"total_anchors": total_anchors,
"confirmed_anchors": confirmed_anchors,
"anchored_activities": anchored_activities,
"unanchored_activities": unanchored_activities
}