python-voting/voting/voting.py

145 lines
4.8 KiB
Python
Raw Permalink Normal View History

2023-04-02 14:31:25 +00:00
"""Voting - A Python module for votings on the c-base space-station."""
__AUTHOR__ = 'Brian Wiborg <baccenfutter@c-base.org>'
__LICENSE__ = 'MIT'
2023-04-03 03:07:00 +00:00
from .quorum import Quorum, QuorumKind
from arrow import Arrow, get as arrow_get
from dataclasses import dataclass
2023-04-02 14:31:25 +00:00
from enum import Enum
from typing import Dict, TextIO, List
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
import abc
import json.tool
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
class Vote(Enum):
"""Vote represents a vote by a voter."""
2023-04-03 11:37:17 +00:00
JA = 0
NEIN = 1
ENTHALTUNG = 2
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
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}")
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
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] = {}
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
if votes:
for voter in votes:
self.votes[voter] = self.votes[voter] if isinstance(votes[voter], Vote) else Vote[votes[voter]]
2023-04-02 14:31:25 +00:00
2023-04-03 11:37:17 +00:00
def __repr__(self) -> str:
return self.title
2023-04-03 03:07:00 +00:00
def is_over(self) -> bool:
return Arrow.utcnow() >= arrow_get(self.start).shift(days=self.days)
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
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()}
}
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
def dumps(self) -> str:
return json.dumps(self.as_dict())
2023-04-02 14:31:25 +00:00
def dump(self, f: TextIO) -> None:
f.write(self.dumps())
2023-04-03 03:07:00 +00:00
@classmethod
2023-04-03 11:37:17 +00:00
def loads(cls, data: str) -> None:
2023-04-03 03:07:00 +00:00
return cls(**json.loads(data))
2023-04-02 14:31:25 +00:00
@classmethod
def load(cls, f: TextIO) -> None:
return cls.loads('\n'.join(f.readlines()))
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
@dataclass
class Result:
voting: Voting
2023-04-02 14:31:25 +00:00
2023-04-03 11:37:17 +00:00
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 ''}")
2023-04-03 17:05:45 +00:00
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])}")
2023-04-03 11:37:17 +00:00
return '\n'.join(out)
2023-04-03 03:07:00 +00:00
@abc.abstractmethod
def result(self) -> Dict:
return {
"state": self.outcome(),
"voting": self.voting.as_dict(),
"votes": self.votes(),
"quorum_reached": self.quorum_reached(),
2023-04-02 14:31:25 +00:00
}
2023-04-03 03:07:00 +00:00
@abc.abstractmethod
2023-04-02 14:31:25 +00:00
def outcome(self) -> str:
2023-04-03 03:07:00 +00:00
if not self.has_ended():
return "AUSSTEHEND"
2023-04-03 03:07:00 +00:00
else:
2023-04-03 11:37:17 +00:00
return "ANGENOMMEN" if self.votes()[Vote.JA.name] > self.votes()[Vote.NEIN.name] and self.quorum_reached() else "ABGELEHNT"
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
def has_ended(self) -> bool:
end_at = arrow_get(self.voting.start).shift(days=self.voting.days)
return Arrow.utcnow() >= end_at
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
def votes(self) -> Dict:
return {
2023-04-03 11:37:17 +00:00
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]),
2023-04-03 03:07:00 +00:00
}
2023-04-02 14:31:25 +00:00
2023-04-03 03:07:00 +00:00
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!")