Commit 322554a5 authored by Envel Le Hir's avatar Envel Le Hir

Initial commit

parents
/conf/conf.inc.php
This diff is collapsed.
Denelezh
Copyright (c) 2017-2019 Envel Le Hir
All files are available under the terms of the GNU Affero General Public License (AGPL). See LICENSE file for details for this license.
This project includes works from third parties:
Wikimedia logos:
* /www/static/img/logo-commons.png
public domain
https://commons.wikimedia.org/wiki/File:Notification-icon-Commons-logo.svg
* /www/static/img/logo-mediawiki.png
CC BY-SA 2.5
Copyright (c) User:Jdforrester (WMF), User:DieBuche, User:Poznaniak
https://commons.wikimedia.org/wiki/File:Notification-icon-MediaWiki-logo.svg
* /www/static/img/logo-meta.png
public domain
https://commons.wikimedia.org/wiki/File:Notification-icon-Meta-logo.svg
* /www/static/img/logo-wikibooks.png
CC BY-SA 3.0
Copyright (c) User:Jdforrester (WMF), User:Ramac, User:Bastique
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikibooks-logo.svg
* /www/static/img/logo-wikidata.png
public domain
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikidata-logo.svg
* /www/static/img/logo-wikinews.png
CC BY-SA 3.0
Copyright (c) User:Jdforrester (WMF), User:Time3000, User:Simon, User:NGerda, User:David Vasquez
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikinews-logo.svg
* /www/static/img/logo-wikipedia.png
CC BY-SA 3.0
Copyright (c) User:Nohat, User:Paullusmagnus
https://commons.wikimedia.org/wiki/File:Wikipedia-logo.png
* /www/static/img/logo-wikiquote.png
public domain
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikiquote.svg
* /www/static/img/logo-wikisource.png
CC BY-SA 3.0
Copyright (c) Jdforrester (WMF), Rei-artur, Nicholas Moreau
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikisource-logo.svg
* /www/static/img/logo-wikispecies.png
CC BY-SA 3.0
Copyright (c) User:Jdforrester (WMF), User:Lycaon
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikispecies-logo.svg
* /www/static/img/logo-wikiversity.png
CC BY-SA 3.0
Copyright (c) User:Jdforrester, User:verdy_p, User:Snorky
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikiversity-logo.svg
* /www/static/img/logo-wikivoyage.png
CC BY-SA 3.0
Copyright (c) User:Jdforrester (WMF), User:AleXXw, User:Danapit
https://commons.wikimedia.org/wiki/File:Notification-icon-Wikivoyage-logo.svg
* /www/static/img/logo-wiktionary.png
CC BY-SA 3.0
Copyright (c) User:Jdforrester (WMF), User:Az1568
https://commons.wikimedia.org/wiki/File:Notification-icon-Wiktionary-logo.svg
JavaScript libraries:
* /www/static/js/moment.min.js
Copyright (c) JS Foundation and other contributors
see /www/static/js/moment.min.js-LICENSE file
see also https://momentjs.com/ and https://github.com/moment/moment
* /www/static/js/chart.min.js
Copyright (c) 2018 Chart.js Contributors
see /www/static/js/chart.min.js-LICENSE file
see also https://www.chartjs.org/ and https://github.com/chartjs/Chart.js
## Description
Denelezh is a website that allows you to explore the gender gap in the content of Wikidata and Wikimedia projects. Data can be aggregated by Wikimedia project and/or by characteristics of human beings (year of birth, country of citizenship, and occupation).
`denelezh-core` is a PHP project. It handles both the import of the data from Wikidata (except for a little part explained later) and the visualization.
## Installation
This documentation covers the installation of Denelezh under a Linux system, at the URL `https://www.denelezh.org/`.
### Requirements
* MySQL 5.7 (compatibility with MariaDB has not been tested)
* a web server (seems OK with Apache and nginx)
* PHP 5.6 (seems OK with PHP 7.2)
* Java 8 (seems OK with Java 10)
### MySQL
Denelezh needs a MySQL database.
You can install the MySQL server using the [official repositories](https://dev.mysql.com/downloads/repo/).
Create a new database on your MySQL server:
CREATE DATABASE `denelezh` DEFAULT CHARACTER SET 'utf8mb4';
Create a user (change the password):
CREATE USER 'denelezh'@'localhost' IDENTIFIED BY 'xxxxxxx';
Grant to the user all rights to the database:
GRANT ALL ON `denelezh`.* TO 'denelezh'@'localhost';
Initialize the schema with the script `doc/schema.sql`.
### Web server
Copy all files from the project `denelezh-core` into `/var/www/denelezh.org`.
Configure a new site on your web server, with `/var/www/denelezh.org/www` as the root directory.
An example of nginx site configuration is available in `doc/nginx.example.conf`.
### Imports
A step of the import is done by the project [`denelezh-import`](https://framagit.org/wikimedia-france/denelezh-import).
Copy the file `denelezh-import.jar` (generated by the project `denelezh-import`) into the `scripts` directory.
By default, Wikidata JSON dumps are stored in `/home/wikidata/dumpfiles` (see the project `denelezh-import` to change that). This directory must be readable and writable by the user who starts the imports.
## Configuration
Create the file `conf/conf.inc.php`, using the example `conf/conf.example.php`.
Most important variables are:
* `DB_HOST`: host of the MySQL server
* `DB_USER`: MySQL user
* `DB_PASSWORD`: password of the MySQL user
* `DB_NAME`: name of the schema
* `SITE_DIR`: URL of Denelezh
* `SITE_STATIC_DIR`: URL of the directory with static web resources of Denelezh
## Imports
To start an import, go into the `scripts` directory and use the following command:
bash import.sh
By default, the import use the last JSON dump, downloading it if it is not already present on the server.
You can import another dump by specifying the date of the dump as a parameter:
bash import.sh 20180604
In that case, the JSON dump must be already present on the server.
## Copyright
This project is under AGPLv3 license. See LICENSE and NOTICE files.
## See also
* [denelezh-import](https://framagit.org/wikimedia-france/denelezh-import)
<?php
/*
rename this file conf.inc.php
*/
define('DB_HOST', 'localhost');
define('DB_USER', 'denelezh');
define('DB_PASSWORD', '');
define('DB_NAME', 'denelezh');
define('SITE_DIR', 'https://www.denelezh.org/');
define('SITE_STATIC_DIR', 'https://www.denelezh.org/static/');
define('SITE_TITLE', 'Denelezh');
define('GAPS_MINIMUM_THRESHOLD_COUNTRY', 200);
define('GAPS_MINIMUM_THRESHOLD_OCCUPATION', 1000);
define('GAPS_DEFAULT_YEAR_START', 1800);
define('GAPS_DEFAULT_YEAR_END', date('Y'));
?>
# https challenge (on http)
# http redirect to https://www.denelezh.org
server {
listen 80;
listen [::]:80;
location '/.well-known/acme-challenge' {
default_type "text/plain";
root /var/www/denelezh.org/www;
}
location / {
return 301 https://www.denelezh.org$request_uri;
}
}
# https://denelezh.dicare.org redirect to https://www.denelezh.org
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name denelezh.dicare.org;
ssl_certificate /etc/letsencrypt/live/denelezh.dicare.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/denelezh.dicare.org/privkey.pem;
return 301 https://www.denelezh.org$request_uri;
}
# https://denelezh.wikimedia.fr redirect to https://www.denelezh.org
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name denelezh.wikimedia.fr;
ssl_certificate /etc/letsencrypt/live/denelezh.wikimedia.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/denelezh.wikimedia.fr/privkey.pem;
return 301 https://www.denelezh.org$request_uri;
}
# https://denelezh.org redirect to https://www.denelezh.org
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name denelezh.org;
ssl_certificate /etc/letsencrypt/live/denelezh.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/denelezh.org/privkey.pem;
return 301 https://www.denelezh.org$request_uri;
}
# https://www.denelezh.org
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.denelezh.org;
ssl_certificate /etc/letsencrypt/live/www.denelezh.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.denelezh.org/privkey.pem;
root /var/www/denelezh.org/www;
index index.php;
error_page 404 /404.php;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
break;
}
access_log /var/log/nginx/access-denelezh.org.log;
error_log /var/log/nginx/error-denelezh.org.log;
}
/*
Note: to improve performance, all foreign keys are implicit. Don't try this at home.
*/
/* import tables */
DROP TABLE IF EXISTS `human`;
CREATE TABLE `human` (
`id` INT UNSIGNED NOT NULL,
`gender` INT UNSIGNED DEFAULT NULL,
`birthyear` SMALLINT DEFAULT NULL,
`sitelinks` SMALLINT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `human_country`;
CREATE TABLE `human_country` (
`human` INT UNSIGNED NOT NULL,
`country` INT UNSIGNED NOT NULL,
PRIMARY KEY (`human`,`country`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `human_occupation`;
CREATE TABLE `human_occupation` (
`human` INT UNSIGNED NOT NULL,
`occupation` INT UNSIGNED NOT NULL,
PRIMARY KEY (`human`,`occupation`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `human_sitelink`;
CREATE TABLE `human_sitelink` (
`human` INT UNSIGNED NOT NULL,
`sitelink` VARCHAR(32) CHARACTER SET ascii NOT NULL,
PRIMARY KEY (`human`,`sitelink`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `label`;
CREATE TABLE `label` (
`id` INT UNSIGNED NOT NULL,
`label` VARCHAR(500) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `occupation_parent`;
CREATE TABLE `occupation_parent` (
`occupation` INT UNSIGNED NOT NULL,
`parent` INT UNSIGNED NOT NULL,
PRIMARY KEY (`occupation`,`parent`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/* reference tables */
DROP TABLE IF EXISTS `dump`;
CREATE TABLE `dump` (
`date` DATE NOT NULL,
`step` TINYINT NOT NULL,
PRIMARY KEY (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `year`;
CREATE TABLE `year` (
`id` SMALLINT NOT NULL AUTO_INCREMENT,
`year` SMALLINT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `year_UNIQUE` (`year`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `country`;
CREATE TABLE `country` (
`id` INT UNSIGNED NOT NULL,
`label` VARCHAR(500) DEFAULT NULL,
`date` DATE NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `occupation`;
CREATE TABLE `occupation` (
`id` INT UNSIGNED NOT NULL,
`label` VARCHAR(500) DEFAULT NULL,
`date` DATE NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `project`;
CREATE TABLE `project` (
`id` SMALLINT NOT NULL AUTO_INCREMENT,
`code` VARCHAR(32) CHARACTER SET ascii NOT NULL,
`type` VARCHAR(32) DEFAULT NULL,
`label` VARCHAR(128) DEFAULT NULL,
`url` VARCHAR(128) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `code_UNIQUE` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/* stats */
DROP TABLE IF EXISTS `kpi`;
CREATE TABLE `kpi` (
`dump` DATE NOT NULL,
`birthyear` SMALLINT NOT NULL,
`country` INT UNSIGNED NOT NULL,
`occupation` INT UNSIGNED NOT NULL,
`project` SMALLINT NOT NULL,
`humans` INT NOT NULL,
`females` INT UNSIGNED NOT NULL,
`males` INT UNSIGNED NOT NULL,
`others` INT UNSIGNED NOT NULL,
`humans_with_sitelink` INT NOT NULL,
`females_with_sitelink` INT UNSIGNED NOT NULL,
`males_with_sitelink` INT UNSIGNED NOT NULL,
`others_with_sitelink` INT UNSIGNED NOT NULL,
`humans_sitelinks` INT NOT NULL,
`females_sitelinks` INT UNSIGNED NOT NULL,
`males_sitelinks` INT UNSIGNED NOT NULL,
`others_sitelinks` INT UNSIGNED NOT NULL,
PRIMARY KEY (`birthyear`,`country`,`occupation`,`project`,`dump`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY LIST COLUMNS(`dump`) (PARTITION `dump20010101` VALUES IN ('2001-01-01'));
</div>
<div id="footer"><p><img src="<?php echo SITE_STATIC_DIR; ?>img/elh.png" alt="" /> <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a> / 2017-<?php echo date('Y'); ?> / <a href="<?php echo SITE_DIR; ?>"><?php echo SITE_TITLE; ?></a> &mdash; <a href="https://www.lehir.net/">Envel Le Hir</a> &amp; <a href="https://www.wikimedia.fr/">Wikimédia France</a></p></div>
</body>
</html>
\ No newline at end of file
<?php
require_once __DIR__.'/load.inc.php';
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?php $title = htmlentities(strip_tags(PAGE_TITLE)); echo $title; if ($title !== SITE_TITLE): echo ' &mdash; '.SITE_TITLE; endif; ?></title>
<link rel="stylesheet" type="text/css" href="<?php echo SITE_STATIC_DIR; ?>css/style.css" />
<script type="text/javascript" src="<?php echo SITE_STATIC_DIR; ?>js/moment.min.js"></script>
<script type="text/javascript" src="<?php echo SITE_STATIC_DIR; ?>js/chart.min.js"></script>
<script type="text/javascript" src="<?php echo SITE_STATIC_DIR; ?>js/script.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="<?php echo $title; if ($title !== SITE_TITLE): echo ' &mdash; '.SITE_TITLE; endif; ?>" />
<meta name="twitter:description" content="<?php echo SITE_TITLE; ?>: a tool to estimate the gender gap in Wikimedia projects." />
<meta name="twitter:image" content="<?php echo SITE_STATIC_DIR; ?>img/twitter.png" />
<meta property="og:title" content="<?php echo $title; if ($title !== SITE_TITLE): echo ' &mdash; '.SITE_TITLE; endif; ?>" />
<meta property="og:description" content="<?php echo SITE_TITLE; ?>: a tool to estimate the gender gap in Wikimedia projects." />
<meta property="og:image" content="<?php echo SITE_STATIC_DIR; ?>img/twitter.png" />
<?php if (!empty(page::get('canonicalUrl'))): echo '<link rel="canonical" href="'.htmlentities(page::get('canonicalUrl')).'" />'."\n"; endif; ?>
<?php if (page::get('index') === false): echo '<meta name="robots" content="noindex, follow" />'."\n"; endif; ?>
</head>
<body>
<div id="header">
<h1><?php echo '<a href="'.SITE_DIR.'">'.htmlentities(SITE_TITLE).'</a>'; if ($title != SITE_TITLE): echo ' &mdash; '.PAGE_TITLE; endif; ?></h1>
</div>
<div id="menu">
<ul>
<li><a href="<?php echo SITE_DIR; ?>gender-gap.php">Gender Gap</a></li>
<li><a href="<?php echo SITE_DIR; ?>evolution.php">Evolution</a></li>
<li><a href="<?php echo SITE_DIR; ?>doc.php">Documentation</a></li>
</ul>
</div>
<div id="content">
<?php
class db {
private static $connected = false;
private static $mysqli;
private static $fail_on_warning = false;
private function __construct() {
}
// opens connection if not already established
private static function open() {
if (self::$connected != true) {
self::$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
if (self::$mysqli->connect_errno != 0) {
throw new Exception('Error establishing database connection'."\n".'MySQL Error #'.self::$mysqli->connect_errno."\n".self::$mysqli->connect_error);
}
self::$mysqli->set_charset('utf8');
self::$mysqli->autocommit(false);
self::$connected = true;
}
}
// secures string
public static function sec($val) {
self::open();
return self::$mysqli->real_escape_string($val);
}
/**
* Executes query and returns result.
* @param string $query
* @return mysqli_result
* @throws Exception
*/
public static function query($query) {
self::open();
$result = @self::$mysqli->query($query);
if ($result === false) {
throw new Exception('MySQL Error #'.self::$mysqli->errno."\n".$query."\n".self::$mysqli->error);
}
if (self::$fail_on_warning) {
// TODO
}
return $result;
}
// commits transaction
public static function commit() {
self::$mysqli->commit();
}
// rollbacks transaction
public static function rollback() {
self::$mysqli->rollback();
}
// last inserted id
public static function insert_id() {
return self::$mysqli->insert_id;
}
// number of affected rows
public static function affected_rows() {
return self::$mysqli->affected_rows;
}
// number of affected rows
public static function set_fail_on_warning() {
self::$fail_on_warning = true;
}
// closes connection
public static function close() {
if (self::$connected != false) {
self::$connected = false;
self::$mysqli->close();
}
}
}
?>
\ No newline at end of file
<?php
class display {
public static function tableHeader($title, $sortable = false, $anchor = null) {
if ($sortable) {
echo '<tr>
<th class="title'.((GAPS_SORT == 'label') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'label', 'anchor' => $anchor)).'">'.$title.' ▴</a></th>
<th class="data'.((GAPS_SORT == 'total') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'total', 'anchor' => $anchor)).'">Total ▾</a></th>
<th class="data'.((GAPS_SORT == 'total_with_gender') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'total_with_gender', 'anchor' => $anchor)).'">Total with gender ▾</a></th>
<th class="data'.((GAPS_SORT == 'females') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'females', 'anchor' => $anchor)).'">Females ▾</a></th>
<th class="data'.((GAPS_SORT == 'percent_females') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'percent_females', 'anchor' => $anchor)).'">% Fem. ▾</a></th>
<th class="gap">Gap</th><th class="data'.((GAPS_SORT == 'males') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'males', 'anchor' => $anchor)).'">Males ▾</a></th>
<th class="data'.((GAPS_SORT == 'percent_males') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'percent_males', 'anchor' => $anchor)).'">% Mal. ▾</a></th>
<th class="data'.((GAPS_SORT == 'others') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'others', 'anchor' => $anchor)).'">Others ▾</a></th>
<th class="data'.((GAPS_SORT == 'percent_others') ? ' selected' : '').'"><a href="'.gaps::buildLink(array('sort' => 'percent_others', 'anchor' => $anchor)).'">% Oth. ▾</a></th>
</tr>'."\n";
} else {
echo '<tr><th class="title">'.$title.'</th><th class="data">Total</th><th class="data">Total with gender</th><th class="data">Females</th><th class="data">% Females</th><th class="gap">Gap</th><th class="data">Males</th><th class="data">% Males</th><th class="data">Others</th><th class="data">% Others</th></tr>'."\n";
}
}
public static function tableRow($title, $humans, $females, $males, $others) {
$humans_with_gender = $females + $males + $others;
echo '<tr><th class="row"><span title="'.trim(strip_tags($title)).'">'.$title.'</span></th><td class="data">'.number_format($humans).'</td><td class="data">'.number_format($humans_with_gender).'</td><td class="data">'.number_format($females).'</td><td class="data">'.self::percent($humans_with_gender, $females).'</td><td class="gap">';
if ($humans_with_gender >= GAPS_PEOPLE_THRESHOLD) {
echo '<div style="width: '.number_format(100 / $humans_with_gender * $females, 3).'%;" class="female"></div><div style="width: '.number_format(100 / $humans_with_gender * $males, 3).'%;" class="male"></div>';
} else {
echo '<em>total under the threshold of '.GAPS_PEOPLE_THRESHOLD.' people</em>';
}
echo '</td><td class="data">'.number_format($males).'</td><td class="data">'.self::percent($humans_with_gender, $males).'</td><td class="data">'.number_format($others).'</td><td class="data">'.self::percent($humans_with_gender, $others).'</td></tr>'."\n";
}
public static function tableEmptyTableRow($message) {
echo '<tr><th class="row"></th><td class="data"></td><td class="data"></td><td class="data"></td><td class="data"></td><td class="gap"><em>'.htmlentities($message).'</em></td><td class="data"></td><td class="data"></td><td class="data"></td><td class="data"></td></tr>'."\n";
}
public static function buildLabel($id, $label) {
if (!empty($label)) {
return htmlentities($label);
} else {
return 'Q'.$id;
}
}
public static function percent($total, $share) {
if ($total == 0) {
return 'N/A';
}
return number_format(100 / $total * $share, 3).' %';
}
}
?>
\ No newline at end of file
<?php
class gaps {
private static $dumps = null;
private static $kpis = null;
private static $sorts = null;
private static $isDefaultFilters = true;
private static $usedFilters = array();
public static function dumps() {
if (self::$dumps == null) {
self::$dumps = array();
$res = db::query('SELECT `date` FROM `dump` WHERE `step` = 1 ORDER BY `date` DESC');
while ($dump = $res->fetch_object()) {
self::$dumps[] = $dump->date;
}
}
return self::$dumps;
}
public static function kpis() {
if (self::$kpis == null) {
self::$kpis = array(
'humans' => '',
'with_sitelink' => '_with_sitelink',
'sitelinks' => '_sitelinks',
);
}
return self::$kpis;
}
public static function sorts() {
if (self::$sorts == null) {
self::$sorts = array(
'label' => '`label` ASC',
'total' => 'SUM(`kpi`.`humans'.self::kpis()[GAPS_KPI].'`) DESC, `label` ASC',
'total_with_gender' => 'SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, `label` ASC',
'females' => 'SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'`) DESC, SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, `label` ASC',
'percent_females' => '100 * SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'`) / SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'`) DESC, SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, `label` ASC',
'males' => 'SUM(`kpi`.`males'.self::kpis()[GAPS_KPI].'`) DESC, SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, `label` ASC',
'percent_males' => '100 * SUM(`kpi`.`males'.self::kpis()[GAPS_KPI].'`) / SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, SUM(`kpi`.`males'.self::kpis()[GAPS_KPI].'`) DESC, SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, `label` ASC',
'others' => 'SUM(`kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, SUM(`kpi`.`females'.self::kpis()[GAPS_KPI].'` + `kpi`.`males'.self::kpis()[GAPS_KPI].'` + `kpi`.`others'.self::kpis()[GAPS_KPI].'`) DESC, `label` ASC',
'percent_others' => '100 * SUM(`kpi`.`others'.self::kpis()[GAPS_KPI].'`) / SUM(`kpi`.`females'.self::