178 lines
6.0 KiB
Python
178 lines
6.0 KiB
Python
|
"""Voting - A Python module for votings on the c-base space-station."""
|
||
|
|
||
|
__AUTHOR__ = 'Brian Wiborg <baccenfutter@c-base.org>'
|
||
|
__LICENSE__ = 'MIT'
|
||
|
|
||
|
from arrow import Arrow
|
||
|
from enum import Enum
|
||
|
from typing import Dict, List, Union
|
||
|
|
||
|
TIMEFRAME_IN_DAYS: int = 7
|
||
|
|
||
|
|
||
|
def timeframe_is_over(started: Arrow) -> bool:
|
||
|
"""Check if the current time is less than the given timestamp plus TIMEFRAME_IN_DAYS."""
|
||
|
return Arrow.utcnow() > started.shift(days=TIMEFRAME_IN_DAYS)
|
||
|
|
||
|
|
||
|
class QuorumKind(Enum):
|
||
|
"""QuorumKind defines the kind of quorum.
|
||
|
|
||
|
Kinds:
|
||
|
- NONE - there ain't no quorum
|
||
|
- ABSOLUTE - absolute number of minimum voters is required
|
||
|
- PERCENT - minimum percentage of valid voters must have voted
|
||
|
"""
|
||
|
NONE = 0
|
||
|
ABSOLUTE = 1
|
||
|
PERCENT = 2
|
||
|
|
||
|
|
||
|
class Quorum(object):
|
||
|
"""Quorm defines the required quorum of a Voting.
|
||
|
|
||
|
Absolute quorums must be greater than 0.
|
||
|
Percent quorums must be greather than 0.0 and less then or equal to 100.0.
|
||
|
"""
|
||
|
def __init__(self, kind: QuorumKind = QuorumKind.NONE, value: Union[float, int, None] = None):
|
||
|
self.__kind: QuorumKind = kind
|
||
|
self.__value: Union[float, int, None] = None
|
||
|
|
||
|
if kind == QuorumKind.ABSOLUTE:
|
||
|
self.__value = int(value)
|
||
|
if self.__value <= 0:
|
||
|
raise ValueError(f"Value can not be less than or equal to zero: {self.__value}")
|
||
|
elif kind == QuorumKind.PERCENT:
|
||
|
self.__value = float(value)
|
||
|
if self.__value <= 0.0 or self.__value > 100.0:
|
||
|
raise ValueError(f"Value must be greater than 0.0 and less then or equal to 100.0: {self.__value}")
|
||
|
|
||
|
@property
|
||
|
def kind(self) -> bool:
|
||
|
return self.__kind
|
||
|
|
||
|
@property
|
||
|
def value(self) -> Union[float, int]:
|
||
|
return self.__value
|
||
|
|
||
|
|
||
|
class Vote(Enum):
|
||
|
"""Vote represents a vote by a voter."""
|
||
|
YES = 0
|
||
|
NO = 1
|
||
|
ABSTENTION = 2
|
||
|
|
||
|
|
||
|
class Result(object):
|
||
|
"""Result represents a voting result."""
|
||
|
def __init__(self, started: Arrow, quorum: Quorum, voters: int, yes: int, no: int, absent: int):
|
||
|
self.__started: Arrow = started
|
||
|
self.__quroum: Quorum = quorum
|
||
|
self.__voters: int = voters
|
||
|
self.__votes_yes: int = yes
|
||
|
self.__votes_no: int = no
|
||
|
self.__votes_absent: int = absent
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
data = {
|
||
|
'STIMMEN': self.__voters,
|
||
|
'JA': self.__votes_yes,
|
||
|
'NEIN': self.__votes_no,
|
||
|
'ENTHALTUNGEN': self.__votes_absent,
|
||
|
}
|
||
|
|
||
|
out = ' | '.join([f"{e}: {data[e]}" for e in data])
|
||
|
if self.status == 'ready':
|
||
|
out += f"\nDamit is der Antrag: {self.outcome}"
|
||
|
return out
|
||
|
|
||
|
@property
|
||
|
def status(self) -> str:
|
||
|
"""Returns the human-readable status of the result."""
|
||
|
is_running = self.__started is not None
|
||
|
is_ready = is_running and timeframe_is_over(self.__started)
|
||
|
out = "not started"
|
||
|
if is_ready:
|
||
|
out = "ready"
|
||
|
elif is_running:
|
||
|
out = "running"
|
||
|
return out
|
||
|
|
||
|
@property
|
||
|
def quorum_reached(self) -> bool:
|
||
|
"""Checks if the quorum has been reached."""
|
||
|
total_votes = sum([self.__votes_yes, self.__votes_no, self.__votes_absent])
|
||
|
if self.__quroum.kind == QuorumKind.NONE:
|
||
|
return True
|
||
|
elif self.__quroum.kind == QuorumKind.ABSOLUTE:
|
||
|
return total_votes >= self.__quroum.value
|
||
|
elif self.__quroum.kind == QuorumKind.PERCENT:
|
||
|
return total_votes * 100 / self.__voters >= self.__quroum.value
|
||
|
else:
|
||
|
raise RuntimeError("BUG! this code should never be reached!")
|
||
|
|
||
|
@property
|
||
|
def outcome(self) -> str:
|
||
|
"""Returns the human-readable outcome."""
|
||
|
return 'ANGENOMMEN' if self.quorum_reached and self.__votes_yes > self.__votes_no else 'ABGELEHNT'
|
||
|
|
||
|
|
||
|
class Voting(object):
|
||
|
"""Voting represents a voting."""
|
||
|
def __init__(self, title: str, quorum: Quorum, voters: List[str]):
|
||
|
if len(voters) == 0:
|
||
|
raise RuntimeError(f"Can not start a voting with 0 voters: {voters}")
|
||
|
self.__title: str = title
|
||
|
self.__quorum: Quorum = quorum
|
||
|
self.__voters: List[str] = voters
|
||
|
self.__start: Union[Arrow, None] = None
|
||
|
self.__votes: Dict[str, Vote] = {}
|
||
|
|
||
|
@property
|
||
|
def title(self) -> str:
|
||
|
return self.__title
|
||
|
|
||
|
@property
|
||
|
def voters(self) -> List[str]:
|
||
|
return self.__voters
|
||
|
|
||
|
def start(self) -> None:
|
||
|
"""Starts the voting."""
|
||
|
if self.__start is not None:
|
||
|
raise RuntimeWarning(f"Voting already running since: {self.__start}")
|
||
|
self.__start = Arrow.utcnow()
|
||
|
|
||
|
def is_running(self) -> bool:
|
||
|
"""Checks if the voting is currently running."""
|
||
|
return self.__start is not None and not timeframe_is_over(self.__start)
|
||
|
|
||
|
def is_over(self) -> bool:
|
||
|
"""Checks if the voting is over."""
|
||
|
return self.is_running() and timeframe_is_over(self.__start)
|
||
|
|
||
|
def vote(self, voter: str, vote: Vote) -> None:
|
||
|
"""Makes a vote.
|
||
|
|
||
|
The voter must be in the list of legal voters.
|
||
|
If a voter votes multiple times, the latest vote wins.
|
||
|
"""
|
||
|
if not self.is_running() or self.is_over():
|
||
|
raise RuntimeError(f"Voting has ended or hasn't even been started! (started: {self.__start})")
|
||
|
if voter not in self.__voters:
|
||
|
raise ValueError(f"{voter} not in {self.__voters}")
|
||
|
self.__votes[voter] = vote
|
||
|
|
||
|
def result(self) -> Result:
|
||
|
"""Obtains the current result for this voting."""
|
||
|
if self.__start is None:
|
||
|
return RuntimeError("Voting hasn't even been started, yet!")
|
||
|
|
||
|
return Result(
|
||
|
quorum=self.__quorum,
|
||
|
started=self.__start,
|
||
|
voters=len(self.__voters),
|
||
|
yes=len([e for e in self.__votes if self.__votes[e] == Vote.YES]),
|
||
|
no=len([e for e in self.__votes if self.__votes[e] == Vote.NO]),
|
||
|
absent=len([e for e in self.__votes if self.__votes[e] == Vote.ABSTENTION]),
|
||
|
)
|