Fix for exporting a large number of lists.
[openemr.git] / library / report_database.inc
blob049c8535900435a55568d39b99126c3b55233f42
1 <?php
3 /**
4  * Report tracking, storing and viewing functions using the report_results sql table.
5  *
6  * Supports generic tracking, storing and viewing of reports by utilizing a vertical
7  * table entitled report_results. This allows flexible placement of tokens for report
8  * setting etc. Also supports itemization of results (per patient tracking).
9  * <pre>Tokens that are reserved include:
10  *   'bookmark'          - Allows bookmarking of a new report id (used to allow tracking
11  *                         progress via ajax calls). If exist, value is always set to '1'.
12  *   'progress'          - Either set to 'pending' or 'complete'.
13  *   'type'              - Set to type of report
14  *   'total_items'       - Set to total number of items that will be processed (ie. such as patients)
15  *   'progress_items'    - Set to number of items (ie. such as patients)
16  *   'data'              - Contains the data of the report
17  *   'date_report'       - Set to date of the report (date and time)
18  *   'date_report_complete'       - Set to date of the report completion (date and time)
19  * </pre>
20  *
21  * Copyright (C) 2012 Brady Miller <brady.g.miller@gmail.com>
22  *
23  * LICENSE: This program is free software; you can redistribute it and/or
24  * modify it under the terms of the GNU General Public License
25  * as published by the Free Software Foundation; either version 2
26  * of the License, or (at your option) any later version.
27  * This program is distributed in the hope that it will be useful,
28  * but WITHOUT ANY WARRANTY; without even the implied warranty of
29  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30  * GNU General Public License for more details.
31  * You should have received a copy of the GNU General Public License
32  * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>;.
33  *
34  * @package OpenEMR
35  * @author  Brady Miller <brady.g.miller@gmail.com>
36  * @link    http://www.open-emr.org
37  */
39 use OpenEMR\Common\Csrf\CsrfUtils;
41 /**
42  * Return listing of report results.
43  *
44  * @param   timestamp  $start  Start of date range
45  * @param   timestamp  $end    End of date range
46  * @return  sql-query          Listing of report results
47  */
48 function listingReportDatabase($start_date = '', $end_date = '')
51   // set $end_date to today's date if empty
52     $end_date = ($end_date) ? $end_date : date('Y-m-d H:i:s');
54   // Collect pertinent information as a pivot table (ie. converting vertical to horizontal row)
55     if (empty($start_date)) {
56         $res = sqlStatement("SELECT *, TIMESTAMPDIFF(MINUTE,pt.date_report,pt.date_report_complete) as `report_time_processing`
57                          FROM (
58                            SELECT `report_id`,
59                            MAX(if( `field_id` = 'date_report', `field_value`, 0 )) as `date_report`,
60                            MAX(if( `field_id` = 'date_report_complete', `field_value`, 0 )) as `date_report_complete`,
61                            MAX(if( `field_id` = 'progress', `field_value`, 0 )) as `progress`,
62                            MAX(if( `field_id` = 'total_items', `field_value`, 0 )) as `total_items`,
63                            MAX(if( `field_id` = 'progress_items', `field_value`, 0 )) as `progress_items`,
64                            MAX(if( `field_id` = 'type', `field_value`, 0 )) as `type`
65                            FROM `report_results`
66                            GROUP BY `report_id`
67                          ) AS pt
68                          WHERE pt.date_report < ?
69                          ORDER BY pt.report_id", array($end_date));
70     } else {
71         $res = sqlStatement("SELECT *, TIMESTAMPDIFF(MINUTE,pt.date_report,pt.date_report_complete) as `report_time_processing`
72                          FROM (
73                            SELECT `report_id`,
74                            MAX(if( `field_id` = 'date_report', `field_value`, 0 )) as `date_report`,
75                            MAX(if( `field_id` = 'date_report_complete', `field_value`, 0 )) as `date_report_complete`,
76                            MAX(if( `field_id` = 'progress', `field_value`, 0 )) as `progress`,
77                            MAX(if( `field_id` = 'total_items', `field_value`, 0 )) as `total_items`,
78                            MAX(if( `field_id` = 'progress_items', `field_value`, 0 )) as `progress_items`,
79                            MAX(if( `field_id` = 'type', `field_value`, 0 )) as `type`
80                            FROM `report_results`
81                            GROUP BY `report_id`
82                          ) AS pt
83                          WHERE pt.date_report > ? AND pt.date_report < ?
84                          ORDER BY pt.report_id", array($start_date,$end_date));
85     }
87     return $res;
90 /**
91  * Simply reserves a report id for use in the report results tracking/storing/viewing item in database..
92  *
93  * @return  integer           Report id that was assigned in database
94  */
95 function bookmarkReportDatabase()
98   // Retrieve a new report id
99     $query = sqlQuery("SELECT max(`report_id`) as max_report_id FROM `report_results`");
100     if (empty($query)) {
101         $new_report_id = 1;
102     } else {
103         $new_report_id = $query['max_report_id'] + 1;
104     }
106   // Set the bookmark token
107     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($new_report_id,"bookmark",1));
109     return $new_report_id;
113  * Initiate a report results tracking/storing/viewing item in database.
115  * @param   string   $type       Report type identifier
116  * @param   array    $fields     Array containing pertinent report details (Do NOT use 'bookmark', 'progress','type','progress_patients', 'data', 'date_report' or 'no_json_support' as keys in array; they will be ignored)
117  * @param   integer  $report_id  Report id (if have already bookmarked a report id)
118  * @return  integer              Report id that is assigned to the report
119  */
120 function beginReportDatabase($type, $fields, $report_id = null)
123   // Retrieve a new report id, if needed.
124     if (empty($report_id)) {
125         $query = sqlQuery("SELECT max(`report_id`) as max_report_id FROM `report_results`");
126         if (empty($query)) {
127             $new_report_id = 1;
128         } else {
129             $new_report_id = $query['max_report_id'] + 1;
130         }
131     } else {
132         $new_report_id = $report_id;
133     }
135   // Set the required tokens
136     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($new_report_id,"progress","pending"));
137     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($new_report_id,"type",$type));
138     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($new_report_id,"progress_items","0"));
139     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($new_report_id,"data",""));
140     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($new_report_id,"date_report",date("Y-m-d H:i:s")));
142   // Set the fields tokens
143     if (!empty($fields)) {
144         foreach ($fields as $key => $value) {
145             // skip the special tokens
146             if (
147                 ($key == "type") ||
148                 ($key == "data") ||
149                 ($key == "progress") ||
150                 ($key == "progress_items") ||
151                 ($key == "total_items") ||
152                 ($key == "date_report") ||
153                 ($key == "date_report_complete") ||
154                 ($key == "bookmark")
155             ) {
156                 continue;
157             }
159             // place the token
160             sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($new_report_id,$key,$value));
161         }
162     }
164   // Return the report id
165     return $new_report_id;
169  * Insert total items to process in database.
170  * For performance reasons, it is assumed that the total_items does not already exists in current database entry.
172  * @param   integer  $report_id    Report id
173  * @param   integer  $total_items  Total number of items that will be processed
174  */
175 function setTotalItemsReportDatabase($report_id, $total_items)
177   // Insert the total items that are to be processed
178     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($report_id,"total_items",$total_items));
182  * Update report results in database(basically update number of items (patients) that has been processed in pending reports).
183  * For performance reasons, it is assumed that the progress_items token already exists in current database entry.
185  * @param   integer  $report_id           Report id
186  * @param   integer  $items_processed  Number of items that have been processed
187  */
188 function updateReportDatabase($report_id, $items_processed)
190   // Update the items that have been processed
191     sqlStatement("UPDATE `report_results` SET `field_value`=? WHERE `report_id`=? AND `field_id`='progress_items'", array($items_processed,$report_id));
195  * Store (finished) report results (in json format) in database.
196  * For performance reasons, it is assumed that the data and progress tokens already exists in current database entry.
197  * For performance reasons, it is assumed that the date_report_complete does not already exists in current database entry.
199  * @param   integer  $report_id  Report id
200  * @param   string   $data       Report results/data
201  */
202 function finishReportDatabase($report_id, $data)
205   // Record the data
206     sqlStatement("UPDATE `report_results` SET `field_value`=? WHERE `report_id`=? AND `field_id`='data'", array($data,$report_id));
208   // Record the finish date/time
209     sqlStatement("INSERT INTO `report_results` (`report_id`,`field_id`,`field_value`) VALUES (?,?,?)", array ($report_id,"date_report_complete",date("Y-m-d H:i:s")));
211   // Set progress to complete
212     sqlStatement("UPDATE `report_results` SET `field_value`='complete' WHERE `report_id`=? AND `field_id`='progress'", array($report_id));
216  * Collect report results from database.
218  * @param   integer  $report_id  Report id
219  * @return  array                Array of id/values for a report
220  */
221 function collectReportDatabase($report_id)
224   // Collect the rows of data
225     $res = sqlStatement("SELECT * FROM `report_results` WHERE `report_id`=?", array($report_id));
227   // Convert data into an array
228     $final_array = array();
229     while ($row = sqlFetchArray($res)) {
230         $final_array = array_merge($final_array, array($row['field_id'] => $row['field_value']));
231     }
233     return $final_array;
237  * Get status of report from database.
239  * @param   integer  $report_id  Report id
240  * @return  string               Status report (PENDING, COMPLETE, or return a string with progress)
241  */
242 function getStatusReportDatabase($report_id)
245   // Collect the pertinent rows of data
246     $res = sqlStatement("SELECT `field_id`, `field_value` FROM `report_results` WHERE `report_id`=? AND (`field_id`='progress' OR `field_id`='total_items' OR `field_id`='progress_items')", array($report_id));
248   // If empty, then just return Pending, since stil haven't likely created the entries yet
249     if (sqlNumRows($res) < 1) {
250         return "PENDING";
251     }
253   // Place into an array for quick processing
254     $final_array = array();
255     while ($row = sqlFetchArray($res)) {
256         $final_array = array_merge($final_array, array($row['field_id'] => $row['field_value']));
257     }
259     if ($final_array['progress'] == "complete") {
260         // return COMPLETE
261         return "COMPLETE";
262     } else {
263         $final_array['progress_items'] = ($final_array['progress_items']) ? $final_array['progress_items'] : 0;
264         return $final_array['progress_items'] . " / " . $final_array['total_items'] . " " . xl("Patients");
265     }
269  * Insert itemization item into database.
271  * @param   integer  $report_id         Report id
272  * @param   integer  $itemized_test_id  Itemized test id
273  * @param   integer  $pass              0 is fail, 1 is pass, 2 is exclude
274  * @param   integer  $patient_id        Patient pid
275  * @param   string   $numerator_label  Numerator label (if applicable)
276  * @param   string   $rule_id  The name of the rule that was used to generate this item
277  * @param   string   $itemized_details  JSON of itemized details about this rule (if applicable)
278  */
279 function insertItemReportTracker($report_id, $itemized_test_id, $pass, $patient_id, $numerator_label = '', $rule_id = '', $itemizedDetails = '')
281     $sqlParameters = array($report_id,$itemized_test_id,$numerator_label,$pass,$patient_id, $rule_id, $itemizedDetails);
282     sqlStatementCdrEngine("INSERT INTO `report_itemized` (`report_id`,`itemized_test_id`,`numerator_label`,`pass`,`pid`,`rule_id`,`item_details`) VALUES (?,?,?,?,?,?,?)", $sqlParameters);
286  * Collect a rules display title for itemized report.
288  * @param   integer  $report_id         Report id
289  * @param   integer  $itemized_test_id  Itemized test id
290  * @param   integer  $numerator_label   Numerator label (if applicable)
291  * @return  string/boolean              Rule title for itemization display (false if nothing found)
292  */
293 function collectItemizedRuleDisplayTitle($report_id, $itemized_test_id, $numerator_label = '')
295     $dispTitle = "";
296     $report_view = collectReportDatabase($report_id);
297     $type_report = $report_view['type'];
298     $dataSheet = json_decode($report_view['data'], true);
299     $display_group_provider_info = false;
300     $group_label = '';
301     $group_provider_label = '';
303     // for our group calculation reports we want to display the group and the provider label information
304     if ($report_view['provider'] == 'group_calculation') {
305         $display_group_provider_info = true;
306     }
307     foreach ($dataSheet as $row) {
308         if ($display_group_provider_info) {
309             if (isset($row['is_provider_group'])) {
310                 $group_label = xlt('Group') . ': ' . text($row['name']) . ' ( ' . xlt('TIN') . ' '
311                     . text($row['federaltaxid']) . ' )';
312                 $group_provider_label = $group_label;
313             } else if (isset($row['is_provider'])) {
314                 $provider_label = ' ' . xlt('Provider') . ': ' . text($row['prov_fname']) . ',' . text($row['prov_lname'])
315                     . ' ( ' . xlt('NPI') . ' ' . text($row['npi']) . ' ) ';
316                 if (isset($row['is_provider_in_group'])) {
317                     $group_provider_label = $group_label . ' ' . $provider_label;
318                 } else {
319                     $group_provider_label = $provider_label;
320                 }
321             }
322         }
324         if (isset($row['is_main']) || isset($row['is_sub'])) {
325             if (isset($row['is_main'])) {
326                 // Store this
327                 $dispTitle = $group_provider_label . ' ' . generate_display_field(array('data_type' => '1','list_id' => 'clinical_rules'), $row['id']);
328             }
330             if (($row['itemized_test_id'] == $itemized_test_id) && (($row['numerator_label'] ?? '') == $numerator_label)) {
331                 // We have a hit, build on the $dispTitle created above
332                 if (isset($row['is_main'])) {
333                     $tempCqmAmcString = "";
334                     if (($type_report == "cqm") || ($type_report == "cqm_2011") || ($type_report == "cqm_2014")) {
335                         if (!empty($row['cqm_pqri_code'])) {
336                             $tempCqmAmcString .= " " . xlt('PQRI') . ":" . text($row['cqm_pqri_code']) . " ";
337                         }
339                         if (!empty($row['cqm_nqf_code'])) {
340                             $tempCqmAmcString .= " " . xlt('NQF') . ":" . text($row['cqm_nqf_code']) . " ";
341                         }
342                     }
344                     if ($type_report == "amc") {
345                         if (!empty($row['amc_code'])) {
346                             $tempCqmAmcString .= " " . xlt('AMC-2011') . ":" . text($row['amc_code']) . " ";
347                         }
349                         if (!empty($row['amc_code_2014'])) {
350                             $tempCqmAmcString .= " " . xlt('AMC-2014') . ":" . text($row['amc_code_2014']) . " ";
351                         }
352                     }
354                     if ($type_report == "amc_2011") {
355                         if (!empty($row['amc_code'])) {
356                             $tempCqmAmcString .= " " . xlt('AMC-2011') . ":" . text($row['amc_code']) . " ";
357                         }
358                     }
360                     if ($type_report == "amc_2014_stage1") {
361                         if (!empty($row['amc_code_2014'])) {
362                             $tempCqmAmcString .= " " . xlt('AMC-2014 Stage I') . ":" . text($row['amc_code_2014']) . " ";
363                         }
364                     }
366                     if ($type_report == "amc_2014_stage2") {
367                         if (!empty($row['amc_code_2014'])) {
368                             $tempCqmAmcString .= " " . xlt('AMC-2014 Stage II') . ":" . text($row['amc_code_2014']) . " ";
369                         }
370                     }
372                     if (!empty($tempCqmAmcString)) {
373                         $dispTitle .=  "(" . $tempCqmAmcString . ")";
374                     }
376                     if (!(empty($row['concatenated_label']))) {
377                         $dispTitle .= ", " . xlt($row['concatenated_label']) . " ";
378                     }
379                 } else { // isset($row['is_sub']
380                     $dispTitle .= " - " .  generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $row['action_category']);
381                     $dispTitle .= ": " . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $row['action_item']);
382                 }
384                 return $dispTitle;
385             }
386         }
387     }
389     return false;
393  * Collect patient listing from CDR reports itemization.
395  * @param   integer  $report_id         Report id
396  * @param   integer  $itemized_test_id  Itemized test id
397  * @param   string   $pass              options are 'fail', 'pass', 'exclude', 'init_patients', 'exception' and 'all'
398  * @param   integer  $numerator_label   Numerator label (if applicable)
399  * @param   integer  $sqllimit          Sql query pagination info
400  * @param   integer  $fstart            Sql query pagination info
401  * @return  array/integer               Array list or a count
402  */
403 function collectItemizedPatientsCdrReport($report_id, $itemized_test_id, $pass = 'all', $numerator_label = '', $count = false, $sqllimit = 'all', $fstart = 0)
406     if ($count) {
407         $given = " COUNT(DISTINCT `patient_data`.`pid`) AS total_listings ";
408     } else {
409         $given = " DISTINCT `patient_data`.*, DATE_FORMAT(`patient_data`.`DOB`,'%m/%d/%Y') as DOB_TS ";
410     }
412     $orderby = " `patient_data`.`lname` ASC, `patient_data`.`fname` ASC ";
414   // set $pass_sql
415     switch ($pass) {
416         case "fail":
417             $pass_sql = 0;
418             break;
419         case "pass":
420             $pass_sql = 1;
421             break;
422         case "exclude":
423             $pass_sql = 2;
424             break;
425         case "init_patients":
426             $pass_sql = 3;
427             break;
428         case "exception":
429             $pass_sql = 4;
430             break;
431     }
433     $sqlParameters = array($report_id,$itemized_test_id,$numerator_label);
435     if ($pass == "all") {
436         $sql_where = " WHERE `report_itemized`.`pass` != 3 AND `report_itemized`.`report_id` = ? AND `report_itemized`.`itemized_test_id` = ? AND `report_itemized`.`numerator_label` = ? ";
437     } elseif ($pass == "fail") {
438         $exlPidArr = array();
439         $exludeResult = collectItemizedPatientsCdrReport($report_id, $itemized_test_id, 'exclude', $numerator_label, false, $sqllimit, $fstart);
440         foreach ($exludeResult as $exlResArr) {
441             $exlPidArr[] = $exlResArr['pid'];
442         }
444         $sql_where = " WHERE `report_itemized`.`report_id` = ? AND `report_itemized`.`itemized_test_id` = ? AND `report_itemized`.`numerator_label` = ? AND `report_itemized`.`pass` = ? ";
446         if (count($exlPidArr) > 0) {
447             $exlPids = implode(",", $exlPidArr);
448             $sql_where .= " AND patient_data.pid NOT IN(" . add_escape_custom($exlPids) . ") ";
449         }
451         array_push($sqlParameters, $pass_sql);
452     } else {
453         $sql_where = " WHERE `report_itemized`.`report_id` = ? AND `report_itemized`.`itemized_test_id` = ? AND `report_itemized`.`numerator_label` = ? AND `report_itemized`.`pass` = ? ";
454         array_push($sqlParameters, $pass_sql);
455     }
457     $sql_query = "SELECT " . $given . " FROM `patient_data` JOIN `report_itemized` ON `patient_data`.`pid` = `report_itemized`.`pid` " . $sql_where . " ORDER BY " . $orderby;
459     if ($sqllimit != "all") {
460         $sql_query .= " limit " . escape_limit($fstart) . ", " . escape_limit($sqllimit);
461     }
463     if ($count) {
464         $rez = sqlQueryCdrEngine($sql_query, $sqlParameters);
465         return $rez['total_listings'];
466     } else {
467         $rez = sqlStatementCdrEngine($sql_query, $sqlParameters);
468         // create array of listing for return
469         $returnval = array();
470         for ($iter = 0; $row = sqlFetchArray($rez); $iter++) {
471             $returnval[$iter] = $row;
472         }
474         return $returnval;
475     }
479  * Formats the report data into a format that can be consumed by the twig rendering output.
480  * @param $report_id The report that we are formatting
481  * @param $data string json encoded report data retrieved from the database
482  * @param $is_amc boolean True if this is an AMC report
483  * @param $is_cqm boolean True if this is an CQM report
484  * @param $type_report The specific report type (could be a subset of AMC or CQM)
485  * @param $amc_report_types If an AMC report, the specific AMC report type
486  * @return array A formatted array of record rows to be used for displaying a CQM/AMC/Standard report.
487  */
488 function formatReportData($report_id, &$data, $is_amc, $is_cqm, $type_report, $amc_report_types = array())
490     $dataSheet = json_decode($data, true) ?? [];
491     $formatted = [];
492     $main_pass_filter = 0;
493     foreach ($dataSheet as $row) {
494         $row['type'] = $type_report;
495         $row['total_patients'] = $row['total_patients'] ?? 0;
496         $failed_items = null;
497         $displayFieldSubHeader = "";
499         if ($is_cqm) {
500             $row['type'] = 'cqm';
501             $row['total_patients'] = $row['initial_population'] ?? 0;
502             if (isset($row['cqm_pqri_code'])) {
503                 $displayFieldSubHeader .= " " . xl('PQRI') . ":" . $row['cqm_pqri_code'] . " ";
504             }
505             if (isset($row['cqm_nqf_code'])) {
506                 $displayFieldSubHeader .= " " . xl('NQF') . ":" . $row['cqm_nqf_code'] . " ";
507             }
508         } else if ($is_amc) {
509             $row['type'] = 'amc';
510             if (!empty($amc_report_types[$type_report]['code_col'])) {
511                 $code_col = $amc_report_types[$type_report]['code_col'];
512                 $displayFieldSubHeader .= " " . text($amc_report_types[$type_report]['abbr']) . ":"
513                     . text($row[$code_col]) . " ";
514             }
515         }
517         if (isset($row['is_main'])) {
518             // note that the is_main record must always come before is_sub in the report or the data will not work.
519             $main_pass_filter = $row['pass_filter'] ?? 0;
520             $row['display_field'] = generate_display_field(array('data_type' => '1','list_id' => 'clinical_rules'), $row['id']);
521             if ($type_report == "standard") {
522                 // Excluded is not part of denominator in standard rules so do not use in calculation
523                 $failed_items = $row['pass_filter'] - $row['pass_target'];
524             } else {
525                 $failed_items = $row['pass_filter'] - $row['pass_target'] - $row['excluded'];
526             }
527             $row['display_field_sub'] = ($displayFieldSubHeader != "") ? "($displayFieldSubHeader)" : null;
528         } else if (isset($row['is_sub'])) {
529             $row['display_field'] = generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $row['action_category'])
530                 . ': ' . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $row['action_item']);
531             // Excluded is not part of denominator in standard rules so do not use in calculation
532             $failed_items = $main_pass_filter - $row['pass_target'];
533         } else if (isset($row['is_plan'])) {
534             $row['display_field'] = generate_display_field(array('data_type' => '1','list_id' => 'clinical_plans'), $row['id']);
535         }
537         if (isset($row['itemized_test_id'])) {
538             $csrf_token = CsrfUtils::collectCsrfToken();
540             $base_link = sprintf(
541                 "../main/finder/patient_select.php?from_page=cdr_report&report_id=%d"
542                 . "&itemized_test_id=%d&numerator_label=%s&csrf_token_form=%s",
543                 urlencode($report_id),
544                 urlencode($row['itemized_test_id']),
545                 urlencode($row['numerator_label'] ?? ''),
546                 urlencode($csrf_token)
547             );
549             // we need the provider & group id here...
551             // denominator
552             if (isset($row['pass_filter']) && $row['pass_filter'] > 0) {
553                 $row['display_pass_link'] = $base_link . "&pass_id=all";
554             }
556             // excluded denominator
557             if (isset($row['excluded']) && ($row['excluded'] > 0)) {
558                 $row['display_excluded_link'] = $base_link . "&pass_id=exclude";
559             }
561             // passed numerator
562             if (isset($row['pass_target']) && ($row['pass_target'] > 0)) {
563                 $row['display_target_link'] = $base_link . "&pass_id=pass";
564             }
565             // failed numerator
566             if (isset($failed_items) && $failed_items > 0) {
567                 $row['display_failed_link'] = $base_link . "&pass_id=fail";
568             }
569             $row['failed_items'] = $failed_items;
570         }
572         $formatted[] = $row;
573     }
574     return $formatted;