from sqlalchemy import case
from sqlalchemy import cast
from sqlalchemy import Float
from sqlalchemy import func
from sqlalchemy.ext.hybrid import hybrid_property
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sqlalchemy import Column
from sqlalchemy.sql import ColumnElement
[docs]
class DerivedAttributesMixin:
""" A simple mixin to add commonly used functions to ballots and their
results. """
if TYPE_CHECKING:
[docs]
yeas_percentage: Column[float]
nays_percentage: Column[float]
accepted: Column[bool]
@hybrid_property # type: ignore[no-redef]
def yeas_percentage(self) -> float:
""" The percentage of yeas (discounts empty/invalid ballots). """
return self.yeas / ((self.yeas + self.nays) or 1) * 100
@yeas_percentage.expression # type:ignore[no-redef]
def yeas_percentage(cls) -> 'ColumnElement[float]':
# coalesce will pick the first non-null result
# nullif will return null if division by zero
# => when all yeas and nays are zero the yeas percentage is 0%
return 100 * (
cast(cls.yeas, Float) / cast(
func.coalesce(
func.nullif(cls.yeas + cls.nays, 0), 1
),
Float
)
)
@hybrid_property # type:ignore[no-redef]
[docs]
def nays_percentage(self) -> float:
""" The percentage of nays (discounts empty/invalid ballots). """
return 100 - self.yeas_percentage
@hybrid_property # type:ignore[no-redef]
[docs]
def accepted(self) -> bool:
return self.yeas > self.nays if self.counted else None
@accepted.expression # type:ignore[no-redef]
def accepted(cls) -> 'ColumnElement[bool]':
return case({True: cls.yeas > cls.nays}, cls.counted)
[docs]
class DerivedBallotsCountMixin:
""" A simple mixin to add commonly used functions to votes, ballots and
their results. """
if TYPE_CHECKING:
[docs]
cast_ballots: Column[int]
turnout: Column[float]
# forward declare required columns
yeas: Column[int]
nays: Column[int]
empty: Column[int]
invalid: Column[int]
@hybrid_property # type:ignore[no-redef]
def cast_ballots(self) -> int:
return (
(self.yeas or 0) + (self.nays or 0) + (self.empty or 0)
+ (self.invalid or 0)
)
@hybrid_property # type:ignore[no-redef]
[docs]
def turnout(self) -> float:
return (
self.cast_ballots / self.eligible_voters * 100
if self.eligible_voters else 0
)
@turnout.expression # type:ignore[no-redef]
def turnout(cls) -> 'ColumnElement[float]':
return case(
[(
cls.eligible_voters > 0,
cast(cls.cast_ballots, Float)
/ cast(cls.eligible_voters, Float) * 100
)],
else_=0
)