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,
| | | | | |