Merge pull request #2509 from matakuka/majorities
Majorities (issue #2258)
This commit is contained in:
commit
e944f6368b
@ -31,6 +31,7 @@ Motions:
|
||||
- Added recommendations for motions.
|
||||
- Changed label of former state "commited a bill" to "refered to committee".
|
||||
- Added options to calculate percentages on different bases.
|
||||
- Added majority calculation.
|
||||
|
||||
Users:
|
||||
- Added field is_committee and new default group Committees.
|
||||
|
@ -12,7 +12,9 @@ INPUT_TYPE_MAPPING = {
|
||||
'choice': str,
|
||||
'comments': list,
|
||||
'colorpicker': str,
|
||||
'datetimepicker': int}
|
||||
'datetimepicker': int,
|
||||
'majorityMethod': str,
|
||||
}
|
||||
|
||||
|
||||
class ConfigHandler:
|
||||
|
@ -5,6 +5,7 @@
|
||||
// The core module for the OpenSlides site
|
||||
angular.module('OpenSlidesApp.core.site', [
|
||||
'OpenSlidesApp.core',
|
||||
'OpenSlidesApp.poll.majority',
|
||||
'ui.router',
|
||||
'angular-loading-bar',
|
||||
'colorpicker.module',
|
||||
@ -424,7 +425,7 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
'Config',
|
||||
'gettextCatalog',
|
||||
function($parse, Config, gettextCatalog) {
|
||||
function getHtmlType(type) {
|
||||
var getHtmlType = function (type) {
|
||||
return {
|
||||
string: 'text',
|
||||
text: 'textarea',
|
||||
@ -434,8 +435,9 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
comments: 'comments',
|
||||
colorpicker: 'colorpicker',
|
||||
datetimepicker: 'datetimepicker',
|
||||
majorityMethod: 'choice',
|
||||
}[type];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
@ -585,11 +587,12 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
// Config Controller
|
||||
.controller('ConfigCtrl', [
|
||||
'$scope',
|
||||
'MajorityMethodChoices',
|
||||
'Config',
|
||||
'configOptions',
|
||||
'gettextCatalog',
|
||||
'DateTimePickerTranslation',
|
||||
function($scope, Config, configOptions, gettextCatalog, DateTimePickerTranslation) {
|
||||
function($scope, MajorityMethodChoices, Config, configOptions, gettextCatalog, DateTimePickerTranslation) {
|
||||
Config.bindAll({}, $scope, 'configs');
|
||||
$scope.configGroups = configOptions.data.config_groups;
|
||||
$scope.dateTimePickerTranslatedButtons = DateTimePickerTranslation.getButtons();
|
||||
@ -600,7 +603,7 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
Config.save(key);
|
||||
};
|
||||
|
||||
/* For comments input */
|
||||
// For comments input
|
||||
$scope.addComment = function (key, parent) {
|
||||
parent.value.push({
|
||||
name: gettextCatalog.getString('New'),
|
||||
@ -612,6 +615,26 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
parent.value.splice(index, 1);
|
||||
$scope.save(key, parent.value);
|
||||
};
|
||||
|
||||
// For majority method
|
||||
angular.forEach(
|
||||
_.filter($scope.configGroups, function (configGroup) {
|
||||
return configGroup.name === 'Motions' || configGroup.name === 'Elections';
|
||||
}),
|
||||
function (configGroup) {
|
||||
var configItem;
|
||||
_.forEach(configGroup.subgroups, function (subgroup) {
|
||||
configItem = _.find(subgroup.items, ['input_type', 'majorityMethod']);
|
||||
if (configItem !== undefined) {
|
||||
// Break the forEach loop if we found something.
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (configItem !== undefined) {
|
||||
configItem.choices = MajorityMethodChoices;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
])
|
||||
|
||||
|
@ -186,6 +186,17 @@ def get_config_variables():
|
||||
group='Motions',
|
||||
subgroup='Voting and ballot papers')
|
||||
|
||||
# TODO: Add server side validation of the choices.
|
||||
yield ConfigVariable(
|
||||
name='motions_poll_default_majority_method',
|
||||
default_value='simple_majority',
|
||||
input_type='majorityMethod',
|
||||
label='Required majority',
|
||||
help_text='Default method to check whether a motion has reached the required majority.',
|
||||
weight=357,
|
||||
group='Motions',
|
||||
subgroup='Voting and ballot papers')
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_pdf_ballot_papers_selection',
|
||||
default_value='CUSTOM_NUMBER',
|
||||
|
@ -6,6 +6,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'OpenSlidesApp.motions',
|
||||
'OpenSlidesApp.motions.diff',
|
||||
'OpenSlidesApp.motions.motionservices',
|
||||
'OpenSlidesApp.poll.majority',
|
||||
'OpenSlidesApp.core.pdf',
|
||||
'OpenSlidesApp.motions.pdf'
|
||||
])
|
||||
@ -479,6 +480,54 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
}
|
||||
])
|
||||
|
||||
// Cache for MotionPollDetailCtrl so that users choices are keeped during user actions (e. g. save poll form).
|
||||
.value('MotionPollDetailCtrlCache', {})
|
||||
|
||||
// Child controller of MotionDetailCtrl for each single poll.
|
||||
.controller('MotionPollDetailCtrl', [
|
||||
'$scope',
|
||||
'MajorityMethodChoices',
|
||||
'MotionMajority',
|
||||
'Config',
|
||||
'MotionPollDetailCtrlCache',
|
||||
function ($scope, MajorityMethodChoices, MotionMajority, Config, MotionPollDetailCtrlCache) {
|
||||
// Define choices.
|
||||
$scope.methodChoices = MajorityMethodChoices;
|
||||
// TODO: Get $scope.baseChoices from config_variables.py without copying them.
|
||||
|
||||
// Setup empty cache with default values.
|
||||
if (MotionPollDetailCtrlCache[$scope.poll.id] === undefined) {
|
||||
MotionPollDetailCtrlCache[$scope.poll.id] = {
|
||||
isMajorityCalculation: true,
|
||||
isMajorityDetails: false,
|
||||
method: $scope.config('motions_poll_default_majority_method'),
|
||||
base: $scope.config('motions_poll_100_percent_base')
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch users choices from cache.
|
||||
$scope.isMajorityCalculation = MotionPollDetailCtrlCache[$scope.poll.id].isMajorityCalculation;
|
||||
$scope.isMajorityDetails = MotionPollDetailCtrlCache[$scope.poll.id].isMajorityDetails;
|
||||
$scope.method = MotionPollDetailCtrlCache[$scope.poll.id].method;
|
||||
$scope.base = MotionPollDetailCtrlCache[$scope.poll.id].base;
|
||||
|
||||
// Define result function.
|
||||
$scope.isReached = function () {
|
||||
return MotionMajority.isReached($scope.base, $scope.method, $scope.poll);
|
||||
};
|
||||
|
||||
// Save current values to cache on detroy of this controller.
|
||||
$scope.$on('$destroy', function() {
|
||||
MotionPollDetailCtrlCache[$scope.poll.id] = {
|
||||
isMajorityCalculation: $scope.isMajorityCalculation,
|
||||
isMajorityDetails: $scope.isMajorityDetails,
|
||||
method: $scope.method,
|
||||
base: $scope.base
|
||||
};
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('MotionListCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
@ -1594,6 +1643,12 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
gettext('All valid ballots');
|
||||
gettext('All casted ballots');
|
||||
gettext('Disabled (no percents)');
|
||||
gettext('Required majority');
|
||||
gettext('Default method to check whether a motion has reached the required majority.');
|
||||
gettext('Simple majority');
|
||||
gettext('Two-thirds majority');
|
||||
gettext('Three-quarters majority');
|
||||
gettext('Disabled');
|
||||
gettext('Number of ballot papers (selection)');
|
||||
gettext('Number of all delegates');
|
||||
gettext('Number of all participants');
|
||||
|
@ -163,14 +163,16 @@
|
||||
<div class="col-md-4">
|
||||
<h3 ng-if="motion.polls.length > 0" translate>Voting result</h3>
|
||||
<ol>
|
||||
<li ng-repeat="poll in motion.polls" class="spacer"
|
||||
<li ng-controller="MotionPollDetailCtrl" ng-repeat="poll in motion.polls" class="spacer"
|
||||
ng-if="poll.has_votes || operator.hasPerms('motions.can_manage')">
|
||||
<strong translate-comment='ballot of a motion' translate>Vote</strong>
|
||||
|
||||
<!-- Edit poll -->
|
||||
<button os-perms="motions.can_manage" ng-click="openPollDialog(poll, $index+1)"
|
||||
class="btn btn-default btn-xs" title="{{ 'Edit' | translate }}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
|
||||
<!-- Delete poll -->
|
||||
<button os-perms="motions.can_manage" class="btn btn-default btn-xs"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this poll?' | translate }}"
|
||||
@ -178,14 +180,25 @@
|
||||
title="{{ 'Delete' | translate }}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<!-- print poll PDF -->
|
||||
<a ng-click="makePollPDF()" class="btn btn-default btn-xs"
|
||||
|
||||
<!-- Print poll PDF -->
|
||||
<a os-perms="motions.can_manage" ng-click="makePollPDF()" class="btn btn-default btn-xs"
|
||||
title="{{ 'Print ballot paper' | translate }}">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
</a>
|
||||
<!-- template hook for motion poll buttons -->
|
||||
|
||||
<!-- Template hook for motion poll buttons -->
|
||||
<template-hook hook-name="motionPollSmallButtons"></template-hook>
|
||||
|
||||
<!-- Settings for majority calculations -->
|
||||
<div os-perms="motions.can_manage" ng-hide="config('motions_poll_default_majority_method') == 'disabled'" ng-cloak>
|
||||
<div class="input-group">
|
||||
<span><translate>Required majority</translate>: </span>
|
||||
<select ng-init="config('motions_poll_default_majority_method')" ng-model="$parent.method"
|
||||
ng-options="option.value as option.display_name for option in methodChoices" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Poll results -->
|
||||
<div ng-show="poll.has_votes" class="pollresults">
|
||||
<table class="table">
|
||||
@ -252,6 +265,17 @@
|
||||
<span class="result_value">
|
||||
{{ votesCast.value }} {{ votesCast.percentStr }}
|
||||
</span>
|
||||
<!-- majority calculation -->
|
||||
<tr os-perms="motions.can_manage" ng-hide="method == 'disabled'">
|
||||
<td class="icon">
|
||||
<td>
|
||||
<span class="text-warning" ng-if="isReached() === undefined" translate>Calculation impossible</span>
|
||||
<span class="text-success" ng-if="isReached() >= 0" translate>
|
||||
Quorum reached, {{ isReached() }} votes more than needed.
|
||||
</span>
|
||||
<span class="text-danger" ng-if="isReached() < 0" translate>
|
||||
Quorum not reached, {{ -(isReached()) }} <translate>votes missing.
|
||||
</span>
|
||||
</table>
|
||||
</ol>
|
||||
<button ng-if="motion.isAllowed('create_poll')" ng-click="create_poll()" class="btn btn-default btn-sm">
|
||||
|
85
openslides/poll/static/js/poll/majority.js
Normal file
85
openslides/poll/static/js/poll/majority.js
Normal file
@ -0,0 +1,85 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.poll.majority', [])
|
||||
|
||||
.value('MajorityMethodChoices', [
|
||||
{'value': 'simple_majority', 'display_name': 'Simple majority'},
|
||||
{'value': 'two-thirds_majority', 'display_name': 'Two-thirds majority'},
|
||||
{'value': 'three-quarters_majority', 'display_name': 'Three-quarters majority'},
|
||||
{'value': 'disabled', 'display_name': 'Disabled'},
|
||||
])
|
||||
|
||||
.factory('MajorityMethods', [
|
||||
function () {
|
||||
return {
|
||||
'simple_majority': function (vote, base) {
|
||||
return Math.ceil(-(base / 2 - vote)) - 1;
|
||||
},
|
||||
'two-thirds_majority': function (vote, base) {
|
||||
var result = -(base * 2 - vote * 3) / 3;
|
||||
if (result % 1 !== 0) {
|
||||
result = Math.ceil(result) - 1;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
'three-quarters_majority': function (vote, base) {
|
||||
var result = -(base * 3 - vote * 4) / 4;
|
||||
if (result % 1 !== 0) {
|
||||
result = Math.ceil(result) - 1;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
'disabled': function () {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.factory('MotionMajority', [
|
||||
'MajorityMethods',
|
||||
function (MajorityMethods) {
|
||||
return {
|
||||
isReached: function (base, method, votes) {
|
||||
var methodFunction = MajorityMethods[method];
|
||||
var yes = parseInt(votes.yes);
|
||||
var no = parseInt(votes.no);
|
||||
var abstain = parseInt(votes.abstain);
|
||||
var valid = parseInt(votes.votesvalid);
|
||||
var cast = parseInt(votes.votescast);
|
||||
var result;
|
||||
var isValid = function (vote) {
|
||||
return !isNaN(vote) && vote >= 0;
|
||||
};
|
||||
switch (base) {
|
||||
case 'YES_NO_ABSTAIN':
|
||||
if (isValid(yes) && isValid(no) && isValid(abstain)) {
|
||||
result = methodFunction(yes, yes + no + abstain);
|
||||
}
|
||||
break;
|
||||
case 'YES_NO':
|
||||
if (isValid(yes) && isValid(no)) {
|
||||
result = methodFunction(yes, yes + no);
|
||||
}
|
||||
break;
|
||||
case 'VALID':
|
||||
if (isValid(yes) && isValid(valid)) {
|
||||
result = methodFunction(yes, valid);
|
||||
}
|
||||
break;
|
||||
case 'CAST':
|
||||
if (isValid(yes) && isValid(cast)) {
|
||||
result = methodFunction(yes, cast);
|
||||
}
|
||||
break;
|
||||
// case 'DISABLED': result remains undefined
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -143,5 +143,5 @@ class TestConfigDBQueries(TestCase):
|
||||
|
||||
TODO: The last 57 requests are a bug.
|
||||
"""
|
||||
with self.assertNumQueries(60):
|
||||
with self.assertNumQueries(61):
|
||||
self.client.get(reverse('config-list'))
|
||||
|
Loading…
Reference in New Issue
Block a user