Sprint 1 : scaffolding complet de Glibredecision
Plateforme de decisions collectives pour Duniter/G1. Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services, moteur de vote avec formule d'inertie WoT/Smith/TechComm). Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores). Infrastructure Docker + Woodpecker CI + Traefik. Documentation technique et utilisateur (15 fichiers). Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote. 30 tests unitaires (formules, mode params, vote nuance) -- tous verts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
19
backend/app/models/__init__.py
Normal file
19
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from app.models.user import DuniterIdentity, Session
|
||||
from app.models.document import Document, DocumentItem, ItemVersion
|
||||
from app.models.decision import Decision, DecisionStep
|
||||
from app.models.vote import VoteSession, Vote
|
||||
from app.models.mandate import Mandate, MandateStep
|
||||
from app.models.protocol import VotingProtocol, FormulaConfig
|
||||
from app.models.sanctuary import SanctuaryEntry
|
||||
from app.models.cache import BlockchainCache
|
||||
|
||||
__all__ = [
|
||||
"DuniterIdentity", "Session",
|
||||
"Document", "DocumentItem", "ItemVersion",
|
||||
"Decision", "DecisionStep",
|
||||
"VoteSession", "Vote",
|
||||
"Mandate", "MandateStep",
|
||||
"VotingProtocol", "FormulaConfig",
|
||||
"SanctuaryEntry",
|
||||
"BlockchainCache",
|
||||
]
|
||||
18
backend/app/models/cache.py
Normal file
18
backend/app/models/cache.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, DateTime, func
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class BlockchainCache(Base):
|
||||
__tablename__ = "blockchain_cache"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
cache_key: Mapped[str] = mapped_column(String(256), unique=True, nullable=False, index=True)
|
||||
cache_value: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
fetched_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
42
backend/app/models/decision.py
Normal file
42
backend/app/models/decision.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Decision(Base):
|
||||
__tablename__ = "decisions"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
context: Mapped[str | None] = mapped_column(Text)
|
||||
decision_type: Mapped[str] = mapped_column(String(64), nullable=False) # runtime_upgrade, document_change, mandate_vote, custom
|
||||
status: Mapped[str] = mapped_column(String(32), default="draft") # draft, qualification, review, voting, executed, closed
|
||||
voting_protocol_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("voting_protocols.id"))
|
||||
created_by_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("duniter_identities.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
steps: Mapped[list["DecisionStep"]] = relationship(back_populates="decision", cascade="all, delete-orphan", order_by="DecisionStep.step_order")
|
||||
|
||||
|
||||
class DecisionStep(Base):
|
||||
__tablename__ = "decision_steps"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
decision_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("decisions.id"), nullable=False)
|
||||
step_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
step_type: Mapped[str] = mapped_column(String(32), nullable=False) # qualification, review, vote, execution, reporting
|
||||
title: Mapped[str | None] = mapped_column(String(256))
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(String(32), default="pending") # pending, active, completed, skipped
|
||||
vote_session_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("vote_sessions.id"))
|
||||
outcome: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
decision: Mapped["Decision"] = relationship(back_populates="steps")
|
||||
60
backend/app/models/document.py
Normal file
60
backend/app/models/document.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Document(Base):
|
||||
__tablename__ = "documents"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
slug: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
doc_type: Mapped[str] = mapped_column(String(64), nullable=False) # licence, engagement, reglement, constitution
|
||||
version: Mapped[str] = mapped_column(String(32), default="0.1.0")
|
||||
status: Mapped[str] = mapped_column(String(32), default="draft") # draft, active, archived
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
ipfs_cid: Mapped[str | None] = mapped_column(String(128))
|
||||
chain_anchor: Mapped[str | None] = mapped_column(String(128))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
items: Mapped[list["DocumentItem"]] = relationship(back_populates="document", cascade="all, delete-orphan", order_by="DocumentItem.position")
|
||||
|
||||
|
||||
class DocumentItem(Base):
|
||||
__tablename__ = "document_items"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
document_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("documents.id"), nullable=False)
|
||||
position: Mapped[str] = mapped_column(String(16), nullable=False) # "1", "1.1", "3.2"
|
||||
item_type: Mapped[str] = mapped_column(String(32), default="clause") # clause, rule, verification, preamble, section
|
||||
title: Mapped[str | None] = mapped_column(String(256))
|
||||
current_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
voting_protocol_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("voting_protocols.id"))
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
document: Mapped["Document"] = relationship(back_populates="items")
|
||||
versions: Mapped[list["ItemVersion"]] = relationship(back_populates="item", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ItemVersion(Base):
|
||||
__tablename__ = "item_versions"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
item_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("document_items.id"), nullable=False)
|
||||
proposed_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
diff_text: Mapped[str | None] = mapped_column(Text)
|
||||
rationale: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(String(32), default="proposed") # proposed, voting, accepted, rejected
|
||||
decision_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("decisions.id"))
|
||||
proposed_by_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("duniter_identities.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
item: Mapped["DocumentItem"] = relationship(back_populates="versions")
|
||||
43
backend/app/models/mandate.py
Normal file
43
backend/app/models/mandate.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Mandate(Base):
|
||||
__tablename__ = "mandates"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
mandate_type: Mapped[str] = mapped_column(String(64), nullable=False) # techcomm, smith, custom
|
||||
status: Mapped[str] = mapped_column(String(32), default="draft") # draft, candidacy, voting, active, reporting, completed, revoked
|
||||
mandatee_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("duniter_identities.id"))
|
||||
decision_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("decisions.id"))
|
||||
starts_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
steps: Mapped[list["MandateStep"]] = relationship(back_populates="mandate", cascade="all, delete-orphan", order_by="MandateStep.step_order")
|
||||
|
||||
|
||||
class MandateStep(Base):
|
||||
__tablename__ = "mandate_steps"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
mandate_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("mandates.id"), nullable=False)
|
||||
step_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
step_type: Mapped[str] = mapped_column(String(32), nullable=False) # formulation, candidacy, vote, assignment, reporting, completion, revocation
|
||||
title: Mapped[str | None] = mapped_column(String(256))
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(String(32), default="pending") # pending, active, completed, skipped
|
||||
vote_session_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("vote_sessions.id"))
|
||||
outcome: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
mandate: Mapped["Mandate"] = relationship(back_populates="steps")
|
||||
52
backend/app/models/protocol.py
Normal file
52
backend/app/models/protocol.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Integer, Float, Boolean, DateTime, ForeignKey, Text, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class FormulaConfig(Base):
|
||||
__tablename__ = "formula_configs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
# WoT threshold params
|
||||
duration_days: Mapped[int] = mapped_column(Integer, default=30)
|
||||
majority_pct: Mapped[int] = mapped_column(Integer, default=50)
|
||||
base_exponent: Mapped[float] = mapped_column(Float, default=0.1)
|
||||
gradient_exponent: Mapped[float] = mapped_column(Float, default=0.2)
|
||||
constant_base: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
|
||||
# Smith criterion
|
||||
smith_exponent: Mapped[float | None] = mapped_column(Float)
|
||||
|
||||
# TechComm criterion
|
||||
techcomm_exponent: Mapped[float | None] = mapped_column(Float)
|
||||
|
||||
# Nuanced vote
|
||||
nuanced_min_participants: Mapped[int | None] = mapped_column(Integer)
|
||||
nuanced_threshold_pct: Mapped[int | None] = mapped_column(Integer)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
protocols: Mapped[list["VotingProtocol"]] = relationship(back_populates="formula_config")
|
||||
|
||||
|
||||
class VotingProtocol(Base):
|
||||
__tablename__ = "voting_protocols"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
vote_type: Mapped[str] = mapped_column(String(32), nullable=False) # binary, nuanced
|
||||
formula_config_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("formula_configs.id"), nullable=False)
|
||||
mode_params: Mapped[str | None] = mapped_column(String(64)) # e.g. "D30M50B.1G.2T.1"
|
||||
is_meta_governed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
formula_config: Mapped["FormulaConfig"] = relationship(back_populates="protocols")
|
||||
23
backend/app/models/sanctuary.py
Normal file
23
backend/app/models/sanctuary.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Integer, Text, DateTime, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class SanctuaryEntry(Base):
|
||||
__tablename__ = "sanctuary_entries"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
entry_type: Mapped[str] = mapped_column(String(64), nullable=False) # document, decision, vote_result
|
||||
reference_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
|
||||
title: Mapped[str | None] = mapped_column(String(256))
|
||||
content_hash: Mapped[str] = mapped_column(String(128), nullable=False) # SHA-256
|
||||
ipfs_cid: Mapped[str | None] = mapped_column(String(128))
|
||||
chain_tx_hash: Mapped[str | None] = mapped_column(String(128))
|
||||
chain_block: Mapped[int | None] = mapped_column(Integer)
|
||||
metadata_json: Mapped[str | None] = mapped_column(Text) # JSON string for extra data
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
35
backend/app/models/user.py
Normal file
35
backend/app/models/user.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Boolean, DateTime, ForeignKey, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class DuniterIdentity(Base):
|
||||
__tablename__ = "duniter_identities"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
address: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
||||
display_name: Mapped[str | None] = mapped_column(String(128))
|
||||
wot_status: Mapped[str] = mapped_column(String(32), default="unknown") # member, pending, revoked, unknown
|
||||
is_smith: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
is_techcomm: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
sessions: Mapped[list["Session"]] = relationship(back_populates="identity", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Session(Base):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
token_hash: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||
identity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("duniter_identities.id"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
identity: Mapped["DuniterIdentity"] = relationship(back_populates="sessions")
|
||||
71
backend/app/models/vote.py
Normal file
71
backend/app/models/vote.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Integer, Float, Boolean, Text, DateTime, ForeignKey, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class VoteSession(Base):
|
||||
__tablename__ = "vote_sessions"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
decision_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("decisions.id"))
|
||||
item_version_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("item_versions.id"))
|
||||
voting_protocol_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("voting_protocols.id"), nullable=False)
|
||||
|
||||
# Snapshot at session start
|
||||
wot_size: Mapped[int] = mapped_column(Integer, default=0)
|
||||
smith_size: Mapped[int] = mapped_column(Integer, default=0)
|
||||
techcomm_size: Mapped[int] = mapped_column(Integer, default=0)
|
||||
|
||||
# Dates
|
||||
starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
ends_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
# Status
|
||||
status: Mapped[str] = mapped_column(String(32), default="open") # open, closed, tallied
|
||||
|
||||
# Tallies
|
||||
votes_for: Mapped[int] = mapped_column(Integer, default=0)
|
||||
votes_against: Mapped[int] = mapped_column(Integer, default=0)
|
||||
votes_total: Mapped[int] = mapped_column(Integer, default=0)
|
||||
smith_votes_for: Mapped[int] = mapped_column(Integer, default=0)
|
||||
techcomm_votes_for: Mapped[int] = mapped_column(Integer, default=0)
|
||||
threshold_required: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
result: Mapped[str | None] = mapped_column(String(32)) # adopted, rejected, null
|
||||
|
||||
# Chain recording
|
||||
chain_recorded: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
chain_tx_hash: Mapped[str | None] = mapped_column(String(128))
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
votes: Mapped[list["Vote"]] = relationship(back_populates="session", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Vote(Base):
|
||||
__tablename__ = "votes"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
session_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("vote_sessions.id"), nullable=False)
|
||||
voter_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("duniter_identities.id"), nullable=False)
|
||||
vote_value: Mapped[str] = mapped_column(String(32), nullable=False) # for, against, or nuanced levels
|
||||
nuanced_level: Mapped[int | None] = mapped_column(Integer) # 0-5 for nuanced votes
|
||||
comment: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
# Cryptographic proof
|
||||
signature: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
signed_payload: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
# Voter status snapshot
|
||||
voter_wot_status: Mapped[str] = mapped_column(String(32), default="member")
|
||||
voter_is_smith: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
voter_is_techcomm: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
session: Mapped["VoteSession"] = relationship(back_populates="votes")
|
||||
Reference in New Issue
Block a user