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:
136
db.py
136
db.py
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user