Files
microdao-daarion/services/dao-service/repository_proposals.py
Apple 3de3c8cb36 feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
2025-11-27 00:19:40 -08:00

264 lines
9.0 KiB
Python

"""
Proposals Repository — Database operations for proposals
Phase 8: DAO Dashboard
"""
import uuid
from typing import List, Optional
from datetime import datetime
import asyncpg
from models import ProposalCreate, ProposalUpdate, ProposalRead, ProposalWithVotes
from decimal import Decimal
class ProposalRepository:
def __init__(self, db_pool: asyncpg.Pool):
self.db = db_pool
async def create_proposal(
self,
dao_id: str,
created_by_user_id: str,
data: ProposalCreate
) -> ProposalRead:
"""Create new proposal"""
proposal_id = uuid.uuid4()
row = await self.db.fetchrow(
"""
INSERT INTO dao_proposals (
id, dao_id, slug, title, description,
created_by_user_id, start_at, end_at,
governance_model_override, quorum_percent_override
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, dao_id, slug, title, description, created_by_user_id,
created_at, start_at, end_at, status,
governance_model_override, quorum_percent_override
""",
proposal_id,
uuid.UUID(dao_id),
data.slug,
data.title,
data.description,
uuid.UUID(created_by_user_id),
data.start_at,
data.end_at,
data.governance_model_override,
data.quorum_percent_override
)
return self._row_to_proposal(row)
async def update_proposal(
self,
proposal_id: str,
data: ProposalUpdate
) -> Optional[ProposalRead]:
"""Update proposal"""
updates = []
values = []
param_idx = 1
if data.title is not None:
updates.append(f"title = ${param_idx}")
values.append(data.title)
param_idx += 1
if data.description is not None:
updates.append(f"description = ${param_idx}")
values.append(data.description)
param_idx += 1
if data.start_at is not None:
updates.append(f"start_at = ${param_idx}")
values.append(data.start_at)
param_idx += 1
if data.end_at is not None:
updates.append(f"end_at = ${param_idx}")
values.append(data.end_at)
param_idx += 1
if not updates:
return await self.get_proposal(proposal_id)
values.append(uuid.UUID(proposal_id))
query = f"""
UPDATE dao_proposals
SET {', '.join(updates)}
WHERE id = ${param_idx}
RETURNING id, dao_id, slug, title, description, created_by_user_id,
created_at, start_at, end_at, status,
governance_model_override, quorum_percent_override
"""
row = await self.db.fetchrow(query, *values)
if not row:
return None
return self._row_to_proposal(row)
async def update_proposal_status(
self,
proposal_id: str,
status: str
) -> Optional[ProposalRead]:
"""Update proposal status"""
row = await self.db.fetchrow(
"""
UPDATE dao_proposals
SET status = $1
WHERE id = $2
RETURNING id, dao_id, slug, title, description, created_by_user_id,
created_at, start_at, end_at, status,
governance_model_override, quorum_percent_override
""",
status,
uuid.UUID(proposal_id)
)
if not row:
return None
return self._row_to_proposal(row)
async def get_proposal(self, proposal_id: str) -> Optional[ProposalRead]:
"""Get proposal by ID"""
row = await self.db.fetchrow(
"""
SELECT id, dao_id, slug, title, description, created_by_user_id,
created_at, start_at, end_at, status,
governance_model_override, quorum_percent_override
FROM dao_proposals
WHERE id = $1
""",
uuid.UUID(proposal_id)
)
if not row:
return None
return self._row_to_proposal(row)
async def get_proposal_by_slug(
self,
dao_id: str,
slug: str
) -> Optional[ProposalRead]:
"""Get proposal by DAO ID and slug"""
row = await self.db.fetchrow(
"""
SELECT id, dao_id, slug, title, description, created_by_user_id,
created_at, start_at, end_at, status,
governance_model_override, quorum_percent_override
FROM dao_proposals
WHERE dao_id = $1 AND slug = $2
""",
uuid.UUID(dao_id),
slug
)
if not row:
return None
return self._row_to_proposal(row)
async def list_proposals(
self,
dao_id: str,
status: Optional[str] = None
) -> List[ProposalRead]:
"""List all proposals for a DAO, optionally filtered by status"""
if status:
rows = await self.db.fetch(
"""
SELECT id, dao_id, slug, title, description, created_by_user_id,
created_at, start_at, end_at, status,
governance_model_override, quorum_percent_override
FROM dao_proposals
WHERE dao_id = $1 AND status = $2
ORDER BY created_at DESC
""",
uuid.UUID(dao_id),
status
)
else:
rows = await self.db.fetch(
"""
SELECT id, dao_id, slug, title, description, created_by_user_id,
created_at, start_at, end_at, status,
governance_model_override, quorum_percent_override
FROM dao_proposals
WHERE dao_id = $1
ORDER BY created_at DESC
""",
uuid.UUID(dao_id)
)
return [self._row_to_proposal(row) for row in rows]
async def get_proposal_with_votes(
self,
proposal_id: str
) -> Optional[ProposalWithVotes]:
"""Get proposal with voting statistics"""
proposal = await self.get_proposal(proposal_id)
if not proposal:
return None
# Get vote statistics
stats = await self.db.fetchrow(
"""
SELECT
COUNT(CASE WHEN vote_value = 'yes' THEN 1 END) as votes_yes,
COUNT(CASE WHEN vote_value = 'no' THEN 1 END) as votes_no,
COUNT(CASE WHEN vote_value = 'abstain' THEN 1 END) as votes_abstain,
COALESCE(SUM(CASE WHEN vote_value = 'yes' THEN weight ELSE 0 END), 0) as total_weight_yes,
COALESCE(SUM(CASE WHEN vote_value = 'no' THEN weight ELSE 0 END), 0) as total_weight_no,
COALESCE(SUM(CASE WHEN vote_value = 'abstain' THEN weight ELSE 0 END), 0) as total_weight_abstain
FROM dao_votes
WHERE proposal_id = $1
""",
uuid.UUID(proposal_id)
)
# Calculate if passed (simple majority for now)
total_weight = stats['total_weight_yes'] + stats['total_weight_no'] + stats['total_weight_abstain']
quorum_reached = False
is_passed = False
if total_weight > 0:
# For now, simple check: is quorum reached and yes > no
quorum_reached = True # Simplified for MVP
is_passed = stats['total_weight_yes'] > stats['total_weight_no']
return ProposalWithVotes(
**proposal.model_dump(),
votes_yes=stats['votes_yes'] or 0,
votes_no=stats['votes_no'] or 0,
votes_abstain=stats['votes_abstain'] or 0,
total_weight_yes=Decimal(str(stats['total_weight_yes'] or 0)),
total_weight_no=Decimal(str(stats['total_weight_no'] or 0)),
total_weight_abstain=Decimal(str(stats['total_weight_abstain'] or 0)),
quorum_reached=quorum_reached,
is_passed=is_passed
)
def _row_to_proposal(self, row: asyncpg.Record) -> ProposalRead:
"""Convert database row to ProposalRead"""
return ProposalRead(
id=str(row['id']),
dao_id=str(row['dao_id']),
slug=row['slug'],
title=row['title'],
description=row['description'],
created_by_user_id=str(row['created_by_user_id']),
created_at=row['created_at'],
start_at=row['start_at'],
end_at=row['end_at'],
status=row['status'],
governance_model_override=row['governance_model_override'],
quorum_percent_override=row['quorum_percent_override']
)