Merge pull request #3774 from FinnStutzenstein/abstain-no-for-assignment-votes

Added general abstain/no fields for assignments. Used in votes mode.
This commit is contained in:
Emanuel Schütze 2018-08-14 09:34:37 +02:00 committed by GitHub
commit e9ad439cdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 166 additions and 83 deletions

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-07-03 13:23
from __future__ import unicode_literals
from django.db import migrations
from openslides.utils.models import MinMaxIntegerField
class Migration(migrations.Migration):
dependencies = [
('assignments', '0003_candidate_weight'),
]
operations = [
migrations.AddField(
model_name='assignmentpoll',
name='votesabstain',
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
),
migrations.AddField(
model_name='assignmentpoll',
name='votesno',
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
),
]

View File

@ -19,7 +19,7 @@ from openslides.poll.models import (
) )
from openslides.utils.autoupdate import inform_changed_data from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin from openslides.utils.models import MinMaxIntegerField, RESTModelMixin
from .access_permissions import AssignmentAccessPermissions from .access_permissions import AssignmentAccessPermissions
@ -423,6 +423,11 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
max_length=79, max_length=79,
blank=True) blank=True)
votesabstain = MinMaxIntegerField(null=True, blank=True, min_value=-2)
""" General abstain votes, used for pollmethod 'votes' """
votesno = MinMaxIntegerField(null=True, blank=True, min_value=-2)
""" General no votes, used for pollmethod 'votes' """
class Meta: class Meta:
default_permissions = () default_permissions = ()

View File

@ -111,6 +111,8 @@ class AssignmentAllPollSerializer(ModelSerializer):
'description', 'description',
'published', 'published',
'options', 'options',
'votesabstain',
'votesno',
'votesvalid', 'votesvalid',
'votesinvalid', 'votesinvalid',
'votescast', 'votescast',
@ -160,6 +162,8 @@ class AssignmentAllPollSerializer(ModelSerializer):
# Update remaining writeable fields. # Update remaining writeable fields.
instance.description = validated_data.get('description', instance.description) instance.description = validated_data.get('description', instance.description)
instance.published = validated_data.get('published', instance.published) instance.published = validated_data.get('published', instance.published)
instance.votesabstain = validated_data.get('votesabstain', instance.votesabstain)
instance.votesno = validated_data.get('votesno', instance.votesno)
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid) instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid)
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid) instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid)
instance.votescast = validated_data.get('votescast', instance.votescast) instance.votescast = validated_data.get('votescast', instance.votescast)
@ -182,6 +186,8 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
'description', 'description',
'published', 'published',
'options', 'options',
'votesabstain',
'votesno',
'votesvalid', 'votesvalid',
'votesinvalid', 'votesinvalid',
'votescast', 'votescast',

View File

@ -214,7 +214,9 @@ angular.module('OpenSlidesApp.assignments', [])
if (_.findIndex(this.options, predicate) !== -1) { if (_.findIndex(this.options, predicate) !== -1) {
base = void 0; base = void 0;
} else { } else {
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid' && type !== 'votesvalid') { if (typeof base === 'undefined' && type !== 'votesabstain' &&
type !== 'votesno' && type !== 'votescast' &&
type !== 'votesinvalid' && type !== 'votesvalid') {
base = _.reduce(this.options, function (sum, option) { base = _.reduce(this.options, function (sum, option) {
return sum + option.votes[0].weight; return sum + option.votes[0].weight;
}, 0); }, 0);
@ -240,6 +242,12 @@ angular.module('OpenSlidesApp.assignments', [])
config = Config.get('assignments_poll_100_percent_base').value; config = Config.get('assignments_poll_100_percent_base').value;
switch (type) { switch (type) {
case 'votesabstain':
vote = this.votesabstain;
break;
case 'votesno':
vote = this.votesno;
break;
case 'votesinvalid': case 'votesinvalid':
vote = this.votesinvalid; vote = this.votesinvalid;
break; break;

View File

@ -67,7 +67,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
var candidatesText = gettextCatalog.getString("Candidates") + ": "; var candidatesText = gettextCatalog.getString("Candidates") + ": ";
var userList = []; var userList = [];
angular.forEach(candidates, function(assignmentsRelatedUser) { _.forEach(candidates, function(assignmentsRelatedUser) {
userList.push({ userList.push({
text: assignmentsRelatedUser.user.get_full_name(), text: assignmentsRelatedUser.user.get_full_name(),
margin: [0, 0, 0, 10], margin: [0, 0, 0, 10],
@ -114,30 +114,30 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
//creates the voting string for the result table and differentiates between special values //creates the voting string for the result table and differentiates between special values
var parseVoteValue = function(voteObject, printLabel) { var parseVoteValue = function(voteObject, printLabel) {
var voteVal = ""; var voteVal = '';
if (voteObject) { if (voteObject) {
if (printLabel) { if (printLabel) {
voteVal += voteObject.label + ": "; voteVal += voteObject.label + ': ';
} }
voteVal += voteObject.value; voteVal += voteObject.value;
if (voteObject.percentStr) { if (voteObject.percentStr) {
voteVal += " " + voteObject.percentStr; voteVal += ' ' + voteObject.percentStr;
} }
} }
voteVal += "\n"; voteVal += '\n';
return voteVal; return voteVal;
}; };
// creates the election result table // creates the election result table
var createPollResultTable = function() { var createPollResultTable = function() {
var resultBody = []; var resultBody = [];
angular.forEach(assignment.polls, function(poll, pollIndex) { _.forEach(assignment.polls, function(poll, pollIndex) {
if (poll.published) { if (poll.published) {
var pollTableBody = []; var pollTableBody = [];
resultBody.push({ resultBody.push({
text: gettextCatalog.getString("Ballot") + " " + (pollIndex+1), text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1),
bold: true, bold: true,
style: 'textItem', style: 'textItem',
margin: [0, 15, 0, 0] margin: [0, 15, 0, 0]
@ -145,16 +145,16 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
pollTableBody.push([ pollTableBody.push([
{ {
text: gettextCatalog.getString("Candidates"), text: gettextCatalog.getString('Candidates'),
style: 'tableHeader', style: 'tableHeader',
}, },
{ {
text: gettextCatalog.getString("Votes"), text: gettextCatalog.getString('Votes'),
style: 'tableHeader', style: 'tableHeader',
} }
]); ]);
angular.forEach(poll.options, function(pollOption, optionIndex) { _.forEach(poll.options, function(pollOption, optionIndex) {
var candidateName = pollOption.candidate.get_full_name(); var candidateName = pollOption.candidate.get_full_name();
var votes = pollOption.getVotes(); // 0 = yes, 1 = no, 2 = abstain var votes = pollOption.getVotes(); // 0 = yes, 1 = no, 2 = abstain
var tableLine = []; var tableLine = [];
@ -169,7 +169,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
); );
} else { } else {
var resultBlock = []; var resultBlock = [];
angular.forEach(votes, function(vote) { _.forEach(votes, function(vote) {
resultBlock.push(parseVoteValue(vote, true)); resultBlock.push(parseVoteValue(vote, true));
}); });
tableLine.push({ tableLine.push({
@ -181,44 +181,26 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
pollTableBody.push(tableLine); pollTableBody.push(tableLine);
}); });
if (poll.votesvalid) { var pushConcludeRow = function (title, fieldName) {
pollTableBody.push([ if (poll[fieldName]) {
{ pollTableBody.push([
text: gettextCatalog.getString("Valid ballots"), {
style: 'tableConclude' text: gettextCatalog.getString(title),
}, style: 'tableConclude'
{ },
text: parseVoteValue(poll.getVote('votesvalid'), false), {
style: 'tableConclude' text: parseVoteValue(poll.getVote(fieldName), false),
}, style: 'tableConclude'
]); },
} ]);
}
};
if (poll.votesinvalid) { pushConcludeRow('Abstain', 'votesabstain');
pollTableBody.push([ pushConcludeRow('No', 'votesno');
{ pushConcludeRow('Valid ballots', 'votesvalid');
text: gettextCatalog.getString("Invalid ballots"), pushConcludeRow('Invalid ballots', 'votesinvalid');
style: 'tableConclude' pushConcludeRow('Casted ballots', 'votescast');
},
{
text: parseVoteValue(poll.getVote('votesinvalid'), false),
style: 'tableConclude'
},
]);
}
if (poll.votescast) {
pollTableBody.push([
{
text: gettextCatalog.getString("Casted ballots"),
style: 'tableConclude'
},
{
text: parseVoteValue(poll.getVote('votescast'), false),
style: 'tableConclude'
},
]);
}
var resultTableJsonSting = { var resultTableJsonSting = {
table: { table: {
@ -236,7 +218,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
// add the legend to the result body // add the legend to the result body
if (assignment.polls.length > 0 && isElectedSemaphore) { if (assignment.polls.length > 0 && isElectedSemaphore) {
resultBody.push({ resultBody.push({
text: "* = " + gettextCatalog.getString("is elected"), text: '* = ' + gettextCatalog.getString('is elected'),
margin: [0, 5, 0, 0], margin: [0, 5, 0, 0],
}); });
} }
@ -366,12 +348,17 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
var candidateBallotList = []; var candidateBallotList = [];
if (poll.pollmethod == 'votes') { if (poll.pollmethod == 'votes') {
angular.forEach(candidates, function(option) { _.forEach(candidates, function(option) {
var candidate = option.candidate.get_full_name(); var candidate = option.candidate.get_full_name();
candidateBallotList.push(PDFLayout.createBallotEntry(candidate)); candidateBallotList.push(PDFLayout.createBallotEntry(candidate));
}); });
// Add 'no' option
var no = gettextCatalog.getString('No');
var ballotEntry = PDFLayout.createBallotEntry(no);
ballotEntry.margin[1] = 25; // top margin
candidateBallotList.push(ballotEntry);
} else { } else {
angular.forEach(candidates, function(option) { _.forEach(candidates, function(option) {
var candidate; var candidate;
if (option.candidate) { if (option.candidate) {
candidate = option.candidate.get_full_name(); candidate = option.candidate.get_full_name();
@ -565,7 +552,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
}; };
var toc = []; var toc = [];
angular.forEach(assignmentTitles, function(title) { _.forEach(assignmentTitles, function(title) {
toc.push({ toc.push({
text: title, text: title,
style: "tableofcontent" style: "tableofcontent"
@ -656,7 +643,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
createBallotPdf: function (assignment, pollId) { createBallotPdf: function (assignment, pollId) {
var thePoll; var thePoll;
var pollNumber; var pollNumber;
angular.forEach(assignment.polls, function(poll, pollIndex) { _.forEach(assignment.polls, function(poll, pollIndex) {
if (poll.id == pollId) { if (poll.id == pollId) {
thePoll = poll; thePoll = poll;
pollNumber = pollIndex+1; pollNumber = pollIndex+1;

View File

@ -708,7 +708,7 @@ angular.module('OpenSlidesApp.assignments.site', [
// add dynamic form fields // add dynamic form fields
var options = $filter('orderBy')(assignmentpoll.options, 'weight'); var options = $filter('orderBy')(assignmentpoll.options, 'weight');
options.forEach(function(option) { _.forEach(options, function(option) {
var defaultValue; var defaultValue;
if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') { if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') {
defaultValue = {}; defaultValue = {};
@ -783,35 +783,60 @@ angular.module('OpenSlidesApp.assignments.site', [
}); });
} }
}); });
// add general form fields if (assignmentpoll.pollmethod == 'votes'){
$scope.formFields.push( $scope.formFields.push(
{ {
key: 'votesvalid', key: 'votesabstain',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Valid ballots'), label: gettextCatalog.getString('Abstain'),
type: 'number', type: 'number',
min: -2, min: -2,
} }
}, },
{ {
key: 'votesinvalid', key: 'votesno',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Invalid ballots'), label: gettextCatalog.getString('No'),
type: 'number',
min: -2,
}
},
{
key: 'votescast',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Casted ballots'),
type: 'number', type: 'number',
min: -2, min: -2,
} }
} }
);
}
// add general form fields
$scope.formFields.push(
{
template: '<hr class="smallhr">',
},
{
key: 'votesvalid',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Valid ballots'),
type: 'number',
min: -2,
}
},
{
key: 'votesinvalid',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Invalid ballots'),
type: 'number',
min: -2,
}
},
{
key: 'votescast',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Casted ballots'),
type: 'number',
min: -2,
}
}
); );
// save assignmentpoll // save assignmentpoll
@ -820,22 +845,22 @@ angular.module('OpenSlidesApp.assignments.site', [
if (assignmentpoll.pollmethod == 'yna') { 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],
"No": poll['no_' + option.candidate_id], 'No': poll['no_' + option.candidate_id],
"Abstain": poll['abstain_' + option.candidate_id] 'Abstain': poll['abstain_' + option.candidate_id]
}); });
}); });
} else if (assignmentpoll.pollmethod == 'yn') { } 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],
"No": poll['no_' + option.candidate_id] 'No': poll['no_' + option.candidate_id]
}); });
}); });
} else { } else {
assignmentpoll.options.forEach(function(option) { assignmentpoll.options.forEach(function(option) {
votes.push({ votes.push({
"Votes": poll['vote_' + option.candidate_id], 'Votes': poll['vote_' + option.candidate_id],
}); });
}); });
} }
@ -843,9 +868,11 @@ angular.module('OpenSlidesApp.assignments.site', [
poll.DSUpdate({ poll.DSUpdate({
assignment_id: poll.assignment_id, assignment_id: poll.assignment_id,
votes: votes, votes: votes,
votesabstain: poll.votesabstain,
votesno: poll.votesno,
votesvalid: poll.votesvalid, votesvalid: poll.votesvalid,
votesinvalid: poll.votesinvalid, votesinvalid: poll.votesinvalid,
votescast: poll.votescast votescast: poll.votescast,
}) })
.then(function(success) { .then(function(success) {
$scope.alert.show = false; $scope.alert.show = false;

View File

@ -195,10 +195,6 @@
{{ option.candidate.get_full_name() }} {{ option.candidate.get_full_name() }}
</a> </a>
</ul> </ul>
<strong translate>Election method</strong><br>
<span ng-if="poll.pollmethod == 'votes'" translate>One vote per candidate</span>
<span ng-if="poll.pollmethod == 'yna'" translate>Yes/No/Abstain per candidate</span>
<span ng-if="poll.pollmethod == 'yn'" translate>Yes/No per candidate</span>
</div> </div>
<!-- Settings for majority calculations --> <!-- Settings for majority calculations -->
@ -254,6 +250,18 @@
</span> </span>
<!-- total votes (valid/invalid/casts) --> <!-- total votes (valid/invalid/casts) -->
<tr ng-if="poll.pollmethod === 'votes'">
<td>
<translate>Abstain</translate>
<td>
{{ poll.getVote('votesabstain').value }}
{{ poll.getVote('votesabstain').percentStr }}
<tr ng-if="poll.pollmethod === 'votes'">
<td>
<translate>No</translate>
<td>
{{ poll.getVote('votesno').value }}
{{ poll.getVote('votesno').percentStr }}
<tr> <tr>
<td> <td>
<translate>Valid ballots</translate> <translate>Valid ballots</translate>
@ -273,6 +281,11 @@
{{ poll.getVote('votescast').value }} {{ poll.getVote('votescast').value }}
{{ poll.getVote('votescast').percentStr }} {{ poll.getVote('votescast').percentStr }}
</table> </table>
<strong translate>Election method</strong><br>
<span ng-if="poll.pollmethod == 'votes'" translate>One vote per candidate</span>
<span ng-if="poll.pollmethod == 'yna'" translate>Yes/No/Abstain per candidate</span>
<span ng-if="poll.pollmethod == 'yn'" translate>Yes/No per candidate</span>
</div> </div>
</div> </div>
</uib-tab> </uib-tab>

View File

@ -63,7 +63,17 @@
</div> </div>
</div> </div>
<!-- total votes (valid/invalid/casts) --> <!-- total votes (abstain/no/valid/invalid/casts) -->
<tr class="total" ng-if="poll.has_votes && poll.pollmethod === 'votes' && poll.getVote('votesabstain').value !== null">
<td>
<translate>Abstain</translate>
<td ng-init="vote = poll.getVote('votesabstain')">
{{ vote.value }} {{ vote.percentStr }}
<tr class="total" ng-if="poll.has_votes && poll.pollmethod === 'votes' && poll.getVote('votesno').value !== null">
<td>
<translate>No</translate>
<td ng-init="vote = poll.getVote('votesno')">
{{ vote.value }} {{ vote.percentStr }}
<tr class="total" ng-if="poll.has_votes && poll.getVote('votesvalid').value !== null"> <tr class="total" ng-if="poll.has_votes && poll.getVote('votesvalid').value !== null">
<td> <td>
<translate>Valid ballots</translate> <translate>Valid ballots</translate>