"""Voting - A Python module for votings on the c-base space-station.""" __AUTHOR__ = 'Brian Wiborg ' __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]), )