Support for decimal places in motion and assignment polls
This commit is contained in:
parent
dbd808c02b
commit
9bac396b67
@ -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].
|
||||
|
75
openslides/assignments/migrations/0005_auto_20180822_1042.py
Normal file
75
openslides/assignments/migrations/0005_auto_20180822_1042.py
Normal file
@ -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'))]),
|
||||
),
|
||||
]
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
},
|
||||
]);
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -234,7 +234,7 @@
|
||||
<div ng-init="votes = option.getVotes()">
|
||||
<div ng-repeat="vote in votes">
|
||||
<span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span>
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
<div ng-if="vote.percentNumber >= 0">
|
||||
<uib-progressbar ng-if="$index == 0" value="vote.percentNumber" type="success"></uib-progressbar>
|
||||
<uib-progressbar ng-if="$index == 1" value="vote.percentNumber" type="danger"></uib-progressbar>
|
||||
@ -244,10 +244,10 @@
|
||||
</div>
|
||||
<td ng-hide="method === 'disabled'">
|
||||
<span ng-if="option.majorityReached >= 0" class="text-success" translate>
|
||||
Quorum ({{ option.getVoteYes() - option.majorityReached }}) reached.
|
||||
Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) reached.
|
||||
</span>
|
||||
<span ng-if="option.majorityReached < 0" class="text-danger" translate>
|
||||
Quorum ({{ option.getVoteYes() - option.majorityReached }}) not reached.
|
||||
Quorum ({{ (option.getVoteYes() - option.majorityReached) | number:votesPrecision }}) not reached.
|
||||
</span>
|
||||
|
||||
<!-- total votes (valid/invalid/casts) -->
|
||||
@ -255,31 +255,31 @@
|
||||
<td>
|
||||
<translate>Abstain</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesabstain').value }}
|
||||
{{ poll.getVote('votesabstain').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesabstain').percentStr }}
|
||||
<tr ng-if="poll.pollmethod === 'votes'">
|
||||
<td>
|
||||
<translate>No</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesno').value }}
|
||||
{{ poll.getVote('votesno').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesno').percentStr }}
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Valid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesvalid').value }}
|
||||
{{ poll.getVote('votesvalid').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesvalid').percentStr }}
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Invalid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesinvalid').value }}
|
||||
{{ poll.getVote('votesinvalid').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votesinvalid').percentStr }}
|
||||
<tr class="total bg-info">
|
||||
<td>
|
||||
<translate>Casted ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votescast').value }}
|
||||
{{ poll.getVote('votescast').value | number:votesPrecision }}
|
||||
{{ poll.getVote('votescast').percentStr }}
|
||||
</table>
|
||||
|
||||
|
@ -49,16 +49,16 @@
|
||||
<div ng-init="votes = option.getVotes()">
|
||||
<div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">
|
||||
<span ng-show="poll.pollmethod == 'yna'">
|
||||
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br>
|
||||
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}<br>
|
||||
{{ votes[2].label | translate }}: {{ votes[2].value }} {{ votes[2].percentStr }}</span>
|
||||
{{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
|
||||
{{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}<br>
|
||||
{{ votes[2].label | translate }}: {{ votes[2].value | number:votesPrecision }} {{ votes[2].percentStr }}</span>
|
||||
<span ng-show="poll.pollmethod == 'yn'">
|
||||
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br>
|
||||
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}</span>
|
||||
{{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
|
||||
{{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}</span>
|
||||
</div>
|
||||
<div ng-show="poll.pollmethod == 'votes'">
|
||||
<div ng-repeat="vote in votes">
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,29 +68,29 @@
|
||||
<td>
|
||||
<translate>Abstain</translate>
|
||||
<td ng-init="vote = poll.getVote('votesabstain')">
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
{{ vote.value | number:votesPrecision }} {{ 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 }}
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
<tr class="total" ng-if="poll.has_votes && poll.getVote('votesvalid').value !== null">
|
||||
<td>
|
||||
<translate>Valid ballots</translate>
|
||||
<td ng-init="vote = poll.getVote('votesvalid')">
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
|
||||
<tr class="total" ng-if="poll.has_votes && poll.getVote('votesinvalid').value !== null">
|
||||
<td>
|
||||
<translate>Invalid ballots</translate>
|
||||
<td ng-init="vote = poll.getVote('votesinvalid')">
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
|
||||
<tr class="total bg-info" ng-if="poll.has_votes && poll.getVote('votescast').value !== null">
|
||||
<td>
|
||||
<translate>Casted ballots</translate>
|
||||
<td ng-init="vote = poll.getVote('votescast')">
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
{{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
55
openslides/motions/migrations/0010_auto_20180822_1042.py
Normal file
55
openslides/motions/migrations/0010_auto_20180822_1042.py
Normal file
@ -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'))]),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -404,7 +404,7 @@
|
||||
<td ng-init="voteYes = poll.getVote(poll.yes, 'yes')">
|
||||
<span class="result-label"><translate>Yes</translate>:</span>
|
||||
<span class="result_value">
|
||||
{{ voteYes.value }} {{ voteYes.percentStr }}
|
||||
{{ voteYes.value | number:votesPrecision }} {{ voteYes.percentStr }}
|
||||
</span>
|
||||
<div ng-if="voteYes.percentNumber">
|
||||
<uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar>
|
||||
@ -416,7 +416,7 @@
|
||||
<td ng-init="voteNo = poll.getVote(poll.no, 'no')">
|
||||
<span class="result-label"><translate>No</translate>:</span>
|
||||
<span class="result_value" >
|
||||
{{ voteNo.value }} {{ voteNo.percentStr }}
|
||||
{{ voteNo.value | number:votesPrecision }} {{ voteNo.percentStr }}
|
||||
</span>
|
||||
<div ng-if="voteNo.percentNumber">
|
||||
<uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar>
|
||||
@ -428,7 +428,7 @@
|
||||
<td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')">
|
||||
<span class="result-label"><translate>Abstain</translate>:</span>
|
||||
<span class="result_value">
|
||||
{{ voteAbstain.value }} {{ voteAbstain.percentStr }}
|
||||
{{ voteAbstain.value | number:votesPrecision }} {{ voteAbstain.percentStr }}
|
||||
</span>
|
||||
<div ng-if="voteAbstain.percentNumber">
|
||||
<uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar>
|
||||
@ -440,7 +440,7 @@
|
||||
<td ng-init="votesValid = poll.getVote(poll.votesvalid, 'votesvalid')">
|
||||
<span class="result-label"><translate>Valid votes</translate>:</span>
|
||||
<span class="result_value">
|
||||
{{ votesValid.value }} {{ votesValid.percentStr }}
|
||||
{{ votesValid.value | number:votesPrecision }} {{ votesValid.percentStr }}
|
||||
</span>
|
||||
<!-- invalid votes -->
|
||||
<tr ng-if="poll.votesinvalid !== null">
|
||||
@ -449,7 +449,7 @@
|
||||
<td ng-init="votesInvalid = poll.getVote(poll.votesinvalid, 'votesinvalid')">
|
||||
<span class="result-label"><translate>Invalid votes</translate>:</span>
|
||||
<span class="result_value">
|
||||
{{ votesInvalid.value }} {{ votesInvalid.percentStr }}
|
||||
{{ votesInvalid.value | number:votesPrecision }} {{ votesInvalid.percentStr }}
|
||||
</span>
|
||||
<!-- votes cast -->
|
||||
<tr class="total" ng-if="poll.votescast !== null">
|
||||
@ -458,7 +458,7 @@
|
||||
<td ng-init="votesCast = poll.getVote(poll.votescast, 'votescast')">
|
||||
<span class="result-label"><translate>Votes cast</translate>:</span>
|
||||
<span class="result_value">
|
||||
{{ votesCast.value }} {{ votesCast.percentStr }}
|
||||
{{ votesCast.value | number:votesPrecision }} {{ votesCast.percentStr }}
|
||||
</span>
|
||||
|
||||
<!-- majority calculation -->
|
||||
@ -479,10 +479,10 @@
|
||||
<td>
|
||||
<div os-perms="motions.can_manage">
|
||||
<span class="text-success" ng-if="isReached() >= 0" translate>
|
||||
Quorum ({{ voteYes.value - isReached() }}) reached.
|
||||
Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) reached.
|
||||
</span>
|
||||
<span class="text-danger" ng-if="isReached() < 0" translate>
|
||||
Quorum ({{ voteYes.value - isReached() }}) not reached.
|
||||
Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) not reached.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -31,7 +31,7 @@
|
||||
<td ng-init="voteYes = poll.getVote(poll.yes, 'yes')">
|
||||
<span class="result_label"><translate>Yes</translate>:</span>
|
||||
<span class="result_value">
|
||||
{{ voteYes.value }} {{ voteYes.percentStr }}
|
||||
{{ voteYes.value | number:getPollVotesPrecision(poll) }} {{ voteYes.percentStr }}
|
||||
</span>
|
||||
<div ng-if="voteYes.percentNumber">
|
||||
<uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar>
|
||||
@ -43,7 +43,7 @@
|
||||
<td ng-init="voteNo = poll.getVote(poll.no, 'no')">
|
||||
<span class="result_label"><translate>No</translate>:</span>
|
||||
<span class="result_value" >
|
||||
{{ voteNo.value }} {{ voteNo.percentStr }}
|
||||
{{ voteNo.value | number:getPollVotesPrecision(poll) }} {{ voteNo.percentStr }}
|
||||
</span>
|
||||
<div ng-if="voteNo.percentNumber">
|
||||
<uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar>
|
||||
@ -55,7 +55,7 @@
|
||||
<td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')">
|
||||
<span class="result_label"><translate>Abstain</translate>:</span>
|
||||
<span class="result_value">
|
||||
{{ voteAbstain.value }} {{ voteAbstain.percentStr }}
|
||||
{{ voteAbstain.value | number:getPollVotesPrecision(poll) }} {{ voteAbstain.percentStr }}
|
||||
</span>
|
||||
<div ng-if="voteAbstain.percentNumber">
|
||||
<uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user