"""Voting - A Python module for votings on the c-base space-station.""" __AUTHOR__ = 'Brian Wiborg ' __LICENSE__ = 'MIT' from .quorum import Quorum, QuorumKind from arrow import Arrow, get as arrow_get from dataclasses import dataclass from enum import Enum from typing import Dict, TextIO, List import abc import json.tool class Vote(Enum): """Vote represents a vote by a voter.""" JA = 0 NEIN = 1 ENTHALTUNG = 2 class Voting(object): title: str quorum: Quorum voters: List[str] start: str days: int votes: Dict[str, Vote] def __init__(self, title: str, quorum: Quorum, voters: List[str], start: str = "", days: int = 7, votes: Dict[str, Vote] = None ): if len(voters) == 0: raise RuntimeError(f"A voting must have at least one voter: {voters}") self.title: str = title self.quorum: Quorum = quorum if isinstance(quorum, Quorum) else Quorum(**quorum) self.voters: List[str] = voters self.start: str = start if start else str(Arrow.utcnow()) self.days: int = days self.votes: Dict[str, Vote] = {} if votes: for voter in votes: self.votes[voter] = self.votes[voter] if isinstance(votes[voter], Vote) else Vote[votes[voter]] def __repr__(self) -> str: return self.title def is_over(self) -> bool: return Arrow.utcnow() >= arrow_get(self.start).shift(days=self.days) def vote(self, voter: str, vote: Vote) -> None: if voter not in self.voters: raise ValueError(f"{voter} not in {self.voters}") if self.is_over(): raise RuntimeError(f"Voting has ended! ({self.start} - {arrow_get(self.start).shift(days=self.days)})") self.votes[voter] = vote def as_dict(self) -> Dict: return { "title": self.title, "quorum": self.quorum.as_dict(), "voters": self.voters, "start": str(self.start), "days": self.days, "votes": {k: v.name for (k, v) in self.votes.items()} } def dumps(self) -> str: return json.dumps(self.as_dict()) def dump(self, f: TextIO) -> None: f.write(self.dumps()) @classmethod def loads(cls, data: str) -> None: return cls(**json.loads(data)) @classmethod def load(cls, f: TextIO) -> None: return cls.loads('\n'.join(f.readlines())) @dataclass class Result: voting: Voting def __repr__(self): votes = self.votes() out = [] if self.voting.quorum.kind != QuorumKind.NONE: out.append(f"QUORUM: {self.voting.quorum.value}{'%' if self.voting.quorum.kind == QuorumKind.PERCENT else ''}") out.append(f"GESAMTZAHL DER STIMMEN: {len(self.voting.votes)}/{len(self.voting.voters)}") out.append(f"=> DAFUER: {votes[Vote.JA.name]}") out.append(f"=> DAGEGEN: {votes[Vote.NEIN.name]}") out.append(f"=> ENTHALTUNGEN: {votes[Vote.ENTHALTUNG.name]}") out.append(f"=> ANTRAG: {self.outcome()}") out.append(f"NICHT TEILGENOMMEN: {', '.join([v for v in self.voting.voters if v not in self.voting.votes])}") return '\n'.join(out) @abc.abstractmethod def result(self) -> Dict: return { "state": self.outcome(), "voting": self.voting.as_dict(), "votes": self.votes(), "quorum_reached": self.quorum_reached(), } @abc.abstractmethod def outcome(self) -> str: if not self.has_ended(): return "AUSSTEHEND" else: return "ANGENOMMEN" if self.votes()[Vote.JA.name] > self.votes()[Vote.NEIN.name] and self.quorum_reached() else "ABGELEHNT" def has_ended(self) -> bool: end_at = arrow_get(self.voting.start).shift(days=self.voting.days) return Arrow.utcnow() >= end_at def votes(self) -> Dict: return { Vote.JA.name: len([vote for vote in self.voting.votes if self.voting.votes[vote] == Vote.JA]), Vote.NEIN.name: len([vote for vote in self.voting.votes if self.voting.votes[vote] == Vote.NEIN]), Vote.ENTHALTUNG.name: len([vote for vote in self.voting.votes if self.voting.votes[vote] == Vote.ENTHALTUNG]), } def quorum_reached(self) -> bool: if self.voting.quorum.kind == QuorumKind.NONE: return True elif self.voting.quorum.kind == QuorumKind.ABSOLUTE: return sum(self.votes().values()) >= self.voting.quorum.value elif self.voting.quorum.kind == QuorumKind.PERCENT: return sum(self.votes().values()) * 100 / len(self.voting.voters) >= self.voting.quorum.value else: raise RuntimeError("BUG: This code should never be reached!")