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

View File

@ -1,7 +1,6 @@
from django.core.validators import MinValueValidator
from openslides.core.config import ConfigVariable
from openslides.poll.models import PERCENT_BASE_CHOICES
def get_config_variables():
@ -28,10 +27,18 @@ def get_config_variables():
yield ConfigVariable(
name='assignments_poll_100_percent_base',
default_value='WITHOUT_INVALID',
default_value='YES_NO_ABSTAIN',
input_type='choice',
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,
group='Elections',
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
if config['assignments_poll_vote_values'] == 'votes':
yesnoabstain = False
yesno = False
pollmethod = 'votes'
elif config['assignments_poll_vote_values'] == 'yesnoabstain':
yesnoabstain = True
yesno = False
pollmethod = 'yna'
elif config['assignments_poll_vote_values'] == 'yesno':
yesnoabstain = False
yesno = True
pollmethod = 'yn'
else:
# config['assignments_poll_vote_values'] == 'auto'
# candidates <= available posts -> yes/no/abstain
if len(candidates) <= (self.open_posts - self.elected.count()):
yesno = False
yesnoabstain = True
pollmethod = 'yna'
else:
yesno = False
yesnoabstain = False
pollmethod = 'votes'
# Create the poll with the candidates.
poll = self.polls.create(
description=self.poll_description_default,
yesnoabstain=yesnoabstain,
yesno=yesno)
pollmethod=pollmethod)
poll.set_options({'candidate': user} for user in candidates)
# Add all candidates to list of speakers of related agenda item
@ -364,8 +358,9 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin,
Assignment,
on_delete=models.CASCADE,
related_name='polls')
yesnoabstain = models.BooleanField(default=False)
yesno = models.BooleanField(default=False)
pollmethod = models.CharField(
max_length=5,
default='yna')
description = models.CharField(
max_length=79,
blank=True)
@ -377,9 +372,9 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin,
return self.assignment
def get_vote_values(self):
if self.yesnoabstain:
if self.pollmethod == 'yna':
return ['Yes', 'No', 'Abstain']
elif self.yesno:
elif self.pollmethod == 'yn':
return ['Yes', 'No']
else:
return ['Votes']

View File

@ -96,8 +96,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
model = AssignmentPoll
fields = (
'id',
'yesnoabstain',
'yesno',
'pollmethod',
'description',
'published',
'options',
@ -107,7 +106,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
'votes',
'has_votes',
'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,)
def get_has_votes(self, obj):
@ -122,12 +121,12 @@ class AssignmentAllPollSerializer(ModelSerializer):
Customized update method for polls. To update votes use the write
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},
{"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}]
"""
# Update votes.
@ -168,8 +167,7 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
model = AssignmentPoll
fields = (
'id',
'yesnoabstain',
'yesno',
'pollmethod',
'description',
'published',
'options',

View File

@ -21,28 +21,85 @@ angular.module('OpenSlidesApp.assignments', [])
var poll = this.poll;
var votes = [];
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) {
// check for special value
var value;
switch (vote.weight) {
case -1:
value = gettextCatalog.getString('majority');
impossible = true;
break;
case -2:
value = gettextCatalog.getString('undocumented');
impossible = true;
break;
default:
value = vote.weight;
if (vote.weight >= 0) {
value = vote.weight;
} else {
value = 0;
}
break;
}
// calculate percent value
var percentStr, percentNumber;
if (config == "WITHOUT_INVALID" && poll.votesvalid > 0 && vote.weight >= 0) {
percentNumber = Math.round(vote.weight * 100 / poll.votesvalid * 10) / 10;
} else if (config == "WITH_INVALID" && poll.votescast > 0 && vote.weight >= 0) {
percentNumber = Math.round(vote.weight * 100 / (poll.votescast) * 10) / 10;
var percentStr, percentNumber, base;
if (config == "VALID") {
if (poll.votesvalid && poll.votesvalid > 0) {
base = poll.votesvalid;
}
} 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 (percentNumber >= 0 ) {
if (base !== 'undefined' && vote.weight >= 0) {
percentNumber = Math.round(vote.weight * 100 / base * 10) / 10;
}
if (percentNumber >= 0 && percentNumber !== 'undefined') {
percentStr = "(" + percentNumber + "%)";
}
votes.push({
@ -88,34 +145,50 @@ angular.module('OpenSlidesApp.assignments', [])
return name;
},
// returns object with value and percent (for votes valid/invalid/cast only)
getVote: function (vote) {
if (!this.has_votes || !vote) {
return;
}
var value = '';
switch (vote) {
case -1:
value = gettextCatalog.getString('majority');
getVote: function (type) {
var value, percentStr, vote;
switch(type) {
case 'votesinvalid':
vote = this.votesinvalid;
break;
case -2:
value = gettextCatalog.getString('undocumented');
case 'votesvalid':
vote = this.votesvalid;
break;
default:
value = vote;
case 'votescast':
vote = this.votescast;
break;
}
// calculate percent value
var config = Config.get('assignments_poll_100_percent_base').value;
var percent;
if ((config == "WITHOUT_INVALID" && vote == this.votesvalid && vote >= 0) ||
(config == "WITH_INVALID" && vote == this.votescast && vote >= 0)) {
percent = '(100%)';
if (this.has_votes && vote) {
switch (vote) {
case -1:
value = gettextCatalog.getString('majority');
break;
case -2:
value = gettextCatalog.getString('undocumented');
break;
default:
value = vote;
}
if (vote >= 0) {
var config = Config.get('assignments_poll_100_percent_base').value;
var percentNumber;
if (config == "CAST" && this.votescast && this.votescast > 0) {
percentNumber = Math.round(vote * 100 / this.votescast * 10) / 10;
} 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 {
'value': value,
'percent': percent
'percentStr': percentStr
};
},
}
},
relations: {
belongsTo: {

View File

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

View File

@ -160,7 +160,7 @@
<i class="fa fa-video-camera"></i>
4. <translate>Project</translate>
</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-action="deleteBallot(poll)">
<i class="fa fa-times"></i>
@ -178,53 +178,51 @@
<th translate>Candidates
<th ng-if="poll.has_votes" class="col-sm-6" translate>Votes</th>
<!-- candidates (poll options) -->
<tr ng-repeat="option in poll.options">
<!-- candidates (poll options) -->
<tr ng-repeat="option in poll.options">
<!-- elected -->
<td class="minimum">
<button os-perms="assignments.can_manage"
ng-click="markElected(option.candidate_id, option.is_elected)" class="btn btn-default btn-xs">
<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>
</button>
<!-- candidate name -->
<td>
<a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
<!-- elected -->
<td class="minimum">
<button os-perms="assignments.can_manage"
ng-click="markElected(option.candidate_id, option.is_elected)" class="btn btn-default btn-xs">
<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>
</button>
<!-- candidate name -->
<td>
<a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
<!-- votes -->
<td ng-if="poll.has_votes">
<div ng-init="votes = option.getVotes()">
<div ng-repeat="vote in votes">
<span ng-if="poll.yesnoabstain || poll.yesno">{{ vote.label }}:</span>
{{ vote.value }} {{ vote.percentStr }}
<div ng-if="vote.percentNumber">
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
<!-- votes -->
<td ng-if="poll.has_votes">
<div ng-init="votes = option.getVotes()">
<div ng-repeat="vote in votes">
<span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span>
{{ vote.value }} {{ vote.percentStr }}
<div ng-if="vote.percentNumber">
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
</div>
</div>
</div>
</div>
<!-- total votes (valid/invalid/casts) -->
<tr>
<td>
<td>
<translate>Valid votes</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesvalid)">
{{ vote.value }} {{ vote.percent }}
<tr>
<td>
<td>
<translate>Invalid votes</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votesinvalid)">
{{ vote.value }}
<tr class="total bg-info">
<td>
<td>
<translate>Votes cast</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote(poll.votescast)">
{{ vote.value }} {{ vote.percent }}
</table>
<!-- total votes (valid/invalid/casts) -->
<tr>
<td>
<td>
<translate>Valid ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote('votesvalid')">
{{ vote.value }} {{ vote.percentStr }}
<tr>
<td>
<td>
<translate>Invalid ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote('votesinvalid')">
{{ vote.value }} {{ vote.percentStr }}
<tr class="total bg-info">
<td>
<td>
<translate>Casted ballots</translate>
<td ng-if="poll.has_votes" ng-init="vote = poll.getVote('votescast')">
{{ vote.value }} {{ vote.percentStr }}
</table>
</div>
</uib-tab>
</uib-tabset>

View File

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

View File

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

View File

@ -1,7 +1,6 @@
from django.core.validators import MinValueValidator
from openslides.core.config import ConfigVariable
from openslides.poll.models import PERCENT_BASE_CHOICES
from .models import Workflow
@ -23,10 +22,6 @@ def get_config_variables():
papers' and 'PDF'. The generator has to be evaluated during app loading
(see apps.py).
"""
PERCENT_BASE_CHOICES_MOTION = ({
'value': "WITHOUT_ABSTAIN",
'display_name': 'Yes and No votes'},)
PERCENT_BASE_CHOICES_MOTION += PERCENT_BASE_CHOICES
# General
yield ConfigVariable(
@ -180,10 +175,16 @@ def get_config_variables():
yield ConfigVariable(
name='motions_poll_100_percent_base',
default_value='WITHOUT_INVALID',
default_value='YES_NO_ABSTAIN',
input_type='choice',
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,
group='Motions',
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'])
valid, invalid, votescast = ('', '', '')
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:
invalid = "<br/>%s: %s" % (_("Invalid votes"), poll.print_votesinvalid())
invalid = "<br/>%s" % (_("Invalid votes"))
if poll.votescast is not None:
votescast = "<br/>%s: %s" % (_("Votes cast"), poll.print_votescast())
votescast = "<br/>%s" % (_("Votes cast"))
if len(polls) > 1:
cell6b.append(Paragraph("%s. %s" % (ballotcounter, _("Vote")),
stylesheet['Bold']))

View File

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

View File

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

View File

@ -233,7 +233,7 @@
<td class="icon">
<i class="fa fa-check fa-lg"></i>
<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">
{{ votesValid.value }} {{ votesValid.percentStr }}
</span>
@ -242,24 +242,18 @@
<td class="icon">
<i class="fa fa-ban fa-lg"></i>
<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">
{{ votesInvalid.value }}
<span ng-if="config('motions_poll_100_percent_base') == 'WITH_INVALID'">
{{ votesInvalid.percentStr }}
</span>
{{ votesInvalid.value }} {{ votesInvalid.percentStr }}
</span>
<!-- votes cast -->
<tr class="total" ng-if="poll.votescast !== null">
<td class="icon">
<strong style="font-size: 16px">&sum;</strong>
<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">
{{ votesCast.value }}
<span ng-if="config('motions_poll_100_percent_base') == 'WITH_INVALID'">
{{ votesCast.percentStr }}
</span>
{{ votesCast.value }} {{ votesCast.percentStr }}
</span>
</table>
</ol>

View File

@ -65,11 +65,6 @@ class BaseVote(models.Model):
percent_base = 0
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):
"""
@ -91,40 +86,10 @@ class CollectDefaultVotesMixin(models.Model):
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.')
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):
"""