Skip to content
This repository was archived by the owner on Nov 24, 2022. It is now read-only.

Commit 2cc388f

Browse files
authored
api endpoint to get CSV of weekly delta report #69 from ckingbailey/feat/export-report
2 parents 72e9d86 + 3547282 commit 2cc388f

File tree

7 files changed

+216
-13
lines changed

7 files changed

+216
-13
lines changed

api/def/csv.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ function str_putcsv(array $input, $delimiter = ',', $enclosure = '"') {
8080
$cols = $link->getValue('information_schema.columns', 'column_name', null); // returns 50+ columns
8181
$cols = array_map('strtolower', $cols);
8282

83-
$postKeys = array_keys($post[1] + $post[count($post) - 1] + $post[floor((count($post) / 2))]);
83+
$postKeys = array_keys($post[1] + $post[count($post) - 1] + $post[floor((count($post) / 2))]); // grab keys from first, middle, and last element of post data
8484

85-
if (($idIndex = array_search('ID', $postKeys)) !== false) unset($postKeys[$idIndex]);
85+
if (($idIndex = array_search('ID', $postKeys)) !== false) unset($postKeys[$idIndex]); // don't try to match ID col name
8686

8787
foreach ($postKeys as $key) {
8888
if (array_search(strtolower($key), $cols) === false) {
@@ -97,7 +97,7 @@ function str_putcsv(array $input, $delimiter = ',', $enclosure = '"') {
9797
header('Content-Type: text/csv', true);
9898
// header("Access-Control-Allow-Origin: $host");
9999

100-
// if (empty($_SERVER['HTTP_ORIGIN']) || strcasecmp($_SERVER['HTTP_ORIGIN'], $host)) {
100+
// if (empty($_SERVER['HTTP_ORIGIN']) || strcasecmp($_SERVER['HTTP_ORIGIN'], $host)) { // todo: make list specifying which Origins are allowed
101101
// header('No cors allowed, buddy', true, 403);
102102
// exit;
103103
// }

api/report.php

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
use SVBX\Report;
3+
use SVBX\Export;
4+
5+
$basedir = __DIR__ . '/..';
6+
require "$basedir/vendor/autoload.php";
7+
require "$basedir/session.php";
8+
9+
// query params:
10+
// milestone
11+
// TODO: date
12+
// TODO: system
13+
// type
14+
// format => if Export class lacks $format method, return bad http response
15+
if (!method_exists('SVBX\Export', $_GET['format'])) {
16+
header('Malformed request', true, 400);
17+
error_log(__FILE__ . '(' . __LINE__ . ')' . ' Invalid format requested from Report API');
18+
exit;
19+
}
20+
21+
if (!method_exists('SVBX\Report', $_GET['type'])) {
22+
header('Malformed request', true, 400);
23+
error_log(__FILE__ . '(' . __LINE__ . ')' . ' Invalid report type requested from Report API');
24+
exit;
25+
}
26+
27+
try {
28+
$format = $_GET['format'];
29+
$reportType = $_GET['type'];
30+
31+
$report = Report::delta($_GET['milestone'])->get();
32+
$headings = array_keys($report[0]);
33+
array_unshift($report, $headings);
34+
35+
echo Export::$format($report);
36+
} catch (\UnexpectedValueException $e) {
37+
error_log($e);
38+
header('Bad query param', true, 400);
39+
} catch (\Exception $e) {
40+
error_log($e);
41+
header('Internal server error', true, 500);
42+
} catch (\Error $e) {
43+
error_log($e);
44+
header('Internal server error', true, 500);
45+
} finally {
46+
exit;
47+
}

dashboard.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<?php
2+
use SVBX\Report;
3+
24
require 'vendor/autoload.php';
35
// include('SQLFunctions.php');
46
require 'WeeklyDelta.php';
@@ -62,7 +64,7 @@
6264
if (getenv('PHP_ENV') === 'dev') $twig->addExtension(new Twig_Extension_Debug());
6365

6466
// instantiate report object
65-
$weeklySIT3delta = new WeeklyDelta('SIT3');
66-
$context['data']['weeklyReport'] = $weeklySIT3delta->getData();
67+
$sit3delta = Report::delta('SIT3');
68+
$context['data']['weeklyReport'] = $sit3delta->get();
6769

6870
$twig->display('dashboard.html.twig', $context);

src/Export.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
namespace SVBX;
3+
4+
use MysqliDb;
5+
6+
class Export {
7+
public static function csv($data) {
8+
return self::str_putcsv($data); // NOTE: first index of $data must be column headings if headings are desired
9+
}
10+
11+
private static function str_putcsv(array $input, $delimiter = ',', $enclosure = '"') {
12+
$pointer = fopen('php://temp', 'r+b'); // open memory stream with read/write permission and binary mode on
13+
foreach ($input as $line) {
14+
fputcsv($pointer, $line, $delimiter, $enclosure); // puts a single line
15+
}
16+
rewind($pointer);
17+
$data = rtrim(stream_get_contents($pointer), "\n"); // trim whitespace
18+
fclose($pointer);
19+
return $data;
20+
}
21+
}

src/Report.php

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
namespace SVBX;
3+
4+
use MysqliDB;
5+
use Carbon\Carbon;
6+
use Carbon\CarbonImmutable;
7+
8+
class Report {
9+
private $data = [];
10+
private $lastQuery = '';
11+
12+
private $table = null;
13+
private $fields = [];
14+
private $join = [];
15+
private $where = [];
16+
private $groupBy = null;
17+
18+
public static function delta($milestone, $date = null, $system = null) {
19+
$openLastWeek = 'COUNT(CASE'
20+
. ' WHEN CDL.dateCreated > CAST("%1$s" AS DATE) THEN NULL' // didn't yet exist last week
21+
. ' WHEN dateClosed <= CAST("%1$s" AS DATE) THEN NULL' // already closed last week
22+
. ' ELSE defID END) AS openLastWeek';
23+
$toDate = new CarbonImmutable($date);
24+
$fromDate = $toDate->subWeek()->toDateString();
25+
$fields = [
26+
'systemName AS system',
27+
sprintf($openLastWeek, $fromDate),
28+
"COUNT(IF(status = 1, defID, NULL)) as openThisWeek"
29+
];
30+
31+
$join = ['system', 'systemAffected = system.systemID', 'LEFT'];
32+
33+
$link = new MysqliDb(DB_CREDENTIALS);
34+
$whereField = intval($milestone) ? 'reqByID' : 'requiredBy';
35+
$reqByID = $link
36+
->where($whereField, $milestone)
37+
->getValue('requiredBy', 'reqByID');
38+
$where = [
39+
[ 'requiredBy', $reqByID, '<='],
40+
[ 'status', '3', '<>']
41+
];
42+
43+
if (!empty($system)) {
44+
list($groupBy, $where[]) = [ null, [ 'systemAffected', $system ] ];
45+
} else $groupBy = 'systemAffected';
46+
47+
if (empty($reqByID)) throw new \UnexpectedValueException("Could not find milestone for query term $milestone");
48+
49+
return new Report('CDL', $fields, $join, $where, $groupBy);
50+
51+
}
52+
53+
private function __construct($table = null, $fields = [], $join = null, $where = null, $groupBy = null) {
54+
$this->table = $table;
55+
$this->fields = $fields;
56+
57+
if (!empty($join)) {
58+
if (is_array($join[0])) $this->join = $join;
59+
else array_push($this->join, $join);
60+
}
61+
if (!empty($where)) {
62+
if (is_array($where[0])) $this->where = $where;
63+
else $this->where[] = $where;
64+
}
65+
$this->groupBy = $groupBy;
66+
67+
$this->fetch();
68+
}
69+
70+
private function fetch() {
71+
$link = new MysqliDb(DB_CREDENTIALS);
72+
73+
foreach ($this->join as $join) {
74+
if (empty($join[2])) {
75+
$link->join($join[0], $join[1]);
76+
} else $link->join($join[0], $join[1], $join[2]);
77+
}
78+
79+
foreach($this->where as $where) {
80+
if (empty($where[2])) {
81+
$link->where($where[0], $where[1]);
82+
} else $link->where($where[0], $where[1], $where[2]);
83+
}
84+
85+
if (!empty($this->groupBy)) $link->groupBy($this->groupBy);
86+
87+
$result = $link // TODO: what happens if I pass null to any Joshcam metho?
88+
->get($this->table, null, $this->fields);
89+
$this->lastQuery = $link->getLastQuery();
90+
$link->disconnect();
91+
92+
$this->data = $result;
93+
}
94+
95+
public function getQuery() {
96+
return $this->lastQuery;
97+
}
98+
99+
public function get() {
100+
return $this->data;
101+
}
102+
103+
public function __toString() {
104+
print_r($this->get(), true);
105+
}
106+
107+
}

templates/dashboard.html.twig

+25-2
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,33 @@
115115
{% endblock %}
116116

117117
{% block scripts %}
118+
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/download.js"></script>
118119
<script src='https://d3js.org/d3.v5.js'></script>
119120
<script src='js/pie_chart.js'></script>
120121
<script>
121-
window.drawOpenCloseChart(window.d3, {{ data.totalOpen }}, {{ data.totalClosed }})
122-
window.drawSeverityChart(window.d3, {{ data.totalBlocker }}, {{ data.totalCrit }}, {{ data.totalMajor }}, {{ data.totalMinor }})
122+
drawOpenCloseChart(window.d3, {{ data.totalOpen }}, {{ data.totalClosed }})
123+
drawSeverityChart(window.d3, {{ data.totalBlocker }}, {{ data.totalCrit }}, {{ data.totalMajor }}, {{ data.totalMinor }})
124+
125+
function downloadReport() {
126+
fetch('/api/report.php?milestone=sit3&format=csv&type=delta', {
127+
credentials: 'same-origin'
128+
}).then(res => {
129+
if (!res.ok) throw res
130+
return res.text();
131+
}).then(text => {
132+
const date = new Date()
133+
const timestamp = date.getFullYear()
134+
+ '' + date.getMonth()
135+
+ '' + date.getDate()
136+
+ '' + date.getHours()
137+
+ '' + date.getMinutes()
138+
+ '' + date.getSeconds()
139+
download(text, `weekly_delta_${timestamp}.csv`, 'text/csv')
140+
}).catch(err => {
141+
if (err.status) // err was thrown by previous handler, and is a bad http response wrapped in a promise
142+
console.error(`${err.url} ${err.status} ${err.statusText}`)
143+
else console.error(err) // only "network error" results in Promise reject
144+
})
145+
}
123146
</script>
124147
{% endblock %}

templates/weekly-report.html.twig

+9-6
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88
#}
99

1010
{% set thisWeekLabel = "Open this week: " %}
11-
{% set lastWeekLabel = "Open Last week: " %}
11+
{% set lastWeekLabel = "Open last week: " %}
1212
{% set deltaLabel = "Delta this week: " %}
1313
<div id="weeklyReport" class="row item-margin-bottom">
1414
<div class="col-sm-10 offset-sm-1">
1515
<div class="thin-grey-border border-radius">
16-
<header class="pad grey-bg item-margin-bottom">Weekly delta of open Deficiencies that are required for closure prior to Phase 3 start</header>
16+
<header class="d-flex justify-content-between pad grey-bg item-margin-bottom">
17+
<span>Weekly delta of open Deficiencies that are required for closure prior to Phase 3 start</span>
18+
<button id='downloadReport' type='button' onclick='return downloadReport()' class="btn btn-sm btn-success">Download this data</button>
19+
</header>
1720
<div class="row thick-black-line ml-0 mr-0 mb-1 weekly-report__list-heading">
1821
<div class="col-sm-3">System</div>
19-
<div class="col-sm-3 text-right">{{ thisWeekLabel }}</div>
2022
<div class="col-sm-3 text-right">{{ lastWeekLabel }}</div>
23+
<div class="col-sm-3 text-right">{{ thisWeekLabel }}</div>
2124
<div class="col-sm-3 text-right">{{ deltaLabel }}</div>
2225
</div>
2326
{% for row in data.weeklyReport %}
@@ -26,13 +29,13 @@
2629
<div class="weekly-report__system-name">{{ row.system }}</div>
2730
</div>
2831
<div class="col-sm-3 text-right">
29-
<div><span class="weekly-report__label">{{ thisWeekLabel }}</span>{{ row.openThisWeek }}</div>
32+
<div><span class="weekly-report__label">{{ lastWeekLabel }}</span>{{ row.openLastWeek }}</div>
3033
</div>
3134
<div class="col-sm-3 text-right">
32-
<div><span class="weekly-report__label">{{ lastWeekLabel }}</span>{{ row.openLastWeek }}</div>
35+
<div><span class="weekly-report__label">{{ thisWeekLabel }}</span>{{ row.openThisWeek }}</div>
3336
</div>
3437
<div class="col-sm-3 text-right">
35-
<div><span class="weekly-report__label">{{ deltaLabel }}</span>{{ row.openThisWeek - row.openLastWeek }}</div>
38+
<div><span class="weekly-report__label">{{ deltaLabel }}</span>{{ row.openLastWeek - row.openThisWeek }}</div>
3639
</div>
3740
</div>
3841
{% endfor %}

0 commit comments

Comments
 (0)