diff --git a/client/src/app/shared/models/assignments/assignment.ts b/client/src/app/shared/models/assignments/assignment.ts
index 7e763f88a..620126972 100644
--- a/client/src/app/shared/models/assignments/assignment.ts
+++ b/client/src/app/shared/models/assignments/assignment.ts
@@ -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[];
}
diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html
index f400d8ae1..8ab276b7a 100644
--- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html
+++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html
@@ -280,13 +280,13 @@
[form]="assignmentForm"
>
-
+
diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts
index ecbc3353a..bf795e250 100644
--- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts
+++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts
@@ -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: [],
diff --git a/openslides/assignments/access_permissions.py b/openslides/assignments/access_permissions.py
index 96a43f647..9a76c5bd5 100644
--- a/openslides/assignments/access_permissions.py
+++ b/openslides/assignments/access_permissions.py
@@ -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):
diff --git a/openslides/assignments/config_variables.py b/openslides/assignments/config_variables.py
index 5a7e40f9a..f63cbbdf1 100644
--- a/openslides/assignments/config_variables.py
+++ b/openslides/assignments/config_variables.py
@@ -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",
diff --git a/openslides/assignments/migrations/0008_auto_20191017_1040.py b/openslides/assignments/migrations/0008_voting_1.py
similarity index 72%
rename from openslides/assignments/migrations/0008_auto_20191017_1040.py
rename to openslides/assignments/migrations/0008_voting_1.py
index 5a8b4aac2..e220a4748 100644
--- a/openslides/assignments/migrations/0008_auto_20191017_1040.py
+++ b/openslides/assignments/migrations/0008_voting_1.py
@@ -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"),
]
diff --git a/openslides/assignments/migrations/0009_voting_2.py b/openslides/assignments/migrations/0009_voting_2.py
new file mode 100644
index 000000000..da3437572
--- /dev/null
+++ b/openslides/assignments/migrations/0009_voting_2.py
@@ -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),
+ ]
diff --git a/openslides/assignments/migrations/0010_voting_3.py b/openslides/assignments/migrations/0010_voting_3.py
new file mode 100644
index 000000000..5a3b2fc9a
--- /dev/null
+++ b/openslides/assignments/migrations/0010_voting_3.py
@@ -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"),
+ ]
diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py
index 008ecf2ca..4ee22bc9a 100644
--- a/openslides/assignments/models.py
+++ b/openslides/assignments/models.py
@@ -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(
diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py
index 9ef737cb8..13ed729d8 100644
--- a/openslides/assignments/serializers.py
+++ b/openslides/assignments/serializers.py
@@ -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",
diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py
index 3f1a07903..797f81d25 100644
--- a/openslides/motions/access_permissions.py
+++ b/openslides/motions/access_permissions.py
@@ -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):
diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py
index abc052e74..c9251e71d 100644
--- a/openslides/motions/config_variables.py
+++ b/openslides/motions/config_variables.py
@@ -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",
+ )
diff --git a/openslides/motions/migrations/0033_auto_20191017_1100.py b/openslides/motions/migrations/0033_voting_1.py
similarity index 81%
rename from openslides/motions/migrations/0033_auto_20191017_1100.py
rename to openslides/motions/migrations/0033_voting_1.py
index 2dac920de..0d0ba8554 100644
--- a/openslides/motions/migrations/0033_auto_20191017_1100.py
+++ b/openslides/motions/migrations/0033_voting_1.py
@@ -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",
diff --git a/openslides/motions/migrations/0034_voting_2.py b/openslides/motions/migrations/0034_voting_2.py
new file mode 100644
index 000000000..7d1f8fb56
--- /dev/null
+++ b/openslides/motions/migrations/0034_voting_2.py
@@ -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),
+ ]
diff --git a/openslides/motions/models.py b/openslides/motions/models.py
index 46dd0b9f7..dbea8468c 100644
--- a/openslides/motions/models.py
+++ b/openslides/motions/models.py
@@ -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
diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py
index dfcee8b25..2a526abd7 100644
--- a/openslides/motions/serializers.py
+++ b/openslides/motions/serializers.py
@@ -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):
"""
diff --git a/openslides/poll/access_permissions.py b/openslides/poll/access_permissions.py
index ba3d3aa1b..86d1820e2 100644
--- a/openslides/poll/access_permissions.py
+++ b/openslides/poll/access_permissions.py
@@ -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
diff --git a/openslides/poll/majority.py b/openslides/poll/majority.py
deleted file mode 100644
index 7f498542b..000000000
--- a/openslides/poll/majority.py
+++ /dev/null
@@ -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"},
-)
diff --git a/openslides/poll/models.py b/openslides/poll/models.py
index 29c48770e..6415583a5 100644
--- a/openslides/poll/models.py
+++ b/openslides/poll/models.py
@@ -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)
diff --git a/openslides/poll/serializers.py b/openslides/poll/serializers.py
index 9bc312623..74af68396 100644
--- a/openslides/poll/serializers.py
+++ b/openslides/poll/serializers.py
@@ -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()
diff --git a/tests/integration/assignments/test_polls.py b/tests/integration/assignments/test_polls.py
index ef05cb594..acdc1d7ca 100644
--- a/tests/integration/assignments/test_polls.py
+++ b/tests/integration/assignments/test_polls.py
@@ -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,
diff --git a/tests/integration/motions/test_polls.py b/tests/integration/motions/test_polls.py
index ae4b3f977..66c6a320d 100644
--- a/tests/integration/motions/test_polls.py
+++ b/tests/integration/motions/test_polls.py
@@ -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",