python-voting/voting/voting.py

122 lines
3.8 KiB
Python
Raw 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
2023-04-03 03:07:00 +00:00
from typing import Dict, 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."""
YES = 0
NO = 1
ABSTENTION = 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 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 03:07:00 +00:00
@classmethod
def loads(cls, data: Dict):
return cls(**json.loads(data))
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 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 "LAUFEND"
else:
return "ANGENOMMEN" if self.votes()[Vote.YES.name] > self.votes()[Vote.NO.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 {
Vote.YES.name: len([vote for vote in self.voting.votes if self.voting.votes[vote] == Vote.YES]),
Vote.NO.name: len([vote for vote in self.voting.votes if self.voting.votes[vote] == Vote.NO]),
Vote.ABSTENTION.name: len([vote for vote in self.voting.votes if self.voting.votes[vote] == Vote.ABSTENTION]),
}
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!")