From 9bac396b676f1ff43f113e535902f76d17453ddf Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Wed, 22 Aug 2018 17:34:16 +0200 Subject: [PATCH] Support for decimal places in motion and assignment polls --- CHANGELOG.rst | 4 + .../migrations/0005_auto_20180822_1042.py | 75 +++++++++++++++++++ openslides/assignments/models.py | 10 ++- openslides/assignments/serializers.py | 3 +- .../assignments/static/js/assignments/base.js | 27 +++++++ .../assignments/static/js/assignments/pdf.js | 14 ++-- .../static/js/assignments/projector.js | 14 +++- .../assignments/static/js/assignments/site.js | 22 +++++- .../assignments/assignment-detail.html | 16 ++-- .../assignments/slide_assignment.html | 22 +++--- .../migrations/0010_auto_20180822_1042.py | 55 ++++++++++++++ openslides/motions/serializers.py | 11 +-- openslides/motions/static/js/motions/base.js | 20 +++++ openslides/motions/static/js/motions/pdf.js | 16 ++-- .../motions/static/js/motions/projector.js | 17 ++++- openslides/motions/static/js/motions/site.js | 22 ++++-- .../templates/motions/motion-detail.html | 16 ++-- .../templates/motions/slide_motion.html | 6 +- openslides/poll/models.py | 16 ++-- openslides/utils/rest_api.py | 1 + 20 files changed, 320 insertions(+), 67 deletions(-) create mode 100644 openslides/assignments/migrations/0005_auto_20180822_1042.py create mode 100644 openslides/motions/migrations/0010_auto_20180822_1042.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ec57c0864..627007c1e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,9 @@ Version 2.3 (unreleased) Agenda: - New item type 'hidden'. New visibilty filter in agenda [#3790]. +Elections: + - Support to change decimal places for elections with a plugin [#3803] + Motions: - New feature to scroll the projector to a specific line [#3748]. - New possibility to sort submitters [#3647]. @@ -21,6 +24,7 @@ Motions: - New teporal field "modified final version" where the final version can be edited [#3781]. - New config to show amendments also in motions table [#3792] + - Support to change decimal places for polls with a plugin [#3803] Core: - Python 3.4 is not supported anymore [#3777]. diff --git a/openslides/assignments/migrations/0005_auto_20180822_1042.py b/openslides/assignments/migrations/0005_auto_20180822_1042.py new file mode 100644 index 000000000..c53bf1b82 --- /dev/null +++ b/openslides/assignments/migrations/0005_auto_20180822_1042.py @@ -0,0 +1,75 @@ +# Generated by Django 2.1 on 2018-08-22 08:42 + +from decimal import Decimal +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignments', '0004_auto_20180703_1523'), + ] + + operations = [ + migrations.AlterField( + model_name='assignmentpoll', + name='votescast', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='assignmentpoll', + name='votesinvalid', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='assignmentpoll', + name='votesvalid', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='assignmentvote', + name='weight', + field=models.DecimalField( + decimal_places=6, + default=Decimal('1'), + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='assignmentpoll', + name='votesabstain', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='assignmentpoll', + name='votesno', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + ] diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index d6bc5cf7c..54c491dfd 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -1,8 +1,10 @@ from collections import OrderedDict +from decimal import Decimal from typing import Any, Dict, List, Optional # noqa from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation +from django.core.validators import MinValueValidator from django.db import models from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop @@ -19,7 +21,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 MinMaxIntegerField, RESTModelMixin +from openslides.utils.models import RESTModelMixin from .access_permissions import AssignmentAccessPermissions @@ -423,9 +425,11 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore max_length=79, blank=True) - votesabstain = MinMaxIntegerField(null=True, blank=True, min_value=-2) + votesabstain = models.DecimalField(null=True, blank=True, validators=[ + MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) """ General abstain votes, used for pollmethod 'votes' """ - votesno = MinMaxIntegerField(null=True, blank=True, min_value=-2) + votesno = models.DecimalField(null=True, blank=True, validators=[ + MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) """ General no votes, used for pollmethod 'votes' """ class Meta: diff --git a/openslides/assignments/serializers.py b/openslides/assignments/serializers.py index 845ca4e1c..438fd46f8 100644 --- a/openslides/assignments/serializers.py +++ b/openslides/assignments/serializers.py @@ -3,6 +3,7 @@ from django.utils.translation import ugettext as _ from openslides.poll.serializers import default_votes_validator from openslides.utils.rest_api import ( + DecimalField, DictField, IntegerField, ListField, @@ -98,7 +99,7 @@ class AssignmentAllPollSerializer(ModelSerializer): options = AssignmentOptionSerializer(many=True, read_only=True) votes = ListField( child=DictField( - child=IntegerField(min_value=-2)), + child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)), write_only=True, required=False) has_votes = SerializerMethodField() diff --git a/openslides/assignments/static/js/assignments/base.js b/openslides/assignments/static/js/assignments/base.js index a4f64ac67..c971c99a2 100644 --- a/openslides/assignments/static/js/assignments/base.js +++ b/openslides/assignments/static/js/assignments/base.js @@ -14,6 +14,12 @@ angular.module('OpenSlidesApp.assignments', []) return DS.defineResource({ name: 'assignments/polloption', useClass: jsDataModel, + // Change the stringified numbers to floats. + beforeInject: function (resource, instance) { + _.forEach(instance.votes, function (vote) { + vote.weight = parseFloat(vote.weight); + }); + }, methods: { getVotes: function () { if (!this.poll.has_votes) { @@ -154,6 +160,15 @@ angular.module('OpenSlidesApp.assignments', []) return DS.defineResource({ name: name, useClass: jsDataModel, + // Change the stringified numbers to floats. + beforeInject: function (resource, instance) { + var attrs = ['votescast', 'votesinvalid', 'votesvalid', 'votesabstain', 'votesno']; + _.forEach(attrs, function (attr) { + if (instance[attr] !== null) { + instance[attr] = parseFloat(instance[attr]); + } + }); + }, methods: { getResourceName: function () { return name; @@ -307,6 +322,18 @@ angular.module('OpenSlidesApp.assignments', []) } ]) +.provider('AssignmentPollDecimalPlaces', [ + function () { + this.$get = [function () { + return { + getPlaces: function (poll, find) { + return 0; + }, + }; + }]; + } +]) + .factory('AssignmentRelatedUser', [ 'DS', function (DS) { diff --git a/openslides/assignments/static/js/assignments/pdf.js b/openslides/assignments/static/js/assignments/pdf.js index c0f6f0ac7..bbd4399dc 100644 --- a/openslides/assignments/static/js/assignments/pdf.js +++ b/openslides/assignments/static/js/assignments/pdf.js @@ -9,7 +9,8 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) 'HTMLValidizer', 'gettextCatalog', 'PDFLayout', - function($filter, HTMLValidizer, gettextCatalog, PDFLayout) { + 'AssignmentPollDecimalPlaces', + function($filter, HTMLValidizer, gettextCatalog, PDFLayout, AssignmentPollDecimalPlaces) { var createInstance = function(assignment) { @@ -113,13 +114,13 @@ 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 parseVoteValue = function(voteObject, printLabel, precision) { var voteVal = ''; if (voteObject) { if (printLabel) { voteVal += voteObject.label + ': '; } - voteVal += voteObject.value; + voteVal += $filter('number')(voteObject.value, precision); if (voteObject.percentStr) { voteVal += ' ' + voteObject.percentStr; @@ -135,6 +136,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) _.forEach(assignment.polls, function(poll, pollIndex) { if (poll.published) { var pollTableBody = []; + var precision = AssignmentPollDecimalPlaces.getPlaces(poll); resultBody.push({ text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1), @@ -163,14 +165,14 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) if (poll.pollmethod == 'votes') { tableLine.push( { - text: parseVoteValue(votes[0], false), + text: parseVoteValue(votes[0], false, precision), style: PDFLayout.flipTableRowStyle(pollTableBody.length) } ); } else { var resultBlock = []; _.forEach(votes, function(vote) { - resultBlock.push(parseVoteValue(vote, true)); + resultBlock.push(parseVoteValue(vote, true, precision)); }); tableLine.push({ text: resultBlock, @@ -189,7 +191,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf']) style: 'tableConclude' }, { - text: parseVoteValue(poll.getVote(fieldName), false), + text: parseVoteValue(poll.getVote(fieldName), false, precision), style: 'tableConclude' }, ]); diff --git a/openslides/assignments/static/js/assignments/projector.js b/openslides/assignments/static/js/assignments/projector.js index 0e25e8155..ac27a8c21 100644 --- a/openslides/assignments/static/js/assignments/projector.js +++ b/openslides/assignments/static/js/assignments/projector.js @@ -16,15 +16,27 @@ angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignment .controller('SlideAssignmentCtrl', [ '$scope', 'Assignment', + 'AssignmentPoll', 'AssignmentPhases', + 'AssignmentPollDecimalPlaces', 'User', - function($scope, Assignment, AssignmentPhases, User) { + function($scope, Assignment, AssignmentPoll, AssignmentPhases, AssignmentPollDecimalPlaces, User) { // Attention! Each object that is used here has to be dealt on server side. // Add it to the coresponding get_requirements method of the ProjectorElement // class. var id = $scope.element.id; $scope.showResult = $scope.element.poll; + if ($scope.showResult) { + var poll = AssignmentPoll.get($scope.showResult); + $scope.votesPrecision = 0; + if (poll) { + AssignmentPollDecimalPlaces.getPlaces(poll, true).then(function (decimalPlaces) { + $scope.votesPrecision = decimalPlaces; + }); + } + } + Assignment.bindOne(id, $scope, 'assignment'); $scope.phases = AssignmentPhases; User.bindAll({}, $scope, 'users'); diff --git a/openslides/assignments/static/js/assignments/site.js b/openslides/assignments/static/js/assignments/site.js index 7ab0a8511..155a4725d 100644 --- a/openslides/assignments/static/js/assignments/site.js +++ b/openslides/assignments/static/js/assignments/site.js @@ -218,11 +218,15 @@ angular.module('OpenSlidesApp.assignments.site', [ 'Config', 'AssignmentPollDetailCtrlCache', 'AssignmentPoll', - function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache, AssignmentPoll) { + 'AssignmentPollDecimalPlaces', + function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache, + AssignmentPoll, AssignmentPollDecimalPlaces) { // Define choices. $scope.methodChoices = MajorityMethodChoices; // TODO: Get $scope.baseChoices from config_variables.py without copying them. + $scope.votesPrecision = AssignmentPollDecimalPlaces.getPlaces($scope.poll); + // Setup empty cache with default values. if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') { AssignmentPollDetailCtrlCache[$scope.poll.id] = { @@ -689,9 +693,11 @@ angular.module('OpenSlidesApp.assignments.site', [ 'gettextCatalog', 'AssignmentPoll', 'assignmentpollId', + 'AssignmentPollDecimalPlaces', 'ballot', 'ErrorMessage', - function($scope, $filter, gettextCatalog, AssignmentPoll, assignmentpollId, ballot, ErrorMessage) { + function($scope, $filter, gettextCatalog, AssignmentPoll, assignmentpollId, + AssignmentPollDecimalPlaces, ballot, ErrorMessage) { // set initial values for form model by create deep copy of assignmentpoll object // so detail view is not updated while editing poll var assignmentpoll = angular.copy(AssignmentPoll.get(assignmentpollId)); @@ -700,6 +706,9 @@ angular.module('OpenSlidesApp.assignments.site', [ $scope.formFields = []; $scope.alert = {}; + // For number inputs + var step = Math.pow(10, -AssignmentPollDecimalPlaces.getPlaces(assignmentpoll)); + // add dynamic form fields var options = $filter('orderBy')(assignmentpoll.options, 'weight'); _.forEach(options, function(option) { @@ -720,6 +729,7 @@ angular.module('OpenSlidesApp.assignments.site', [ label: gettextCatalog.getString('Yes'), type: 'number', min: -2, + step: step, required: true }, defaultValue: defaultValue.yes @@ -733,6 +743,7 @@ angular.module('OpenSlidesApp.assignments.site', [ label: gettextCatalog.getString('No'), type: 'number', min: -2, + step: step, required: true }, defaultValue: defaultValue.no @@ -747,6 +758,7 @@ angular.module('OpenSlidesApp.assignments.site', [ label: gettextCatalog.getString('Abstain'), type: 'number', min: -2, + step: step, required: true }, defaultValue: defaultValue.abstain @@ -771,6 +783,7 @@ angular.module('OpenSlidesApp.assignments.site', [ label: option.candidate.get_full_name(), type: 'number', min: -2, + step: step, required: true, }, defaultValue: defaultValue @@ -785,6 +798,7 @@ angular.module('OpenSlidesApp.assignments.site', [ templateOptions: { label: gettextCatalog.getString('Abstain'), type: 'number', + step: step, min: -2, } }, @@ -794,6 +808,7 @@ angular.module('OpenSlidesApp.assignments.site', [ templateOptions: { label: gettextCatalog.getString('No'), type: 'number', + step: step, min: -2, } } @@ -810,6 +825,7 @@ angular.module('OpenSlidesApp.assignments.site', [ templateOptions: { label: gettextCatalog.getString('Valid ballots'), type: 'number', + step: step, min: -2, } }, @@ -819,6 +835,7 @@ angular.module('OpenSlidesApp.assignments.site', [ templateOptions: { label: gettextCatalog.getString('Invalid ballots'), type: 'number', + step: step, min: -2, } }, @@ -828,6 +845,7 @@ angular.module('OpenSlidesApp.assignments.site', [ templateOptions: { label: gettextCatalog.getString('Casted ballots'), type: 'number', + step: step, min: -2, } } diff --git a/openslides/assignments/static/templates/assignments/assignment-detail.html b/openslides/assignments/static/templates/assignments/assignment-detail.html index 098240882..d585d345e 100644 --- a/openslides/assignments/static/templates/assignments/assignment-detail.html +++ b/openslides/assignments/static/templates/assignments/assignment-detail.html @@ -234,7 +234,7 @@
{{ vote.label }}: - {{ vote.value }} {{ vote.percentStr }} + {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
@@ -244,10 +244,10 @@
- Quorum ({{ option.getVoteYes() - option.majorityReached }}) reached. + Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) reached. - Quorum ({{ option.getVoteYes() - option.majorityReached }}) not reached. + Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) not reached. @@ -255,31 +255,31 @@ Abstain - {{ poll.getVote('votesabstain').value }} + {{ poll.getVote('votesabstain').value | number:votesPrecision }} {{ poll.getVote('votesabstain').percentStr }} No - {{ poll.getVote('votesno').value }} + {{ poll.getVote('votesno').value | number:votesPrecision }} {{ poll.getVote('votesno').percentStr }} Valid ballots - {{ poll.getVote('votesvalid').value }} + {{ poll.getVote('votesvalid').value | number:votesPrecision }} {{ poll.getVote('votesvalid').percentStr }} Invalid ballots - {{ poll.getVote('votesinvalid').value }} + {{ poll.getVote('votesinvalid').value | number:votesPrecision }} {{ poll.getVote('votesinvalid').percentStr }} Casted ballots - {{ poll.getVote('votescast').value }} + {{ poll.getVote('votescast').value | number:votesPrecision }} {{ poll.getVote('votescast').percentStr }} diff --git a/openslides/assignments/static/templates/assignments/slide_assignment.html b/openslides/assignments/static/templates/assignments/slide_assignment.html index 708bc7339..e3954e0e0 100644 --- a/openslides/assignments/static/templates/assignments/slide_assignment.html +++ b/openslides/assignments/static/templates/assignments/slide_assignment.html @@ -49,16 +49,16 @@
- {{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}
- {{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}
- {{ votes[2].label | translate }}: {{ votes[2].value }} {{ votes[2].percentStr }}
+ {{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}
+ {{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}
+ {{ votes[2].label | translate }}: {{ votes[2].value | number:votesPrecision }} {{ votes[2].percentStr }} - {{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}
- {{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}
+ {{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}
+ {{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}
- {{ vote.value }} {{ vote.percentStr }} + {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
@@ -68,29 +68,29 @@ Abstain - {{ vote.value }} {{ vote.percentStr }} + {{ vote.value | number:votesPrecision }} {{ vote.percentStr }} No - {{ vote.value }} {{ vote.percentStr }} + {{ vote.value | number:votesPrecision }} {{ vote.percentStr }} Valid ballots - {{ vote.value }} {{ vote.percentStr }} + {{ vote.value | number:votesPrecision }} {{ vote.percentStr }} Invalid ballots - {{ vote.value }} {{ vote.percentStr }} + {{ vote.value | number:votesPrecision }} {{ vote.percentStr }} Casted ballots - {{ vote.value }} {{ vote.percentStr }} + {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
diff --git a/openslides/motions/migrations/0010_auto_20180822_1042.py b/openslides/motions/migrations/0010_auto_20180822_1042.py new file mode 100644 index 000000000..fa9fb0522 --- /dev/null +++ b/openslides/motions/migrations/0010_auto_20180822_1042.py @@ -0,0 +1,55 @@ +# Generated by Django 2.1 on 2018-08-22 08:42 + +from decimal import Decimal +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('motions', '0009_motionversion_modified_final_version'), + ] + + operations = [ + migrations.AlterField( + model_name='motionpoll', + name='votescast', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='motionpoll', + name='votesinvalid', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='motionpoll', + name='votesvalid', + field=models.DecimalField( + blank=True, + decimal_places=6, + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + migrations.AlterField( + model_name='motionvote', + name='weight', + field=models.DecimalField( + decimal_places=6, + default=Decimal('1'), + max_digits=15, + null=True, + validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), + ), + ] diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 6f2791bc2..a5979f0b5 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -1,4 +1,4 @@ -from typing import Dict # noqa +from typing import Dict, Optional # noqa from django.db import transaction from django.utils.translation import ugettext as _ @@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _ from ..poll.serializers import default_votes_validator from ..utils.rest_api import ( CharField, + DecimalField, DictField, Field, IntegerField, @@ -212,7 +213,7 @@ class MotionPollSerializer(ModelSerializer): no = SerializerMethodField() abstain = SerializerMethodField() votes = DictField( - child=IntegerField(min_value=-2, allow_null=True), + child=DecimalField(max_digits=15, decimal_places=6, min_value=-2, allow_null=True), write_only=True) has_votes = SerializerMethodField() @@ -238,21 +239,21 @@ class MotionPollSerializer(ModelSerializer): def get_yes(self, obj): try: - result = self.get_votes_dict(obj)['Yes'] + result = str(self.get_votes_dict(obj)['Yes']) # type: Optional[str] except KeyError: result = None return result def get_no(self, obj): try: - result = self.get_votes_dict(obj)['No'] + result = str(self.get_votes_dict(obj)['No']) # type: Optional[str] except KeyError: result = None return result def get_abstain(self, obj): try: - result = self.get_votes_dict(obj)['Abstain'] + result = str(self.get_votes_dict(obj)['Abstain']) # type: Optional[str] except KeyError: result = None return result diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index d2741bb4c..7296b3fc3 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -85,6 +85,14 @@ angular.module('OpenSlidesApp.motions', [ } } }, + beforeInject: function (resource, instance) { + var attrs = ['yes', 'no', 'abstain', 'votescast', 'votesinvalid', 'votesvalid']; + _.forEach(attrs, function (attr) { + if (instance[attr] !== null) { + instance[attr] = parseFloat(instance[attr]); + } + }); + }, methods: { // Returns percent base. Returns undefined if calculation is not possible in general. getPercentBase: function (config, type) { @@ -196,6 +204,18 @@ angular.module('OpenSlidesApp.motions', [ } ]) +.provider('MotionPollDecimalPlaces', [ + function () { + this.$get = [function () { + return { + getPlaces: function (poll, find) { + return 0; + }, + }; + }]; + } +]) + .factory('MotionStateAndRecommendationParser', [ 'DS', 'gettextCatalog', diff --git a/openslides/motions/static/js/motions/pdf.js b/openslides/motions/static/js/motions/pdf.js index f7803f3bc..070918641 100644 --- a/openslides/motions/static/js/motions/pdf.js +++ b/openslides/motions/static/js/motions/pdf.js @@ -17,9 +17,10 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) 'Config', 'Motion', 'MotionComment', + 'MotionPollDecimalPlaces', 'OpenSlidesSettings', function($q, $filter, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, - HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) { + HTMLValidizer, Category, Config, Motion, MotionComment, MotionPollDecimalPlaces, OpenSlidesSettings) { /** * Provides the content as JS objects for Motions in pdfMake context * @constructor @@ -185,40 +186,41 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) column2.push(''); column3.push(''); } + var precision = MotionPollDecimalPlaces.getPlaces(poll); // yes var yes = poll.getVote(poll.yes, 'yes'); column1.push(gettextCatalog.getString('Yes') + ':'); - column2.push(yes.value); + column2.push($filter('number')(yes.value, precision)); column3.push(yes.percentStr); // no var no = poll.getVote(poll.no, 'no'); column1.push(gettextCatalog.getString('No') + ':'); - column2.push(no.value); + column2.push($filter('number')(no.value, precision)); column3.push(no.percentStr); // abstain var abstain = poll.getVote(poll.abstain, 'abstain'); column1.push(gettextCatalog.getString('Abstain') + ':'); - column2.push(abstain.value); + column2.push($filter('number')(abstain.value, precision)); column3.push(abstain.percentStr); // votes valid if (poll.votesvalid) { var valid = poll.getVote(poll.votesvalid, 'votesvalid'); column1.push(gettextCatalog.getString('Valid votes') + ':'); - column2.push(valid.value); + column2.push($filter('number')(valid.value, precision)); column3.push(valid.percentStr); } // votes invalid if (poll.votesvalid) { var invalid = poll.getVote(poll.votesinvalid, 'votesinvalid'); column1.push(gettextCatalog.getString('Invalid votes') + ':'); - column2.push(invalid.value); + column2.push($filter('number')(invalid.value, precision)); column3.push(invalid.percentStr); } // votes cast if (poll.votescast) { var cast = poll.getVote(poll.votescast, 'votescast'); column1.push(gettextCatalog.getString('Votes cast') + ':'); - column2.push(cast.value); + column2.push($filter('number')(cast.value, precision)); column3.push(cast.percentStr); } } diff --git a/openslides/motions/static/js/motions/projector.js b/openslides/motions/static/js/motions/projector.js index 4b2854f30..d7107eded 100644 --- a/openslides/motions/static/js/motions/projector.js +++ b/openslides/motions/static/js/motions/projector.js @@ -26,7 +26,9 @@ angular.module('OpenSlidesApp.motions.projector', [ 'User', 'Notify', 'ProjectorID', - function($scope, Config, Motion, MotionChangeRecommendation, ChangeRecommendationView, User, Notify, ProjectorID) { + 'MotionPollDecimalPlaces', + function($scope, Config, Motion, MotionChangeRecommendation, ChangeRecommendationView, User, + Notify, ProjectorID, MotionPollDecimalPlaces) { // Attention! Each object that is used here has to be dealt on server side. // Add it to the coresponding get_requirements method of the ProjectorElement // class. @@ -67,8 +69,21 @@ angular.module('OpenSlidesApp.motions.projector', [ $scope.motion = Motion.get(motionId); $scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff(); $scope.viewChangeRecommendations.setVersion($scope.motion, $scope.motion.active_version); + _.forEach($scope.motion.polls, function (poll) { + MotionPollDecimalPlaces.getPlaces(poll, true).then(function (decimalPlaces) { + precisionCache[poll.id] = decimalPlaces; + }); + }); }); + var precisionCache = {}; + $scope.getPollVotesPrecision = function (poll) { + if (!precisionCache[poll.id]) { + return 0; + } + return precisionCache[poll.id]; + }; + // Change recommendation viewing $scope.viewChangeRecommendations = ChangeRecommendationView; $scope.viewChangeRecommendations.initProjector($scope, Motion.get(motionId), $scope.mode); diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index 00efc70fc..2bce52ba7 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -750,7 +750,8 @@ angular.module('OpenSlidesApp.motions.site', [ 'gettextCatalog', function (gettextCatalog) { return { - getFormFields: function () { + getFormFields: function (precision) { + var step = Math.pow(10, -precision); return [ { key: 'yes', @@ -758,6 +759,7 @@ angular.module('OpenSlidesApp.motions.site', [ templateOptions: { label: gettextCatalog.getString('Yes'), type: 'number', + step: step, required: true } }, @@ -767,6 +769,7 @@ angular.module('OpenSlidesApp.motions.site', [ templateOptions: { label: gettextCatalog.getString('No'), type: 'number', + step: step, required: true } }, @@ -776,6 +779,7 @@ angular.module('OpenSlidesApp.motions.site', [ templateOptions: { label: gettextCatalog.getString('Abstain'), type: 'number', + step: step, required: true } }, @@ -784,6 +788,7 @@ angular.module('OpenSlidesApp.motions.site', [ type: 'input', templateOptions: { label: gettextCatalog.getString('Valid votes'), + step: step, type: 'number' } }, @@ -792,6 +797,7 @@ angular.module('OpenSlidesApp.motions.site', [ type: 'input', templateOptions: { label: gettextCatalog.getString('Invalid votes'), + step: step, type: 'number' } }, @@ -800,6 +806,7 @@ angular.module('OpenSlidesApp.motions.site', [ type: 'input', templateOptions: { label: gettextCatalog.getString('Votes cast'), + step: step, type: 'number' } }]; @@ -1130,11 +1137,14 @@ angular.module('OpenSlidesApp.motions.site', [ 'MajorityMethodChoices', 'Config', 'MotionPollDetailCtrlCache', - function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache) { + 'MotionPollDecimalPlaces', + function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache, MotionPollDecimalPlaces) { // Define choices. $scope.methodChoices = MajorityMethodChoices; // TODO: Get $scope.baseChoices from config_variables.py without copying them. + $scope.votesPrecision = MotionPollDecimalPlaces.getPlaces($scope.poll); + // Setup empty cache with default values. if (typeof MotionPollDetailCtrlCache[$scope.poll.id] === 'undefined') { MotionPollDetailCtrlCache[$scope.poll.id] = { @@ -2517,17 +2527,19 @@ angular.module('OpenSlidesApp.motions.site', [ 'gettextCatalog', 'MotionPoll', 'MotionPollForm', + 'MotionPollDecimalPlaces', 'motionpollId', 'voteNumber', 'ErrorMessage', - function ($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId, - voteNumber, ErrorMessage) { + function ($scope, gettextCatalog, MotionPoll, MotionPollForm, MotionPollDecimalPlaces, + motionpollId, voteNumber, ErrorMessage) { // set initial values for form model by create deep copy of motionpoll object // so detail view is not updated while editing poll var motionpoll = MotionPoll.get(motionpollId); $scope.model = angular.copy(motionpoll); $scope.voteNumber = voteNumber; - $scope.formFields = MotionPollForm.getFormFields(); + var precision = MotionPollDecimalPlaces.getPlaces(motionpoll); + $scope.formFields = MotionPollForm.getFormFields(precision); $scope.alert = {}; // save motionpoll diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index bf533ad27..720ece363 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -404,7 +404,7 @@ Yes: - {{ voteYes.value }} {{ voteYes.percentStr }} + {{ voteYes.value | number:votesPrecision }} {{ voteYes.percentStr }}
@@ -416,7 +416,7 @@ No: - {{ voteNo.value }} {{ voteNo.percentStr }} + {{ voteNo.value | number:votesPrecision }} {{ voteNo.percentStr }}
@@ -428,7 +428,7 @@ Abstain: - {{ voteAbstain.value }} {{ voteAbstain.percentStr }} + {{ voteAbstain.value | number:votesPrecision }} {{ voteAbstain.percentStr }}
@@ -440,7 +440,7 @@ Valid votes: - {{ votesValid.value }} {{ votesValid.percentStr }} + {{ votesValid.value | number:votesPrecision }} {{ votesValid.percentStr }} @@ -449,7 +449,7 @@ Invalid votes: - {{ votesInvalid.value }} {{ votesInvalid.percentStr }} + {{ votesInvalid.value | number:votesPrecision }} {{ votesInvalid.percentStr }} @@ -458,7 +458,7 @@ Votes cast: - {{ votesCast.value }} {{ votesCast.percentStr }} + {{ votesCast.value | number:votesPrecision }} {{ votesCast.percentStr }} @@ -479,10 +479,10 @@
- Quorum ({{ voteYes.value - isReached() }}) reached. + Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) reached. - Quorum ({{ voteYes.value - isReached() }}) not reached. + Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) not reached.
diff --git a/openslides/motions/static/templates/motions/slide_motion.html b/openslides/motions/static/templates/motions/slide_motion.html index 7916ec22a..abe461ed4 100644 --- a/openslides/motions/static/templates/motions/slide_motion.html +++ b/openslides/motions/static/templates/motions/slide_motion.html @@ -31,7 +31,7 @@ Yes: - {{ voteYes.value }} {{ voteYes.percentStr }} + {{ voteYes.value | number:getPollVotesPrecision(poll) }} {{ voteYes.percentStr }}
@@ -43,7 +43,7 @@ No: - {{ voteNo.value }} {{ voteNo.percentStr }} + {{ voteNo.value | number:getPollVotesPrecision(poll) }} {{ voteNo.percentStr }}
@@ -55,7 +55,7 @@ Abstain: - {{ voteAbstain.value }} {{ voteAbstain.percentStr }} + {{ voteAbstain.value | number:getPollVotesPrecision(poll) }} {{ voteAbstain.percentStr }}
diff --git a/openslides/poll/models.py b/openslides/poll/models.py index 545e3f111..aec301cb3 100644 --- a/openslides/poll/models.py +++ b/openslides/poll/models.py @@ -1,12 +1,12 @@ import locale +from decimal import Decimal from typing import Type # noqa from django.core.exceptions import ObjectDoesNotExist +from django.core.validators import MinValueValidator from django.db import models from django.utils.translation import ugettext as _ -from openslides.utils.models import MinMaxIntegerField - class BaseOption(models.Model): """ @@ -44,7 +44,8 @@ class BaseVote(models.Model): Subclasses have to define an option field. This must be a ForeignKeyField to a subclass of BasePoll. """ - weight = models.IntegerField(default=1, null=True) # Use MinMaxIntegerField + weight = models.DecimalField(default=Decimal('1'), null=True, validators=[ + MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) value = models.CharField(max_length=255, null=True) class Meta: @@ -72,9 +73,12 @@ class CollectDefaultVotesMixin(models.Model): Mixin for a poll to collect the default vote values for valid votes, invalid votes and votes cast. """ - votesvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2) - votesinvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2) - votescast = MinMaxIntegerField(null=True, blank=True, min_value=-2) + votesvalid = models.DecimalField(null=True, blank=True, validators=[ + MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) + votesinvalid = models.DecimalField(null=True, blank=True, validators=[ + MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) + votescast = models.DecimalField(null=True, blank=True, validators=[ + MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) class Meta: abstract = True diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index bd44c5fe9..dad1b61a5 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -18,6 +18,7 @@ from rest_framework.routers import DefaultRouter from rest_framework.serializers import ModelSerializer as _ModelSerializer from rest_framework.serializers import ( # noqa CharField, + DecimalField, DictField, Field, FileField,