feat(poll): enforce governed participation ownership
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
"""v0.1.10 governed signed participation
|
||||
|
||||
Revision ID: 4c5d6e7f8a9b
|
||||
Revises: 3b4c5d6e7f8a
|
||||
Create Date: 2026-07-21 00:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "4c5d6e7f8a9b"
|
||||
down_revision = "3b4c5d6e7f8a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"poll_invitations",
|
||||
sa.Column("response_gateway", sa.JSON(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"poll_invitations",
|
||||
sa.Column("participation_policy", sa.JSON(), nullable=True),
|
||||
)
|
||||
op.create_table(
|
||||
"poll_participation_submissions",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("poll_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("invitation_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("response_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("idempotency_key", sa.String(length=255), nullable=False),
|
||||
sa.Column("request_fingerprint", sa.String(length=64), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["poll_id"],
|
||||
["poll_polls.id"],
|
||||
name=op.f("fk_poll_participation_submissions_poll_id_poll_polls"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["invitation_id"],
|
||||
["poll_invitations.id"],
|
||||
name=op.f(
|
||||
"fk_poll_participation_submissions_invitation_id_poll_invitations"
|
||||
),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["response_id"],
|
||||
["poll_responses.id"],
|
||||
name=op.f("fk_poll_participation_submissions_response_id_poll_responses"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"id",
|
||||
name=op.f("pk_poll_participation_submissions"),
|
||||
),
|
||||
sa.UniqueConstraint(
|
||||
"invitation_id",
|
||||
"idempotency_key",
|
||||
name="uq_poll_participation_submission_idempotency",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_poll_participation_submissions_tenant_id"),
|
||||
"poll_participation_submissions",
|
||||
["tenant_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_poll_participation_submissions_poll_id"),
|
||||
"poll_participation_submissions",
|
||||
["poll_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_poll_participation_submissions_invitation_id"),
|
||||
"poll_participation_submissions",
|
||||
["invitation_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_poll_participation_submissions_response_id"),
|
||||
"poll_participation_submissions",
|
||||
["response_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_poll_participation_submission_poll_created",
|
||||
"poll_participation_submissions",
|
||||
["tenant_id", "poll_id", "created_at"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("poll_participation_submissions")
|
||||
op.drop_column("poll_invitations", "participation_policy")
|
||||
op.drop_column("poll_invitations", "response_gateway")
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
"""v0.1.10 durable Poll participation owner
|
||||
|
||||
Revision ID: 5d6e7f8a9b0c
|
||||
Revises: 4c5d6e7f8a9b
|
||||
Create Date: 2026-07-21 00:00:01.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "5d6e7f8a9b0c"
|
||||
down_revision = "4c5d6e7f8a9b"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _backfill_existing_governed_polls() -> None:
|
||||
"""Retain the owner of installations that already used revision 4c5."""
|
||||
|
||||
polls = sa.table(
|
||||
"poll_polls",
|
||||
sa.column("id", sa.String(length=36)),
|
||||
sa.column("participation_gateway", sa.JSON()),
|
||||
)
|
||||
invitations = sa.table(
|
||||
"poll_invitations",
|
||||
sa.column("id", sa.String(length=36)),
|
||||
sa.column("poll_id", sa.String(length=36)),
|
||||
sa.column("response_gateway", sa.JSON()),
|
||||
)
|
||||
connection = op.get_bind()
|
||||
rows = connection.execute(
|
||||
sa.select(
|
||||
invitations.c.poll_id,
|
||||
invitations.c.response_gateway,
|
||||
).order_by(invitations.c.poll_id.asc(), invitations.c.id.asc())
|
||||
).mappings()
|
||||
claimed_poll_ids: set[str] = set()
|
||||
for row in rows:
|
||||
poll_id = row["poll_id"]
|
||||
gateway = row["response_gateway"]
|
||||
if poll_id in claimed_poll_ids or not isinstance(gateway, dict):
|
||||
continue
|
||||
connection.execute(
|
||||
polls.update()
|
||||
.where(polls.c.id == poll_id)
|
||||
.where(polls.c.participation_gateway.is_(None))
|
||||
.values(participation_gateway=gateway)
|
||||
)
|
||||
claimed_poll_ids.add(poll_id)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"poll_polls",
|
||||
sa.Column("participation_gateway", sa.JSON(), nullable=True),
|
||||
)
|
||||
_backfill_existing_governed_polls()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("poll_polls", "participation_gateway")
|
||||
Reference in New Issue
Block a user