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
|
2023-04-03 05:21:41 +00:00
|
|
|
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
|
|
|
|
2023-04-03 05:21:41 +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
|
|
|
|
2023-04-03 05:21:41 +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():
|
2023-04-03 15:46:28 +00:00
|
|
|
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!")
|