OpenSlides/openslides/poll/models.py

294 lines
8.7 KiB
Python
Raw Normal View History

from decimal import Decimal
2020-02-12 17:18:01 +01:00
from typing import Iterable, Optional, Set, Tuple, Type
2019-10-18 14:18:49 +02:00
from django.conf import settings
from django.core.validators import MinValueValidator
2011-07-31 10:46:29 +02:00
from django.db import models
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
2019-10-18 14:18:49 +02:00
from ..utils.models import SET_NULL_AND_AUTOUPDATE
2012-07-03 00:05:48 +02:00
2019-10-18 14:18:49 +02:00
class BaseVote(models.Model):
"""
All subclasses must have option attribute with the related name "votes"
"""
2019-01-06 16:22:33 +01:00
2019-10-18 14:18:49 +02:00
weight = models.DecimalField(
default=Decimal("1"),
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
value = models.CharField(max_length=1, choices=(("Y", "Y"), ("N", "N"), ("A", "A")))
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
default=None,
null=True,
blank=True,
on_delete=SET_NULL_AND_AUTOUPDATE,
)
class Meta:
abstract = True
2011-07-31 10:46:29 +02:00
2019-10-18 14:18:49 +02:00
class BaseOption(models.Model):
"""
2020-02-12 17:18:01 +01:00
All subclasses must have poll attribute with the related name "options". Also
they must have a "voted" relation to users.
"""
2019-01-06 16:22:33 +01:00
2019-10-18 14:18:49 +02:00
vote_class: Optional[Type["BaseVote"]] = None
2011-07-31 10:46:29 +02:00
class Meta:
abstract = True
2019-10-18 14:18:49 +02:00
@property
def yes(self) -> Decimal:
return self.sum_weight("Y")
@property
def no(self) -> Decimal:
return self.sum_weight("N")
@property
def abstain(self) -> Decimal:
return self.sum_weight("A")
def sum_weight(self, value):
# We could do this in a nice .aggregate(Sum...) querystatement,
# but these might be expensive DB queries, because they are not preloaded.
# With this in-logic-counting, we operate inmemory.
weight_sum = Decimal(0)
for vote in self.votes.all():
if vote.value == value:
weight_sum += vote.weight
return weight_sum
@classmethod
def get_vote_class(cls):
if cls.vote_class is None:
raise NotImplementedError(
f"The option class {cls} has to have an attribute vote_class."
)
return cls.vote_class
2020-02-12 17:18:01 +01:00
def get_votes(self):
"""
Return a QuerySet with all vote objects related to this option.
"""
return self.get_vote_class().objects.filter(option=self)
def pseudoanonymize(self):
for vote in self.get_votes():
vote.user = None
vote.save()
def reset(self):
self.voted.clear()
# Delete votes
votes = self.get_votes()
votes_id = [vote.id for vote in votes]
votes.delete()
collection = self.get_vote_class().get_collection_string()
inform_deleted_data((collection, id) for id in votes_id)
# update self because the changed voted relation
inform_changed_data(self)
2012-02-19 19:27:00 +01:00
2019-10-18 14:18:49 +02:00
class BasePoll(models.Model):
option_class: Optional[Type["BaseOption"]] = None
STATE_CREATED = 1
STATE_STARTED = 2
STATE_FINISHED = 3
STATE_PUBLISHED = 4
STATES = (
(STATE_CREATED, "Created"),
(STATE_STARTED, "Started"),
(STATE_FINISHED, "Finished"),
(STATE_PUBLISHED, "Published"),
)
state = models.IntegerField(choices=STATES, default=STATE_CREATED)
TYPE_ANALOG = "analog"
TYPE_NAMED = "named"
TYPE_PSEUDOANONYMOUS = "pseudoanonymous"
TYPES = (
(TYPE_ANALOG, "Analog"),
(TYPE_NAMED, "Named"),
(TYPE_PSEUDOANONYMOUS, "Pseudoanonymous"),
)
type = models.CharField(max_length=64, blank=False, null=False, choices=TYPES)
2019-10-18 14:18:49 +02:00
title = models.CharField(max_length=255, blank=True, null=False)
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
2019-01-06 16:22:33 +01:00
2019-10-18 14:18:49 +02:00
db_votesvalid = models.DecimalField(
2019-01-06 16:22:33 +01:00
null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
2019-10-18 14:18:49 +02:00
db_votesinvalid = models.DecimalField(
2019-01-06 16:22:33 +01:00
null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
2019-10-18 14:18:49 +02:00
db_votescast = models.DecimalField(
2019-01-06 16:22:33 +01:00
null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
2019-10-29 09:44:19 +01:00
PERCENT_BASE_YN = "YN"
PERCENT_BASE_YNA = "YNA"
PERCENT_BASE_VALID = "valid"
PERCENT_BASE_CAST = "cast"
PERCENT_BASE_DISABLED = "disabled"
PERCENT_BASES: Iterable[Tuple[str, str]] = (
(PERCENT_BASE_YN, "Yes/No per candidate"),
(PERCENT_BASE_YNA, "Yes/No/Abstain per candidate"),
(PERCENT_BASE_VALID, "All valid ballots"),
(PERCENT_BASE_CAST, "All casted ballots"),
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
) # type: ignore
onehundred_percent_base = models.CharField(
max_length=8, blank=False, null=False, choices=PERCENT_BASES
)
MAJORITY_SIMPLE = "simple"
MAJORITY_TWO_THIRDS = "two_thirds"
MAJORITY_THREE_QUARTERS = "three_quarters"
MAJORITY_DISABLED = "disabled"
MAJORITY_METHODS = (
(MAJORITY_SIMPLE, "Simple majority"),
(MAJORITY_TWO_THIRDS, "Two-thirds majority"),
(MAJORITY_THREE_QUARTERS, "Three-quarters majority"),
(MAJORITY_DISABLED, "Disabled"),
)
majority_method = models.CharField(
max_length=14, blank=False, null=False, choices=MAJORITY_METHODS
)
class Meta:
abstract = True
2019-10-18 14:18:49 +02:00
def get_votesvalid(self):
if self.type == self.TYPE_ANALOG:
return self.db_votesvalid
else:
2020-02-12 17:18:01 +01:00
return Decimal(self.amount_valid_votes())
2012-03-03 11:16:10 +01:00
2019-10-18 14:18:49 +02:00
def set_votesvalid(self, value):
if self.type != self.TYPE_ANALOG:
raise ValueError("Do not set votesvalid for non analog polls")
self.db_votesvalid = value
2019-10-18 14:18:49 +02:00
votesvalid = property(get_votesvalid, set_votesvalid)
2012-03-03 11:16:10 +01:00
2019-10-18 14:18:49 +02:00
def get_votesinvalid(self):
if self.type == self.TYPE_ANALOG:
return self.db_votesinvalid
else:
2020-02-12 17:18:01 +01:00
return Decimal(self.amount_invalid_votes())
2012-03-03 11:16:10 +01:00
2019-10-18 14:18:49 +02:00
def set_votesinvalid(self, value):
if self.type != self.TYPE_ANALOG:
raise ValueError("Do not set votesinvalid for non analog polls")
self.db_votesinvalid = value
2019-01-06 16:22:33 +01:00
2019-10-18 14:18:49 +02:00
votesinvalid = property(get_votesinvalid, set_votesinvalid)
2011-07-31 10:46:29 +02:00
2019-10-18 14:18:49 +02:00
def get_votescast(self):
if self.type == self.TYPE_ANALOG:
return self.db_votescast
else:
2020-02-12 17:18:01 +01:00
return Decimal(self.amount_voted_users())
2019-10-18 14:18:49 +02:00
def set_votescast(self, value):
if self.type != self.TYPE_ANALOG:
raise ValueError("Do not set votescast for non analog polls")
self.db_votescast = value
2019-10-18 14:18:49 +02:00
votescast = property(get_votescast, set_votescast)
2012-04-17 17:35:50 +02:00
2020-02-12 17:18:01 +01:00
def get_user_ids_with_valid_votes(self):
2020-02-13 18:24:51 +01:00
if self.get_options().count():
initial_option = self.get_options()[0]
user_ids = set(map(lambda u: u.id, initial_option.voted.all()))
for option in self.get_options():
user_ids = user_ids.intersection(
set(map(lambda u: u.id, option.voted.all()))
)
return list(user_ids)
else:
return []
2012-02-14 16:31:21 +01:00
2020-02-12 17:18:01 +01:00
def get_all_voted_user_ids(self):
user_ids: Set[int] = set()
for option in self.get_options():
2020-02-13 18:24:51 +01:00
user_ids.update(map(lambda u: u.id, option.voted.all()))
2020-02-12 17:18:01 +01:00
return list(user_ids)
def amount_valid_votes(self):
return len(self.get_user_ids_with_valid_votes())
def amount_invalid_votes(self):
return self.amount_voted_users() - self.amount_valid_votes()
def amount_voted_users(self):
return len(self.get_all_voted_user_ids())
2012-02-14 16:31:21 +01:00
2019-10-18 14:18:49 +02:00
def create_options(self):
""" Should be called after creation of this model. """
raise NotImplementedError()
2012-02-14 16:31:21 +01:00
2019-10-18 14:18:49 +02:00
@classmethod
def get_option_class(cls):
if cls.option_class is None:
raise NotImplementedError(
f"The poll class {cls} has to have an attribute option_class."
)
return cls.option_class
2012-02-14 16:31:21 +01:00
2020-02-12 17:18:01 +01:00
def get_options(self):
"""
Returns the option objects for the poll.
"""
2020-02-13 18:24:51 +01:00
return self.options.all()
2020-02-12 17:18:01 +01:00
2019-10-18 14:18:49 +02:00
@classmethod
def get_vote_class(cls):
return cls.get_option_class().get_vote_class()
def get_votes(self):
"""
Return a QuerySet with all vote objects related to this poll.
"""
return self.get_vote_class().objects.filter(option__poll__id=self.id)
2019-10-18 14:18:49 +02:00
def pseudoanonymize(self):
2020-02-12 17:18:01 +01:00
for option in self.get_options():
option.pseudoanonymize()
2019-10-18 14:18:49 +02:00
def reset(self):
2020-02-12 17:18:01 +01:00
for option in self.get_options():
option.reset()
2019-10-18 14:18:49 +02:00
# Reset state
self.state = BasePoll.STATE_CREATED
if self.type == self.TYPE_ANALOG:
self.votesvalid = None
self.votesinvalid = None
self.votescast = None
self.save()