From 003c70f33d7f9eccc336bfbb17fdb0b406210758 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 13 Dec 2023 12:51:01 +0000 Subject: [PATCH] Fix #114 - when downloading, return empty csv file (with headers) when no results --- lib.php | 2 +- locallib.php | 22 +++++++++++++++++++++- tests/behat/behat_report_customsql.php | 25 ++++++++++++++++++++++++- tests/behat/report_customsql.feature | 7 +++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/lib.php b/lib.php index fb9ad89..6c3727c 100644 --- a/lib.php +++ b/lib.php @@ -89,7 +89,7 @@ function report_customsql_pluginfile($course, $cm, $context, $filearea, $args, $ if ($report->runable !== 'manual') { $runtime = $report->lastrun; } - $csvtimestamp = report_customsql_generate_csv($report, $runtime); + $csvtimestamp = report_customsql_generate_csv($report, $runtime, true); } list($csvfilename) = report_customsql_csv_filename($report, $csvtimestamp); diff --git a/locallib.php b/locallib.php index 10b129b..7c9a6e6 100644 --- a/locallib.php +++ b/locallib.php @@ -96,7 +96,14 @@ function report_customsql_get_element_type($name) { return 'text'; } -function report_customsql_generate_csv($report, $timenow) { +/** + * Generate customsql csv file. + * + * @param stdclass $report report record from customsql table. + * @param int $timetimenow unix timestamp - usually "now()" + * @param bool $returnheaderwhenempty if true, a CSV file with headers will always be generated, even if there are no results. + */ +function report_customsql_generate_csv($report, $timenow, $returnheaderwhenempty = false) { global $DB; $starttime = microtime(true); @@ -104,6 +111,14 @@ function report_customsql_generate_csv($report, $timenow) { $queryparams = !empty($report->queryparams) ? unserialize($report->queryparams) : []; $querylimit = $report->querylimit ?? get_config('report_customsql', 'querylimitdefault'); + if ($returnheaderwhenempty) { + // We want the export to always generate a CSV file so we modify the query slightly + // to generate an extra "null" values row, so we can get the column names, + // then we ignore rows that contain null records in every row when generating the csv. + $sql = "SELECT subq.* + FROM (SELECT 1) as ignoreme + LEFT JOIN ($sql) as subq on true"; + } // Query one extra row, so we can tell if we hit the limit. $rs = report_customsql_execute_query($sql, $queryparams, $querylimit + 1); @@ -124,6 +139,11 @@ function report_customsql_generate_csv($report, $timenow) { } $data = get_object_vars($row); + + if ($returnheaderwhenempty && array_unique(array_values($data)) === [null]) { + // This is a row with all null values - ignore it. + continue; + } foreach ($data as $name => $value) { if (report_customsql_get_element_type($name) == 'date_time_selector' && report_customsql_is_integer($value) && $value > 0) { diff --git a/tests/behat/behat_report_customsql.php b/tests/behat/behat_report_customsql.php index 56b124a..c630398 100644 --- a/tests/behat/behat_report_customsql.php +++ b/tests/behat/behat_report_customsql.php @@ -30,7 +30,7 @@ use Behat\Gherkin\Node\PyStringNode as PyStringNode; use Behat\Gherkin\Node\TableNode; - +use Behat\Mink\Exception\ExpectationException; /** * Behat steps for the the custom SQL report. @@ -248,6 +248,29 @@ public function adhoc_database_queries_thinks_the_time_is($time) { set_config('behat_fixed_time', $value, 'report_customsql'); } + /** + * Simulates downloading an empty report to ensure it shows table headers. + * + * For example: + * When downloading the empty custom sql report "Frog" it contains the headers "frogname,freddy" + * + * @Then /^downloading custom sql report "(?P[^"]*)" returns a file with headers "([^"]*)"$/ + * @param string $reportname the name of the report to go to. + * @param string $headers the headers that shuold be returned. + */ + public function downloading_custom_sql_report_x_returns_a_file_with_headers(string $reportname, string $headers) { + $report = $this->get_report_by_name($reportname); + $url = new \moodle_url('/pluginfile.php/1/report_customsql/download/' . $report->id, ['dataformat' => 'csv']); + + $session = $this->getSession()->getCookie('MoodleSession'); + $filecontent = trim(download_file_content($url, ['Cookie' => 'MoodleSession=' . $session])); + $filecontent = core_text::trim_utf8_bom($filecontent); + if ($filecontent != $headers) { + throw new ExpectationException( + "File headers: $filecontent did not match expected: $headers", $this->getSession()); + } + } + /** * Find a report by name and get all the details. * diff --git a/tests/behat/report_customsql.feature b/tests/behat/report_customsql.feature index a831bd1..6c3e893 100644 --- a/tests/behat/report_customsql.feature +++ b/tests/behat/report_customsql.feature @@ -76,6 +76,13 @@ Feature: Ad-hoc database queries report And I view the "Test query" custom sql report Then I should see "This query did not return any data." + Scenario: Download an Ad-hoc database query that returns no data but includes headers + Given the following custom sql report exists: + | name | Test query | + | querysql | SELECT * FROM {config} WHERE name = '-1' | + When I log in as "admin" + Then downloading custom sql report "Test query" returns a file with headers "id,name,value" + Scenario: Create an Ad-hoc database queries category When I log in as "admin" And I navigate to "Reports > Ad-hoc database queries" in site administration