Support for decimal places in motion and assignment polls

This commit is contained in:
FinnStutzenstein 2018-08-22 17:34:16 +02:00
parent dbd808c02b
commit 9bac396b67
20 changed files with 320 additions and 67 deletions

View File

@ -10,6 +10,9 @@ Version 2.3 (unreleased)
Agenda: Agenda:
- New item type 'hidden'. New visibilty filter in agenda [#3790]. - New item type 'hidden'. New visibilty filter in agenda [#3790].
Elections:
- Support to change decimal places for elections with a plugin [#3803]
Motions: Motions:
- New feature to scroll the projector to a specific line [#3748]. - New feature to scroll the projector to a specific line [#3748].
- New possibility to sort submitters [#3647]. - New possibility to sort submitters [#3647].
@ -21,6 +24,7 @@ Motions:
- New teporal field "modified final version" where the final version can - New teporal field "modified final version" where the final version can
be edited [#3781]. be edited [#3781].
- New config to show amendments also in motions table [#3792] - New config to show amendments also in motions table [#3792]
- Support to change decimal places for polls with a plugin [#3803]
Core: Core:
- Python 3.4 is not supported anymore [#3777]. - Python 3.4 is not supported anymore [#3777].

View 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'))]),
),
]

View File

@ -1,8 +1,10 @@
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal
from typing import Any, Dict, List, Optional # noqa from typing import Any, Dict, List, Optional # noqa
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop 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.autoupdate import inform_changed_data
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import MinMaxIntegerField, RESTModelMixin from openslides.utils.models import RESTModelMixin
from .access_permissions import AssignmentAccessPermissions from .access_permissions import AssignmentAccessPermissions
@ -423,9 +425,11 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
max_length=79, max_length=79,
blank=True) blank=True)
votesabstain = MinMaxIntegerField(null=True, blank=True, min_value=-2) votesabstain = models.DecimalField(null=True, blank=True, validators=[
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
""" General abstain votes, used for pollmethod 'votes' """ """ 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' """ """ General no votes, used for pollmethod 'votes' """
class Meta: class Meta:

View File

@ -3,6 +3,7 @@ from django.utils.translation import ugettext as _
from openslides.poll.serializers import default_votes_validator from openslides.poll.serializers import default_votes_validator
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
DecimalField,
DictField, DictField,
IntegerField, IntegerField,
ListField, ListField,
@ -98,7 +99,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
options = AssignmentOptionSerializer(many=True, read_only=True) options = AssignmentOptionSerializer(many=True, read_only=True)
votes = ListField( votes = ListField(
child=DictField( child=DictField(
child=IntegerField(min_value=-2)), child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)),
write_only=True, write_only=True,
required=False) required=False)
has_votes = SerializerMethodField() has_votes = SerializerMethodField()

View File

@ -14,6 +14,12 @@ angular.module('OpenSlidesApp.assignments', [])
return DS.defineResource({ return DS.defineResource({
name: 'assignments/polloption', name: 'assignments/polloption',
useClass: jsDataModel, useClass: jsDataModel,
// Change the stringified numbers to floats.
beforeInject: function (resource, instance) {
_.forEach(instance.votes, function (vote) {
vote.weight = parseFloat(vote.weight);
});
},
methods: { methods: {
getVotes: function () { getVotes: function () {
if (!this.poll.has_votes) { if (!this.poll.has_votes) {
@ -154,6 +160,15 @@ angular.module('OpenSlidesApp.assignments', [])
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, 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: { methods: {
getResourceName: function () { getResourceName: function () {
return name; 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', [ .factory('AssignmentRelatedUser', [
'DS', 'DS',
function (DS) { function (DS) {

View File

@ -9,7 +9,8 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
'HTMLValidizer', 'HTMLValidizer',
'gettextCatalog', 'gettextCatalog',
'PDFLayout', 'PDFLayout',
function($filter, HTMLValidizer, gettextCatalog, PDFLayout) { 'AssignmentPollDecimalPlaces',
function($filter, HTMLValidizer, gettextCatalog, PDFLayout, AssignmentPollDecimalPlaces) {
var createInstance = function(assignment) { 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 //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 = ''; var voteVal = '';
if (voteObject) { if (voteObject) {
if (printLabel) { if (printLabel) {
voteVal += voteObject.label + ': '; voteVal += voteObject.label + ': ';
} }
voteVal += voteObject.value; voteVal += $filter('number')(voteObject.value, precision);
if (voteObject.percentStr) { if (voteObject.percentStr) {
voteVal += ' ' + voteObject.percentStr; voteVal += ' ' + voteObject.percentStr;
@ -135,6 +136,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
_.forEach(assignment.polls, function(poll, pollIndex) { _.forEach(assignment.polls, function(poll, pollIndex) {
if (poll.published) { if (poll.published) {
var pollTableBody = []; var pollTableBody = [];
var precision = AssignmentPollDecimalPlaces.getPlaces(poll);
resultBody.push({ resultBody.push({
text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1), text: gettextCatalog.getString('Ballot') + ' ' + (pollIndex+1),
@ -163,14 +165,14 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
if (poll.pollmethod == 'votes') { if (poll.pollmethod == 'votes') {
tableLine.push( tableLine.push(
{ {
text: parseVoteValue(votes[0], false), text: parseVoteValue(votes[0], false, precision),
style: PDFLayout.flipTableRowStyle(pollTableBody.length) style: PDFLayout.flipTableRowStyle(pollTableBody.length)
} }
); );
} else { } else {
var resultBlock = []; var resultBlock = [];
_.forEach(votes, function(vote) { _.forEach(votes, function(vote) {
resultBlock.push(parseVoteValue(vote, true)); resultBlock.push(parseVoteValue(vote, true, precision));
}); });
tableLine.push({ tableLine.push({
text: resultBlock, text: resultBlock,
@ -189,7 +191,7 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
style: 'tableConclude' style: 'tableConclude'
}, },
{ {
text: parseVoteValue(poll.getVote(fieldName), false), text: parseVoteValue(poll.getVote(fieldName), false, precision),
style: 'tableConclude' style: 'tableConclude'
}, },
]); ]);

View File

@ -16,15 +16,27 @@ angular.module('OpenSlidesApp.assignments.projector', ['OpenSlidesApp.assignment
.controller('SlideAssignmentCtrl', [ .controller('SlideAssignmentCtrl', [
'$scope', '$scope',
'Assignment', 'Assignment',
'AssignmentPoll',
'AssignmentPhases', 'AssignmentPhases',
'AssignmentPollDecimalPlaces',
'User', '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. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; var id = $scope.element.id;
$scope.showResult = $scope.element.poll; $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'); Assignment.bindOne(id, $scope, 'assignment');
$scope.phases = AssignmentPhases; $scope.phases = AssignmentPhases;
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');

View File

@ -218,11 +218,15 @@ angular.module('OpenSlidesApp.assignments.site', [
'Config', 'Config',
'AssignmentPollDetailCtrlCache', 'AssignmentPollDetailCtrlCache',
'AssignmentPoll', 'AssignmentPoll',
function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache, AssignmentPoll) { 'AssignmentPollDecimalPlaces',
function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache,
AssignmentPoll, AssignmentPollDecimalPlaces) {
// Define choices. // Define choices.
$scope.methodChoices = MajorityMethodChoices; $scope.methodChoices = MajorityMethodChoices;
// TODO: Get $scope.baseChoices from config_variables.py without copying them. // TODO: Get $scope.baseChoices from config_variables.py without copying them.
$scope.votesPrecision = AssignmentPollDecimalPlaces.getPlaces($scope.poll);
// Setup empty cache with default values. // Setup empty cache with default values.
if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') { if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
AssignmentPollDetailCtrlCache[$scope.poll.id] = { AssignmentPollDetailCtrlCache[$scope.poll.id] = {
@ -689,9 +693,11 @@ angular.module('OpenSlidesApp.assignments.site', [
'gettextCatalog', 'gettextCatalog',
'AssignmentPoll', 'AssignmentPoll',
'assignmentpollId', 'assignmentpollId',
'AssignmentPollDecimalPlaces',
'ballot', 'ballot',
'ErrorMessage', '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 // set initial values for form model by create deep copy of assignmentpoll object
// so detail view is not updated while editing poll // so detail view is not updated while editing poll
var assignmentpoll = angular.copy(AssignmentPoll.get(assignmentpollId)); var assignmentpoll = angular.copy(AssignmentPoll.get(assignmentpollId));
@ -700,6 +706,9 @@ angular.module('OpenSlidesApp.assignments.site', [
$scope.formFields = []; $scope.formFields = [];
$scope.alert = {}; $scope.alert = {};
// For number inputs
var step = Math.pow(10, -AssignmentPollDecimalPlaces.getPlaces(assignmentpoll));
// add dynamic form fields // add dynamic form fields
var options = $filter('orderBy')(assignmentpoll.options, 'weight'); var options = $filter('orderBy')(assignmentpoll.options, 'weight');
_.forEach(options, function(option) { _.forEach(options, function(option) {
@ -720,6 +729,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: gettextCatalog.getString('Yes'), label: gettextCatalog.getString('Yes'),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true required: true
}, },
defaultValue: defaultValue.yes defaultValue: defaultValue.yes
@ -733,6 +743,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: gettextCatalog.getString('No'), label: gettextCatalog.getString('No'),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true required: true
}, },
defaultValue: defaultValue.no defaultValue: defaultValue.no
@ -747,6 +758,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: gettextCatalog.getString('Abstain'), label: gettextCatalog.getString('Abstain'),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true required: true
}, },
defaultValue: defaultValue.abstain defaultValue: defaultValue.abstain
@ -771,6 +783,7 @@ angular.module('OpenSlidesApp.assignments.site', [
label: option.candidate.get_full_name(), label: option.candidate.get_full_name(),
type: 'number', type: 'number',
min: -2, min: -2,
step: step,
required: true, required: true,
}, },
defaultValue: defaultValue defaultValue: defaultValue
@ -785,6 +798,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Abstain'), label: gettextCatalog.getString('Abstain'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
}, },
@ -794,6 +808,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('No'), label: gettextCatalog.getString('No'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
} }
@ -810,6 +825,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Valid ballots'), label: gettextCatalog.getString('Valid ballots'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
}, },
@ -819,6 +835,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Invalid ballots'), label: gettextCatalog.getString('Invalid ballots'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
}, },
@ -828,6 +845,7 @@ angular.module('OpenSlidesApp.assignments.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Casted ballots'), label: gettextCatalog.getString('Casted ballots'),
type: 'number', type: 'number',
step: step,
min: -2, min: -2,
} }
} }

View File

@ -234,7 +234,7 @@
<div ng-init="votes = option.getVotes()"> <div ng-init="votes = option.getVotes()">
<div ng-repeat="vote in votes"> <div ng-repeat="vote in votes">
<span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span> <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"> <div ng-if="vote.percentNumber >= 0">
<uib-progressbar ng-if="$index == 0" value="vote.percentNumber" type="success"></uib-progressbar> <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> <uib-progressbar ng-if="$index == 1" value="vote.percentNumber" type="danger"></uib-progressbar>
@ -244,10 +244,10 @@
</div> </div>
<td ng-hide="method === 'disabled'"> <td ng-hide="method === 'disabled'">
<span ng-if="option.majorityReached >= 0" class="text-success" translate> <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>
<span ng-if="option.majorityReached < 0" class="text-danger" translate> <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> </span>
<!-- total votes (valid/invalid/casts) --> <!-- total votes (valid/invalid/casts) -->
@ -255,31 +255,31 @@
<td> <td>
<translate>Abstain</translate> <translate>Abstain</translate>
<td> <td>
{{ poll.getVote('votesabstain').value }} {{ poll.getVote('votesabstain').value | number:votesPrecision }}
{{ poll.getVote('votesabstain').percentStr }} {{ poll.getVote('votesabstain').percentStr }}
<tr ng-if="poll.pollmethod === 'votes'"> <tr ng-if="poll.pollmethod === 'votes'">
<td> <td>
<translate>No</translate> <translate>No</translate>
<td> <td>
{{ poll.getVote('votesno').value }} {{ poll.getVote('votesno').value | number:votesPrecision }}
{{ poll.getVote('votesno').percentStr }} {{ poll.getVote('votesno').percentStr }}
<tr> <tr>
<td> <td>
<translate>Valid ballots</translate> <translate>Valid ballots</translate>
<td> <td>
{{ poll.getVote('votesvalid').value }} {{ poll.getVote('votesvalid').value | number:votesPrecision }}
{{ poll.getVote('votesvalid').percentStr }} {{ poll.getVote('votesvalid').percentStr }}
<tr> <tr>
<td> <td>
<translate>Invalid ballots</translate> <translate>Invalid ballots</translate>
<td> <td>
{{ poll.getVote('votesinvalid').value }} {{ poll.getVote('votesinvalid').value | number:votesPrecision }}
{{ poll.getVote('votesinvalid').percentStr }} {{ poll.getVote('votesinvalid').percentStr }}
<tr class="total bg-info"> <tr class="total bg-info">
<td> <td>
<translate>Casted ballots</translate> <translate>Casted ballots</translate>
<td> <td>
{{ poll.getVote('votescast').value }} {{ poll.getVote('votescast').value | number:votesPrecision }}
{{ poll.getVote('votescast').percentStr }} {{ poll.getVote('votescast').percentStr }}
</table> </table>

View File

@ -49,16 +49,16 @@
<div ng-init="votes = option.getVotes()"> <div ng-init="votes = option.getVotes()">
<div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'"> <div ng-show="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">
<span ng-show="poll.pollmethod == 'yna'"> <span ng-show="poll.pollmethod == 'yna'">
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br> {{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}<br> {{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}<br>
{{ votes[2].label | translate }}: {{ votes[2].value }} {{ votes[2].percentStr }}</span> {{ votes[2].label | translate }}: {{ votes[2].value | number:votesPrecision }} {{ votes[2].percentStr }}</span>
<span ng-show="poll.pollmethod == 'yn'"> <span ng-show="poll.pollmethod == 'yn'">
{{ votes[0].label | translate }}: {{ votes[0].value }} {{ votes[0].percentStr }}<br> {{ votes[0].label | translate }}: {{ votes[0].value | number:votesPrecision }} {{ votes[0].percentStr }}<br>
{{ votes[1].label | translate }}: {{ votes[1].value }} {{ votes[1].percentStr }}</span> {{ votes[1].label | translate }}: {{ votes[1].value | number:votesPrecision }} {{ votes[1].percentStr }}</span>
</div> </div>
<div ng-show="poll.pollmethod == 'votes'"> <div ng-show="poll.pollmethod == 'votes'">
<div ng-repeat="vote in votes"> <div ng-repeat="vote in votes">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
</div> </div>
</div> </div>
</div> </div>
@ -68,29 +68,29 @@
<td> <td>
<translate>Abstain</translate> <translate>Abstain</translate>
<td ng-init="vote = poll.getVote('votesabstain')"> <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"> <tr class="total" ng-if="poll.has_votes && poll.pollmethod === 'votes' && poll.getVote('votesno').value !== null">
<td> <td>
<translate>No</translate> <translate>No</translate>
<td ng-init="vote = poll.getVote('votesno')"> <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"> <tr class="total" ng-if="poll.has_votes && poll.getVote('votesvalid').value !== null">
<td> <td>
<translate>Valid ballots</translate> <translate>Valid ballots</translate>
<td ng-init="vote = poll.getVote('votesvalid')"> <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"> <tr class="total" ng-if="poll.has_votes && poll.getVote('votesinvalid').value !== null">
<td> <td>
<translate>Invalid ballots</translate> <translate>Invalid ballots</translate>
<td ng-init="vote = poll.getVote('votesinvalid')"> <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"> <tr class="total bg-info" ng-if="poll.has_votes && poll.getVote('votescast').value !== null">
<td> <td>
<translate>Casted ballots</translate> <translate>Casted ballots</translate>
<td ng-init="vote = poll.getVote('votescast')"> <td ng-init="vote = poll.getVote('votescast')">
{{ vote.value }} {{ vote.percentStr }} {{ vote.value | number:votesPrecision }} {{ vote.percentStr }}
</table> </table>
</div> </div>

View 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'))]),
),
]

View File

@ -1,4 +1,4 @@
from typing import Dict # noqa from typing import Dict, Optional # noqa
from django.db import transaction from django.db import transaction
from django.utils.translation import ugettext as _ 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 ..poll.serializers import default_votes_validator
from ..utils.rest_api import ( from ..utils.rest_api import (
CharField, CharField,
DecimalField,
DictField, DictField,
Field, Field,
IntegerField, IntegerField,
@ -212,7 +213,7 @@ class MotionPollSerializer(ModelSerializer):
no = SerializerMethodField() no = SerializerMethodField()
abstain = SerializerMethodField() abstain = SerializerMethodField()
votes = DictField( 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) write_only=True)
has_votes = SerializerMethodField() has_votes = SerializerMethodField()
@ -238,21 +239,21 @@ class MotionPollSerializer(ModelSerializer):
def get_yes(self, obj): def get_yes(self, obj):
try: try:
result = self.get_votes_dict(obj)['Yes'] result = str(self.get_votes_dict(obj)['Yes']) # type: Optional[str]
except KeyError: except KeyError:
result = None result = None
return result return result
def get_no(self, obj): def get_no(self, obj):
try: try:
result = self.get_votes_dict(obj)['No'] result = str(self.get_votes_dict(obj)['No']) # type: Optional[str]
except KeyError: except KeyError:
result = None result = None
return result return result
def get_abstain(self, obj): def get_abstain(self, obj):
try: try:
result = self.get_votes_dict(obj)['Abstain'] result = str(self.get_votes_dict(obj)['Abstain']) # type: Optional[str]
except KeyError: except KeyError:
result = None result = None
return result return result

View File

@ -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: { methods: {
// Returns percent base. Returns undefined if calculation is not possible in general. // Returns percent base. Returns undefined if calculation is not possible in general.
getPercentBase: function (config, type) { 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', [ .factory('MotionStateAndRecommendationParser', [
'DS', 'DS',
'gettextCatalog', 'gettextCatalog',

View File

@ -17,9 +17,10 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'Config', 'Config',
'Motion', 'Motion',
'MotionComment', 'MotionComment',
'MotionPollDecimalPlaces',
'OpenSlidesSettings', 'OpenSlidesSettings',
function($q, $filter, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, 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 * Provides the content as JS objects for Motions in pdfMake context
* @constructor * @constructor
@ -185,40 +186,41 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
column2.push(''); column2.push('');
column3.push(''); column3.push('');
} }
var precision = MotionPollDecimalPlaces.getPlaces(poll);
// yes // yes
var yes = poll.getVote(poll.yes, 'yes'); var yes = poll.getVote(poll.yes, 'yes');
column1.push(gettextCatalog.getString('Yes') + ':'); column1.push(gettextCatalog.getString('Yes') + ':');
column2.push(yes.value); column2.push($filter('number')(yes.value, precision));
column3.push(yes.percentStr); column3.push(yes.percentStr);
// no // no
var no = poll.getVote(poll.no, 'no'); var no = poll.getVote(poll.no, 'no');
column1.push(gettextCatalog.getString('No') + ':'); column1.push(gettextCatalog.getString('No') + ':');
column2.push(no.value); column2.push($filter('number')(no.value, precision));
column3.push(no.percentStr); column3.push(no.percentStr);
// abstain // abstain
var abstain = poll.getVote(poll.abstain, 'abstain'); var abstain = poll.getVote(poll.abstain, 'abstain');
column1.push(gettextCatalog.getString('Abstain') + ':'); column1.push(gettextCatalog.getString('Abstain') + ':');
column2.push(abstain.value); column2.push($filter('number')(abstain.value, precision));
column3.push(abstain.percentStr); column3.push(abstain.percentStr);
// votes valid // votes valid
if (poll.votesvalid) { if (poll.votesvalid) {
var valid = poll.getVote(poll.votesvalid, 'votesvalid'); var valid = poll.getVote(poll.votesvalid, 'votesvalid');
column1.push(gettextCatalog.getString('Valid votes') + ':'); column1.push(gettextCatalog.getString('Valid votes') + ':');
column2.push(valid.value); column2.push($filter('number')(valid.value, precision));
column3.push(valid.percentStr); column3.push(valid.percentStr);
} }
// votes invalid // votes invalid
if (poll.votesvalid) { if (poll.votesvalid) {
var invalid = poll.getVote(poll.votesinvalid, 'votesinvalid'); var invalid = poll.getVote(poll.votesinvalid, 'votesinvalid');
column1.push(gettextCatalog.getString('Invalid votes') + ':'); column1.push(gettextCatalog.getString('Invalid votes') + ':');
column2.push(invalid.value); column2.push($filter('number')(invalid.value, precision));
column3.push(invalid.percentStr); column3.push(invalid.percentStr);
} }
// votes cast // votes cast
if (poll.votescast) { if (poll.votescast) {
var cast = poll.getVote(poll.votescast, 'votescast'); var cast = poll.getVote(poll.votescast, 'votescast');
column1.push(gettextCatalog.getString('Votes cast') + ':'); column1.push(gettextCatalog.getString('Votes cast') + ':');
column2.push(cast.value); column2.push($filter('number')(cast.value, precision));
column3.push(cast.percentStr); column3.push(cast.percentStr);
} }
} }

View File

@ -26,7 +26,9 @@ angular.module('OpenSlidesApp.motions.projector', [
'User', 'User',
'Notify', 'Notify',
'ProjectorID', '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. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
@ -67,7 +69,20 @@ angular.module('OpenSlidesApp.motions.projector', [
$scope.motion = Motion.get(motionId); $scope.motion = Motion.get(motionId);
$scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff(); $scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff();
$scope.viewChangeRecommendations.setVersion($scope.motion, $scope.motion.active_version); $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 // Change recommendation viewing
$scope.viewChangeRecommendations = ChangeRecommendationView; $scope.viewChangeRecommendations = ChangeRecommendationView;

View File

@ -750,7 +750,8 @@ angular.module('OpenSlidesApp.motions.site', [
'gettextCatalog', 'gettextCatalog',
function (gettextCatalog) { function (gettextCatalog) {
return { return {
getFormFields: function () { getFormFields: function (precision) {
var step = Math.pow(10, -precision);
return [ return [
{ {
key: 'yes', key: 'yes',
@ -758,6 +759,7 @@ angular.module('OpenSlidesApp.motions.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Yes'), label: gettextCatalog.getString('Yes'),
type: 'number', type: 'number',
step: step,
required: true required: true
} }
}, },
@ -767,6 +769,7 @@ angular.module('OpenSlidesApp.motions.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('No'), label: gettextCatalog.getString('No'),
type: 'number', type: 'number',
step: step,
required: true required: true
} }
}, },
@ -776,6 +779,7 @@ angular.module('OpenSlidesApp.motions.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Abstain'), label: gettextCatalog.getString('Abstain'),
type: 'number', type: 'number',
step: step,
required: true required: true
} }
}, },
@ -784,6 +788,7 @@ angular.module('OpenSlidesApp.motions.site', [
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Valid votes'), label: gettextCatalog.getString('Valid votes'),
step: step,
type: 'number' type: 'number'
} }
}, },
@ -792,6 +797,7 @@ angular.module('OpenSlidesApp.motions.site', [
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Invalid votes'), label: gettextCatalog.getString('Invalid votes'),
step: step,
type: 'number' type: 'number'
} }
}, },
@ -800,6 +806,7 @@ angular.module('OpenSlidesApp.motions.site', [
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Votes cast'), label: gettextCatalog.getString('Votes cast'),
step: step,
type: 'number' type: 'number'
} }
}]; }];
@ -1130,11 +1137,14 @@ angular.module('OpenSlidesApp.motions.site', [
'MajorityMethodChoices', 'MajorityMethodChoices',
'Config', 'Config',
'MotionPollDetailCtrlCache', 'MotionPollDetailCtrlCache',
function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache) { 'MotionPollDecimalPlaces',
function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache, MotionPollDecimalPlaces) {
// Define choices. // Define choices.
$scope.methodChoices = MajorityMethodChoices; $scope.methodChoices = MajorityMethodChoices;
// TODO: Get $scope.baseChoices from config_variables.py without copying them. // TODO: Get $scope.baseChoices from config_variables.py without copying them.
$scope.votesPrecision = MotionPollDecimalPlaces.getPlaces($scope.poll);
// Setup empty cache with default values. // Setup empty cache with default values.
if (typeof MotionPollDetailCtrlCache[$scope.poll.id] === 'undefined') { if (typeof MotionPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
MotionPollDetailCtrlCache[$scope.poll.id] = { MotionPollDetailCtrlCache[$scope.poll.id] = {
@ -2517,17 +2527,19 @@ angular.module('OpenSlidesApp.motions.site', [
'gettextCatalog', 'gettextCatalog',
'MotionPoll', 'MotionPoll',
'MotionPollForm', 'MotionPollForm',
'MotionPollDecimalPlaces',
'motionpollId', 'motionpollId',
'voteNumber', 'voteNumber',
'ErrorMessage', 'ErrorMessage',
function ($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId, function ($scope, gettextCatalog, MotionPoll, MotionPollForm, MotionPollDecimalPlaces,
voteNumber, ErrorMessage) { motionpollId, voteNumber, ErrorMessage) {
// set initial values for form model by create deep copy of motionpoll object // set initial values for form model by create deep copy of motionpoll object
// so detail view is not updated while editing poll // so detail view is not updated while editing poll
var motionpoll = MotionPoll.get(motionpollId); var motionpoll = MotionPoll.get(motionpollId);
$scope.model = angular.copy(motionpoll); $scope.model = angular.copy(motionpoll);
$scope.voteNumber = voteNumber; $scope.voteNumber = voteNumber;
$scope.formFields = MotionPollForm.getFormFields(); var precision = MotionPollDecimalPlaces.getPlaces(motionpoll);
$scope.formFields = MotionPollForm.getFormFields(precision);
$scope.alert = {}; $scope.alert = {};
// save motionpoll // save motionpoll

View File

@ -404,7 +404,7 @@
<td ng-init="voteYes = poll.getVote(poll.yes, 'yes')"> <td ng-init="voteYes = poll.getVote(poll.yes, 'yes')">
<span class="result-label"><translate>Yes</translate>:</span> <span class="result-label"><translate>Yes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteYes.value }} {{ voteYes.percentStr }} {{ voteYes.value | number:votesPrecision }} {{ voteYes.percentStr }}
</span> </span>
<div ng-if="voteYes.percentNumber"> <div ng-if="voteYes.percentNumber">
<uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar> <uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar>
@ -416,7 +416,7 @@
<td ng-init="voteNo = poll.getVote(poll.no, 'no')"> <td ng-init="voteNo = poll.getVote(poll.no, 'no')">
<span class="result-label"><translate>No</translate>:</span> <span class="result-label"><translate>No</translate>:</span>
<span class="result_value" > <span class="result_value" >
{{ voteNo.value }} {{ voteNo.percentStr }} {{ voteNo.value | number:votesPrecision }} {{ voteNo.percentStr }}
</span> </span>
<div ng-if="voteNo.percentNumber"> <div ng-if="voteNo.percentNumber">
<uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar> <uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar>
@ -428,7 +428,7 @@
<td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')"> <td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')">
<span class="result-label"><translate>Abstain</translate>:</span> <span class="result-label"><translate>Abstain</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteAbstain.value }} {{ voteAbstain.percentStr }} {{ voteAbstain.value | number:votesPrecision }} {{ voteAbstain.percentStr }}
</span> </span>
<div ng-if="voteAbstain.percentNumber"> <div ng-if="voteAbstain.percentNumber">
<uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar> <uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar>
@ -440,7 +440,7 @@
<td ng-init="votesValid = poll.getVote(poll.votesvalid, 'votesvalid')"> <td ng-init="votesValid = poll.getVote(poll.votesvalid, 'votesvalid')">
<span class="result-label"><translate>Valid votes</translate>:</span> <span class="result-label"><translate>Valid votes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesValid.value }} {{ votesValid.percentStr }} {{ votesValid.value | number:votesPrecision }} {{ votesValid.percentStr }}
</span> </span>
<!-- invalid votes --> <!-- invalid votes -->
<tr ng-if="poll.votesinvalid !== null"> <tr ng-if="poll.votesinvalid !== null">
@ -449,7 +449,7 @@
<td ng-init="votesInvalid = poll.getVote(poll.votesinvalid, 'votesinvalid')"> <td ng-init="votesInvalid = poll.getVote(poll.votesinvalid, 'votesinvalid')">
<span class="result-label"><translate>Invalid votes</translate>:</span> <span class="result-label"><translate>Invalid votes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesInvalid.value }} {{ votesInvalid.percentStr }} {{ votesInvalid.value | number:votesPrecision }} {{ votesInvalid.percentStr }}
</span> </span>
<!-- votes cast --> <!-- votes cast -->
<tr class="total" ng-if="poll.votescast !== null"> <tr class="total" ng-if="poll.votescast !== null">
@ -458,7 +458,7 @@
<td ng-init="votesCast = poll.getVote(poll.votescast, 'votescast')"> <td ng-init="votesCast = poll.getVote(poll.votescast, 'votescast')">
<span class="result-label"><translate>Votes cast</translate>:</span> <span class="result-label"><translate>Votes cast</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ votesCast.value }} {{ votesCast.percentStr }} {{ votesCast.value | number:votesPrecision }} {{ votesCast.percentStr }}
</span> </span>
<!-- majority calculation --> <!-- majority calculation -->
@ -479,10 +479,10 @@
<td> <td>
<div os-perms="motions.can_manage"> <div os-perms="motions.can_manage">
<span class="text-success" ng-if="isReached() >= 0" translate> <span class="text-success" ng-if="isReached() >= 0" translate>
Quorum ({{ voteYes.value - isReached() }}) reached. Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) reached.
</span> </span>
<span class="text-danger" ng-if="isReached() < 0" translate> <span class="text-danger" ng-if="isReached() < 0" translate>
Quorum ({{ voteYes.value - isReached() }}) not reached. Quorum ({{ (voteYes.value - isReached()) | number:votesPrecision }}) not reached.
</span> </span>
</div> </div>

View File

@ -31,7 +31,7 @@
<td ng-init="voteYes = poll.getVote(poll.yes, 'yes')"> <td ng-init="voteYes = poll.getVote(poll.yes, 'yes')">
<span class="result_label"><translate>Yes</translate>:</span> <span class="result_label"><translate>Yes</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteYes.value }} {{ voteYes.percentStr }} {{ voteYes.value | number:getPollVotesPrecision(poll) }} {{ voteYes.percentStr }}
</span> </span>
<div ng-if="voteYes.percentNumber"> <div ng-if="voteYes.percentNumber">
<uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar> <uib-progressbar value="voteYes.percentNumber" type="success"></uib-progressbar>
@ -43,7 +43,7 @@
<td ng-init="voteNo = poll.getVote(poll.no, 'no')"> <td ng-init="voteNo = poll.getVote(poll.no, 'no')">
<span class="result_label"><translate>No</translate>:</span> <span class="result_label"><translate>No</translate>:</span>
<span class="result_value" > <span class="result_value" >
{{ voteNo.value }} {{ voteNo.percentStr }} {{ voteNo.value | number:getPollVotesPrecision(poll) }} {{ voteNo.percentStr }}
</span> </span>
<div ng-if="voteNo.percentNumber"> <div ng-if="voteNo.percentNumber">
<uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar> <uib-progressbar value="voteNo.percentNumber" type="danger"></uib-progressbar>
@ -55,7 +55,7 @@
<td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')"> <td ng-init="voteAbstain = poll.getVote(poll.abstain, 'abstain')">
<span class="result_label"><translate>Abstain</translate>:</span> <span class="result_label"><translate>Abstain</translate>:</span>
<span class="result_value"> <span class="result_value">
{{ voteAbstain.value }} {{ voteAbstain.percentStr }} {{ voteAbstain.value | number:getPollVotesPrecision(poll) }} {{ voteAbstain.percentStr }}
</span> </span>
<div ng-if="voteAbstain.percentNumber"> <div ng-if="voteAbstain.percentNumber">
<uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar> <uib-progressbar value="voteAbstain.percentNumber" type="warning"></uib-progressbar>

View File

@ -1,12 +1,12 @@
import locale import locale
from decimal import Decimal
from typing import Type # noqa from typing import Type # noqa
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openslides.utils.models import MinMaxIntegerField
class BaseOption(models.Model): class BaseOption(models.Model):
""" """
@ -44,7 +44,8 @@ class BaseVote(models.Model):
Subclasses have to define an option field. This must be a ForeignKeyField Subclasses have to define an option field. This must be a ForeignKeyField
to a subclass of BasePoll. 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) value = models.CharField(max_length=255, null=True)
class Meta: class Meta:
@ -72,9 +73,12 @@ class CollectDefaultVotesMixin(models.Model):
Mixin for a poll to collect the default vote values for valid votes, Mixin for a poll to collect the default vote values for valid votes,
invalid votes and votes cast. invalid votes and votes cast.
""" """
votesvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2) votesvalid = models.DecimalField(null=True, blank=True, validators=[
votesinvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2) MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
votescast = MinMaxIntegerField(null=True, blank=True, min_value=-2) 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: class Meta:
abstract = True abstract = True

View File

@ -18,6 +18,7 @@ from rest_framework.routers import DefaultRouter
from rest_framework.serializers import ModelSerializer as _ModelSerializer from rest_framework.serializers import ModelSerializer as _ModelSerializer
from rest_framework.serializers import ( # noqa from rest_framework.serializers import ( # noqa
CharField, CharField,
DecimalField,
DictField, DictField,
Field, Field,
FileField, FileField,