Added vote weight and fixed named voting

This commit is contained in:
FinnStutzenstein 2020-04-06 14:14:00 +02:00 committed by Emanuel Schütze
parent 04a7ce22fd
commit 7882ea1a25
13 changed files with 125 additions and 26 deletions

View File

@ -1,6 +1,6 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { BaseModel } from '../base/base-model';
import { BaseDecimalModel } from '../base/base-decimal-model';
/**
* Iterable pre selection of genders (sexes)
@ -14,7 +14,7 @@ export type UserAuthType = 'default' | 'saml';
* Representation of a user in contrast to the operator.
* @ignore
*/
export class User extends BaseModel<User> {
export class User extends BaseDecimalModel<User> {
public static COLLECTIONSTRING = 'users/user';
public id: number;
@ -35,8 +35,13 @@ export class User extends BaseModel<User> {
public is_active?: boolean;
public default_password?: string;
public auth_type?: UserAuthType;
public vote_weight: number;
public constructor(input?: Partial<User>) {
super(User.COLLECTIONSTRING, input);
}
protected getDecimalFields(): string[] {
return ["vote_weight"];
}
}

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.12 on 2020-04-06 11:26
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("assignments", "0011_voting_4"),
]
operations = [
migrations.AlterUniqueTogether(
name="assignmentvote", unique_together={("user", "option")},
),
]

View File

@ -245,6 +245,7 @@ class AssignmentVote(RESTModelMixin, BaseVote):
class Meta:
default_permissions = ()
unique_together = ("user", "option")
class AssignmentOptionManager(BaseManager):

View File

@ -2,6 +2,7 @@ from decimal import Decimal
from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.utils import IntegrityError
from openslides.poll.views import BaseOptionViewSet, BasePollViewSet, BaseVoteViewSet
from openslides.utils.auth import has_perm
@ -507,12 +508,18 @@ class AssignmentPollViewSet(BasePollViewSet):
def create_votes_type_named_pseudoanonymous(
self, data, poll, check_user, vote_user
):
""" check_user is used for the voted-array, vote_user is the one put into the vote """
"""
check_user is used for the voted-array and weight of the vote,
vote_user is the one put into the vote
"""
options = poll.get_options()
for option_id, result in data.items():
option = options.get(pk=option_id)
vote = AssignmentVote.objects.create(
option=option, user=vote_user, value=result
option=option,
user=vote_user,
value=result,
weight=check_user.vote_weight,
)
inform_changed_data(vote, no_delete_on_restriction=True)
inform_changed_data(option, no_delete_on_restriction=True)
@ -520,6 +527,13 @@ class AssignmentPollViewSet(BasePollViewSet):
poll.voted.add(check_user)
def handle_named_vote(self, data, poll, user):
try:
with transaction.atomic():
return self.try_handle_named_vote(data, poll, user)
except IntegrityError:
raise ValidationError({"detail": "You have already voted"})
def try_handle_named_vote(self, data, poll, user):
if user in poll.voted.all():
raise ValidationError({"detail": "You have already voted"})
@ -532,6 +546,13 @@ class AssignmentPollViewSet(BasePollViewSet):
self.create_votes_type_named_pseudoanonymous(data, poll, user, user)
def handle_pseudoanonymous_vote(self, data, poll, user):
try:
with transaction.atomic():
return self.try_handle_pseudoanonymous_vote(data, poll, user)
except IntegrityError:
raise ValidationError({"detail": "You have already voted"})
def try_handle_pseudoanonymous_vote(self, data, poll, user):
if user in poll.voted.all():
raise ValidationError({"detail": "You have already voted"})

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.12 on 2020-04-06 11:26
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("motions", "0034_voting_2"),
]
operations = [
migrations.AlterUniqueTogether(
name="motionvote", unique_together={("user", "option")},
),
]

View File

@ -880,6 +880,7 @@ class MotionVote(RESTModelMixin, BaseVote):
class Meta:
default_permissions = ()
unique_together = ("user", "option")
class MotionOptionManager(BaseManager):

View File

@ -1,4 +1,3 @@
from decimal import Decimal
from typing import List, Set
import jsonschema
@ -1223,29 +1222,32 @@ class MotionPollViewSet(BasePollViewSet):
elif poll.pollmethod == MotionPoll.POLLMETHOD_YN and data not in ("Y", "N"):
raise ValidationError("Data must be Y or N")
if poll.type == MotionPoll.TYPE_PSEUDOANONYMOUS:
if user in poll.voted.all():
raise ValidationError("You already voted on this poll")
def handle_named_vote(self, data, poll, user):
if user in poll.voted.all():
raise ValidationError({"detail": "You have already voted"})
poll.voted.add(user)
poll.save()
option = poll.options.get()
vote, _ = MotionVote.objects.get_or_create(user=user, option=option)
vote = MotionVote.objects.create(user=user, option=option)
self.handle_named_and_pseudoanonymous_vote(data, user, poll, option, vote)
def handle_pseudoanonymous_vote(self, data, poll, user):
if user in poll.voted.all():
raise ValidationError({"detail": "You have already voted"})
poll.voted.add(user)
poll.save()
option = poll.options.get()
vote = MotionVote.objects.create(user=None, option=option)
self.handle_named_and_pseudoanonymous_vote(data, user, poll, option, vote)
def handle_named_and_pseudoanonymous_vote(self, data, user, poll, option, vote):
vote.value = data
vote.weight = Decimal("1")
vote.weight = user.vote_weight
vote.save(no_delete_on_restriction=True)
inform_changed_data(option)
poll.voted.add(user)
poll.save()
class MotionOptionViewSet(BaseOptionViewSet):
queryset = MotionOption.objects.all()

View File

@ -115,6 +115,7 @@ class BasePollViewSet(ModelViewSet):
poll.save()
@detail_route(methods=["POST"])
@transaction.atomic
def start(self, request, pk):
poll = self.get_object()
if poll.state != BasePoll.STATE_CREATED:
@ -126,6 +127,7 @@ class BasePollViewSet(ModelViewSet):
return Response()
@detail_route(methods=["POST"])
@transaction.atomic
def stop(self, request, pk):
poll = self.get_object()
# Analog polls could not be stopped; they are stopped when
@ -145,6 +147,7 @@ class BasePollViewSet(ModelViewSet):
return Response()
@detail_route(methods=["POST"])
@transaction.atomic
def publish(self, request, pk):
poll = self.get_object()
if poll.state != BasePoll.STATE_FINISHED:
@ -157,6 +160,7 @@ class BasePollViewSet(ModelViewSet):
return Response()
@detail_route(methods=["POST"])
@transaction.atomic
def pseudoanonymize(self, request, pk):
poll = self.get_object()
@ -173,6 +177,7 @@ class BasePollViewSet(ModelViewSet):
return Response()
@detail_route(methods=["POST"])
@transaction.atomic
def reset(self, request, pk):
poll = self.get_object()
poll.reset()
@ -226,11 +231,11 @@ class BasePollViewSet(ModelViewSet):
if poll.state != BasePoll.STATE_STARTED:
raise ValidationError("You can only vote on a started poll.")
if not request.user.is_present or not in_some_groups(
request.user.id,
list(poll.groups.values_list("pk", flat=True)),
exact=True,
request.user.id,
list(poll.groups.values_list("pk", flat=True)),
exact=True,
):
self.permission_denied(request)
self.permission_denied(request)
def parse_vote_value(self, obj, key):
""" Raises a ValidationError on incorrect values, including None """

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.12 on 2020-04-06 10:34
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0012_user_auth_type"),
]
operations = [
migrations.AddField(
model_name="user",
name="vote_weight",
field=models.DecimalField(
blank=True, decimal_places=6, default=Decimal("1"), max_digits=15
),
),
]

View File

@ -1,4 +1,5 @@
import smtplib
from decimal import Decimal
from django.conf import settings
from django.contrib.auth.hashers import make_password
@ -159,6 +160,10 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
is_committee = models.BooleanField(default=False)
vote_weight = models.DecimalField(
default=Decimal("1"), max_digits=15, decimal_places=6, null=False, blank=True
)
objects = UserManager()
class Meta:

View File

@ -25,6 +25,7 @@ USERCANSEESERIALIZER_FIELDS = (
"groups",
"is_present",
"is_committee",
"vote_weight",
)
@ -38,7 +39,7 @@ USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + (
)
class UserFullSerializer(ModelSerializer):
class UserSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.

View File

@ -775,7 +775,7 @@ class VoteMotionPollNamed(TestCase):
response = self.client.post(
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
)
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
poll = MotionPoll.objects.get()
self.assertEqual(poll.votesvalid, Decimal("1"))
self.assertEqual(poll.votesinvalid, Decimal("0"))
@ -783,8 +783,8 @@ class VoteMotionPollNamed(TestCase):
self.assertEqual(poll.get_votes().count(), 1)
option = poll.options.get()
self.assertEqual(option.yes, Decimal("0"))
self.assertEqual(option.no, Decimal("0"))
self.assertEqual(option.abstain, Decimal("1"))
self.assertEqual(option.no, Decimal("1"))
self.assertEqual(option.abstain, Decimal("0"))
vote = option.votes.get()
self.assertEqual(vote.user, self.admin)

View File

@ -1,7 +1,7 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from openslides.users.serializers import UserFullSerializer
from openslides.users.serializers import UserSerializer
from openslides.utils.rest_api import ValidationError
@ -10,7 +10,7 @@ class UserCreateUpdateSerializerTest(TestCase):
"""
Tests, that the validator raises a ValidationError, if not data is given.
"""
serializer = UserFullSerializer()
serializer = UserSerializer()
data: object = {}
with self.assertRaises(ValidationError):
@ -22,7 +22,7 @@ class UserCreateUpdateSerializerTest(TestCase):
Tests, that an empty username is generated.
"""
generate_username.return_value = "test_value"
serializer = UserFullSerializer()
serializer = UserSerializer()
data = {"first_name": "TestName"}
new_data = serializer.validate(data)
@ -34,7 +34,7 @@ class UserCreateUpdateSerializerTest(TestCase):
Tests, that an empty username is not set in a patch request context.
"""
view = MagicMock(action="partial_update")
serializer = UserFullSerializer(context={"view": view})
serializer = UserSerializer(context={"view": view})
data = {"first_name": "TestName"}
new_data = serializer.validate(data)