Majority calculation for assignments.
Majorities when config YN(A) and simple voting method.
This commit is contained in:
parent
9440842e6a
commit
32aaaf5b9e
@ -15,6 +15,7 @@ Agenda:
|
||||
Assignments:
|
||||
- Remove unused assignment config to publish winner election results only.
|
||||
- Added options to calculate percentages on different bases.
|
||||
- Added majority calculation.
|
||||
|
||||
Core:
|
||||
- Added support for big assemblies with lots of users.
|
||||
|
@ -44,6 +44,17 @@ def get_config_variables():
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers')
|
||||
|
||||
# TODO: Add server side validation of the choices.
|
||||
yield ConfigVariable(
|
||||
name='assignments_poll_default_majority_method',
|
||||
default_value='simple_majority',
|
||||
input_type='majorityMethod',
|
||||
label='Required majority',
|
||||
help_text='Default method to check whether a candidate has reached the required majority.',
|
||||
weight=425,
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers')
|
||||
|
||||
yield ConfigVariable(
|
||||
name='assignments_pdf_ballot_papers_selection',
|
||||
default_value='CUSTOM_NUMBER',
|
||||
|
@ -5,7 +5,8 @@
|
||||
angular.module('OpenSlidesApp.assignments.site', [
|
||||
'OpenSlidesApp.assignments',
|
||||
'OpenSlidesApp.core.pdf',
|
||||
'OpenSlidesApp.assignments.pdf'
|
||||
'OpenSlidesApp.assignments.pdf',
|
||||
'OpenSlidesApp.poll.majority'
|
||||
])
|
||||
|
||||
.config([
|
||||
@ -244,6 +245,100 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
}
|
||||
])
|
||||
|
||||
// Cache for AssignmentPollDetailCtrl so that users choices are keeped during user actions (e. g. save poll form).
|
||||
.value('AssignmentPollDetailCtrlCache', {})
|
||||
|
||||
// Child controller of AssignmentDetailCtrl for each single poll.
|
||||
.controller('AssignmentPollDetailCtrl', [
|
||||
'$scope',
|
||||
'MajorityMethodChoices',
|
||||
'MajorityCalculation',
|
||||
'Config',
|
||||
'AssignmentPollDetailCtrlCache',
|
||||
function ($scope, MajorityMethodChoices, MajorityCalculation, Config, AssignmentPollDetailCtrlCache) {
|
||||
$scope.poll_options_with_majorities = $scope.poll.options;
|
||||
// Define choices.
|
||||
$scope.methodChoices = MajorityMethodChoices;
|
||||
// TODO: Get $scope.baseChoices from config_variables.py without copying them.
|
||||
|
||||
// Setup empty cache with default values.
|
||||
if (AssignmentPollDetailCtrlCache[$scope.poll.id] === undefined) {
|
||||
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
||||
isMajorityCalculation: true,
|
||||
isMajorityDetails: false,
|
||||
method: $scope.config('assignments_poll_default_majority_method'),
|
||||
base: $scope.config('assignments_poll_100_percent_base')
|
||||
};
|
||||
}
|
||||
|
||||
// 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.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.method = method;
|
||||
$scope.calculationError = false;
|
||||
var base_nmbr = $scope.calculateBase();
|
||||
_.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);
|
||||
|
||||
// Save current values to cache on destroy of this controller.
|
||||
$scope.$on('$destroy', function() {
|
||||
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
|
||||
isMajorityCalculation: $scope.isMajorityCalculation,
|
||||
isMajorityDetails: $scope.isMajorityDetails,
|
||||
method: $scope.method,
|
||||
base: $scope.base
|
||||
};
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.controller('AssignmentListCtrl', [
|
||||
'$scope',
|
||||
'ngDialog',
|
||||
@ -883,6 +978,12 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
gettext('Number of all participants');
|
||||
gettext('Use the following custom number');
|
||||
gettext('Custom number of ballot papers');
|
||||
gettext('Required majority');
|
||||
gettext('Default method to check whether a candidate has reached the required majority.');
|
||||
gettext('Simple majority');
|
||||
gettext('Two-thirds majority');
|
||||
gettext('Three-quarters majority');
|
||||
gettext('Disabled');
|
||||
gettext('Title for PDF document (all elections)');
|
||||
gettext('Preamble text for PDF document (all elections)');
|
||||
//other translations
|
||||
|
@ -126,119 +126,133 @@
|
||||
|
||||
<uib-tabset ng-if="assignment.polls.length > 0" class="spacer ballot-tabs" active="activeTab">
|
||||
<uib-tab ng-repeat="poll in assignment.polls | orderBy:'-id'"
|
||||
index="$index"
|
||||
heading="{{ 'Ballot' | translate }} {{ assignment.polls.length - $index }}">
|
||||
|
||||
<!-- action buttons -->
|
||||
<div class="pull-right">
|
||||
<!-- delete -->
|
||||
<a class="btn btn-danger btn-xs"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}"
|
||||
ng-bootbox-confirm-action="deleteBallot(poll)">
|
||||
<i class="fa fa-trash"></i>
|
||||
<translate>Delete</translate>
|
||||
</a>
|
||||
</div>
|
||||
<div os-perms="assignments.can_manage" class="spacer " role="group">
|
||||
<!-- angular requires to open the link in new tab with "target='_blank'".
|
||||
Otherwise the pdf url can't be open in same window; angular redirects to "/". -->
|
||||
<!-- PDF -->
|
||||
<a ng-click="makePDF_assignmentpoll(poll.id)" target="_blank" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
<translate>Print ballot paper</translate>
|
||||
</a>
|
||||
<!-- Edit -->
|
||||
<button ng-click="editPollDialog(poll, $index+1)"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<translate>Enter votes</translate>
|
||||
</button>
|
||||
<!-- Publish -->
|
||||
<button ng-click="togglePublishBallot(poll)"
|
||||
ng-class="poll.published ? 'btn-primary' : 'btn-default'"
|
||||
class="btn btn-sm">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Publish</translate>
|
||||
</button>
|
||||
<!-- Project -->
|
||||
<projector-button model="assignment" default-projector-id="defaultProjectorId"
|
||||
arg="poll.id" content="{{ 'Project' | translate }}">
|
||||
</projector-button>
|
||||
</div>
|
||||
|
||||
<!-- template hook for assignment poll small buttons -->
|
||||
<template-hook hook-name="assignmentPollSmallButtons"></template-hook>
|
||||
|
||||
<div class="results spacer-top-lg">
|
||||
|
||||
<!-- list of candidates of selected poll (without election result) -->
|
||||
<div ng-if="!poll.has_votes">
|
||||
<strong translate>Candidates</strong>
|
||||
<ul class="list-unstyled">
|
||||
<li ng-repeat="option in poll.options">
|
||||
<a ui-sref="users.user.detail({id: option.candidate.id})">
|
||||
{{ option.candidate.get_full_name() }}
|
||||
</a>
|
||||
</ul>
|
||||
<strong translate>Election method</strong><br>
|
||||
<span ng-if="poll.pollmethod == 'votes'" translate>One vote per candidate</span>
|
||||
<span ng-if="poll.pollmethod == 'yna'" translate>Yes/No/Abstain per candidate</span>
|
||||
<span ng-if="poll.pollmethod == 'yn'" translate>Yes/No per candidate</span>
|
||||
index="$index" heading="{{ 'Ballot' | translate }} {{ assignment.polls.length - $index }}">
|
||||
<div ng-controller="AssignmentPollDetailCtrl">
|
||||
<!-- action buttons -->
|
||||
<div class="pull-right">
|
||||
<!-- delete -->
|
||||
<a class="btn btn-danger btn-xs"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}"
|
||||
ng-bootbox-confirm-action="deleteBallot(poll)">
|
||||
<i class="fa fa-trash"></i>
|
||||
<translate>Delete</translate>
|
||||
</a>
|
||||
</div>
|
||||
<div os-perms="assignments.can_manage" class="spacer " role="group">
|
||||
<!-- angular requires to open the link in new tab with "target='_blank'".
|
||||
Otherwise the pdf url can't be open in same window; angular redirects to "/". -->
|
||||
<!-- PDF -->
|
||||
<a ng-click="makePDF_assignmentpoll(poll.id)" target="_blank" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
<translate>Print ballot paper</translate>
|
||||
</a>
|
||||
<!-- Edit -->
|
||||
<button ng-click="editPollDialog(poll, $index+1)"
|
||||
class="btn btn-default btn-sm">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<translate>Enter votes</translate>
|
||||
</button>
|
||||
<!-- Publish -->
|
||||
<button ng-click="togglePublishBallot(poll)"
|
||||
ng-class="poll.published ? 'btn-primary' : 'btn-default'"
|
||||
class="btn btn-sm">
|
||||
<i class="fa fa-eye"></i>
|
||||
<translate>Publish</translate>
|
||||
</button>
|
||||
<!-- Project -->
|
||||
<projector-button model="assignment" default-projector-id="defaultProjectorId"
|
||||
arg="poll.id" content="{{ 'Project' | translate }}">
|
||||
</projector-button>
|
||||
</div>
|
||||
|
||||
<!-- election result of poll -->
|
||||
<table ng-if="poll.has_votes" class="table table-bordered table-striped minimumTable">
|
||||
<tr>
|
||||
<th translate>Candidates
|
||||
<th translate>Votes</th>
|
||||
<!-- template hook for assignment poll small buttons -->
|
||||
<template-hook hook-name="assignmentPollSmallButtons"></template-hook>
|
||||
|
||||
<!-- candidates (poll options) -->
|
||||
<tr ng-repeat="option in poll.options">
|
||||
<!-- candidate name -->
|
||||
<td>
|
||||
<span os-perms="assignments.can_manage">
|
||||
<i ng-if="option.is_elected" class="fa fa-check-square-o"
|
||||
ng-click="markElected(option.candidate_id, option.is_elected)"
|
||||
title="{{ 'is elected' | translate }}"></i>
|
||||
<i ng-if="!option.is_elected" class="fa fa-square-o"
|
||||
ng-click="markElected(option.candidate_id, option.is_elected)"
|
||||
title="{{ 'is not elected' | translate }}"></i>
|
||||
|
||||
</span>
|
||||
<a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
|
||||
<div class="results spacer-top-lg">
|
||||
|
||||
<!-- votes -->
|
||||
<td>
|
||||
<div ng-init="votes = option.getVotes()">
|
||||
<div ng-repeat="vote in votes">
|
||||
<span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span>
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
<div ng-if="vote.percentNumber">
|
||||
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
|
||||
<!-- list of candidates of selected poll (without election result) -->
|
||||
<div ng-if="!poll.has_votes">
|
||||
<strong translate>Candidates</strong>
|
||||
<ul class="list-unstyled">
|
||||
<li ng-repeat="option in options">
|
||||
<a ui-sref="users.user.detail({id: option.candidate.id})">
|
||||
{{ option.candidate.get_full_name() }}
|
||||
</a>
|
||||
</ul>
|
||||
<strong translate>Election method</strong><br>
|
||||
<span ng-if="poll.pollmethod == 'votes'" translate>One vote per candidate</span>
|
||||
<span ng-if="poll.pollmethod == 'yna'" translate>Yes/No/Abstain per candidate</span>
|
||||
<span ng-if="poll.pollmethod == 'yn'" translate>Yes/No per candidate</span>
|
||||
</div>
|
||||
|
||||
<!-- 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 class="input-group">
|
||||
<span><translate>Required majority</translate>: </span>
|
||||
<select ng-model="method"
|
||||
ng-change="recalculateMajorities(method)" ng-options="option.value as option.display_name | translate for option in methodChoices" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- election result of poll -->
|
||||
<table ng-if="poll.has_votes" class="table table-bordered table-striped minimumTable spacer">
|
||||
<tr>
|
||||
<th translate>Candidates
|
||||
<th translate>Votes
|
||||
<th ng-if="method != 'disabled'" translate>Quorum reached
|
||||
</th>
|
||||
<!-- candidates (poll options) -->
|
||||
<tr ng-repeat="option in poll_options_with_majorities">
|
||||
<!-- candidate name -->
|
||||
<td>
|
||||
<span os-perms="assignments.can_manage">
|
||||
<i ng-if="option.is_elected" class="fa fa-check-square-o"
|
||||
ng-click="markElected(option.candidate_id, option.is_elected)"
|
||||
title="{{ 'is elected' | translate }}"></i>
|
||||
<i ng-if="!option.is_elected" class="fa fa-square-o"
|
||||
ng-click="markElected(option.candidate_id, option.is_elected)"
|
||||
title="{{ 'is not elected' | translate }}"></i>
|
||||
|
||||
</span>
|
||||
<a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
|
||||
|
||||
<!-- votes -->
|
||||
<td>
|
||||
<div ng-init="votes = option.getVotes()">
|
||||
<div ng-repeat="vote in votes">
|
||||
<span ng-if="poll.pollmethod == 'yna' || poll.pollmethod == 'yn'">{{ vote.label }}:</span>
|
||||
{{ vote.value }} {{ vote.percentStr }}
|
||||
<div ng-if="vote.percentNumber">
|
||||
<uib-progressbar value="vote.percentNumber" type="success"></uib-progressbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<td ng-if="method != 'disabled'">
|
||||
<span ng-if="option.isReached < 0" class="text-danger" translate> Quorum not reached, {{ -option.isReached }} votes missing.</span>
|
||||
<span ng-if="option.isReached >= 0" class="text-success" translate> Quorum reached, {{ option.isReached }} votes more than needed.</span>
|
||||
|
||||
<!-- total votes (valid/invalid/casts) -->
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Valid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesvalid').value }}
|
||||
{{ poll.getVote('votesvalid').percentStr }}
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Invalid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesinvalid').value }}
|
||||
{{ poll.getVote('votesinvalid').percentStr }}
|
||||
<tr class="total bg-info">
|
||||
<td>
|
||||
<translate>Casted ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votescast').value }}
|
||||
{{ poll.getVote('votescast').percentStr }}
|
||||
</table>
|
||||
<!-- total votes (valid/invalid/casts) -->
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Valid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesvalid').value }}
|
||||
{{ poll.getVote('votesvalid').percentStr }}
|
||||
<tr>
|
||||
<td>
|
||||
<translate>Invalid ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votesinvalid').value }}
|
||||
{{ poll.getVote('votesinvalid').percentStr }}
|
||||
<tr class="total bg-info">
|
||||
<td>
|
||||
<translate>Casted ballots</translate>
|
||||
<td>
|
||||
{{ poll.getVote('votescast').value }}
|
||||
{{ poll.getVote('votescast').percentStr }}
|
||||
</table>
|
||||
<!-- majority calculation message-->
|
||||
<span class="text-warning" ng-if="calculationError" translate>Calculation impossible</span>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
|
@ -740,10 +740,10 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
.controller('MotionPollDetailCtrl', [
|
||||
'$scope',
|
||||
'MajorityMethodChoices',
|
||||
'MotionMajority',
|
||||
'MajorityCalculation',
|
||||
'Config',
|
||||
'MotionPollDetailCtrlCache',
|
||||
function ($scope, MajorityMethodChoices, MotionMajority, Config, MotionPollDetailCtrlCache) {
|
||||
function ($scope, MajorityMethodChoices, MajorityCalculation, Config, MotionPollDetailCtrlCache) {
|
||||
// Define choices.
|
||||
$scope.methodChoices = MajorityMethodChoices;
|
||||
// TODO: Get $scope.baseChoices from config_variables.py without copying them.
|
||||
@ -766,7 +766,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
|
||||
// Define result function.
|
||||
$scope.isReached = function () {
|
||||
return MotionMajority.isReached($scope.base, $scope.method, $scope.poll);
|
||||
return MajorityCalculation.isReached($scope.base, $scope.method, $scope.poll);
|
||||
};
|
||||
|
||||
// Save current values to cache on detroy of this controller.
|
||||
|
@ -38,14 +38,41 @@ angular.module('OpenSlidesApp.poll.majority', [])
|
||||
}
|
||||
])
|
||||
|
||||
.factory('MotionMajority', [
|
||||
.factory('MajorityCalculation', [
|
||||
'MajorityMethods',
|
||||
function (MajorityMethods) {
|
||||
return {
|
||||
isReached: function (base, method, votes) {
|
||||
//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);
|
||||
@ -53,31 +80,35 @@ angular.module('OpenSlidesApp.poll.majority', [])
|
||||
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
|
||||
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;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
@ -144,7 +144,7 @@ class TestConfigDBQueries(TestCase):
|
||||
|
||||
TODO: The last 57 requests are a bug.
|
||||
"""
|
||||
with self.assertNumQueries(61):
|
||||
with self.assertNumQueries(62):
|
||||
self.client.get(reverse('config-list'))
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user