majorities in polls

This commit is contained in:
FinnStutzenstein 2019-10-29 09:44:19 +01:00
parent 5fa8341614
commit 1246dd54ad
22 changed files with 995 additions and 477 deletions

View File

@ -7,7 +7,7 @@ export interface AssignmentWithoutNestedModels extends BaseModelWithAgendaItemAn
description: string;
open_posts: number;
phase: number; // see Openslides constants
poll_description_default: number;
default_poll_description: string;
tags_id: number[];
attachments_id: number[];
}

View File

@ -280,13 +280,13 @@
[form]="assignmentForm"
></os-agenda-content-object-form>
<!-- poll_description_default -->
<!-- default_poll_description -->
<div>
<mat-form-field>
<input
matInput
placeholder="{{ 'Default comment on the ballot paper' | translate }}"
formControlName="poll_description_default"
formControlName="default_poll_description"
/>
</mat-form-field>
</div>

View File

@ -189,7 +189,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
attachments_id: [],
title: ['', Validators.required],
description: [''],
poll_description_default: [''],
default_poll_description: [''],
open_posts: [1, [Validators.required, Validators.min(1)]],
agenda_create: [''],
agenda_parent_id: [],

View File

@ -1,8 +1,9 @@
import json
from typing import Any, Dict, List
from ..poll.access_permissions import BaseVoteAccessPermissions
from ..poll.views import BasePoll
from ..poll.access_permissions import (
BasePollAccessPermissions,
BaseVoteAccessPermissions,
)
from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import async_has_perm
@ -47,39 +48,10 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
return data
class AssignmentPollAccessPermissions(BaseAccessPermissions):
class AssignmentPollAccessPermissions(BasePollAccessPermissions):
base_permission = "assignments.can_see"
async def get_restricted_data(
self, full_data: List[Dict[str, Any]], user_id: int
) -> List[Dict[str, Any]]:
"""
Poll-managers have full access, even during an active poll.
Non-published polls will be restricted:
- Remove votes* values from the poll
- Remove yes/no/abstain fields from options
- Remove voted_id field from the poll
"""
if await async_has_perm(user_id, "assignments.can_manage_polls"):
data = full_data
else:
data = []
for poll in full_data:
if poll["state"] != BasePoll.STATE_PUBLISHED:
poll = json.loads(
json.dumps(poll)
) # copy, so we can remove some fields.
del poll["votesvalid"]
del poll["votesinvalid"]
del poll["votescast"]
del poll["voted_id"]
for option in poll["options"]:
del option["yes"]
del option["no"]
del option["abstain"]
data.append(poll)
return data
manage_permission = "assignments.can_manage_polls"
additional_fields = ["amount_global_no", "amount_global_abstain"]
class AssignmentVoteAccessPermissions(BaseVoteAccessPermissions):

View File

@ -1,7 +1,5 @@
from django.core.validators import MinValueValidator
from openslides.assignments.models import AssignmentPoll
from openslides.core.config import ConfigVariable
from openslides.poll.majority import majorityMethods
def get_config_variables():
@ -11,87 +9,47 @@ def get_config_variables():
They are grouped in 'Ballot and ballot papers' and 'PDF'. The generator has
to be evaluated during app loading (see apps.py).
"""
# Ballot and ballot papers
# Polls
yield ConfigVariable(
name="assignments_poll_100_percent_base",
default_value="YES_NO_ABSTAIN",
name="assignment_poll_default_100_percent_base",
default_value="YNA",
input_type="choice",
label="The 100-%-base of an election result consists of",
choices=(
{"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain per candidate"},
{"value": "YES_NO", "display_name": "Yes/No per candidate"},
{"value": "VALID", "display_name": "All valid ballots"},
{"value": "CAST", "display_name": "All casted ballots"},
{"value": "DISABLED", "display_name": "Disabled (no percents)"},
choices=tuple(
{"value": base[0], "display_name": base[1]}
for base in AssignmentPoll.PERCENT_BASES
),
help_text=(
"For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base "
"depends on the election method: If there is only one option per candidate, "
"the sum of all votes of all candidates is 100 %. Otherwise for each "
"candidate the sum of all votes is 100 %."
),
weight=420,
group="Elections",
subgroup="Ballot and ballot papers",
weight=400,
group="Polls",
subgroup="Elections",
)
# TODO: Add server side validation of the choices.
yield ConfigVariable(
name="assignments_poll_default_majority_method",
default_value=majorityMethods[0]["value"],
name="assignment_poll_default_majority_method",
default_value="simple",
input_type="choice",
choices=majorityMethods,
choices=tuple(
{"value": method[0], "display_name": method[1]}
for method in AssignmentPoll.MAJORITY_METHODS
),
label="Required majority",
help_text="Default method to check whether a candidate has reached the required majority.",
weight=425,
group="Elections",
subgroup="Ballot and ballot papers",
weight=405,
group="Polls",
subgroup="Elections",
)
yield ConfigVariable(
name="assignments_pdf_ballot_papers_selection",
default_value="CUSTOM_NUMBER",
input_type="choice",
label="Number of ballot papers (selection)",
choices=(
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
{
"value": "NUMBER_OF_ALL_PARTICIPANTS",
"display_name": "Number of all participants",
},
{
"value": "CUSTOM_NUMBER",
"display_name": "Use the following custom number",
},
),
weight=430,
group="Elections",
subgroup="Ballot and ballot papers",
)
yield ConfigVariable(
name="assignments_pdf_ballot_papers_number",
default_value=8,
input_type="integer",
label="Custom number of ballot papers",
weight=435,
group="Elections",
subgroup="Ballot and ballot papers",
validators=(MinValueValidator(1),),
)
yield ConfigVariable(
name="assignments_add_candidates_to_list_of_speakers",
name="assignment_poll_add_candidates_to_list_of_speakers",
default_value=True,
input_type="boolean",
label="Put all candidates on the list of speakers",
weight=440,
group="Elections",
subgroup="Ballot and ballot papers",
weight=410,
group="Polls",
subgroup="Elections",
)
# PDF
yield ConfigVariable(
name="assignments_pdf_title",
default_value="Elections",

View File

@ -21,8 +21,6 @@ class Migration(migrations.Migration):
migrations.RenameField(
model_name="assignmentoption", old_name="candidate", new_name="user"
),
migrations.RemoveField(model_name="assignmentpoll", name="description"),
migrations.RemoveField(model_name="assignmentpoll", name="published"),
migrations.AddField(
model_name="assignmentpoll",
name="global_abstain",
@ -99,6 +97,53 @@ class Migration(migrations.Migration):
name="allow_multiple_votes_per_candidate",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="assignmentpoll",
name="majority_method",
field=models.CharField(
choices=[
("simple", "Simple majority"),
("two_thirds", "Two-thirds majority"),
("three_quarters", "Three-quarters majority"),
("disabled", "Disabled"),
],
default="",
max_length=14,
),
preserve_default=False,
),
migrations.AddField(
model_name="assignmentpoll",
name="onehundred_percent_base",
field=models.CharField(
choices=[
("YN", "Yes/No per candidate"),
("YNA", "Yes/No/Abstain per candidate"),
("votes", "Sum of votes inclusive global ones"),
("valid", "All valid ballots"),
("cast", "All casted ballots"),
("disabled", "Disabled (no percents)"),
],
default="",
max_length=8,
),
preserve_default=False,
),
migrations.AlterField(
model_name="assignment",
name="poll_description_default",
field=models.CharField(blank=True, max_length=255),
),
migrations.RenameField(
model_name="assignment",
old_name="poll_description_default",
new_name="default_poll_description",
),
migrations.AlterField(
model_name="assignmentpoll",
name="description",
field=models.CharField(blank=True, max_length=255),
),
migrations.AlterField(
model_name="assignmentpoll",
name="pollmethod",
@ -106,13 +151,6 @@ class Migration(migrations.Migration):
choices=[("YN", "YN"), ("YNA", "YNA"), ("votes", "votes")], max_length=5
),
),
migrations.AlterField(
model_name="assignmentvote",
name="value",
field=models.CharField(
choices=[("Y", "Y"), ("N", "N"), ("A", "A")], max_length=1
),
),
migrations.AlterField(
model_name="assignmentvote",
name="weight",
@ -134,6 +172,4 @@ class Migration(migrations.Migration):
migrations.RenameField(
model_name="assignmentpoll", old_name="votesvalid", new_name="db_votesvalid"
),
migrations.RemoveField(model_name="assignmentpoll", name="votesabstain"),
migrations.RemoveField(model_name="assignmentpoll", name="votesno"),
]

View File

@ -0,0 +1,145 @@
# Generated by Finn Stutzenstein on 2019-10-29 10:55
from decimal import Decimal
from django.db import migrations, transaction
def change_pollmethods(apps, schema_editor):
""" yn->YN, yna->YNA """
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
pollmethod_map = {
"yn": "YN",
"yna": "YNA",
"votes": "votes",
}
for poll in AssignmentPoll.objects.all():
poll.pollmethod = pollmethod_map.get(poll.pollmethod, "YNA")
poll.save(skip_autoupdate=True)
def set_poll_titles(apps, schema_editor):
"""
Sets titles to their indexes
"""
Assignment = apps.get_model("assignments", "Assignment")
for assignment in Assignment.objects.all():
for i, poll in enumerate(assignment.polls.order_by("pk").all()):
poll.title = str(i + 1)
poll.save(skip_autoupdate=True)
def set_onehunderd_percent_bases(apps, schema_editor):
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
ConfigStore = apps.get_model("core", "ConfigStore")
base_map = {
"YES_NO_ABSTAIN": "YNA",
"YES_NO": "YN",
"VALID": "valid",
"CAST": "cast",
"DISABLED": "disabled",
}
try:
config = ConfigStore.objects.get(key="assignments_poll_100_percent_base")
value = base_map[config.value]
except (ConfigStore.DoesNotExist, KeyError):
value = "YNA"
for poll in AssignmentPoll.objects.all():
if poll.pollmethod == "votes" and value in ("YN", "YNA"):
poll.onehundred_percent_base = "votes"
elif poll.pollmethod == "YN" and value == "YNA":
poll.onehundred_percent_base = "YN"
else:
poll.onehundred_percent_base = value
poll.save(skip_autoupdate=True)
def set_majority_methods(apps, schema_editor):
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
ConfigStore = apps.get_model("core", "ConfigStore")
majority_map = {
"simple_majority": "simple",
"two-thirds_majority": "two_thirds",
"three-quarters_majority": "three_quarters",
"disabled": "disabled",
}
try:
config = ConfigStore.objects.get(key="assignments_poll_default_majority_method")
value = majority_map[config.value]
except (ConfigStore.DoesNotExist, KeyError):
value = "simple"
for poll in AssignmentPoll.objects.all():
poll.majority_method = value
poll.save(skip_autoupdate=True)
def convert_votes(apps, schema_editor):
AssignmentVote = apps.get_model("assignments", "AssignmentVote")
value_map = {
"Yes": "Y",
"No": "N",
"Abstain": "A",
"Votes": "Y",
}
for vote in AssignmentVote.objects.all():
vote.value = value_map[vote.value]
vote.save(skip_autoupdate=True)
def convert_votesabstain(apps, schema_editor):
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
AssignmentVote = apps.get_model("assignments", "AssignmentVote")
for poll in AssignmentPoll.objects.all():
if poll.votesabstain is not None and poll.votesabstain > Decimal(0):
with transaction.atomic():
option = poll.options.first()
vote = AssignmentVote(
option=option, value="A", weight=poll.votesabstain
)
vote.save(skip_autoupdate=True)
def convert_votesno(apps, schema_editor):
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
AssignmentVote = apps.get_model("assignments", "AssignmentVote")
for poll in AssignmentPoll.objects.all():
if poll.votesno is not None and poll.votesno > Decimal(0):
with transaction.atomic():
option = poll.options.first()
vote = AssignmentVote(option=option, value="N", weight=poll.votesno)
vote.save(skip_autoupdate=True)
def set_correct_state(apps, schema_editor):
""" if poll.published, set state to published """
AssignmentPoll = apps.get_model("assignments", "AssignmentPoll")
AssignmentVote = apps.get_model("assignments", "AssignmentVote")
for poll in AssignmentPoll.objects.all():
# Polls, that are published (old field) but have no votes, will be
# left at the created state...
if AssignmentVote.objects.filter(option__poll__pk=poll.pk).exists():
if poll.published:
poll.state = 4 # published
else:
poll.state = 3 # finished
poll.save(skip_autoupdate=True)
class Migration(migrations.Migration):
dependencies = [
("assignments", "0008_voting_1"),
]
operations = [
migrations.RunPython(change_pollmethods),
migrations.RunPython(set_poll_titles),
migrations.RunPython(set_onehunderd_percent_bases),
migrations.RunPython(set_majority_methods),
migrations.RunPython(convert_votes),
migrations.RunPython(convert_votesabstain),
migrations.RunPython(convert_votesno),
migrations.RunPython(set_correct_state),
]

View File

@ -0,0 +1,23 @@
# Generated by Finn Stutzenstein on 2019-10-29 11:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assignments", "0009_voting_2"),
]
operations = [
migrations.AlterField(
model_name="assignmentvote",
name="value",
field=models.CharField(
choices=[("Y", "Y"), ("N", "N"), ("A", "A")], max_length=1
),
),
migrations.RemoveField(model_name="assignmentpoll", name="votesabstain"),
migrations.RemoveField(model_name="assignmentpoll", name="votesno"),
migrations.RemoveField(model_name="assignmentpoll", name="published"),
]

View File

@ -1,5 +1,4 @@
from collections import OrderedDict
from typing import Any, Dict, List
from decimal import Decimal
from django.conf import settings
from django.core.validators import MinValueValidator
@ -120,7 +119,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
The number of members to be elected.
"""
poll_description_default = models.CharField(max_length=79, blank=True)
default_poll_description = models.CharField(max_length=255, blank=True)
"""
Default text for the poll description.
"""
@ -230,40 +229,6 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
self.phase = phase
def vote_results(self, only_published):
"""
Returns a table represented as a list with all candidates from all
related polls and their vote results.
"""
vote_results_dict: Dict[Any, List[AssignmentVote]] = OrderedDict()
polls = self.polls.all()
if only_published:
polls = polls.filter(published=True)
# All PollOption-Objects related to this assignment
options: List[AssignmentOption] = []
for poll in polls:
options += poll.get_options()
for option in options:
candidate = option.candidate
if candidate in vote_results_dict:
continue
vote_results_dict[candidate] = []
for poll in polls:
votes: Any = {}
try:
# candidate related to this poll
poll_option = poll.get_options().get(candidate=candidate)
for vote in poll_option.get_votes():
votes[vote.value] = vote.print_weight()
except AssignmentOption.DoesNotExist:
# candidate not in related to this poll
votes = None
vote_results_dict[candidate].append(votes)
return vote_results_dict
def get_title_information(self):
return {"title": self.title}
@ -330,9 +295,6 @@ class AssignmentPollManager(models.Manager):
)
# Meta-TODO: Is this todo resolved?
# TODO: remove the type-ignoring in the next line, after this is solved:
# https://github.com/python/mypy/issues/3855
class AssignmentPoll(RESTModelMixin, BasePoll):
access_permissions = AssignmentPollAccessPermissions()
objects = AssignmentPollManager()
@ -343,12 +305,32 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
Assignment, on_delete=models.CASCADE, related_name="polls"
)
description = models.CharField(max_length=255, blank=True)
POLLMETHOD_YN = "YN"
POLLMETHOD_YNA = "YNA"
POLLMETHOD_VOTES = "votes"
POLLMETHODS = (("YN", "YN"), ("YNA", "YNA"), ("votes", "votes"))
pollmethod = models.CharField(max_length=5, choices=POLLMETHODS)
PERCENT_BASE_YN = "YN"
PERCENT_BASE_YNA = "YNA"
PERCENT_BASE_VOTES = "votes"
PERCENT_BASE_VALID = "valid"
PERCENT_BASE_CAST = "cast"
PERCENT_BASE_DISABLED = "disabled"
PERCENT_BASES = (
(PERCENT_BASE_YN, "Yes/No per candidate"),
(PERCENT_BASE_YNA, "Yes/No/Abstain per candidate"),
(PERCENT_BASE_VOTES, "Sum of votes inclusive global ones"),
(PERCENT_BASE_VALID, "All valid ballots"),
(PERCENT_BASE_CAST, "All casted ballots"),
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
)
onehundred_percent_base = models.CharField(
max_length=8, blank=False, null=False, choices=PERCENT_BASES
)
global_abstain = models.BooleanField(default=True)
global_no = models.BooleanField(default=True)
@ -360,6 +342,27 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
class Meta:
default_permissions = ()
@property
def amount_global_no(self):
if self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES or not self.global_no:
return None
no_sum = Decimal(0)
for option in self.options.all():
no_sum += option.no
return no_sum
@property
def amount_global_abstain(self):
if (
self.pollmethod != AssignmentPoll.POLLMETHOD_VOTES
or not self.global_abstain
):
return None
abstain_sum = Decimal(0)
for option in self.options.all():
abstain_sum += option.abstain
return abstain_sum
def create_options(self):
related_users = AssignmentRelatedUser.objects.filter(
assignment__id=self.assignment.id
@ -374,7 +377,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
inform_changed_data(self)
# Add all candidates to list of speakers of related agenda item
if config["assignments_add_candidates_to_list_of_speakers"]:
if config["assignment_poll_add_candidates_to_list_of_speakers"]:
for related_user in related_users:
try:
Speaker.objects.add(

View File

@ -2,19 +2,19 @@ from openslides.poll.serializers import (
BASE_OPTION_FIELDS,
BASE_POLL_FIELDS,
BASE_VOTE_FIELDS,
BaseOptionSerializer,
BasePollSerializer,
BaseVoteSerializer,
)
from openslides.utils.rest_api import (
BooleanField,
CharField,
DecimalField,
IdPrimaryKeyRelatedField,
IntegerField,
ModelSerializer,
SerializerMethodField,
ValidationError,
)
from ..utils.auth import get_group_model, has_perm
from ..utils.auth import has_perm
from ..utils.autoupdate import inform_changed_data
from ..utils.validate import validate_html
from .models import (
@ -47,40 +47,29 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
fields = ("id", "user", "elected", "weight")
class AssignmentVoteSerializer(ModelSerializer):
class AssignmentVoteSerializer(BaseVoteSerializer):
"""
Serializer for assignment.models.AssignmentVote objects.
"""
pollstate = SerializerMethodField()
class Meta:
model = AssignmentVote
fields = ("pollstate",) + BASE_VOTE_FIELDS
fields = BASE_VOTE_FIELDS
read_only_fields = BASE_VOTE_FIELDS
def get_pollstate(self, vote):
return vote.option.poll.state
class AssignmentOptionSerializer(ModelSerializer):
class AssignmentOptionSerializer(BaseOptionSerializer):
"""
Serializer for assignment.models.AssignmentOption objects.
"""
yes = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
no = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
abstain = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
class Meta:
model = AssignmentOption
fields = ("user", "weight") + BASE_OPTION_FIELDS
read_only_fields = ("user", "weight") + BASE_OPTION_FIELDS
class AssignmentPollSerializer(ModelSerializer):
class AssignmentPollSerializer(BasePollSerializer):
"""
Serializer for assignment.models.AssignmentPoll objects.
@ -88,20 +77,10 @@ class AssignmentPollSerializer(ModelSerializer):
"""
options = AssignmentOptionSerializer(many=True, read_only=True)
title = CharField(allow_blank=False, required=True)
groups = IdPrimaryKeyRelatedField(
many=True, required=False, queryset=get_group_model().objects.all()
)
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
votesvalid = DecimalField(
amount_global_no = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
votesinvalid = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
votescast = DecimalField(
amount_global_abstain = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
@ -109,19 +88,55 @@ class AssignmentPollSerializer(ModelSerializer):
model = AssignmentPoll
fields = (
"assignment",
"description",
"pollmethod",
"votes_amount",
"allow_multiple_votes_per_candidate",
"global_no",
"amount_global_no",
"global_abstain",
"amount_global_abstain",
) + BASE_POLL_FIELDS
read_only_fields = ("state",)
def update(self, instance, validated_data):
""" Prevent from updating the assignment """
""" Prevent updating the assignment """
validated_data.pop("assignment", None)
return super().update(instance, validated_data)
def norm_100_percent_base_to_pollmethod(
self, onehundred_percent_base, pollmethod, old_100_percent_base=None
):
"""
Returns None, if the 100-%-base must not be changed, otherwise the correct 100-%-base.
"""
if pollmethod == AssignmentPoll.POLLMETHOD_YN and onehundred_percent_base in (
AssignmentPoll.PERCENT_BASE_VOTES,
AssignmentPoll.PERCENT_BASE_YNA,
):
return AssignmentPoll.PERCENT_BASE_YN
if (
pollmethod == AssignmentPoll.POLLMETHOD_YNA
and onehundred_percent_base == AssignmentPoll.PERCENT_BASE_VOTES
):
if old_100_percent_base is None:
return AssignmentPoll.PERCENT_BASE_YNA
else:
if old_100_percent_base in (
AssignmentPoll.PERCENT_BASE_YN,
AssignmentPoll.PERCENT_BASE_YNA,
):
return old_100_percent_base
else:
return pollmethod
if (
pollmethod == AssignmentPoll.POLLMETHOD_VOTES
and onehundred_percent_base
in (AssignmentPoll.PERCENT_BASE_YN, AssignmentPoll.PERCENT_BASE_YNA)
):
return AssignmentPoll.PERCENT_BASE_VOTES
return None
class AssignmentSerializer(ModelSerializer):
"""
@ -146,7 +161,7 @@ class AssignmentSerializer(ModelSerializer):
"open_posts",
"phase",
"assignment_related_users",
"poll_description_default",
"default_poll_description",
"agenda_item_id",
"list_of_speakers_id",
"agenda_create",

View File

@ -1,8 +1,10 @@
import json
from typing import Any, Dict, List
from ..poll.access_permissions import BaseVoteAccessPermissions
from ..poll.views import BasePoll
from ..poll.access_permissions import (
BasePollAccessPermissions,
BaseVoteAccessPermissions,
)
from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import async_has_perm, async_in_some_groups
@ -183,39 +185,9 @@ class StateAccessPermissions(BaseAccessPermissions):
base_permission = "motions.can_see"
class MotionPollAccessPermissions(BaseAccessPermissions):
class MotionPollAccessPermissions(BasePollAccessPermissions):
base_permission = "motions.can_see"
async def get_restricted_data(
self, full_data: List[Dict[str, Any]], user_id: int
) -> List[Dict[str, Any]]:
"""
Poll-managers have full access, even during an active poll.
Non-published polls will be restricted:
- Remove votes* values from the poll
- Remove yes/no/abstain fields from options
- Remove voted_id field from the poll
"""
if await async_has_perm(user_id, "motions.can_manage_polls"):
data = full_data
else:
data = []
for poll in full_data:
if poll["state"] != BasePoll.STATE_PUBLISHED:
poll = json.loads(
json.dumps(poll)
) # copy, so we can remove some fields.
del poll["votesvalid"]
del poll["votesinvalid"]
del poll["votescast"]
del poll["voted_id"]
for option in poll["options"]:
del option["yes"]
del option["no"]
del option["abstain"]
data.append(poll)
return data
manage_permission = "motions.can_manage_polls"
class MotionVoteAccessPermissions(BaseVoteAccessPermissions):

View File

@ -1,7 +1,7 @@
from django.core.validators import MinValueValidator
from openslides.core.config import ConfigVariable
from openslides.poll.majority import majorityMethods
from openslides.motions.models import MotionPoll
from .models import Workflow
@ -348,51 +348,6 @@ def get_config_variables():
subgroup="Voting and ballot papers",
)
# TODO: Add server side validation of the choices.
yield ConfigVariable(
name="motions_poll_default_majority_method",
default_value=majorityMethods[0]["value"],
input_type="choice",
choices=majorityMethods,
label="Required majority",
help_text="Default method to check whether a motion has reached the required majority.",
weight=372,
group="Motions",
subgroup="Voting and ballot papers",
)
yield ConfigVariable(
name="motions_pdf_ballot_papers_selection",
default_value="CUSTOM_NUMBER",
input_type="choice",
label="Number of ballot papers (selection)",
choices=(
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
{
"value": "NUMBER_OF_ALL_PARTICIPANTS",
"display_name": "Number of all participants",
},
{
"value": "CUSTOM_NUMBER",
"display_name": "Use the following custom number",
},
),
weight=374,
group="Motions",
subgroup="Voting and ballot papers",
)
yield ConfigVariable(
name="motions_pdf_ballot_papers_number",
default_value=8,
input_type="integer",
label="Custom number of ballot papers",
weight=376,
group="Motions",
subgroup="Voting and ballot papers",
validators=(MinValueValidator(1),),
)
# PDF export
yield ConfigVariable(
@ -432,3 +387,33 @@ def get_config_variables():
group="Motions",
subgroup="PDF export",
)
# Polls
yield ConfigVariable(
name="motion_poll_default_100_percent_base",
default_value="YNA",
input_type="choice",
label="The 100-%-base of an election result consists of",
choices=tuple(
{"value": base[0], "display_name": base[1]}
for base in MotionPoll.PERCENT_BASES
),
weight=420,
group="Polls",
subgroup="Motions",
)
yield ConfigVariable(
name="motion_poll_default_majority_method",
default_value="simple",
input_type="choice",
choices=tuple(
{"value": method[0], "display_name": method[1]}
for method in MotionPoll.MAJORITY_METHODS
),
label="Required majority",
help_text="Default method to check whether a candidate has reached the required majority.",
weight=425,
group="Polls",
subgroup="Motions",
)

View File

@ -81,6 +81,37 @@ class Migration(migrations.Migration):
name="voted",
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name="motionpoll",
name="majority_method",
field=models.CharField(
choices=[
("simple", "Simple majority"),
("two_thirds", "Two-thirds majority"),
("three_quarters", "Three-quarters majority"),
("disabled", "Disabled"),
],
default="",
max_length=14,
),
preserve_default=False,
),
migrations.AddField(
model_name="motionpoll",
name="onehundred_percent_base",
field=models.CharField(
choices=[
("YN", "Yes/No per candidate"),
("YNA", "Yes/No/Abstain per candidate"),
("valid", "All valid ballots"),
("cast", "All casted ballots"),
("disabled", "Disabled (no percents)"),
],
default="",
max_length=8,
),
preserve_default=False,
),
migrations.AlterField(
model_name="motionvote",
name="option",

View File

@ -0,0 +1,107 @@
# Generated by Finn Stutzenstein on 2019-10-30 13:43
from django.db import migrations
def change_pollmethods(apps, schema_editor):
""" yn->YN, yna->YNA """
MotionPoll = apps.get_model("motions", "MotionPoll")
pollmethod_map = {
"yn": "YN",
"yna": "YNA",
}
for poll in MotionPoll.objects.all():
poll.pollmethod = pollmethod_map.get(poll.pollmethod, "YNA")
poll.save(skip_autoupdate=True)
def set_poll_titles(apps, schema_editor):
"""
Sets titles to their indexes
"""
Motion = apps.get_model("motions", "Motion")
for motion in Motion.objects.all():
for i, poll in enumerate(motion.polls.order_by("pk").all()):
poll.title = str(i + 1)
poll.save(skip_autoupdate=True)
def set_onehunderd_percent_bases(apps, schema_editor):
MotionPoll = apps.get_model("motions", "MotionPoll")
ConfigStore = apps.get_model("core", "ConfigStore")
base_map = {
"YES_NO_ABSTAIN": "YNA",
"YES_NO": "YN",
"VALID": "valid",
"CAST": "cast",
"DISABLED": "disabled",
}
try:
config = ConfigStore.objects.get(key="motions_poll_100_percent_base")
value = base_map[config.value]
except (ConfigStore.DoesNotExist, KeyError):
value = "YNA"
for poll in MotionPoll.objects.all():
# The pollmethod is new (default is YNA), so we do not need
# to check, if the 100% base is valid.
poll.onehundred_percent_base = value
poll.save(skip_autoupdate=True)
def set_majority_methods(apps, schema_editor):
MotionPoll = apps.get_model("motions", "MotionPoll")
ConfigStore = apps.get_model("core", "ConfigStore")
majority_map = {
"simple_majority": "simple",
"two-thirds_majority": "two_thirds",
"three-quarters_majority": "three_quarters",
"disabled": "disabled",
}
try:
config = ConfigStore.objects.get(key="motions_poll_default_majority_method")
value = majority_map[config.value]
except (ConfigStore.DoesNotExist, KeyError):
value = "simple"
for poll in MotionPoll.objects.all():
poll.majority_method = value
poll.save(skip_autoupdate=True)
def convert_votes(apps, schema_editor):
MotionVote = apps.get_model("motions", "MotionVote")
value_map = {
"Yes": "Y",
"No": "N",
"Abstain": "A",
}
for vote in MotionVote.objects.all():
vote.value = value_map[vote.value]
vote.save(skip_autoupdate=True)
def set_correct_state(apps, schema_editor):
""" If there are votes, set the state to finished """
MotionPoll = apps.get_model("motions", "MotionPoll")
MotionVote = apps.get_model("motions", "MotionVote")
for poll in MotionPoll.objects.all():
if MotionVote.objects.filter(option__poll__pk=poll.pk).exists():
poll.state = 3 # finished
poll.save(skip_autoupdate=True)
class Migration(migrations.Migration):
dependencies = [
("motions", "0033_voting_1"),
]
operations = [
migrations.RunPython(change_pollmethods),
migrations.RunPython(set_poll_titles),
migrations.RunPython(set_onehunderd_percent_bases),
migrations.RunPython(set_majority_methods),
migrations.RunPython(convert_votes),
migrations.RunPython(set_correct_state),
]

View File

@ -923,9 +923,6 @@ class MotionPollManager(models.Manager):
)
# Meta-TODO: Is this todo resolved?
# TODO: remove the type-ignoring in the next line, after this is solved:
# https://github.com/python/mypy/issues/3855
class MotionPoll(RESTModelMixin, BasePoll):
access_permissions = MotionPollAccessPermissions()
option_class = MotionOption

View File

@ -5,6 +5,9 @@ from openslides.poll.serializers import (
BASE_OPTION_FIELDS,
BASE_POLL_FIELDS,
BASE_VOTE_FIELDS,
BaseOptionSerializer,
BasePollSerializer,
BaseVoteSerializer,
)
from ..core.config import config
@ -13,7 +16,6 @@ from ..utils.autoupdate import inform_changed_data
from ..utils.rest_api import (
BooleanField,
CharField,
DecimalField,
Field,
IdPrimaryKeyRelatedField,
IntegerField,
@ -224,64 +226,47 @@ class AmendmentParagraphsJSONSerializerField(Field):
return data
class MotionVoteSerializer(ModelSerializer):
pollstate = SerializerMethodField()
class MotionVoteSerializer(BaseVoteSerializer):
class Meta:
model = MotionVote
fields = ("pollstate",) + BASE_VOTE_FIELDS
fields = BASE_VOTE_FIELDS
read_only_fields = BASE_VOTE_FIELDS
def get_pollstate(self, vote):
return vote.option.poll.state
class MotionOptionSerializer(ModelSerializer):
yes = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
no = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
abstain = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
class MotionOptionSerializer(BaseOptionSerializer):
class Meta:
model = MotionOption
fields = BASE_OPTION_FIELDS
read_only_fields = BASE_OPTION_FIELDS
class MotionPollSerializer(ModelSerializer):
class MotionPollSerializer(BasePollSerializer):
"""
Serializer for motion.models.MotionPoll objects.
"""
options = MotionOptionSerializer(many=True, read_only=True)
title = CharField(allow_blank=False, required=True)
groups = IdPrimaryKeyRelatedField(
many=True, required=False, queryset=get_group_model().objects.all()
)
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
votesvalid = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
votesinvalid = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
votescast = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
class Meta:
model = MotionPoll
fields = ("motion", "pollmethod") + BASE_POLL_FIELDS
read_only_fields = ("state",)
def update(self, instance, validated_data):
""" Prevent from updating the motion """
""" Prevent updating the motion """
validated_data.pop("motion", None)
return super().update(instance, validated_data)
def norm_100_percent_base_to_pollmethod(
self, onehundred_percent_base, pollmethod, old_100_percent_base=None
):
if (
pollmethod == MotionPoll.POLLMETHOD_YN
and onehundred_percent_base == MotionPoll.PERCENT_BASE_YNA
):
return MotionPoll.PERCENT_BASE_YN
return None
class MotionChangeRecommendationSerializer(ModelSerializer):
"""

View File

@ -1,3 +1,4 @@
import json
from typing import Any, Dict, List
from ..poll.views import BasePoll
@ -5,6 +6,47 @@ from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import async_has_perm
class BasePollAccessPermissions(BaseAccessPermissions):
manage_permission = "" # set by subclass
additional_fields: List[str] = []
""" Add fields to be removed from each unpublished poll """
async def get_restricted_data(
self, full_data: List[Dict[str, Any]], user_id: int
) -> List[Dict[str, Any]]:
"""
Poll-managers have full access, even during an active poll.
Non-published polls will be restricted:
- Remove votes* values from the poll
- Remove yes/no/abstain fields from options
- Remove voted_id field from the poll
- Remove fields given in self.assitional_fields from the poll
"""
if await async_has_perm(user_id, self.manage_permission):
data = full_data
else:
data = []
for poll in full_data:
if poll["state"] != BasePoll.STATE_PUBLISHED:
poll = json.loads(
json.dumps(poll)
) # copy, so we can remove some fields.
del poll["votesvalid"]
del poll["votesinvalid"]
del poll["votescast"]
del poll["voted_id"]
for field in self.additional_fields:
del poll[field]
for option in poll["options"]:
del option["yes"]
del option["no"]
del option["abstain"]
data.append(poll)
return data
class BaseVoteAccessPermissions(BaseAccessPermissions):
manage_permission = "" # set by subclass

View File

@ -1,7 +0,0 @@
# Common majority methods for all apps using polls. The first one should be the default.
majorityMethods = (
{"value": "simple_majority", "display_name": "Simple majority"},
{"value": "two-thirds_majority", "display_name": "Two-thirds majority"},
{"value": "three-quarters_majority", "display_name": "Three-quarters majority"},
{"value": "disabled", "display_name": "Disabled"},
)

View File

@ -1,5 +1,5 @@
from decimal import Decimal
from typing import Optional, Type
from typing import Iterable, Optional, Tuple, Type
from django.conf import settings
from django.core.validators import MinValueValidator
@ -131,6 +131,36 @@ class BasePoll(models.Model):
decimal_places=6,
)
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
@ -201,8 +231,6 @@ class BasePoll(models.Model):
def get_votes(self):
"""
Return a QuerySet with all vote objects related to this poll.
TODO: This might be a performance issue when used in properties that are serialized.
"""
return self.get_vote_class().objects.filter(option__poll__id=self.id)

View File

@ -1,3 +1,34 @@
from ..utils.auth import get_group_model
from ..utils.rest_api import (
CharField,
DecimalField,
IdPrimaryKeyRelatedField,
ModelSerializer,
SerializerMethodField,
)
BASE_VOTE_FIELDS = ("id", "weight", "value", "user", "option", "pollstate")
class BaseVoteSerializer(ModelSerializer):
pollstate = SerializerMethodField()
def get_pollstate(self, vote):
return vote.option.poll.state
BASE_OPTION_FIELDS = ("id", "yes", "no", "abstain")
class BaseOptionSerializer(ModelSerializer):
yes = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
no = DecimalField(max_digits=15, decimal_places=6, min_value=-2, read_only=True)
abstain = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
BASE_POLL_FIELDS = (
"state",
"type",
@ -9,8 +40,62 @@ BASE_POLL_FIELDS = (
"options",
"voted",
"id",
"onehundred_percent_base",
"majority_method",
)
BASE_OPTION_FIELDS = ("id", "yes", "no", "abstain")
BASE_VOTE_FIELDS = ("id", "weight", "value", "user", "option")
class BasePollSerializer(ModelSerializer):
title = CharField(allow_blank=False, required=True)
groups = IdPrimaryKeyRelatedField(
many=True, required=False, queryset=get_group_model().objects.all()
)
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
votesvalid = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
votesinvalid = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
votescast = DecimalField(
max_digits=15, decimal_places=6, min_value=-2, read_only=True
)
def create(self, validated_data):
"""
Match the 100 percent base to the pollmethod. Change the base, if it does not
fit to the pollmethod
"""
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
validated_data["onehundred_percent_base"], validated_data["pollmethod"]
)
if new_100_percent_base is not None:
validated_data["onehundred_percent_base"] = new_100_percent_base
return super().create(validated_data)
def update(self, instance, validated_data):
"""
Adjusts the 100%-base to the pollmethod. This might be needed,
if at least one of them was changed. Wrong comobinations should be
also handled by the client, but here we make it sure aswell!
E.g. the pollmethod is YN, but the 100%-base is YNA, this micht noght be
possible (see implementing serializers to see forbidden combinations)
"""
old_100_percent_base = instance.onehundred_percent_base
instance = super().update(instance, validated_data)
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
instance.onehundred_percent_base, instance.pollmethod, old_100_percent_base
)
if new_100_percent_base is not None:
instance.onehundred_percent_base = new_100_percent_base
instance.save()
return instance
def norm_100_percent_base_to_pollmethod(
self, onehundred_percent_base, pollmethod, old_100_percent_base=None
):
raise NotImplementedError()

View File

@ -91,7 +91,7 @@ def create_assignment_polls():
class CreateAssignmentPoll(TestCase):
def advancedSetUp(self):
self.assignment = Assignment.objects.create(
title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1
title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1,
)
self.assignment.add_candidate(self.admin)
@ -100,24 +100,28 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"),
{
"title": "test_title_ailai4toogh3eefaa2Vo",
"pollmethod": "YNA",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(AssignmentPoll.objects.exists())
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.title, "test_title_ailai4toogh3eefaa2Vo")
self.assertEqual(poll.pollmethod, "YNA")
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YNA)
self.assertEqual(poll.type, "named")
# Check defaults
self.assertTrue(poll.global_no)
self.assertTrue(poll.global_abstain)
self.assertEqual(poll.amount_global_no, None)
self.assertEqual(poll.amount_global_abstain, None)
self.assertFalse(poll.allow_multiple_votes_per_candidate)
self.assertEqual(poll.votes_amount, 1)
self.assertEqual(poll.assignment.id, self.assignment.id)
self.assertEqual(poll.description, "")
self.assertTrue(poll.options.exists())
option = AssignmentOption.objects.get()
self.assertTrue(option.user.id, self.admin.id)
@ -127,26 +131,29 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"),
{
"title": "test_title_ahThai4pae1pi4xoogoo",
"pollmethod": "YN",
"pollmethod": AssignmentPoll.POLLMETHOD_YN,
"type": "pseudoanonymous",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
"majority_method": AssignmentPoll.MAJORITY_THREE_QUARTERS,
"global_no": False,
"global_abstain": False,
"allow_multiple_votes_per_candidate": True,
"votes_amount": 5,
"description": "test_description_ieM8ThuasoSh8aecai8p",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(AssignmentPoll.objects.exists())
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.title, "test_title_ahThai4pae1pi4xoogoo")
self.assertEqual(poll.pollmethod, "YN")
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YN)
self.assertEqual(poll.type, "pseudoanonymous")
self.assertFalse(poll.global_no)
self.assertFalse(poll.global_abstain)
self.assertTrue(poll.allow_multiple_votes_per_candidate)
self.assertEqual(poll.votes_amount, 5)
self.assertEqual(poll.description, "test_description_ieM8ThuasoSh8aecai8p")
def test_no_candidates(self):
self.assignment.remove_candidate(self.admin)
@ -154,62 +161,34 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"),
{
"title": "test_title_eing5eipue5cha2Iefai",
"pollmethod": "YNA",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_missing_title(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{"pollmethod": "YNA", "type": "named", "assignment_id": self.assignment.id},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_missing_pollmethod(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_OoCh9aitaeyaeth8nom1",
"type": "named",
"assignment_id": self.assignment.id,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_missing_type(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_Ail9Eizohshim0fora6o",
"pollmethod": "YNA",
"assignment_id": self.assignment.id,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_missing_assignment_id(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_eic7ooxaht5mee3quohK",
"pollmethod": "YNA",
"type": "named",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_missing_keys(self):
complete_request_data = {
"title": "test_title_keugh8Iu9ciyooGaevoh",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
}
for key in complete_request_data.keys():
request_data = {
_key: value
for _key, value in complete_request_data.items()
if _key != key
}
response = self.client.post(reverse("assignmentpoll-list"), request_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_with_groups(self):
group1 = get_group_model().objects.get(pk=1)
@ -218,12 +197,13 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"),
{
"title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": "YNA",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
"groups_id": [1, 2],
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
poll = AssignmentPoll.objects.get()
@ -235,12 +215,13 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"),
{
"title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": "YNA",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
"groups_id": [],
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
poll = AssignmentPoll.objects.get()
@ -251,11 +232,12 @@ class CreateAssignmentPoll(TestCase):
reverse("assignmentpoll-list"),
{
"title": "test_title_yaiyeighoh0Iraet3Ahc",
"pollmethod": "YNA",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "not_existing",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
@ -268,12 +250,93 @@ class CreateAssignmentPoll(TestCase):
"pollmethod": "not_existing",
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_not_supported_onehundred_percent_base(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": "invalid base",
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_not_supported_majority_method(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YN,
"majority_method": "invalid majority method",
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.exists())
def test_wrong_pollmethod_onehundred_percent_base_combination_1(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_VOTES,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_YNA)
def test_wrong_pollmethod_onehundred_percent_base_combination_2(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": AssignmentPoll.POLLMETHOD_YN,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_VOTES,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_YN)
def test_wrong_pollmethod_onehundred_percent_base_combination_3(self):
response = self.client.post(
reverse("assignmentpoll-list"),
{
"title": "test_title_Thoo2eiphohhi1eeXoow",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES,
"type": "named",
"assignment_id": self.assignment.id,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_YNA,
"majority_method": AssignmentPoll.MAJORITY_SIMPLE,
},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
poll = AssignmentPoll.objects.get()
self.assertEqual(
poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_VOTES
)
class UpdateAssignmentPoll(TestCase):
"""
@ -289,8 +352,10 @@ class UpdateAssignmentPoll(TestCase):
self.poll = AssignmentPoll.objects.create(
assignment=self.assignment,
title="test_title_beeFaihuNae1vej2ai8m",
pollmethod="votes",
pollmethod=AssignmentPoll.POLLMETHOD_VOTES,
type=BasePoll.TYPE_NAMED,
onehundred_percent_base=AssignmentPoll.PERCENT_BASE_VOTES,
majority_method=AssignmentPoll.MAJORITY_SIMPLE,
)
self.poll.create_options()
self.poll.groups.add(self.group)
@ -317,11 +382,13 @@ class UpdateAssignmentPoll(TestCase):
def test_patch_pollmethod(self):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]), {"pollmethod": "YNA"}
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{"pollmethod": AssignmentPoll.POLLMETHOD_YNA},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.pollmethod, "YNA")
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_YNA)
self.assertEqual(poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_YNA)
def test_patch_invalid_pollmethod(self):
response = self.client.patch(
@ -330,7 +397,7 @@ class UpdateAssignmentPoll(TestCase):
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.pollmethod, "votes")
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_VOTES)
def test_patch_type(self):
response = self.client.patch(
@ -350,9 +417,7 @@ class UpdateAssignmentPoll(TestCase):
def test_patch_groups_to_empty(self):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{"groups_id": []},
format="json",
reverse("assignmentpoll-detail", args=[self.poll.pk]), {"groups_id": []},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
@ -363,7 +428,6 @@ class UpdateAssignmentPoll(TestCase):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{"groups_id": [group2.id]},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
@ -381,12 +445,50 @@ class UpdateAssignmentPoll(TestCase):
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.title, "test_title_beeFaihuNae1vej2ai8m")
def test_patch_100_percent_base(self):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{"onehundred_percent_base": "cast"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.onehundred_percent_base, "cast")
def test_patch_wrong_100_percent_base(self):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{"onehundred_percent_base": "invalid"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
poll = AssignmentPoll.objects.get()
self.assertEqual(
poll.onehundred_percent_base, AssignmentPoll.PERCENT_BASE_VOTES
)
def test_patch_majority_method(self):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.majority_method, AssignmentPoll.MAJORITY_TWO_THIRDS)
def test_patch_wrong_majority_method(self):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{"majority_method": "invalid majority method"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.majority_method, AssignmentPoll.MAJORITY_SIMPLE)
def test_patch_multiple_fields(self):
response = self.client.patch(
reverse("assignmentpoll-detail", args=[self.poll.pk]),
{
"title": "test_title_ees6Tho8ahheen4cieja",
"pollmethod": "votes",
"pollmethod": AssignmentPoll.POLLMETHOD_VOTES,
"global_no": True,
"global_abstain": False,
"allow_multiple_votes_per_candidate": True,
@ -396,9 +498,11 @@ class UpdateAssignmentPoll(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = AssignmentPoll.objects.get()
self.assertEqual(poll.title, "test_title_ees6Tho8ahheen4cieja")
self.assertEqual(poll.pollmethod, "votes")
self.assertEqual(poll.pollmethod, AssignmentPoll.POLLMETHOD_VOTES)
self.assertTrue(poll.global_no)
self.assertFalse(poll.global_abstain)
self.assertEqual(poll.amount_global_no, Decimal("0"))
self.assertEqual(poll.amount_global_abstain, None)
self.assertTrue(poll.allow_multiple_votes_per_candidate)
self.assertEqual(poll.votes_amount, 42)
@ -462,7 +566,6 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
"votesvalid": "4.64",
"votesinvalid": "-2",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(AssignmentVote.objects.count(), 6)
@ -490,7 +593,6 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
"2": {"Y": "1", "N": "2.35", "A": "-1"},
}
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -501,7 +603,6 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": {"1": {"Y": "1", "N": "2.35", "A": "-1"}}},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -517,7 +618,6 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
"3": {"Y": "1", "N": "2.35", "A": "-1"},
}
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -543,9 +643,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
def test_wrong_data_format(self):
self.start_poll()
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
[1, 2, 5],
format="json",
reverse("assignmentpoll-vote", args=[self.poll.pk]), [1, 2, 5],
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -555,7 +653,6 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": [1, "string"]},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
@ -565,7 +662,6 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": {"string": "some_other_string"}},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -575,7 +671,6 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]),
{"options": {"1": [None]}},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -586,7 +681,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
data = {"options": {"1": {"Y": "1", "N": "3", "A": "-1"}}}
del data["options"]["1"][value]
response = self.client.post(
reverse("assignmentpoll-vote", args=[self.poll.pk]), data, format="json"
reverse("assignmentpoll-vote", args=[self.poll.pk]), data
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentVote.objects.exists())
@ -861,6 +956,8 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("2"))
self.assertEqual(option.abstain, Decimal("0"))
self.assertEqual(poll.amount_global_no, Decimal("2"))
self.assertEqual(poll.amount_global_abstain, Decimal("0"))
def test_global_no_forbidden(self):
self.poll.global_no = False
@ -871,6 +968,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
self.assertEqual(AssignmentPoll.objects.get().amount_global_no, None)
def test_global_abstain(self):
self.poll.votes_amount = 2
@ -885,6 +983,8 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("0"))
self.assertEqual(option.abstain, Decimal("2"))
self.assertEqual(poll.amount_global_no, Decimal("0"))
self.assertEqual(poll.amount_global_abstain, Decimal("2"))
def test_global_abstain_forbidden(self):
self.poll.global_abstain = False
@ -895,6 +995,7 @@ class VoteAssignmentPollNamedVotes(VoteAssignmentPollBaseTestClass):
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
self.assertEqual(AssignmentPoll.objects.get().amount_global_abstain, None)
def test_negative_vote(self):
self.start_poll()
@ -1463,12 +1564,16 @@ class VoteAssignmentPollAutoupdatesBaseClass(TestCase):
title="test_assignment_" + self._get_random_string(), open_posts=1
)
self.assignment.add_candidate(self.admin)
self.description = "test_description_paiquei5ahpie1wu8ohW"
self.poll = AssignmentPoll.objects.create(
assignment=self.assignment,
title="test_title_" + self._get_random_string(),
pollmethod=AssignmentPoll.POLLMETHOD_YNA,
type=self.poll_type,
state=AssignmentPoll.STATE_STARTED,
onehundred_percent_base=AssignmentPoll.PERCENT_BASE_CAST,
majority_method=AssignmentPoll.MAJORITY_TWO_THIRDS,
description=self.description,
)
self.poll.create_options()
self.poll.groups.add(self.delegate_group)
@ -1495,6 +1600,8 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"assignment_id": 1,
"global_abstain": True,
"global_no": True,
"amount_global_abstain": None,
"amount_global_no": None,
"groups_id": [GROUP_DELEGATE_PK],
"id": 1,
"options": [
@ -1510,7 +1617,10 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"state": AssignmentPoll.STATE_STARTED,
"title": self.poll.title,
"description": self.description,
"type": AssignmentPoll.TYPE_NAMED,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
"voted_id": [self.user.id],
"votes_amount": 1,
"votescast": "1.000000",
@ -1558,7 +1668,10 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"state": AssignmentPoll.STATE_STARTED,
"type": AssignmentPoll.TYPE_NAMED,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
"title": self.poll.title,
"description": self.description,
"groups_id": [GROUP_DELEGATE_PK],
"options": [{"id": 1, "user_id": self.admin.id, "weight": 1}],
"id": 1,
@ -1593,6 +1706,8 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"assignment_id": 1,
"global_abstain": True,
"global_no": True,
"amount_global_abstain": None,
"amount_global_no": None,
"groups_id": [GROUP_DELEGATE_PK],
"id": 1,
"options": [
@ -1608,7 +1723,10 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"state": AssignmentPoll.STATE_STARTED,
"title": self.poll.title,
"description": self.description,
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
"voted_id": [self.user.id],
"votes_amount": 1,
"votescast": "1.000000",
@ -1642,7 +1760,10 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
"state": AssignmentPoll.STATE_STARTED,
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
"title": self.poll.title,
"description": self.description,
"groups_id": [GROUP_DELEGATE_PK],
"options": [{"id": 1, "user_id": self.admin.id, "weight": 1}],
"id": 1,

View File

@ -95,8 +95,9 @@ class CreateMotionPoll(TestCase):
"pollmethod": "YNA",
"type": "named",
"motion_id": self.motion.id,
"onehundred_percent_base": "YN",
"majority_method": "simple",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(MotionPoll.objects.exists())
@ -107,53 +108,24 @@ class CreateMotionPoll(TestCase):
self.assertEqual(poll.motion.id, self.motion.id)
self.assertTrue(poll.options.exists())
def test_missing_title(self):
response = self.client.post(
reverse("motionpoll-list"),
{"pollmethod": "YNA", "type": "named", "motion_id": self.motion.id},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.exists())
def test_missing_pollmethod(self):
response = self.client.post(
reverse("motionpoll-list"),
{
"title": "test_title_OoCh9aitaeyaeth8nom1",
"type": "named",
"motion_id": self.motion.id,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.exists())
def test_missing_type(self):
response = self.client.post(
reverse("motionpoll-list"),
{
"title": "test_title_Ail9Eizohshim0fora6o",
"pollmethod": "YNA",
"motion_id": self.motion.id,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.exists())
def test_missing_assignment_id(self):
response = self.client.post(
reverse("motionpoll-list"),
{
"title": "test_title_eic7ooxaht5mee3quohK",
"pollmethod": "YNA",
"type": "named",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.exists())
def test_missing_keys(self):
complete_request_data = {
"title": "test_title_OoCh9aitaeyaeth8nom1",
"type": "named",
"pollmethod": "YNA",
"motion_id": self.motion.id,
"onehundred_percent_base": "YN",
"majority_method": "simple",
}
for key in complete_request_data.keys():
request_data = {
_key: value
for _key, value in complete_request_data.items()
if _key != key
}
response = self.client.post(reverse("motionpoll-list"), request_data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.exists())
def test_with_groups(self):
group1 = get_group_model().objects.get(pk=1)
@ -165,9 +137,10 @@ class CreateMotionPoll(TestCase):
"pollmethod": "YNA",
"type": "named",
"motion_id": self.motion.id,
"onehundred_percent_base": "YN",
"majority_method": "simple",
"groups_id": [1, 2],
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
poll = MotionPoll.objects.get()
@ -182,9 +155,10 @@ class CreateMotionPoll(TestCase):
"pollmethod": "YNA",
"type": "named",
"motion_id": self.motion.id,
"onehundred_percent_base": "YN",
"majority_method": "simple",
"groups_id": [],
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
poll = MotionPoll.objects.get()
@ -199,7 +173,6 @@ class CreateMotionPoll(TestCase):
"type": "not_existing",
"motion_id": self.motion.id,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.exists())
@ -213,7 +186,6 @@ class CreateMotionPoll(TestCase):
"type": "named",
"motion_id": self.motion.id,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.exists())
@ -236,8 +208,10 @@ class UpdateMotionPoll(TestCase):
self.poll = MotionPoll.objects.create(
motion=self.motion,
title="test_title_beeFaihuNae1vej2ai8m",
pollmethod="YN",
pollmethod="YNA",
type="named",
onehundred_percent_base="YN",
majority_method="simple",
)
self.poll.create_options()
self.poll.groups.add(self.group)
@ -266,11 +240,12 @@ class UpdateMotionPoll(TestCase):
def test_patch_pollmethod(self):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]), {"pollmethod": "YNA"}
reverse("motionpoll-detail", args=[self.poll.pk]), {"pollmethod": "YN"}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.pollmethod, "YNA")
self.assertEqual(poll.pollmethod, "YN")
self.assertEqual(poll.onehundred_percent_base, "YN")
def test_patch_invalid_pollmethod(self):
response = self.client.patch(
@ -278,7 +253,7 @@ class UpdateMotionPoll(TestCase):
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
poll = MotionPoll.objects.get()
self.assertEqual(poll.pollmethod, "YN")
self.assertEqual(poll.pollmethod, "YNA")
def test_patch_type(self):
response = self.client.patch(
@ -296,11 +271,45 @@ class UpdateMotionPoll(TestCase):
poll = MotionPoll.objects.get()
self.assertEqual(poll.type, "named")
def test_patch_groups_to_empty(self):
def test_patch_100_percent_base(self):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]),
{"groups_id": []},
format="json",
{"onehundred_percent_base": "cast"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.onehundred_percent_base, "cast")
def test_patch_wrong_100_percent_base(self):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]),
{"onehundred_percent_base": "invalid"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
poll = MotionPoll.objects.get()
self.assertEqual(poll.onehundred_percent_base, "YN")
def test_patch_majority_method(self):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]),
{"majority_method": "two_thirds"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.majority_method, "two_thirds")
def test_patch_wrong_majority_method(self):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]),
{"majority_method": "invalid majority method"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
poll = MotionPoll.objects.get()
self.assertEqual(poll.majority_method, "simple")
def test_patch_groups_to_empty(self):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]), {"groups_id": []},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -311,7 +320,6 @@ class UpdateMotionPoll(TestCase):
response = self.client.patch(
reverse("motionpoll-detail", args=[self.poll.pk]),
{"groups_id": [group2.id]},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -378,7 +386,6 @@ class VoteMotionPollAnalog(TestCase):
"votesvalid": "4.64",
"votesinvalid": "-2",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -403,9 +410,7 @@ class VoteMotionPollAnalog(TestCase):
def test_vote_missing_data(self):
self.start_poll()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"Y": "4", "N": "22.6"},
format="json",
reverse("motionpoll-vote", args=[self.poll.pk]), {"Y": "4", "N": "22.6"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -413,7 +418,7 @@ class VoteMotionPollAnalog(TestCase):
def test_vote_wrong_data_format(self):
self.start_poll()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5], format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -423,7 +428,6 @@ class VoteMotionPollAnalog(TestCase):
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]),
{"Y": "some string", "N": "-2", "A": "3"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -477,7 +481,7 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -498,11 +502,11 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -524,7 +528,7 @@ class VoteMotionPollNamed(TestCase):
config["general_system_enable_anonymous"] = True
guest_client = APIClient()
response = guest_client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "Y", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "Y"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -572,7 +576,7 @@ class VoteMotionPollNamed(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5], format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -608,6 +612,8 @@ class VoteMotionPollNamedAutoupdates(TestCase):
pollmethod="YNA",
type=BasePoll.TYPE_NAMED,
state=MotionPoll.STATE_STARTED,
onehundred_percent_base="YN",
majority_method="simple",
)
self.poll.create_options()
self.poll.groups.add(self.delegate_group)
@ -631,6 +637,8 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"state": 2,
"type": "named",
"title": "test_title_tho8PhiePh8upaex6phi",
"onehundred_percent_base": "YN",
"majority_method": "simple",
"groups_id": [GROUP_DELEGATE_PK],
"votesvalid": "1.000000",
"votesinvalid": "0.000000",
@ -685,6 +693,8 @@ class VoteMotionPollNamedAutoupdates(TestCase):
"state": 2,
"type": "named",
"title": "test_title_tho8PhiePh8upaex6phi",
"onehundred_percent_base": "YN",
"majority_method": "simple",
"groups_id": [GROUP_DELEGATE_PK],
"options": [{"id": 1}],
"id": 1,
@ -726,6 +736,8 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
pollmethod="YNA",
type=BasePoll.TYPE_PSEUDOANONYMOUS,
state=MotionPoll.STATE_STARTED,
onehundred_percent_base="YN",
majority_method="simple",
)
self.poll.create_options()
self.poll.groups.add(self.delegate_group)
@ -749,6 +761,8 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"state": 2,
"type": "pseudoanonymous",
"title": "test_title_cahP1umooteehah2jeey",
"onehundred_percent_base": "YN",
"majority_method": "simple",
"groups_id": [GROUP_DELEGATE_PK],
"votesvalid": "1.000000",
"votesinvalid": "0.000000",
@ -789,6 +803,8 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
"state": 2,
"type": "pseudoanonymous",
"title": "test_title_cahP1umooteehah2jeey",
"onehundred_percent_base": "YN",
"majority_method": "simple",
"groups_id": [GROUP_DELEGATE_PK],
"options": [{"id": 1}],
"id": 1,
@ -847,7 +863,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
@ -869,11 +885,11 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "N", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "N"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
option = MotionPoll.objects.get().options.get()
@ -889,7 +905,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
config["general_system_enable_anonymous"] = True
guest_client = APIClient()
response = guest_client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "Y", format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), "Y"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -928,7 +944,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
self.make_admin_delegate()
self.make_admin_present()
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5], format="json"
reverse("motionpoll-vote", args=[self.poll.pk]), [1, 2, 5]
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
@ -976,6 +992,8 @@ class PublishMotionPoll(TestCase):
title="test_title_Nufae0iew7Iorox2thoo",
pollmethod="YNA",
type=BasePoll.TYPE_PSEUDOANONYMOUS,
onehundred_percent_base="YN",
majority_method="simple",
)
self.poll.create_options()
option = self.poll.options.get()
@ -1003,6 +1021,8 @@ class PublishMotionPoll(TestCase):
"state": 4,
"type": "pseudoanonymous",
"title": "test_title_Nufae0iew7Iorox2thoo",
"onehundred_percent_base": "YN",
"majority_method": "simple",
"groups_id": [],
"votesvalid": "0.000000",
"votesinvalid": "0.000000",