Refactor motion ans assignment percent and majority calculation.
This commit is contained in:
parent
32aaaf5b9e
commit
639978a5dc
@ -9,97 +9,55 @@ angular.module('OpenSlidesApp.assignments', [])
|
|||||||
'jsDataModel',
|
'jsDataModel',
|
||||||
'gettextCatalog',
|
'gettextCatalog',
|
||||||
'Config',
|
'Config',
|
||||||
function (DS, jsDataModel, gettextCatalog, Config) {
|
'MajorityMethods',
|
||||||
|
function (DS, jsDataModel, gettextCatalog, Config, MajorityMethods) {
|
||||||
return DS.defineResource({
|
return DS.defineResource({
|
||||||
name: 'assignments/polloption',
|
name: 'assignments/polloption',
|
||||||
useClass: jsDataModel,
|
useClass: jsDataModel,
|
||||||
methods: {
|
methods: {
|
||||||
getVotes: function () {
|
getVotes: function () {
|
||||||
if (!this.poll.has_votes) {
|
if (!this.poll.has_votes) {
|
||||||
|
// Return undefined if this poll has no votes.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var poll = this.poll;
|
|
||||||
var votes = [];
|
// Initial values for the option
|
||||||
var config = Config.get('assignments_poll_100_percent_base').value;
|
var votes = [],
|
||||||
var impossible = false;
|
config = Config.get('assignments_poll_100_percent_base').value;
|
||||||
var yes = null, no = null, abstain = null;
|
|
||||||
angular.forEach(this.votes, function(vote) {
|
var base = this.poll.getPercentBase(config);
|
||||||
if (vote.value == "Yes" || vote.value == "Votes") {
|
if (typeof base === 'object') {
|
||||||
yes = vote.weight;
|
// this.poll.pollmethod === 'yna'
|
||||||
} else if (vote.value == "No") {
|
base = base[this.id];
|
||||||
no = vote.weight;
|
|
||||||
} else if (vote.value == "Abstain") {
|
|
||||||
abstain = vote.weight;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
//calculation for several candidates without yes/no options
|
_.forEach(this.votes, function (vote) {
|
||||||
var do_sum_of_all = false;
|
// Initial values for the vote
|
||||||
var sum_of_votes = 0;
|
var value = '',
|
||||||
if (poll.options.length > 1 && poll.pollmethod == 'votes') {
|
percentStr = '',
|
||||||
do_sum_of_all = true;
|
percentNumber;
|
||||||
}
|
|
||||||
if (do_sum_of_all === true) {
|
// Check for special value
|
||||||
angular.forEach(poll.options, function(option) {
|
|
||||||
angular.forEach(option.votes, function(vote) {
|
|
||||||
if (vote.value == "Votes") {
|
|
||||||
if (vote.weight >= 0 ) {
|
|
||||||
sum_of_votes = sum_of_votes + vote.weight;
|
|
||||||
} else {
|
|
||||||
impossible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
angular.forEach(this.votes, function(vote) {
|
|
||||||
// check for special value
|
|
||||||
var value;
|
|
||||||
switch (vote.weight) {
|
switch (vote.weight) {
|
||||||
case -1:
|
case -1:
|
||||||
value = gettextCatalog.getString('majority');
|
value = gettextCatalog.getString('majority');
|
||||||
impossible = true;
|
|
||||||
break;
|
break;
|
||||||
case -2:
|
case -2:
|
||||||
value = gettextCatalog.getString('undocumented');
|
value = gettextCatalog.getString('undocumented');
|
||||||
impossible = true;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (vote.weight >= 0) {
|
if (vote.weight >= 0) {
|
||||||
value = vote.weight;
|
value = vote.weight;
|
||||||
} else {
|
} else {
|
||||||
value = 0;
|
value = 0; // Vote was not defined. Set value to 0.
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// calculate percent value
|
|
||||||
var percentStr, percentNumber, base;
|
|
||||||
if (config == "VALID") {
|
|
||||||
if (poll.votesvalid && poll.votesvalid > 0) {
|
|
||||||
base = poll.votesvalid;
|
|
||||||
}
|
|
||||||
} else if ( config == "CAST") {
|
|
||||||
if (poll.votescast && poll.votescast > 0) {
|
|
||||||
base = poll.votescast;
|
|
||||||
}
|
|
||||||
} else if (config == "YES_NO" && !impossible) {
|
|
||||||
if (vote.value == "Yes" || vote.value == "No" || vote.value == "Votes"){
|
|
||||||
if (do_sum_of_all) {
|
|
||||||
base = sum_of_votes;
|
|
||||||
} else {
|
|
||||||
base = yes + no;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (config == "YES_NO_ABSTAIN" && !impossible) {
|
|
||||||
if (do_sum_of_all) {
|
// Special case where to skip percents
|
||||||
base = sum_of_votes;
|
var skipPercents = config === 'YES_NO' && vote.value === 'Abstain';
|
||||||
} else {
|
|
||||||
base = yes + no + abstain;
|
if (base && !skipPercents) {
|
||||||
}
|
|
||||||
}
|
|
||||||
if (base !== 'undefined' && vote.weight >= 0) {
|
|
||||||
percentNumber = Math.round(vote.weight * 100 / base * 10) / 10;
|
percentNumber = Math.round(vote.weight * 100 / base * 10) / 10;
|
||||||
}
|
|
||||||
if (percentNumber >= 0 && percentNumber !== 'undefined') {
|
|
||||||
percentStr = "(" + percentNumber + "%)";
|
percentStr = "(" + percentNumber + "%)";
|
||||||
}
|
}
|
||||||
votes.push({
|
votes.push({
|
||||||
@ -110,6 +68,45 @@ angular.module('OpenSlidesApp.assignments', [])
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return votes;
|
return votes;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns 0 or positive integer if quorum is reached or surpassed.
|
||||||
|
// Returns negativ integer if quorum is not reached.
|
||||||
|
// Returns undefined if we can not calculate the quorum.
|
||||||
|
isReached: function (method) {
|
||||||
|
if (!this.poll.has_votes) {
|
||||||
|
// Return undefined if this poll has no votes.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var isReached;
|
||||||
|
var config = Config.get('assignments_poll_100_percent_base').value;
|
||||||
|
var base = this.poll.getPercentBase(config);
|
||||||
|
if (typeof base === 'object') {
|
||||||
|
// this.poll.pollmethod === 'yna'
|
||||||
|
base = base[this.id];
|
||||||
|
}
|
||||||
|
if (base) {
|
||||||
|
// Provide result only if base is not undefined and not 0.
|
||||||
|
isReached = MajorityMethods[method](this.getVoteYes(), base);
|
||||||
|
}
|
||||||
|
return isReached;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns the weight for the vote or the vote 'yes' in case of YNA poll method.
|
||||||
|
getVoteYes: function () {
|
||||||
|
var voteYes = 0;
|
||||||
|
if (this.poll.pollmethod === 'yna') {
|
||||||
|
var voteObj = _.find(this.votes, function (vote) {
|
||||||
|
return vote.value === 'Yes';
|
||||||
|
});
|
||||||
|
if (voteObj) {
|
||||||
|
voteYes = voteObj.weight;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// pollmethod === 'votes'
|
||||||
|
voteYes = this.votes[0].weight;
|
||||||
|
}
|
||||||
|
return voteYes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
relations: {
|
relations: {
|
||||||
@ -144,10 +141,88 @@ angular.module('OpenSlidesApp.assignments', [])
|
|||||||
getResourceName: function () {
|
getResourceName: function () {
|
||||||
return name;
|
return name;
|
||||||
},
|
},
|
||||||
// returns object with value and percent (for votes valid/invalid/cast only)
|
|
||||||
|
// Returns percent base. Returns undefined if calculation is not possible in general.
|
||||||
|
getPercentBase: function (config, type) {
|
||||||
|
var base;
|
||||||
|
switch (config) {
|
||||||
|
case 'CAST':
|
||||||
|
if (this.votescast <= 0 || this.votesinvalid < 0) {
|
||||||
|
// It would be OK to check only this.votescast < 0 because 0
|
||||||
|
// is checked again later but this is a little bit faster.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
base = this.votescast;
|
||||||
|
/* falls through */
|
||||||
|
case 'VALID':
|
||||||
|
if (this.votesvalid < 0) {
|
||||||
|
base = void 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid') {
|
||||||
|
base = this.votesvalid;
|
||||||
|
}
|
||||||
|
/* falls through */
|
||||||
|
case 'YES_NO_ABSTAIN':
|
||||||
|
case 'YES_NO':
|
||||||
|
if (this.pollmethod === 'yna') {
|
||||||
|
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid' && type !== 'votesvalid') {
|
||||||
|
base = {};
|
||||||
|
_.forEach(this.options, function (option) {
|
||||||
|
var allVotes = option.votes;
|
||||||
|
if (config === 'YES_NO') {
|
||||||
|
allVotes = _.filter(allVotes, function (vote) {
|
||||||
|
// Extract abstain votes in case of YES_NO percent base.
|
||||||
|
// Do not extract abstain vote if it is set to majority so the predicate later
|
||||||
|
// fails and therefor we get an undefined base. Reason: It should not be possible
|
||||||
|
// to set abstain to majority and nevertheless calculate percents of yes and no.
|
||||||
|
return vote.value !== 'Abstain' || vote.weight === -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var predicate = function (vote) {
|
||||||
|
return vote.weight < 0;
|
||||||
|
};
|
||||||
|
if (_.findIndex(allVotes, predicate) === -1) {
|
||||||
|
base[option.id] = _.reduce(allVotes, function (sum, vote) {
|
||||||
|
return sum + vote.weight;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this.pollmethod === 'votes'
|
||||||
|
var predicate = function (option) {
|
||||||
|
return option.votes[0].weight < 0;
|
||||||
|
};
|
||||||
|
if (_.findIndex(this.options, predicate) !== -1) {
|
||||||
|
base = void 0;
|
||||||
|
} else {
|
||||||
|
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid' && type !== 'votesvalid') {
|
||||||
|
base = _.reduce(this.options, function (sum, option) {
|
||||||
|
return sum + option.votes[0].weight;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns object with value and percent for this poll (for votes valid/invalid/cast only).
|
||||||
getVote: function (type) {
|
getVote: function (type) {
|
||||||
var value, percentStr, vote;
|
if (!this.has_votes) {
|
||||||
switch(type) {
|
// Return undefined if this poll has no votes.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial values
|
||||||
|
var value = '',
|
||||||
|
percentStr = '',
|
||||||
|
percentNumber,
|
||||||
|
vote,
|
||||||
|
config = Config.get('assignments_poll_100_percent_base').value;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
case 'votesinvalid':
|
case 'votesinvalid':
|
||||||
vote = this.votesinvalid;
|
vote = this.votesinvalid;
|
||||||
break;
|
break;
|
||||||
@ -158,7 +233,8 @@ angular.module('OpenSlidesApp.assignments', [])
|
|||||||
vote = this.votescast;
|
vote = this.votescast;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this.has_votes && vote) {
|
|
||||||
|
// Check special values
|
||||||
switch (vote) {
|
switch (vote) {
|
||||||
case -1:
|
case -1:
|
||||||
value = gettextCatalog.getString('majority');
|
value = gettextCatalog.getString('majority');
|
||||||
@ -167,26 +243,24 @@ angular.module('OpenSlidesApp.assignments', [])
|
|||||||
value = gettextCatalog.getString('undocumented');
|
value = gettextCatalog.getString('undocumented');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
value = vote;
|
|
||||||
}
|
|
||||||
if (vote >= 0) {
|
if (vote >= 0) {
|
||||||
var config = Config.get('assignments_poll_100_percent_base').value;
|
value = vote;
|
||||||
var percentNumber;
|
} else {
|
||||||
if (config == "CAST" && this.votescast && this.votescast > 0) {
|
value = 0; // value was not defined
|
||||||
percentNumber = Math.round(vote * 100 / this.votescast * 10) / 10;
|
|
||||||
} else if (config == "VALID" && this.votesvalid && this.votesvalid >= 0) {
|
|
||||||
if (type === 'votesvalid'){
|
|
||||||
percentNumber = Math.round(vote * 100 / this.votesvalid * 10) / 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (percentNumber !== 'undefined' && percentNumber >= 0 && percentNumber <=100) {
|
|
||||||
percentStr = "(" + percentNumber + "%)";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate percent value
|
||||||
|
var base = this.getPercentBase(config, type);
|
||||||
|
if (base) {
|
||||||
|
percentNumber = Math.round(vote * 100 / (base) * 10) / 10;
|
||||||
|
percentStr = '(' + percentNumber + ' %)';
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'value': value,
|
'value': value,
|
||||||
'percentStr': percentStr
|
'percentStr': percentStr,
|
||||||
|
'percentNumber': percentNumber,
|
||||||
|
'display': value + ' ' + percentStr
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -252,77 +252,27 @@ angular.module('OpenSlidesApp.assignments.site', [
|
|||||||
.controller('AssignmentPollDetailCtrl', [
|
.controller('AssignmentPollDetailCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'MajorityMethodChoices',
|
'MajorityMethodChoices',
|
||||||
'MajorityCalculation',
|
|
||||||
'Config',
|
'Config',
|
||||||
'AssignmentPollDetailCtrlCache',
|
'AssignmentPollDetailCtrlCache',
|
||||||
function ($scope, MajorityMethodChoices, MajorityCalculation, Config, AssignmentPollDetailCtrlCache) {
|
function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache) {
|
||||||
$scope.poll_options_with_majorities = $scope.poll.options;
|
|
||||||
// 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.
|
||||||
|
|
||||||
// Setup empty cache with default values.
|
// Setup empty cache with default values.
|
||||||
if (AssignmentPollDetailCtrlCache[$scope.poll.id] === undefined) {
|
if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
|
||||||
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
||||||
isMajorityCalculation: true,
|
|
||||||
isMajorityDetails: false,
|
|
||||||
method: $scope.config('assignments_poll_default_majority_method'),
|
method: $scope.config('assignments_poll_default_majority_method'),
|
||||||
base: $scope.config('assignments_poll_100_percent_base')
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch users choices from cache.
|
// Fetch users choices from cache.
|
||||||
$scope.isMajorityCalculation = AssignmentPollDetailCtrlCache[$scope.poll.id].isMajorityCalculation;
|
|
||||||
$scope.isMajorityDetails = AssignmentPollDetailCtrlCache[$scope.poll.id].isMajorityDetails;
|
|
||||||
$scope.method = AssignmentPollDetailCtrlCache[$scope.poll.id].method;
|
$scope.method = AssignmentPollDetailCtrlCache[$scope.poll.id].method;
|
||||||
$scope.base = AssignmentPollDetailCtrlCache[$scope.poll.id].base;
|
|
||||||
|
|
||||||
// (re)calculate the base of poll calculations.
|
|
||||||
$scope.calculateBase = function() {
|
|
||||||
var base;
|
|
||||||
switch($scope.base) {
|
|
||||||
case 'YES_NO_ABSTAIN':
|
|
||||||
case 'YES_NO':
|
|
||||||
if ($scope.poll.pollmethod == 'votes') {
|
|
||||||
base = MajorityCalculation.options_yes_sum($scope.poll);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'VALID':
|
|
||||||
base = $scope.poll.votesvalid;
|
|
||||||
break;
|
|
||||||
case 'CAST':
|
|
||||||
base = $scope.poll.votescast;
|
|
||||||
break;
|
|
||||||
// case 'DISABLED have no bases
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
};
|
|
||||||
|
|
||||||
// calculates if majority thresholds have been reached. Returns 0+ when reached, or negative int when not reached
|
|
||||||
$scope.recalculateMajorities = function (method) {
|
$scope.recalculateMajorities = function (method) {
|
||||||
$scope.method = method;
|
$scope.method = method;
|
||||||
$scope.calculationError = false;
|
_.forEach($scope.poll.options, function (option) {
|
||||||
var base_nmbr = $scope.calculateBase();
|
option.majorityReached = option.isReached(method);
|
||||||
_.forEach($scope.poll_options_with_majorities, function(option) {
|
|
||||||
var optionvotes = {};
|
|
||||||
_.forEach(option.votes, function(vote) {
|
|
||||||
switch (vote.value) {
|
|
||||||
case 'Yes':
|
|
||||||
case 'Votes':
|
|
||||||
optionvotes.yes = vote.weight;
|
|
||||||
break;
|
|
||||||
case 'No':
|
|
||||||
optionvotes.no = vote.weight;
|
|
||||||
break;
|
|
||||||
case 'Abstain':
|
|
||||||
optionvotes.abstain = vote.weight;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
option.isReached = MajorityCalculation.isReached($scope.base, method, optionvotes, base_nmbr);
|
|
||||||
if (option.reached === 'undefined'){
|
|
||||||
$scope.calculationError = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
$scope.recalculateMajorities($scope.method);
|
$scope.recalculateMajorities($scope.method);
|
||||||
@ -330,10 +280,7 @@ angular.module('OpenSlidesApp.assignments.site', [
|
|||||||
// Save current values to cache on destroy of this controller.
|
// Save current values to cache on destroy of this controller.
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
||||||
isMajorityCalculation: $scope.isMajorityCalculation,
|
|
||||||
isMajorityDetails: $scope.isMajorityDetails,
|
|
||||||
method: $scope.method,
|
method: $scope.method,
|
||||||
base: $scope.base
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@
|
|||||||
<div ng-if="!poll.has_votes">
|
<div ng-if="!poll.has_votes">
|
||||||
<strong translate>Candidates</strong>
|
<strong translate>Candidates</strong>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li ng-repeat="option in options">
|
<li ng-repeat="option in poll.options">
|
||||||
<a ui-sref="users.user.detail({id: option.candidate.id})">
|
<a ui-sref="users.user.detail({id: option.candidate.id})">
|
||||||
{{ option.candidate.get_full_name() }}
|
{{ option.candidate.get_full_name() }}
|
||||||
</a>
|
</a>
|
||||||
@ -186,11 +186,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings for majority calculations -->
|
<!-- Settings for majority calculations -->
|
||||||
<div os-perms="assignments.can_manage" ng-hide="config('assignments_poll_default_majority_method') == 'disabled' || !poll.has_votes" ng-cloak>
|
<div os-perms="assignments.can_manage" ng-show="poll.has_votes" ng-cloak>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span><translate>Required majority</translate>: </span>
|
<span><translate>Required majority</translate>: </span>
|
||||||
<select ng-model="method"
|
<select ng-model="method"
|
||||||
ng-change="recalculateMajorities(method)" ng-options="option.value as option.display_name | translate for option in methodChoices" />
|
ng-change="recalculateMajorities(method)"
|
||||||
|
ng-options="option.value as option.display_name | translate for option in methodChoices" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- election result of poll -->
|
<!-- election result of poll -->
|
||||||
@ -198,10 +199,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th translate>Candidates
|
<th translate>Candidates
|
||||||
<th translate>Votes
|
<th translate>Votes
|
||||||
<th ng-if="method != 'disabled'" translate>Quorum reached
|
<th translate ng-hide="method === 'disabled'">Quorum
|
||||||
</th>
|
</th>
|
||||||
<!-- candidates (poll options) -->
|
<!-- candidates (poll options) -->
|
||||||
<tr ng-repeat="option in poll_options_with_majorities">
|
<tr ng-repeat="option in poll.options">
|
||||||
<!-- candidate name -->
|
<!-- candidate name -->
|
||||||
<td>
|
<td>
|
||||||
<span os-perms="assignments.can_manage">
|
<span os-perms="assignments.can_manage">
|
||||||
@ -221,14 +222,18 @@
|
|||||||
<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 }} {{ vote.percentStr }}
|
||||||
<div ng-if="vote.percentNumber">
|
<div ng-if="vote.percentNumber >= 0">
|
||||||
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
|
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<td ng-if="method != 'disabled'">
|
<td ng-hide="method === 'disabled'">
|
||||||
<span ng-if="option.isReached < 0" class="text-danger" translate> Quorum not reached, {{ -option.isReached }} votes missing.</span>
|
<span ng-if="option.majorityReached >= 0" class="text-success" translate>
|
||||||
<span ng-if="option.isReached >= 0" class="text-success" translate> Quorum reached, {{ option.isReached }} votes more than needed.</span>
|
Quorum ({{ option.getVoteYes() - option.majorityReached }}) reached.
|
||||||
|
</span>
|
||||||
|
<span ng-if="option.majorityReached < 0" class="text-danger" translate>
|
||||||
|
Quorum ({{ option.getVoteYes() - option.majorityReached }}) not reached.
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- total votes (valid/invalid/casts) -->
|
<!-- total votes (valid/invalid/casts) -->
|
||||||
<tr>
|
<tr>
|
||||||
@ -250,8 +255,6 @@
|
|||||||
{{ poll.getVote('votescast').value }}
|
{{ poll.getVote('votescast').value }}
|
||||||
{{ poll.getVote('votescast').percentStr }}
|
{{ poll.getVote('votescast').percentStr }}
|
||||||
</table>
|
</table>
|
||||||
<!-- majority calculation message-->
|
|
||||||
<span class="text-warning" ng-if="calculationError" translate>Calculation impossible</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
@ -7,6 +7,7 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
'OpenSlidesApp.motions.lineNumbering',
|
'OpenSlidesApp.motions.lineNumbering',
|
||||||
'OpenSlidesApp.motions.diff',
|
'OpenSlidesApp.motions.diff',
|
||||||
'OpenSlidesApp.motions.DOCX',
|
'OpenSlidesApp.motions.DOCX',
|
||||||
|
'OpenSlidesApp.poll.majority',
|
||||||
'OpenSlidesApp.users',
|
'OpenSlidesApp.users',
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -66,7 +67,8 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
'DS',
|
'DS',
|
||||||
'gettextCatalog',
|
'gettextCatalog',
|
||||||
'Config',
|
'Config',
|
||||||
function (DS, gettextCatalog, Config) {
|
'MajorityMethods',
|
||||||
|
function (DS, gettextCatalog, Config, MajorityMethods) {
|
||||||
return DS.defineResource({
|
return DS.defineResource({
|
||||||
name: 'motions/motionpoll',
|
name: 'motions/motionpoll',
|
||||||
relations: {
|
relations: {
|
||||||
@ -78,61 +80,84 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// returns object with value and percent
|
// Returns percent base. Returns undefined if calculation is not possible in general.
|
||||||
|
getPercentBase: function (config, type) {
|
||||||
|
var base;
|
||||||
|
switch (config) {
|
||||||
|
case 'CAST':
|
||||||
|
if (this.votescast <= 0 || this.votesinvalid < 0) {
|
||||||
|
// It would be OK to check only this.votescast < 0 because 0
|
||||||
|
// is checked again later but this is a little bit faster.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
base = this.votescast;
|
||||||
|
/* falls through */
|
||||||
|
case 'VALID':
|
||||||
|
if (this.votesvalid < 0) {
|
||||||
|
base = void 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid') {
|
||||||
|
base = this.votesvalid;
|
||||||
|
}
|
||||||
|
/* falls through */
|
||||||
|
case 'YES_NO_ABSTAIN':
|
||||||
|
if (this.abstain < 0) {
|
||||||
|
base = void 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (typeof base === 'undefined' && type !== 'votescast' && type !== 'votesinvalid' && type !== 'votesvalid') {
|
||||||
|
base = this.yes + this.no + this.abstain;
|
||||||
|
}
|
||||||
|
/* falls through */
|
||||||
|
case 'YES_NO':
|
||||||
|
if (this.yes < 0 || this.no < 0 || this.abstain === -1 ) {
|
||||||
|
// It is not allowed to set 'Abstain' to 'majority' but exclude it from calculation.
|
||||||
|
// Setting 'Abstain' to 'undocumented' is possible, of course.
|
||||||
|
base = void 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (typeof base === 'undefined' && (type === 'yes' || type === 'no')) {
|
||||||
|
base = this.yes + this.no;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns object with value and percent for this poll.
|
||||||
getVote: function (vote, type) {
|
getVote: function (vote, type) {
|
||||||
if (!this.has_votes) {
|
if (!this.has_votes) {
|
||||||
|
// Return undefined if this poll has no votes.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var impossible = false;
|
|
||||||
var value = '';
|
// Initial values
|
||||||
|
var value = '',
|
||||||
|
percentStr = '',
|
||||||
|
percentNumber,
|
||||||
|
config = Config.get('motions_poll_100_percent_base').value;
|
||||||
|
|
||||||
|
// Check special values
|
||||||
switch (vote) {
|
switch (vote) {
|
||||||
case -1:
|
case -1:
|
||||||
value = gettextCatalog.getString('majority');
|
value = gettextCatalog.getString('majority');
|
||||||
impossible = true;
|
|
||||||
break;
|
break;
|
||||||
case -2:
|
case -2:
|
||||||
value = gettextCatalog.getString('undocumented');
|
value = gettextCatalog.getString('undocumented');
|
||||||
impossible = true;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (vote >= 0) {
|
if (vote >= 0) {
|
||||||
value = vote;
|
value = vote;
|
||||||
} else {
|
} else {
|
||||||
value = 0; //value was not defined
|
value = 0; // Vote was not defined. Set value to 0.
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// 100% base impossible if at leat one value has set an
|
|
||||||
// speacial value (-1 or -2).
|
|
||||||
if (this.yes < 0 || this.no < 0 || this.abstain < 0) {
|
|
||||||
impossible = true;
|
|
||||||
}
|
|
||||||
// calculate percent value
|
|
||||||
var config = Config.get('motions_poll_100_percent_base').value;
|
|
||||||
var percentStr = '';
|
|
||||||
var percentNumber = null;
|
|
||||||
var base = null;
|
|
||||||
if (!impossible) {
|
|
||||||
if (config == "YES_NO_ABSTAIN") {
|
|
||||||
if (type == 'yes' || type == 'no' || type == 'abstain') {
|
|
||||||
base = this.yes + this.no + this.abstain;
|
|
||||||
}
|
|
||||||
} else if (config == "YES_NO") {
|
|
||||||
if (type == 'yes' || type == 'no') {
|
|
||||||
base = this.yes + this.no;
|
|
||||||
}
|
|
||||||
} else if (config == "VALID" && type !== 'votescast' && type !== 'votesinvalid' &&
|
|
||||||
this.votesvalid > 0) {
|
|
||||||
base = this.votesvalid;
|
|
||||||
} else if (config == "CAST" && this.votescast > 0) {
|
|
||||||
base = this.votescast;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (base !== null) {
|
|
||||||
|
// Calculate percent value
|
||||||
|
var base = this.getPercentBase(config, type);
|
||||||
|
if (base) {
|
||||||
percentNumber = Math.round(vote * 100 / (base) * 10) / 10;
|
percentNumber = Math.round(vote * 100 / (base) * 10) / 10;
|
||||||
}
|
percentStr = '(' + percentNumber + ' %)';
|
||||||
if (percentNumber !== null) {
|
|
||||||
percentStr = "(" + percentNumber + "%)";
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'value': value,
|
'value': value,
|
||||||
@ -140,6 +165,25 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
'percentNumber': percentNumber,
|
'percentNumber': percentNumber,
|
||||||
'display': value + ' ' + percentStr
|
'display': value + ' ' + percentStr
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Returns 0 or positive integer if quorum is reached or surpassed.
|
||||||
|
// Returns negativ integer if quorum is not reached.
|
||||||
|
// Returns undefined if we can not calculate the quorum.
|
||||||
|
isReached: function (method) {
|
||||||
|
if (!this.has_votes) {
|
||||||
|
// Return undefined if this poll has no votes.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isReached;
|
||||||
|
var config = Config.get('motions_poll_100_percent_base').value;
|
||||||
|
var base = this.getPercentBase(config, 'yes');
|
||||||
|
if (base) {
|
||||||
|
// Provide result only if base is not undefined and not 0.
|
||||||
|
isReached = MajorityMethods[method](this.yes, base);
|
||||||
|
}
|
||||||
|
return isReached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -246,7 +290,7 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
|
|
||||||
for (var i = 0; i < changes.length; i++) {
|
for (var i = 0; i < changes.length; i++) {
|
||||||
var change = changes[i];
|
var change = changes[i];
|
||||||
if (statusCompareCb === undefined || statusCompareCb(change.rejected)) {
|
if (typeof statusCompareCb === 'undefined' || statusCompareCb(change.rejected)) {
|
||||||
html = lineNumberingService.insertLineNumbers(html, lineLength);
|
html = lineNumberingService.insertLineNumbers(html, lineLength);
|
||||||
html = diffService.replaceLines(html, change.text, change.line_from, change.line_to);
|
html = diffService.replaceLines(html, change.text, change.line_from, change.line_to);
|
||||||
}
|
}
|
||||||
|
@ -740,42 +740,37 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
.controller('MotionPollDetailCtrl', [
|
.controller('MotionPollDetailCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'MajorityMethodChoices',
|
'MajorityMethodChoices',
|
||||||
'MajorityCalculation',
|
|
||||||
'Config',
|
'Config',
|
||||||
'MotionPollDetailCtrlCache',
|
'MotionPollDetailCtrlCache',
|
||||||
function ($scope, MajorityMethodChoices, MajorityCalculation, Config, MotionPollDetailCtrlCache) {
|
function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache) {
|
||||||
// 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.
|
||||||
|
|
||||||
// Setup empty cache with default values.
|
// Setup empty cache with default values.
|
||||||
if (MotionPollDetailCtrlCache[$scope.poll.id] === undefined) {
|
if (typeof MotionPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
|
||||||
MotionPollDetailCtrlCache[$scope.poll.id] = {
|
MotionPollDetailCtrlCache[$scope.poll.id] = {
|
||||||
isMajorityCalculation: true,
|
|
||||||
isMajorityDetails: false,
|
|
||||||
method: $scope.config('motions_poll_default_majority_method'),
|
method: $scope.config('motions_poll_default_majority_method'),
|
||||||
base: $scope.config('motions_poll_100_percent_base')
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch users choices from cache.
|
// 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.method = MotionPollDetailCtrlCache[$scope.poll.id].method;
|
||||||
$scope.base = MotionPollDetailCtrlCache[$scope.poll.id].base;
|
|
||||||
|
|
||||||
// Define result function.
|
// Define result function.
|
||||||
$scope.isReached = function () {
|
$scope.isReached = function () {
|
||||||
return MajorityCalculation.isReached($scope.base, $scope.method, $scope.poll);
|
return $scope.poll.isReached($scope.method);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define template controll function
|
||||||
|
$scope.hideMajorityCalculation = function () {
|
||||||
|
return typeof $scope.isReached() === 'undefined' && $scope.method !== 'disabled';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save current values to cache on detroy of this controller.
|
// Save current values to cache on detroy of this controller.
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
MotionPollDetailCtrlCache[$scope.poll.id] = {
|
MotionPollDetailCtrlCache[$scope.poll.id] = {
|
||||||
isMajorityCalculation: $scope.isMajorityCalculation,
|
|
||||||
isMajorityDetails: $scope.isMajorityDetails,
|
|
||||||
method: $scope.method,
|
method: $scope.method,
|
||||||
base: $scope.base
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1146,7 +1141,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
// open dialog for new amendment
|
// open dialog for new amendment
|
||||||
$scope.newAmendment = function () {
|
$scope.newAmendment = function () {
|
||||||
var dialog = MotionForm.getDialog();
|
var dialog = MotionForm.getDialog();
|
||||||
if (dialog.scope === undefined) {
|
if (typeof dialog.scope === 'undefined') {
|
||||||
dialog.scope = {};
|
dialog.scope = {};
|
||||||
}
|
}
|
||||||
dialog.scope = $scope;
|
dialog.scope = $scope;
|
||||||
|
@ -295,12 +295,11 @@
|
|||||||
<td>
|
<td>
|
||||||
<td>
|
<td>
|
||||||
<div os-perms="motions.can_manage"
|
<div os-perms="motions.can_manage"
|
||||||
ng-hide="config('motions_poll_default_majority_method') == 'disabled' ||
|
ng-hide="hideMajorityCalculation()"
|
||||||
isReached() === undefined" ng-cloak>
|
ng-cloak>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span><translate>Required majority</translate>: </span>
|
<span><translate>Required majority</translate>: </span>
|
||||||
<select ng-init="config('motions_poll_default_majority_method')"
|
<select ng-model="$parent.method"
|
||||||
ng-model="$parent.method"
|
|
||||||
ng-options="option.value as option.display_name | translate for option in methodChoices">
|
ng-options="option.value as option.display_name | translate for option in methodChoices">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -309,10 +308,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 reached, {{ isReached() }} votes more than needed.
|
Quorum ({{ voteYes.value - isReached() }}) reached.
|
||||||
</span>
|
</span>
|
||||||
<span class="text-danger" ng-if="isReached() < 0" translate>
|
<span class="text-danger" ng-if="isReached() < 0" translate>
|
||||||
Quorum not reached, {{ -(isReached()) }} votes missing.
|
Quorum ({{ voteYes.value - isReached() }}) not reached.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -36,81 +36,6 @@ angular.module('OpenSlidesApp.poll.majority', [])
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
])
|
|
||||||
|
|
||||||
.factory('MajorityCalculation', [
|
|
||||||
'MajorityMethods',
|
|
||||||
function (MajorityMethods) {
|
|
||||||
return {
|
|
||||||
//calculate the base for yes-based multi-option polls
|
|
||||||
options_yes_sum: function(poll){
|
|
||||||
var yes = 0;
|
|
||||||
var error = false;
|
|
||||||
if (poll.options) {
|
|
||||||
_.forEach(poll.options, function(option) {
|
|
||||||
_.forEach(option.votes, function(vote) {
|
|
||||||
if (vote.value == 'Yes' || vote.value == 'Votes'){
|
|
||||||
if (vote.weight >= 0){
|
|
||||||
yes = yes + vote.weight;
|
|
||||||
} else {
|
|
||||||
error = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (!error) {
|
|
||||||
return yes;
|
|
||||||
}
|
|
||||||
} else if (poll.yes && poll.yes >= 0) {
|
|
||||||
return poll.yes;
|
|
||||||
} // else: undefined
|
|
||||||
},
|
|
||||||
|
|
||||||
// returns 0 or positive integer if quorum is reached or surpassed
|
|
||||||
// sum (optional): a different base to calculate with
|
|
||||||
isReached: function (base, method, votes, sum) {
|
|
||||||
var methodFunction = MajorityMethods[method];
|
|
||||||
var yes = parseInt(votes.yes);
|
|
||||||
var no = parseInt(votes.no);
|
|
||||||
var alt_base = parseInt(sum);
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
if (isValid(alt_base) && isValid(yes)) {
|
|
||||||
result = methodFunction(yes, alt_base);
|
|
||||||
} else {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}());
|
}());
|
||||||
|
Loading…
Reference in New Issue
Block a user