diff --git a/openslides/assignments/migrations/0004_auto_20180703_1523.py b/openslides/assignments/migrations/0004_auto_20180703_1523.py new file mode 100644 index 000000000..23d73f129 --- /dev/null +++ b/openslides/assignments/migrations/0004_auto_20180703_1523.py @@ -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), + ), + ] diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index a5494e13b..d6bc5cf7c 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -19,7 +19,7 @@ from openslides.poll.models import ( ) from openslides.utils.autoupdate import inform_changed_data from openslides.utils.exceptions import OpenSlidesError -from openslides.utils.models import RESTModelMixin +from openslides.utils.models import MinMaxIntegerField, RESTModelMixin from .access_permissions import AssignmentAccessPermissions @@ -423,6 +423,11 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore max_length=79, 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: default_permissions = () diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py index 218513679..66c46af43 100644 --- a/openslides/assignments/serializers.py +++ b/openslides/assignments/serializers.py @@ -111,6 +111,8 @@ class AssignmentAllPollSerializer(ModelSerializer): 'description', 'published', 'options', + 'votesabstain', + 'votesno', 'votesvalid', 'votesinvalid', 'votescast', @@ -160,6 +162,8 @@ class AssignmentAllPollSerializer(ModelSerializer): # Update remaining writeable fields. instance.description = validated_data.get('description', instance.description) 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.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid) instance.votescast = validated_data.get('votescast', instance.votescast) @@ -182,6 +186,8 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer): 'description', 'published', 'options', + 'votesabstain', + 'votesno', 'votesvalid', 'votesinvalid', 'votescast', diff --git a/openslides/assignments/static/js/assignments/base.js b/openslides/assignments/static/js/assignments/base.js index 59ce54898..a4f64ac67 100644 --- a/openslides/assignments/static/js/assignments/base.js +++ b/openslides/assignments/static/js/assignments/base.js @@ -214,7 +214,9 @@ angular.module('OpenSlidesApp.assignments', []) if (_.findIndex(this.options, predicate) !== -1) { base = void 0; } 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) { return sum + option.votes[0].weight; }, 0); @@ -240,6 +242,12 @@ angular.module('OpenSlidesApp.assignments', []) config = Config.get('assignments_poll_100_percent_base').value; switch (type) { + case 'votesabstain': + vote = this.votesabstain; + break; + case 'votesno': + vote = this.votesno; + break; case 'votesinvalid': vote = this.votesinvalid; break; diff --git a/openslides/assignments/static/js/assignments/pdf.js b/openslides/assignments/static/js/assignments/pdf.js index feeb31c99..c0f6f0ac7 100644 --- a/openslides/assignments/static/js/assignments/pdf.js +++ b/openslides/assignments/static/js/assignments/pdf.js @@ -67,7 +67,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) var candidatesText = gettextCatalog.getString("Candidates") + ": "; var userList = []; - angular.forEach(candidates, function(assignmentsRelatedUser) { + _.forEach(candidates, function(assignmentsRelatedUser) { userList.push({ text: assignmentsRelatedUser.user.get_full_name(), 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 var parseVoteValue = function(voteObject, printLabel) { - var voteVal = ""; + var voteVal = ''; if (voteObject) { if (printLabel) { - voteVal += voteObject.label + ": "; + voteVal += voteObject.label + ': '; } voteVal += voteObject.value; if (voteObject.percentStr) { - voteVal += " " + voteObject.percentStr; + voteVal += ' ' + voteObject.percentStr; } } - voteVal += "\n"; + voteVal += '\n'; return voteVal; }; // creates the election result table var createPollResultTable = function() { var resultBody = []; - angular.forEach(assignment.polls, function(poll, pollIndex) { + _.forEach(assignment.polls, function(poll, pollIndex) { if (poll.published) { var pollTableBody = []; resultBody.push({ - text: gettextCatalog.getString("Ballot") + " " + (pollIndex+1), + text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1), bold: true, style: 'textItem', margin: [0, 15, 0, 0] @@ -145,16 +145,16 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) pollTableBody.push([ { - text: gettextCatalog.getString("Candidates"), + text: gettextCatalog.getString('Candidates'), style: 'tableHeader', }, { - text: gettextCatalog.getString("Votes"), + text: gettextCatalog.getString('Votes'), style: 'tableHeader', } ]); - angular.forEach(poll.options, function(pollOption, optionIndex) { + _.forEach(poll.options, function(pollOption, optionIndex) { var candidateName = pollOption.candidate.get_full_name(); var votes = pollOption.getVotes(); // 0 = yes, 1 = no, 2 = abstain var tableLine = []; @@ -169,7 +169,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) ); } else { var resultBlock = []; - angular.forEach(votes, function(vote) { + _.forEach(votes, function(vote) { resultBlock.push(parseVoteValue(vote, true)); }); tableLine.push({ @@ -181,44 +181,26 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) pollTableBody.push(tableLine); }); - if (poll.votesvalid) { - pollTableBody.push([ - { - text: gettextCatalog.getString("Valid ballots"), - style: 'tableConclude' - }, - { - text: parseVoteValue(poll.getVote('votesvalid'), false), - style: 'tableConclude' - }, - ]); - } + var pushConcludeRow = function (title, fieldName) { + if (poll[fieldName]) { + pollTableBody.push([ + { + text: gettextCatalog.getString(title), + style: 'tableConclude' + }, + { + text: parseVoteValue(poll.getVote(fieldName), false), + style: 'tableConclude' + }, + ]); + } + }; - if (poll.votesinvalid) { - pollTableBody.push([ - { - text: gettextCatalog.getString("Invalid ballots"), - style: 'tableConclude' - }, - { - 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' - }, - ]); - } + pushConcludeRow('Abstain', 'votesabstain'); + pushConcludeRow('No', 'votesno'); + pushConcludeRow('Valid ballots', 'votesvalid'); + pushConcludeRow('Invalid ballots', 'votesinvalid'); + pushConcludeRow('Casted ballots', 'votescast'); var resultTableJsonSting = { table: { @@ -236,7 +218,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) // add the legend to the result body if (assignment.polls.length > 0 && isElectedSemaphore) { resultBody.push({ - text: "* = " + gettextCatalog.getString("is elected"), + text: '* = ' + gettextCatalog.getString('is elected'), margin: [0, 5, 0, 0], }); } @@ -366,12 +348,17 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) var candidateBallotList = []; if (poll.pollmethod == 'votes') { - angular.forEach(candidates, function(option) { + _.forEach(candidates, function(option) { var candidate = option.candidate.get_full_name(); 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 { - angular.forEach(candidates, function(option) { + _.forEach(candidates, function(option) { var candidate; if (option.candidate) { candidate = option.candidate.get_full_name(); @@ -565,7 +552,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) }; var toc = []; - angular.forEach(assignmentTitles, function(title) { + _.forEach(assignmentTitles, function(title) { toc.push({ text: title, style: "tableofcontent" @@ -656,7 +643,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) createBallotPdf: function (assignment, pollId) { var thePoll; var pollNumber; - angular.forEach(assignment.polls, function(poll, pollIndex) { + _.forEach(assignment.polls, function(poll, pollIndex) { if (poll.id == pollId) { thePoll = poll; pollNumber = pollIndex+1; diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js index 84cd2f6d5..4efd5ca40 100644 --- a/openslides/assignments/static/js/assignments/site.js +++ b/openslides/assignments/static/js/assignments/site.js @@ -708,7 +708,7 @@ angular.module('OpenSlidesApp.assignments.site', [ // add dynamic form fields var options = $filter('orderBy')(assignmentpoll.options, 'weight'); - options.forEach(function(option) { + _.forEach(options, function(option) { var defaultValue; if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') { defaultValue = {}; @@ -783,35 +783,60 @@ angular.module('OpenSlidesApp.assignments.site', [ }); } }); - // add general form fields - $scope.formFields.push( + if (assignmentpoll.pollmethod == 'votes'){ + $scope.formFields.push( { - key: 'votesvalid', + key: 'votesabstain', type: 'input', templateOptions: { - label: gettextCatalog.getString('Valid ballots'), + label: gettextCatalog.getString('Abstain'), type: 'number', min: -2, } }, { - key: 'votesinvalid', + key: 'votesno', type: 'input', templateOptions: { - label: gettextCatalog.getString('Invalid ballots'), - type: 'number', - min: -2, - } - }, - { - key: 'votescast', - type: 'input', - templateOptions: { - label: gettextCatalog.getString('Casted ballots'), + label: gettextCatalog.getString('No'), type: 'number', min: -2, } } + ); + } + // add general form fields + $scope.formFields.push( + { + template: '