diff --git a/CHANGELOG b/CHANGELOG
index 54ea48a31..2245ca1a7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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.
diff --git a/openslides/assignments/config_variables.py b/openslides/assignments/config_variables.py
index e742293d2..06f52a6d8 100644
--- a/openslides/assignments/config_variables.py
+++ b/openslides/assignments/config_variables.py
@@ -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')
diff --git a/openslides/assignments/migrations/0003_auto_20160907_0946.py b/openslides/assignments/migrations/0003_auto_20160907_0946.py
new file mode 100644
index 000000000..b45036247
--- /dev/null
+++ b/openslides/assignments/migrations/0003_auto_20160907_0946.py
@@ -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),
+ ),
+ ]
diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py
index 5b1b15c65..b997c33b8 100644
--- a/openslides/assignments/models.py
+++ b/openslides/assignments/models.py
@@ -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']
diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py
index de1603e09..a61d8c8d9 100644
--- a/openslides/assignments/serializers.py
+++ b/openslides/assignments/serializers.py
@@ -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',
diff --git a/openslides/assignments/static/js/assignments/base.js b/openslides/assignments/static/js/assignments/base.js
index e3928c054..e580f0a0b 100644
--- a/openslides/assignments/static/js/assignments/base.js
+++ b/openslides/assignments/static/js/assignments/base.js
@@ -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: {
diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js
index 23074e1b2..53ac5f4bd 100644
--- a/openslides/assignments/static/js/assignments/site.js
+++ b/openslides/assignments/static/js/assignments/site.js
@@ -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');
diff --git a/openslides/assignments/static/templates/assignments/assignment-detail.html b/openslides/assignments/static/templates/assignments/assignment-detail.html
index 54dc164ac..3ecb28029 100644
--- a/openslides/assignments/static/templates/assignments/assignment-detail.html
+++ b/openslides/assignments/static/templates/assignments/assignment-detail.html
@@ -160,7 +160,7 @@
4.