from __future__ import annotations
from onegov.core.orm import Base
from onegov.core.orm.mixins import TimestampMixin
from onegov.election_day.models.vote.mixins import DerivedAttributesMixin
from onegov.election_day.models.vote.mixins import DerivedBallotsCountMixin
from sqlalchemy import func
from sqlalchemy import ForeignKey
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped
from uuid import uuid4
from uuid import UUID
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from onegov.election_day.models import Ballot
from sqlalchemy.sql import ColumnElement
[docs]
class BallotResult(Base, TimestampMixin, DerivedAttributesMixin,
DerivedBallotsCountMixin):
""" The result of a specific ballot. Each ballot may have multiple
results. Those results may be aggregated or not.
"""
[docs]
__tablename__ = 'ballot_results'
#: identifies the result, may be used in the url
[docs]
id: Mapped[UUID] = mapped_column(
primary_key=True,
default=uuid4
)
#: The entity id (e.g. BFS number).
#: the name of the entity
#: the district this entity belongs to
[docs]
district: Mapped[str | None]
#: True if the result has been counted and no changes will be made anymore.
#: If the result is definite, all the values below must be specified.
#: number of yeas, in case of variants, the number of yeas for the first
#: option of the tie breaker
[docs]
yeas: Mapped[int] = mapped_column(default=lambda: 0)
#: number of nays, in case of variants, the number of nays for the first
#: option of the tie breaker (so a yay for the second option)
[docs]
nays: Mapped[int] = mapped_column(default=lambda: 0)
#: number of empty votes
[docs]
empty: Mapped[int] = mapped_column(default=lambda: 0)
#: number of invalid votes
[docs]
invalid: Mapped[int] = mapped_column(default=lambda: 0)
#: number of eligible voters
[docs]
eligible_voters: Mapped[int] = mapped_column(default=lambda: 0)
#: number of expats
[docs]
expats: Mapped[int | None]
#: number of votes received (total cast ballots from source data)
[docs]
received: Mapped[int | None] = mapped_column(default=lambda: None)
@hybrid_property
[docs]
def cast_ballots(self) -> int:
if self.received is not None:
return self.received
return (
(self.yeas or 0) + (self.nays or 0) + (self.empty or 0)
+ (self.invalid or 0)
)
@cast_ballots.inplace.expression
@classmethod
[docs]
def _cast_ballots_expression(cls) -> ColumnElement[int]:
return func.coalesce(
cls.received,
cls.yeas + cls.nays + cls.empty + cls.invalid
)
#: the id of the ballot this result belongs to
[docs]
ballot_id: Mapped[UUID] = mapped_column(
ForeignKey('ballots.id', ondelete='CASCADE')
)
#: the ballot this result belongs to
[docs]
ballot: Mapped[Ballot] = relationship(back_populates='results')