New calculate percentage options (fixes issue #2182)

This commit is contained in:
Maximilian Krambach 2016-08-26 13:46:57 +02:00
parent 66aa42021a
commit e8fa0eca5a
16 changed files with 282 additions and 196 deletions

View File

@ -14,6 +14,7 @@ Agenda:
Assignments: Assignments:
- Remove unused assignment config to publish winner election results only. - Remove unused assignment config to publish winner election results only.
- Added options to calculate percentages on different bases.
Core: Core:
- Added support for big assemblies with lots of users. - Added support for big assemblies with lots of users.
@ -27,6 +28,7 @@ Motions:
- Added configurable fields for comments. - Added configurable fields for comments.
- Added recommendations for motions. - Added recommendations for motions.
- Changed label of former state "commited a bill" to "refered to committee". - Changed label of former state "commited a bill" to "refered to committee".
- Added options to calculate percentages on different bases.
Users: Users:
- Added field is_committee and new default group Committees. - Added field is_committee and new default group Committees.

View File

@ -1,7 +1,6 @@
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from openslides.core.config import ConfigVariable from openslides.core.config import ConfigVariable
from openslides.poll.models import PERCENT_BASE_CHOICES
def get_config_variables(): def get_config_variables():
@ -28,10 +27,18 @@ def get_config_variables():
yield ConfigVariable( yield ConfigVariable(
name='assignments_poll_100_percent_base', name='assignments_poll_100_percent_base',
default_value='WITHOUT_INVALID', default_value='YES_NO_ABSTAIN',
input_type='choice', input_type='choice',
label='The 100 % base of an election result consists of', label='The 100 % base of an election result consists of',
choices=PERCENT_BASE_CHOICES, 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)'}),
help_text="For Yes/No/Abstain and Yes/No the 100 % base depends on the election method: If there are " +
"more candidates than open posts, the sum of all votes of all candidates is 100%. Otherwise " +
"the sum of all votes per candidate is 100 %.",
weight=420, weight=420,
group='Elections', group='Elections',
subgroup='Ballot and ballot papers') subgroup='Ballot and ballot papers')

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.9 on 2016-09-07 09:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assignments', '0002_assignmentpoll_yesno'),
]
operations = [
migrations.RemoveField(
model_name='assignmentpoll',
name='yesno',
),
migrations.RemoveField(
model_name='assignmentpoll',
name='yesnoabstain',
),
migrations.AddField(
model_name='assignmentpoll',
name='pollmethod',
field=models.CharField(default='yna', max_length=5),
),
]

View File

@ -208,29 +208,23 @@ class Assignment(RESTModelMixin, models.Model):
# Find out the method of the election # Find out the method of the election
if config['assignments_poll_vote_values'] == 'votes': if config['assignments_poll_vote_values'] == 'votes':
yesnoabstain = False pollmethod = 'votes'
yesno = False
elif config['assignments_poll_vote_values'] == 'yesnoabstain': elif config['assignments_poll_vote_values'] == 'yesnoabstain':
yesnoabstain = True pollmethod = 'yna'
yesno = False
elif config['assignments_poll_vote_values'] == 'yesno': elif config['assignments_poll_vote_values'] == 'yesno':
yesnoabstain = False pollmethod = 'yn'
yesno = True
else: else:
# config['assignments_poll_vote_values'] == 'auto' # config['assignments_poll_vote_values'] == 'auto'
# candidates <= available posts -> yes/no/abstain # candidates <= available posts -> yes/no/abstain
if len(candidates) <= (self.open_posts - self.elected.count()): if len(candidates) <= (self.open_posts - self.elected.count()):
yesno = False pollmethod = 'yna'
yesnoabstain = True
else: else:
yesno = False pollmethod = 'votes'
yesnoabstain = False
# Create the poll with the candidates. # Create the poll with the candidates.
poll = self.polls.create( poll = self.polls.create(
description=self.poll_description_default, description=self.poll_description_default,
yesnoabstain=yesnoabstain, pollmethod=pollmethod)
yesno=yesno)
poll.set_options({'candidate': user} for user in candidates) poll.set_options({'candidate': user} for user in candidates)
# Add all candidates to list of speakers of related agenda item # Add all candidates to list of speakers of related agenda item
@ -364,8 +358,9 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin,
Assignment, Assignment,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='polls') related_name='polls')
yesnoabstain = models.BooleanField(default=False) pollmethod = models.CharField(
yesno = models.BooleanField(default=False) max_length=5,
default='yna')
description = models.CharField( description = models.CharField(
max_length=79, max_length=79,
blank=True) blank=True)
@ -377,9 +372,9 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin,
return self.assignment return self.assignment
def get_vote_values(self): def get_vote_values(self):
if self.yesnoabstain: if self.pollmethod == 'yna':
return ['Yes', 'No', 'Abstain'] return ['Yes', 'No', 'Abstain']
elif self.yesno: elif self.pollmethod == 'yn':
return ['Yes', 'No'] return ['Yes', 'No']
else: else:
return ['Votes'] return ['Votes']

View File

@ -96,8 +96,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
model = AssignmentPoll model = AssignmentPoll
fields = ( fields = (
'id', 'id',
'yesnoabstain', 'pollmethod',
'yesno',
'description', 'description',
'published', 'published',
'options', 'options',
@ -107,7 +106,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
'votes', 'votes',
'has_votes', 'has_votes',
'assignment') # js-data needs the assignment-id in the nested object to define relations. 'assignment') # js-data needs the assignment-id in the nested object to define relations.
read_only_fields = ('yesnoabstain',) read_only_fields = ('pollmethod',)
validators = (default_votes_validator,) validators = (default_votes_validator,)
def get_has_votes(self, obj): def get_has_votes(self, obj):
@ -122,12 +121,12 @@ class AssignmentAllPollSerializer(ModelSerializer):
Customized update method for polls. To update votes use the write Customized update method for polls. To update votes use the write
only field 'votes'. only field 'votes'.
Example data for a 'yesnoabstain'=true poll with two candidates: Example data for a 'pollmethod'='yna' poll with two candidates:
"votes": [{"Yes": 10, "No": 4, "Abstain": -2}, "votes": [{"Yes": 10, "No": 4, "Abstain": -2},
{"Yes": -1, "No": 0, "Abstain": -2}] {"Yes": -1, "No": 0, "Abstain": -2}]
Example data for a 'yesnoabstain'=false poll with two candidates: Example data for a 'pollmethod' ='yn' poll with two candidates:
"votes": [{"Votes": 10}, {"Votes": 0}] "votes": [{"Votes": 10}, {"Votes": 0}]
""" """
# Update votes. # Update votes.
@ -168,8 +167,7 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
model = AssignmentPoll model = AssignmentPoll
fields = ( fields = (
'id', 'id',
'yesnoabstain', 'pollmethod',
'yesno',
'description', 'description',
'published', 'published',
'options', 'options',

View File

@ -21,28 +21,85 @@ angular.module('OpenSlidesApp.assignments', [])
var poll = this.poll; var poll = this.poll;
var votes = []; var votes = [];
var config = Config.get('assignments_poll_100_percent_base').value; var config = Config.get('assignments_poll_100_percent_base').value;
var impossible = false;
var yes = null, no = null, abstain = null;
angular.forEach(this.votes, function(vote) {
if (vote.value == "Yes" || vote.value == "Votes") {
yes = vote.weight;
} else if (vote.value == "No") {
no = vote.weight;
} else if (vote.value == "Abstain") {
abstain = vote.weight;
}
});
//calculation for several candidates without yes/no options
var do_sum_of_all = false;
var sum_of_votes = 0;
if (poll.options.length > 1 && poll.pollmethod == 'votes') {
do_sum_of_all = true;
}
if (do_sum_of_all === true) {
angular.forEach(poll.options, function(option) {
angular.forEach(option.votes, function(vote) {
if (vote.value == "Votes") {
if (vote.weight >= 0 ) {
sum_of_votes = sum_of_votes + vote.weight;
} else {
impossible = true;
}
}
});
});
}
angular.forEach(this.votes, function(vote) { angular.forEach(this.votes, function(vote) {
// check for special value // check for special value
var value; var value;
switch (vote.weight) { switch (vote.weight) {
case -1: case -1:
value = gettextCatalog.getString('majority'); value = gettextCatalog.getString('majority');
impossible = true;
break; break;
case -2: case -2:
value = gettextCatalog.getString('undocumented'); value = gettextCatalog.getString('undocumented');
impossible = true;
break; break;
default: default:
if (vote.weight >= 0) {
value = vote.weight; value = vote.weight;
} else {
value = 0;
}
break; break;
} }
// calculate percent value // calculate percent value
var percentStr, percentNumber; var percentStr, percentNumber, base;
if (config == "WITHOUT_INVALID" && poll.votesvalid > 0 && vote.weight >= 0) { if (config == "VALID") {
percentNumber = Math.round(vote.weight * 100 / poll.votesvalid * 10) / 10; if (poll.votesvalid && poll.votesvalid > 0) {
} else if (config == "WITH_INVALID" && poll.votescast > 0 && vote.weight >= 0) { base = poll.votesvalid;
percentNumber = Math.round(vote.weight * 100 / (poll.votescast) * 10) / 10;
} }
if (percentNumber >= 0 ) { } else if ( config == "CAST") {
if (poll.votescast && poll.votescast > 0) {
base = poll.votescast;
}
} else if (config == "YES_NO" && !impossible) {
if (vote.value == "Yes" || vote.value == "No" || vote.value == "Votes"){
if (do_sum_of_all) {
base = sum_of_votes;
} else {
base = yes + no;
}
}
} else if (config == "YES_NO_ABSTAIN" && !impossible) {
if (do_sum_of_all) {
base = sum_of_votes;
} else {
base = yes + no + abstain;
}
}
if (base !== 'undefined' && vote.weight >= 0) {
percentNumber = Math.round(vote.weight * 100 / base * 10) / 10;
}
if (percentNumber >= 0 && percentNumber !== 'undefined') {
percentStr = "(" + percentNumber + "%)"; percentStr = "(" + percentNumber + "%)";
} }
votes.push({ votes.push({
@ -88,11 +145,20 @@ angular.module('OpenSlidesApp.assignments', [])
return name; return name;
}, },
// returns object with value and percent (for votes valid/invalid/cast only) // returns object with value and percent (for votes valid/invalid/cast only)
getVote: function (vote) { getVote: function (type) {
if (!this.has_votes || !vote) { var value, percentStr, vote;
return; switch(type) {
case 'votesinvalid':
vote = this.votesinvalid;
break;
case 'votesvalid':
vote = this.votesvalid;
break;
case 'votescast':
vote = this.votescast;
break;
} }
var value = ''; if (this.has_votes && vote) {
switch (vote) { switch (vote) {
case -1: case -1:
value = gettextCatalog.getString('majority'); value = gettextCatalog.getString('majority');
@ -102,20 +168,27 @@ angular.module('OpenSlidesApp.assignments', [])
break; break;
default: default:
value = vote; value = vote;
break;
} }
// calculate percent value if (vote >= 0) {
var config = Config.get('assignments_poll_100_percent_base').value; var config = Config.get('assignments_poll_100_percent_base').value;
var percent; var percentNumber;
if ((config == "WITHOUT_INVALID" && vote == this.votesvalid && vote >= 0) || if (config == "CAST" && this.votescast && this.votescast > 0) {
(config == "WITH_INVALID" && vote == this.votescast && vote >= 0)) { percentNumber = Math.round(vote * 100 / this.votescast * 10) / 10;
percent = '(100%)'; } else if (config == "VALID" && this.votesvalid && this.votesvalid >= 0) {
if (type === 'votesvalid'){
percentNumber = Math.round(vote * 100 / this.votesvalid * 10) / 10;
}
}
if (percentNumber !== 'undefined' && percentNumber >= 0 && percentNumber <=100) {
percentStr = "(" + percentNumber + "%)";
}
}
} }
return { return {
'value': value, 'value': value,
'percent': percent 'percentStr': percentStr
}; };
}, }
}, },
relations: { relations: {
belongsTo: { belongsTo: {

View File

@ -460,6 +460,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
$scope.alert = { type: 'danger', msg: message, show: true }; $scope.alert = { type: 'danger', msg: message, show: true };
}); });
}; };
// mark candidate as (not) elected // mark candidate as (not) elected
$scope.markElected = function (user, reverse) { $scope.markElected = function (user, reverse) {
if (reverse) { if (reverse) {
@ -590,8 +591,8 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
// add dynamic form fields // add dynamic form fields
assignmentpoll.options.forEach(function(option) { assignmentpoll.options.forEach(function(option) {
var defaultValue; var defaultValue;
if (assignmentpoll.yesnoabstain || assignmentpoll.yesno) { if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') {
if (assignmentpoll.yesnoabstain) { if (assignmentpoll.pollmethod == 'yna') {
defaultValue = { defaultValue = {
'yes': '', 'yes': '',
'no': '', 'no': '',
@ -608,7 +609,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
if (option.votes.length) { if (option.votes.length) {
defaultValue.yes = option.votes[0].weight; defaultValue.yes = option.votes[0].weight;
defaultValue.no = option.votes[1].weight; defaultValue.no = option.votes[1].weight;
if (assignmentpoll.yesnoabstain){ if (assignmentpoll.pollmethod == 'yna'){
defaultValue.abstain = option.votes[2].weight; defaultValue.abstain = option.votes[2].weight;
} }
} }
@ -637,7 +638,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
}, },
defaultValue: defaultValue.no defaultValue: defaultValue.no
}); });
if (assignmentpoll.yesnoabstain){ if (assignmentpoll.pollmethod == 'yna'){
$scope.formFields.push( $scope.formFields.push(
{ {
key:'abstain_' + option.candidate_id, key:'abstain_' + option.candidate_id,
@ -673,7 +674,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
key: 'votesvalid', key: 'votesvalid',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Votes valid'), label: gettextCatalog.getString('Valid ballots'),
type: 'number' type: 'number'
} }
}, },
@ -681,7 +682,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
key: 'votesinvalid', key: 'votesinvalid',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Votes invalid'), label: gettextCatalog.getString('Invalid ballots'),
type: 'number' type: 'number'
} }
}, },
@ -689,7 +690,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
key: 'votescast', key: 'votescast',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Votes cast'), label: gettextCatalog.getString('Casted ballots'),
type: 'number' type: 'number'
} }
}, },
@ -707,7 +708,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
// save assignmentpoll // save assignmentpoll
$scope.save = function (poll) { $scope.save = function (poll) {
var votes = []; var votes = [];
if (assignmentpoll.yesnoabstain) { if (assignmentpoll.pollmethod == 'yna') {
assignmentpoll.options.forEach(function(option) { assignmentpoll.options.forEach(function(option) {
votes.push({ votes.push({
"Yes": poll['yes_' + option.candidate_id], "Yes": poll['yes_' + option.candidate_id],
@ -715,7 +716,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
"Abstain": poll['abstain_' + option.candidate_id] "Abstain": poll['abstain_' + option.candidate_id]
}); });
}); });
} else if (assignmentpoll.yesno) { } else if (assignmentpoll.pollmethod == 'yn') {
assignmentpoll.options.forEach(function(option) { assignmentpoll.options.forEach(function(option) {
votes.push({ votes.push({
"Yes": poll['yes_' + option.candidate_id], "Yes": poll['yes_' + option.candidate_id],
@ -764,8 +765,13 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
gettext('Elections'); gettext('Elections');
gettext('Ballot and ballot papers'); gettext('Ballot and ballot papers');
gettext('The 100 % base of an election result consists of'); gettext('The 100 % base of an election result consists of');
gettext('All valid votes (Yes/No/Abstain)'); gettext('For Yes/No/Abstain and Yes/No the 100 % base depends ' +
gettext('All votes cast (including invalid votes)'); 'on the election method: If there are more candidates ' +
'than open posts, the sum of all votes of all candidates is 100 %.');
gettext('Yes/No/Abstain per candidate');
gettext('Yes/No per candidate');
gettext('All valid ballots');
gettext('All casted ballots');
gettext('Disabled (no percents)'); gettext('Disabled (no percents)');
gettext('Number of ballot papers (selection)'); gettext('Number of ballot papers (selection)');
gettext('Number of all delegates'); gettext('Number of all delegates');

View File

@ -160,7 +160,7 @@
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
4. <translate>Project</translate> 4. <translate>Project</translate>
</button> </button>
| <a class="btn btn-danger btn-sm" <a class="btn btn-danger btn-sm"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}" ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}"
ng-bootbox-confirm-action="deleteBallot(poll)"> ng-bootbox-confirm-action="deleteBallot(poll)">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
@ -180,7 +180,6 @@
<!-- candidates (poll options) --> <!-- candidates (poll options) -->
<tr ng-repeat="option in poll.options"> <tr ng-repeat="option in poll.options">
<!-- elected --> <!-- elected -->
<td class="minimum"> <td class="minimum">
<button os-perms="assignments.can_manage" <button os-perms="assignments.can_manage"
@ -188,7 +187,6 @@
<i ng-if="option.is_elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i> <i ng-if="option.is_elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
<i ng-if="!option.is_elected" class="fa fa-star-o" title="{{ 'is not elected' | translate }}"></i> <i ng-if="!option.is_elected" class="fa fa-star-o" title="{{ 'is not elected' | translate }}"></i>
</button> </button>
<!-- candidate name --> <!-- candidate name -->
<td> <td>
<a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a> <a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
@ -197,7 +195,7 @@
<td ng-if="poll.has_votes"> <td ng-if="poll.has_votes">
<div ng-init="votes = option.getVotes()"> <div ng-init="votes = option.getVotes()">
<div ng-repeat="vote in votes"> <div ng-repeat="vote in votes">
<span ng-if="poll.yesnoabstain || poll.yesno">{{ vote.label }}:</span> <span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span>
{{ vote.value }} {{ vote.percentStr }} {{ vote.value }} {{ vote.percentStr }}
<div ng-if="vote.percentNumber"> <div ng-if="vote.percentNumber">
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar> <uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
@ -209,21 +207,21 @@
<tr> <tr>
<td> <td>
<td> <td>
<translate>Valid votes</translate> <translate>Valid ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesvalid)"> <td ng-if="poll.has_votes" ng-init="vote = poll.getVote('votesvalid')">
{{ vote.value }} {{ vote.percent }} {{ vote.value }} {{ vote.percentStr }}
<tr> <tr>
<td> <td>
<td> <td>
<translate>Invalid votes</translate> <translate>Invalid ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesinvalid)"> <td ng-if="poll.has_votes" ng-init="vote = poll.getVote('votesinvalid')">
{{ vote.value }} {{ vote.value }} {{ vote.percentStr }}
<tr class="total bg-info"> <tr class="total bg-info">
<td> <td>
<td> <td>
<translate>Votes cast</translate> <translate>Casted ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votescast)"> <td ng-if="poll.has_votes" ng-init="vote = poll.getVote('votescast')">
{{ vote.value }} {{ vote.percent }} {{ vote.value }} {{ vote.percentStr }}
</table> </table>
</div> </div>
</uib-tab> </uib-tab>

View File

@ -51,12 +51,12 @@
<!-- votes --> <!-- votes -->
<td ng-if="poll.has_votes"> <td ng-if="poll.has_votes">
<div ng-init="votes = option.getVotes()"> <div ng-init="votes = option.getVotes()">
<div ng-show="poll.yesnoabstain || poll.yesno"> <div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">
<span ng-show="poll.yesnoabstain"> <span ng-show="poll.pollmethod == 'yna'">
{{ votes[0].label }}: <strong>{{ votes[0].value }}</strong> · {{ votes[0].label }}: <strong>{{ votes[0].value }}</strong> ·
{{ votes[1].label }}: {{ votes[1].value }} · {{ votes[1].label }}: {{ votes[1].value }} ·
{{ votes[2].label }}: {{ votes[2].value }} </span> {{ votes[2].label }}: {{ votes[2].value }} </span>
<span ng-show="poll.yesno"> <span ng-show="poll.pollmethod == 'yn'">
{{ votes[0].label }}: <strong>{{ votes[0].value }}</strong> · {{ votes[0].label }}: <strong>{{ votes[0].value }}</strong> ·
{{ votes[1].label }}: {{ votes[1].value }}</span> {{ votes[1].label }}: {{ votes[1].value }}</span>
<uib-progress ng-if="votes[0].percentNumber>=0"> <uib-progress ng-if="votes[0].percentNumber>=0">
@ -71,7 +71,7 @@
</uib-bar> </uib-bar>
</uib-progress> </uib-progress>
</div> </div>
<div ng-hide="poll.yesnoabstain || poll.yesno"> <div ng-show="poll.pollmethod == 'votes'">
<div ng-repeat="vote in votes"> <div ng-repeat="vote in votes">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value }} {{ vote.percentStr }}
<div ng-if="vote.percentNumber >= 0"> <div ng-if="vote.percentNumber >= 0">
@ -84,19 +84,19 @@
<!-- total votes (valid/invalid/casts) --> <!-- total votes (valid/invalid/casts) -->
<tr class="total"> <tr class="total">
<td> <td>
<translate>Valid votes</translate> <translate>Valid ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesvalid)"> <td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesvalid)">
{{ vote.value }} {{ vote.percent }} {{ vote.value }} {{ vote.percentStr }}
<tr class="total"> <tr class="total">
<td> <td>
<translate>Invalid votes</translate> <translate>Invalid ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesinvalid)"> <td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesinvalid)">
{{ vote.value }} {{ vote.value }} {{ vote.percentStr }}
<tr class="total bg-info"> <tr class="total bg-info">
<td> <td>
<translate>Votes cast</translate> <translate>Casted ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votescast)"> <td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votescast)">
{{ vote.value }} {{ vote.percent }} {{ vote.value }} {{ vote.percentStr }}
</table> </table>
</div> </div>
</div> </div>

View File

@ -358,7 +358,7 @@ class AssignmentPDF(PDFView):
footrow_one.append(_("Valid votes")) footrow_one.append(_("Valid votes"))
votesvalid_is_used = False votesvalid_is_used = False
for poll in polls: for poll in polls:
footrow_one.append(poll.print_votesvalid()) footrow_one.append(poll.votesvalid)
if poll.votesvalid is not None: if poll.votesvalid is not None:
votesvalid_is_used = True votesvalid_is_used = True
if votesvalid_is_used: if votesvalid_is_used:
@ -369,7 +369,7 @@ class AssignmentPDF(PDFView):
footrow_two.append(_("Invalid votes")) footrow_two.append(_("Invalid votes"))
votesinvalid_is_used = False votesinvalid_is_used = False
for poll in polls: for poll in polls:
footrow_two.append(poll.print_votesinvalid()) footrow_two.append(poll.votesinvalid)
if poll.votesinvalid is not None: if poll.votesinvalid is not None:
votesinvalid_is_used = True votesinvalid_is_used = True
if votesinvalid_is_used: if votesinvalid_is_used:
@ -380,7 +380,7 @@ class AssignmentPDF(PDFView):
footrow_three.append(_("Votes cast")) footrow_three.append(_("Votes cast"))
votescast_is_used = False votescast_is_used = False
for poll in polls: for poll in polls:
footrow_three.append(poll.print_votescast()) footrow_three.append(poll.votescast)
if poll.votescast is not None: if poll.votescast is not None:
votescast_is_used = True votescast_is_used = True
if votescast_is_used: if votescast_is_used:
@ -488,7 +488,7 @@ class AssignmentPollPDF(PDFView):
counter = 0 counter = 0
cellcolumnA = [] cellcolumnA = []
# Choose kind of ballot paper (YesNoAbstain, YesNo or Yes) # Choose kind of ballot paper (YesNoAbstain, YesNo or Yes)
if self.poll.yesnoabstain or self.poll.yesno: # YesNoAbstain/YesNo ballot: max 27 candidates if self.poll.pollmethod in ['yna', 'yn']: # YesNoAbstain/YesNo ballot: max 27 candidates
for option in options: for option in options:
counter += 1 counter += 1
candidate = option.candidate candidate = option.candidate
@ -498,7 +498,7 @@ class AssignmentPollPDF(PDFView):
cell.append(Paragraph( cell.append(Paragraph(
"(%s)" % candidate.structure_level, "(%s)" % candidate.structure_level,
stylesheet['Ballot_option_suffix_YNA'])) stylesheet['Ballot_option_suffix_YNA']))
if self.poll.yesnoabstain: if self.poll.pollmethod == 'yna':
cell.append(Paragraph( cell.append(Paragraph(
"&nbsp;", stylesheet['Ballot_option_suffix_YNA'])) "&nbsp;", stylesheet['Ballot_option_suffix_YNA']))
cell.append(Paragraph("<font name='circlefont' size='15'>%(circle)s</font> \ cell.append(Paragraph("<font name='circlefont' size='15'>%(circle)s</font> \

View File

@ -1,7 +1,6 @@
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from openslides.core.config import ConfigVariable from openslides.core.config import ConfigVariable
from openslides.poll.models import PERCENT_BASE_CHOICES
from .models import Workflow from .models import Workflow
@ -23,10 +22,6 @@ def get_config_variables():
papers' and 'PDF'. The generator has to be evaluated during app loading papers' and 'PDF'. The generator has to be evaluated during app loading
(see apps.py). (see apps.py).
""" """
PERCENT_BASE_CHOICES_MOTION = ({
'value': "WITHOUT_ABSTAIN",
'display_name': 'Yes and No votes'},)
PERCENT_BASE_CHOICES_MOTION += PERCENT_BASE_CHOICES
# General # General
yield ConfigVariable( yield ConfigVariable(
@ -180,10 +175,16 @@ def get_config_variables():
yield ConfigVariable( yield ConfigVariable(
name='motions_poll_100_percent_base', name='motions_poll_100_percent_base',
default_value='WITHOUT_INVALID', default_value='YES_NO_ABSTAIN',
input_type='choice', input_type='choice',
label='The 100 % base of a voting result consists of', label='The 100 % base of a voting result consists of',
choices=PERCENT_BASE_CHOICES_MOTION, choices=(
{'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain'},
{'value': 'YES_NO', 'display_name': 'Yes/No'},
{'value': 'VALID', 'display_name': 'All valid ballots'},
{'value': 'CAST', 'display_name': 'All casted ballots'},
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'}
),
weight=355, weight=355,
group='Motions', group='Motions',
subgroup='Voting and ballot papers') subgroup='Voting and ballot papers')

View File

@ -112,11 +112,11 @@ def motion_to_pdf(pdf, motion):
yes, no, abstain = (option['Yes'], option['No'], option['Abstain']) yes, no, abstain = (option['Yes'], option['No'], option['Abstain'])
valid, invalid, votescast = ('', '', '') valid, invalid, votescast = ('', '', '')
if poll.votesvalid is not None: if poll.votesvalid is not None:
valid = "<br/>%s: %s" % (_("Valid votes"), poll.print_votesvalid()) valid = "<br/>%s" % (_("Valid votes"))
if poll.votesinvalid is not None: if poll.votesinvalid is not None:
invalid = "<br/>%s: %s" % (_("Invalid votes"), poll.print_votesinvalid()) invalid = "<br/>%s" % (_("Invalid votes"))
if poll.votescast is not None: if poll.votescast is not None:
votescast = "<br/>%s: %s" % (_("Votes cast"), poll.print_votescast()) votescast = "<br/>%s" % (_("Votes cast"))
if len(polls) > 1: if len(polls) > 1:
cell6b.append(Paragraph("%s. %s" % (ballotcounter, _("Vote")), cell6b.append(Paragraph("%s. %s" % (ballotcounter, _("Vote")),
stylesheet['Bold'])) stylesheet['Bold']))

View File

@ -82,30 +82,48 @@ angular.module('OpenSlidesApp.motions', [
if (!this.has_votes) { if (!this.has_votes) {
return; return;
} }
var impossible = false;
var value = ''; var value = '';
switch (vote) { switch (vote) {
case -1: case -1:
value = gettextCatalog.getString('majority'); value = gettextCatalog.getString('majority');
impossible = true;
break; break;
case -2: case -2:
value = gettextCatalog.getString('undocumented'); value = gettextCatalog.getString('undocumented');
impossible = true;
break; break;
default: default:
if (vote >= 0) {
value = vote; value = vote;
} else {
value = 0; //value was not defined
}
break; break;
} }
// calculate percent value // calculate percent value
var config = Config.get('motions_poll_100_percent_base').value; var config = Config.get('motions_poll_100_percent_base').value;
var percentStr; var percentStr;
var percentNumber = null; var percentNumber = null;
if (config == "WITHOUT_INVALID" && this.votesvalid > 0 && vote >= 0) { var base = null;
percentNumber = Math.round(vote * 100 / this.votesvalid * 10) / 10; if (!impossible) {
} else if (config == "WITH_INVALID" && this.votescast > 0 && vote >= 0) { if (config == "YES_NO_ABSTAIN") {
percentNumber = Math.round(vote * 100 / (this.votescast) * 10) / 10; if (type == 'yes' || type == 'no' || type == 'abstain') {
} else if (config == "WITHOUT_ABSTAIN" && vote >= 0) { base = this.yes + this.no + this.abstain;
if (type == 'yes' || type == 'no') {
percentNumber = Math.round(vote * 100 / (this.yes + this.no) * 10) / 10;
} }
} else if (config == "YES_NO") {
if (type == 'yes' || type == 'no') {
base = this.yes + this.no;
}
} else if (config == "VALID" && type !== 'votescast' && type !== 'votesinvalid' &&
this.votesvalid > 0) {
base = this.votesvalid;
} else if (config == "CAST" && this.votescast > 0) {
base = this.votescast;
}
}
if (base !== null) {
percentNumber = Math.round(vote * 100 / (base) * 10) / 10;
} }
if (percentNumber !== null) { if (percentNumber !== null) {
percentStr = "(" + percentNumber + "%)"; percentStr = "(" + percentNumber + "%)";

View File

@ -85,8 +85,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
no = poll.no ? poll.no : '-', no = poll.no ? poll.no : '-',
noRelative = poll.getVote(poll.no, 'no').percentStr, noRelative = poll.getVote(poll.no, 'no').percentStr,
abstain = poll.abstain ? poll.abstain : '-', abstain = poll.abstain ? poll.abstain : '-',
abstainrelativeGet = poll.getVote(poll.abstain, 'abstain').percentStr, abstainRelative = poll.getVote(poll.abstain, 'abstain').percentStr,
abstainRelative = abstainrelativeGet ? abstainrelativeGet : '',
valid = poll.votesvalid ? poll.votesvalid : '-', valid = poll.votesvalid ? poll.votesvalid : '-',
validRelative = poll.getVote(poll.votesvalid, 'votesvalid').percentStr, validRelative = poll.getVote(poll.votesvalid, 'votesvalid').percentStr,
number = { number = {
@ -122,7 +121,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
yesPart = converter.createElement("columns", createPart(gettextCatalog.getString("Yes"), yes, yesRelative)), yesPart = converter.createElement("columns", createPart(gettextCatalog.getString("Yes"), yes, yesRelative)),
noPart = converter.createElement("columns", createPart(gettextCatalog.getString("No"), no, noRelative)), noPart = converter.createElement("columns", createPart(gettextCatalog.getString("No"), no, noRelative)),
abstainPart = converter.createElement("columns", createPart(gettextCatalog.getString("Abstain"), abstain, abstainRelative)), abstainPart = converter.createElement("columns", createPart(gettextCatalog.getString("Abstain"), abstain, abstainRelative)),
totalPart = converter.createElement("columns", createPart(gettextCatalog.getString("Valid votes"), valid, validRelative)), totalPart = converter.createElement("columns", createPart(gettextCatalog.getString("Valid ballots"), valid, validRelative)),
heading = converter.createElement("columns", [number, headerText]), heading = converter.createElement("columns", [number, headerText]),
pollResult = converter.createElement("stack", [ pollResult = converter.createElement("stack", [
heading, yesPart, noPart, abstainPart, totalPart heading, yesPart, noPart, abstainPart, totalPart
@ -794,7 +793,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
key: 'votesvalid', key: 'votesvalid',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Valid votes'), label: gettextCatalog.getString('Valid ballots'),
type: 'number' type: 'number'
} }
}, },
@ -802,7 +801,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
key: 'votesinvalid', key: 'votesinvalid',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Invalid votes'), label: gettextCatalog.getString('Invalid ballots'),
type: 'number' type: 'number'
} }
}, },
@ -810,7 +809,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
key: 'votescast', key: 'votescast',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Votes cast'), label: gettextCatalog.getString('Casted ballots'),
type: 'number' type: 'number'
} }
}]; }];
@ -1863,8 +1862,10 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
// subgroup Voting and ballot papers // subgroup Voting and ballot papers
gettext('Voting and ballot papers'); gettext('Voting and ballot papers');
gettext('The 100 % base of a voting result consists of'); gettext('The 100 % base of a voting result consists of');
gettext('All valid votes (Yes/No/Abstain)'); gettext('Yes/No/Abstain');
gettext('All votes cast (including invalid votes)'); gettext('Yes/No');
gettext('All valid ballots');
gettext('All casted ballots');
gettext('Disabled (no percents)'); gettext('Disabled (no percents)');
gettext('Yes and No votes'); gettext('Yes and No votes');
gettext('Number of ballot papers (selection)'); gettext('Number of ballot papers (selection)');

View File

@ -233,7 +233,7 @@
<td class="icon"> <td class="icon">
<i class="fa fa-check fa-lg"></i> <i class="fa fa-check fa-lg"></i>
<td ng-init="votesValid = poll.getVote(poll.votesvalid, 'votesvalid')"> <td ng-init="votesValid = poll.getVote(poll.votesvalid, 'votesvalid')">
<span class="result_label"><translate>Valid votes</translate>:</span> <span class="result_label"><translate>Valid ballots</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesValid.value }} {{ votesValid.percentStr }} {{ votesValid.value }} {{ votesValid.percentStr }}
</span> </span>
@ -242,24 +242,18 @@
<td class="icon"> <td class="icon">
<i class="fa fa-ban fa-lg"></i> <i class="fa fa-ban fa-lg"></i>
<td ng-init="votesInvalid = poll.getVote(poll.votesinvalid, 'votesinvalid')"> <td ng-init="votesInvalid = poll.getVote(poll.votesinvalid, 'votesinvalid')">
<span class="result_label"><translate>Invalid votes</translate>:</span> <span class="result_label"><translate>Invalid ballots</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesInvalid.value }} {{ votesInvalid.value }} {{ votesInvalid.percentStr }}
<span ng-if="config('motions_poll_100_percent_base') == 'WITH_INVALID'">
{{ votesInvalid.percentStr }}
</span>
</span> </span>
<!-- votes cast --> <!-- votes cast -->
<tr class="total" ng-if="poll.votescast !== null"> <tr class="total" ng-if="poll.votescast !== null">
<td class="icon"> <td class="icon">
<strong style="font-size: 16px">&sum;</strong> <strong style="font-size: 16px">&sum;</strong>
<td ng-init="votesCast = poll.getVote(poll.votescast, 'votescast')"> <td ng-init="votesCast = poll.getVote(poll.votescast, 'votescast')">
<span class="result_label"><translate>Votes cast</translate>:</span> <span class="result_label"><translate>Casted ballots</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesCast.value }} {{ votesCast.value }} {{ votesCast.percentStr }}
<span ng-if="config('motions_poll_100_percent_base') == 'WITH_INVALID'">
{{ votesCast.percentStr }}
</span>
</span> </span>
</table> </table>
</ol> </ol>

View File

@ -65,11 +65,6 @@ class BaseVote(models.Model):
percent_base = 0 percent_base = 0
return print_value(self.weight, percent_base) return print_value(self.weight, percent_base)
PERCENT_BASE_CHOICES = (
{'value': 'WITHOUT_INVALID', 'display_name': 'All valid votes (Yes/No/Abstain)'},
{'value': 'WITH_INVALID', 'display_name': 'All votes cast (including invalid votes)'},
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'})
class CollectDefaultVotesMixin(models.Model): class CollectDefaultVotesMixin(models.Model):
""" """
@ -91,40 +86,10 @@ class CollectDefaultVotesMixin(models.Model):
def get_percent_base_choice(self): def get_percent_base_choice(self):
""" """
Returns one of the three strings in PERCENT_BASE_CHOICES. Returns one of the strings of the percent base.
""" """
raise NotImplementedError('You have to provide a get_percent_base_choice() method.') raise NotImplementedError('You have to provide a get_percent_base_choice() method.')
def print_votesvalid(self):
if self.get_percent_base_choice() == 'DISABLED':
value = print_value(self.votesvalid, None)
else:
value = print_value(self.votesvalid, self.get_percent_base())
return value
def print_votesinvalid(self):
if self.get_percent_base_choice() == 'WITH_INVALID':
value = print_value(self.votesinvalid, self.get_percent_base())
else:
value = print_value(self.votesinvalid, None)
return value
def print_votescast(self):
if self.get_percent_base_choice() == 'WITH_INVALID':
value = print_value(self.votescast, self.get_percent_base())
else:
value = print_value(self.votescast, None)
return value
def get_percent_base(self):
if self.get_percent_base_choice() == "WITHOUT_INVALID" and self.votesvalid and self.votesvalid > 0:
base = 100 / float(self.votesvalid)
elif self.get_percent_base_choice() == "WITH_INVALID" and self.votescast and self.votescast > 0:
base = 100 / float(self.votescast)
else:
base = None
return base
class PublishPollMixin(models.Model): class PublishPollMixin(models.Model):
""" """