feat: Added VideoConsent and ConsentAuditLog models with ConsentField e…
- "backend/models.py" - "alembic/versions/017_add_consent_tables.py" GSD-Task: S03/T01
This commit is contained in:
parent
58865f5634
commit
4b7511d363
2 changed files with 139 additions and 0 deletions
51
alembic/versions/017_add_consent_tables.py
Normal file
51
alembic/versions/017_add_consent_tables.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""Add video_consents and consent_audit_log tables for per-video consent management.
|
||||||
|
|
||||||
|
Revision ID: 017_add_consent_tables
|
||||||
|
Revises: 016_add_users_and_invite_codes
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
|
revision = "017_add_consent_tables"
|
||||||
|
down_revision = "016_add_users_and_invite_codes"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create video_consents table
|
||||||
|
op.create_table(
|
||||||
|
"video_consents",
|
||||||
|
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.func.gen_random_uuid()),
|
||||||
|
sa.Column("source_video_id", UUID(as_uuid=True), sa.ForeignKey("source_videos.id", ondelete="CASCADE"), nullable=False),
|
||||||
|
sa.Column("creator_id", UUID(as_uuid=True), sa.ForeignKey("creators.id", ondelete="CASCADE"), nullable=False),
|
||||||
|
sa.Column("kb_inclusion", sa.Boolean(), nullable=False, server_default="false"),
|
||||||
|
sa.Column("training_usage", sa.Boolean(), nullable=False, server_default="false"),
|
||||||
|
sa.Column("public_display", sa.Boolean(), nullable=False, server_default="true"),
|
||||||
|
sa.Column("updated_by", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="RESTRICT"), nullable=False),
|
||||||
|
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||||
|
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||||
|
sa.UniqueConstraint("source_video_id", name="uq_video_consent_video"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create consent_audit_log table
|
||||||
|
op.create_table(
|
||||||
|
"consent_audit_log",
|
||||||
|
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.func.gen_random_uuid()),
|
||||||
|
sa.Column("video_consent_id", UUID(as_uuid=True), sa.ForeignKey("video_consents.id", ondelete="CASCADE"), nullable=False),
|
||||||
|
sa.Column("version", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("field_name", sa.String(50), nullable=False),
|
||||||
|
sa.Column("old_value", sa.Boolean(), nullable=True),
|
||||||
|
sa.Column("new_value", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("changed_by", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="RESTRICT"), nullable=False),
|
||||||
|
sa.Column("ip_address", sa.String(45), nullable=True),
|
||||||
|
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||||
|
)
|
||||||
|
op.create_index("ix_consent_audit_log_video_consent_id", "consent_audit_log", ["video_consent_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_consent_audit_log_video_consent_id", table_name="consent_audit_log")
|
||||||
|
op.drop_table("consent_audit_log")
|
||||||
|
op.drop_table("video_consents")
|
||||||
|
|
@ -566,3 +566,91 @@ class PipelineEvent(Base):
|
||||||
run: Mapped[PipelineRun | None] = sa_relationship(
|
run: Mapped[PipelineRun | None] = sa_relationship(
|
||||||
back_populates="events", foreign_keys=[run_id]
|
back_populates="events", foreign_keys=[run_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Consent Enums ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ConsentField(str, enum.Enum):
|
||||||
|
"""Fields that can be individually consented to per video."""
|
||||||
|
kb_inclusion = "kb_inclusion"
|
||||||
|
training_usage = "training_usage"
|
||||||
|
public_display = "public_display"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Consent Models ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class VideoConsent(Base):
|
||||||
|
"""Current consent state for a source video.
|
||||||
|
|
||||||
|
One row per video. Mutable — updated when a creator toggles consent.
|
||||||
|
The full change history lives in ConsentAuditLog.
|
||||||
|
"""
|
||||||
|
__tablename__ = "video_consents"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("source_video_id", name="uq_video_consent_video"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = _uuid_pk()
|
||||||
|
source_video_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("source_videos.id", ondelete="CASCADE"), nullable=False,
|
||||||
|
)
|
||||||
|
creator_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("creators.id", ondelete="CASCADE"), nullable=False,
|
||||||
|
)
|
||||||
|
kb_inclusion: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, default=False, server_default="false",
|
||||||
|
)
|
||||||
|
training_usage: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, default=False, server_default="false",
|
||||||
|
)
|
||||||
|
public_display: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, default=True, server_default="true",
|
||||||
|
)
|
||||||
|
updated_by: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("users.id", ondelete="RESTRICT"), nullable=False,
|
||||||
|
)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
default=_now, server_default=func.now()
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
default=_now, server_default=func.now(), onupdate=_now
|
||||||
|
)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
source_video: Mapped[SourceVideo] = sa_relationship()
|
||||||
|
creator: Mapped[Creator] = sa_relationship()
|
||||||
|
audit_entries: Mapped[list[ConsentAuditLog]] = sa_relationship(
|
||||||
|
back_populates="video_consent", order_by="ConsentAuditLog.version"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsentAuditLog(Base):
|
||||||
|
"""Append-only versioned record of per-field consent changes.
|
||||||
|
|
||||||
|
Each row captures a single field change. Version is auto-assigned
|
||||||
|
in application code (max(version) + 1 per video_consent_id).
|
||||||
|
"""
|
||||||
|
__tablename__ = "consent_audit_log"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = _uuid_pk()
|
||||||
|
video_consent_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("video_consents.id", ondelete="CASCADE"), nullable=False, index=True,
|
||||||
|
)
|
||||||
|
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
field_name: Mapped[str] = mapped_column(
|
||||||
|
String(50), nullable=False, doc="ConsentField value: kb_inclusion, training_usage, public_display"
|
||||||
|
)
|
||||||
|
old_value: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||||
|
new_value: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||||
|
changed_by: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
ForeignKey("users.id", ondelete="RESTRICT"), nullable=False,
|
||||||
|
)
|
||||||
|
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
default=_now, server_default=func.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
video_consent: Mapped[VideoConsent] = sa_relationship(
|
||||||
|
back_populates="audit_entries"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue