python-voting/voting/voting.py
2023-04-03 17:55:43 +02:00

141 lines
4.6 KiB
Python

"""Voting - A Python module for votings on the c-base space-station."""
__AUTHOR__ = 'Brian Wiborg <baccenfutter@c-base.org>'
__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"GESAMT: {len(self.voting.votes)} => DAFUER: {votes[Vote.JA.name]} DAGEGEN: {votes[Vote.NEIN.name]} ENTHALTUNGEN: {votes[Vote.ENTHALTUNG.name]}")
out.append(f"ERGEBNIS: {self.outcome()}")
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!")