Commit 0457fb66 authored by David Matamoros's avatar David Matamoros Committed by Paul Holden
Browse files

MDL-70795 reportbuilder: allow user to view custom reports.



Reports can be viewed via two methods. Non-editing users can
access them via their report listing page, and report editors
can switch between editing and preview mode while working on
their reports.

Clean up remaining string definitions.

Co-authored-by: Mikel Martín Corrales's avatarMikel Martín <mikel@moodle.com>
parent f47e89a9
......@@ -38,6 +38,8 @@ $string['aggregationsum'] = 'Sum';
$string['apply'] = 'Apply';
$string['columnadded'] = 'Added column \'{$a}\'';
$string['columnaggregated'] = 'Aggregated column \'{$a}\'';
$string['columndeleted'] = 'Deleted column \'{$a}\'';
$string['columnmoved'] = 'Moved column \'{$a}\'';
$string['columnsortdirectionasc'] = 'Sort column \'{$a}\' ascending';
$string['columnsortdirectiondesc'] = 'Sort column \'{$a}\' descending';
$string['columnsortdisable'] = 'Disable sorting for column \'{$a}\'';
......@@ -54,23 +56,24 @@ $string['coursefullnamewithlink'] = 'Course full name with link';
$string['courseidnumberewithlink'] = 'Course ID number with link';
$string['courseshortnamewithlink'] = 'Course short name with link';
$string['customfieldcolumn'] = '{$a}';
$string['customreports'] = 'Custom reports';
$string['deletecolumn'] = 'Delete column \'{$a}\'';
$string['deletecolumnconfirm'] = 'Are you sure you want to delete the column \'{$a}\'?';
$string['deletecondition'] = 'Delete condition \'{$a}\'';
$string['deleteconditionconfirm'] = 'Are you sure you want to delete the condition \'{$a}\'?';
$string['deletefilter'] = 'Delete filter \'{$a}\'';
$string['deletefilterconfirm'] = 'Are you sure you want to delete the filter \'{$a}\'?';
$string['deletereport'] = 'Delete report';
$string['deletereportconfirm'] = 'Are you sure you want to delete the report \'{$a}\' and all associated data?';
$string['editdetails'] = 'Edit details';
$string['editreportcontent'] = 'Edit report content';
$string['editreportdetails'] = 'Edit report details';
$string['editreportname'] = 'Edit report name';
$string['columnmoved'] = 'Moved column \'{$a}\'';
$string['customreports'] = 'Custom reports';
$string['deletecolumn'] = 'Delete column \'{$a}\'';
$string['deletecolumnconfirm'] = 'Are you sure you want to delete the column \'{$a}\'?';
$string['deletefilter'] = 'Delete filter \'{$a}\'';
$string['deletefilterconfirm'] = 'Are you sure you want to delete the filter \'{$a}\'?';
$string['entitycourse'] = 'Course';
$string['entityuser'] = 'User';
$string['errorreportaccess'] = 'You can not view this report';
$string['errorreportcreate'] = 'You can not create a new report';
$string['errorreportedit'] = 'You can not edit this report';
$string['errorreportview'] = 'You can not view this report';
$string['errorsourceinvalid'] = 'Could not find valid report source';
$string['errorsourceunavailable'] = 'Report source is not available';
$string['filteradded'] = 'Added filter \'{$a}\'';
......@@ -159,4 +162,5 @@ $string['timemodified'] = 'Time modified';
$string['userfullnamewithlink'] = 'Full name with link';
$string['userfullnamewithpicture'] = 'Full name with picture';
$string['userfullnamewithpicturelink'] = 'Full name with picture and link';
$string['usermodified'] = 'Modified by';
$string['userpicture'] = 'User picture';
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -39,3 +39,18 @@ export const deleteReport = reportId => {
return Ajax.call([request])[0];
};
/**
* Get report content
*
* @param {Number} reportId
* @param {Boolean} editMode
* @return {Promise}
*/
export const getReport = (reportId, editMode) => {
const request = {
methodname: 'core_reportbuilder_reports_get',
args: {reportid: reportId, editmode: editMode}
};
return Ajax.call([request])[0];
};
......@@ -60,6 +60,7 @@ const SELECTORS = {
reportToggleColumnSort: '[data-action="report-toggle-column-sorting"]',
reportToggleColumnSortDirection: '[data-action="report-toggle-sort-direction"]',
sidebarSearch: '[data-action="sidebar-search"]',
toggleEditPreview: '[data-action="toggle-edit-preview"]',
},
};
......
......@@ -49,16 +49,25 @@ class custom_report_exporter extends persistent_exporter {
/** @var bool $showeditbutton When showing the report on view.php the Edit button has to be hidden */
protected $showeditbutton;
/** @var string */
protected $download;
/**
* report_exporter constructor.
*
* @param persistent $persistent
* @param array $related
* @param bool $editmode
* @param bool $showeditbutton
* @param string $download
*/
public function __construct(persistent $persistent, array $related = array(), bool $editmode = true, bool $showeditbutton = true) {
public function __construct(persistent $persistent, array $related = [], bool $editmode = true,
bool $showeditbutton = true, string $download = '') {
parent::__construct($persistent, $related);
$this->editmode = $editmode;
$this->showeditbutton = $showeditbutton;
$this->download = $download;
}
/**
* Return the name of the class we are exporting
......@@ -116,7 +125,17 @@ class custom_report_exporter extends persistent_exporter {
$table = custom_report_table::create($this->persistent->get('id'));
$table->set_filterset(new custom_report_table_filterset());
} else {
$table = custom_report_table_view::create($this->persistent->get('id'), $this->download);
$table->set_filterset(new custom_report_table_view_filterset());
// Generate filters form if report contains any filters.
$source = $this->persistent->get('source');
/** @var datasource $datasource */
$datasource = new $source($this->persistent);
if (!empty($datasource->get_active_filters())) {
$filtersform = $this->generate_filters_form()->render();
}
}
$report = manager::get_report_from_persistent($this->persistent);
......
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\external\reports;
use external_api;
use external_value;
use external_single_structure;
use external_function_parameters;
use core_reportbuilder\manager;
use core_reportbuilder\permission;
use core_reportbuilder\output\custom_report;
use core_reportbuilder\external\custom_report_exporter;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("{$CFG->libdir}/externallib.php");
/**
* External method for getting a custom report
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get extends external_api {
/**
* External method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'reportid' => new external_value(PARAM_INT, 'Report ID'),
'editmode' => new external_value(PARAM_BOOL, 'Whether editing mode is enabled', VALUE_DEFAULT, 0),
]);
}
/**
* External method execution
*
* @param int $reportid
* @param bool $editmode
* @return array
*/
public static function execute(int $reportid, bool $editmode): array {
global $PAGE, $OUTPUT;
[
'reportid' => $reportid,
'editmode' => $editmode,
] = self::validate_parameters(self::execute_parameters(), [
'reportid' => $reportid,
'editmode' => $editmode,
]);
$report = manager::get_report_from_id($reportid);
self::validate_context($report->get_context());
if ($editmode) {
permission::require_can_edit_report($report->get_report_persistent());
} else {
permission::require_can_view_report($report->get_report_persistent());
}
// Set current URL and force bootstrap_renderer to initiate moodle page.
$PAGE->set_url(new moodle_url('/'));
$OUTPUT->header();
$PAGE->start_collecting_javascript_requirements();
$renderer = $PAGE->get_renderer('core_reportbuilder');
$context = (new custom_report($report->get_report_persistent(), $editmode))->export_for_template($renderer);
$context->javascript = $PAGE->requires->get_end_code();
return (array)$context;
}
/**
* External method return value
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return custom_report_exporter::get_read_structure();
}
}
......@@ -19,11 +19,13 @@ declare(strict_types=1);
namespace core_reportbuilder\form;
use context;
use core_reportbuilder\local\report\base;
use core_reportbuilder\permission;
use moodle_url;
use core_form\dynamic_form;
use core_reportbuilder\manager;
use core_reportbuilder\system_report;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\models\filter as filter_model;
/**
* Dynamic filter form
......@@ -37,16 +39,13 @@ class filter extends dynamic_form {
/**
* Return instance of the system report using the filter form
*
* @return system_report
* @return base
*/
private function get_system_report(): system_report {
$report = new report($this->optional_param('reportid', 0, PARAM_INT));
private function get_report(): base {
$reportpersistent = new report($this->optional_param('reportid', 0, PARAM_INT));
$parameters = (array) json_decode($this->optional_param('parameters', '', PARAM_RAW));
/** @var system_report $systemreport */
$systemreport = manager::get_report_from_persistent($report, $parameters);
return $systemreport;
return manager::get_report_from_persistent($reportpersistent, $parameters);
}
/**
......@@ -55,7 +54,7 @@ class filter extends dynamic_form {
* @return context
*/
protected function get_context_for_dynamic_submission(): context {
return ($this->get_system_report())->get_context();
return ($this->get_report())->get_context();
}
/**
......@@ -64,7 +63,12 @@ class filter extends dynamic_form {
* A {@see \core_reportbuilder\report_access_exception} will be thrown if they can't
*/
protected function check_access_for_dynamic_submission(): void {
$this->get_system_report()->require_can_view();
$reportpersistent = $this->get_report()->get_report_persistent();
if ($reportpersistent->get('type') === base::TYPE_CUSTOM_REPORT) {
permission::require_can_view_report($reportpersistent);
} else {
$this->get_report()->require_can_view();
}
}
/**
......@@ -77,9 +81,9 @@ class filter extends dynamic_form {
// Remove some unneeded fields, apply filters.
unset($values->reportid, $values->parameters);
$this->get_system_report()->set_filter_values((array) $values);
$this->get_report()->set_filter_values((array) $values);
return $this->get_system_report()->get_applied_filter_count();
return $this->get_report()->get_applied_filter_count();
}
/**
......@@ -91,7 +95,7 @@ class filter extends dynamic_form {
'parameters' => $this->optional_param('parameters', 0, PARAM_RAW),
];
$this->set_data(array_merge($defaults, $this->get_system_report()->get_filter_values()));
$this->set_data(array_merge($defaults, $this->get_report()->get_filter_values()));
}
/**
......@@ -118,9 +122,17 @@ class filter extends dynamic_form {
$mform->setType('parameters', PARAM_RAW);
// Allow each filter instance to add itself to this form, wrapping each inside custom header/footer template.
foreach ($this->get_system_report()->get_filter_instances() as $filterinstance) {
$filterinstances = $this->get_report()->get_filter_instances();
foreach ($filterinstances as $filterinstance) {
// Check if filter has a custom header set.
if ($filterinstance->get_filter_persistent() && !empty($filterinstance->get_filter_persistent()->get('heading'))) {
$header = $filterinstance->get_filter_persistent()->get('heading');
} else {
$header = $filterinstance->get_header();
}
$mform->addElement('html', $OUTPUT->render_from_template('core_reportbuilder/local/filters/header', [
'name' => $filterinstance->get_header(),
'name' => $header,
]));
$filterinstance->setup_form($mform);
......
......@@ -20,6 +20,7 @@ namespace core_reportbuilder\local\filters;
use MoodleQuickForm;
use core_reportbuilder\local\report\filter;
use core_reportbuilder\local\models\filter as filter_model;
/**
* Base class for all report filters
......@@ -80,6 +81,17 @@ abstract class base {
return $this->filter->get_entity_name();
}
/**
* Returns the filter persistent
*
* Note that filters for system reports don't store a persistent and will return null.
*
* @return filter_model|null
*/
final public function get_filter_persistent(): ?filter_model {
return $this->filter->get_persistent();
}
/**
* Adds filter-specific form elements
*
......
......@@ -246,6 +246,18 @@ class reports_list extends system_report {
})
);
// Preview action.
$this->add_action((new action(
new moodle_url('/reportbuilder/view.php', ['id' => ':id']),
new pix_icon('i/search', get_string('view', 'moodle')),
[]
))
->add_callback(function(stdClass $row): bool {
// We check this only to give the action to editors, because normal users can just click on the report name.
return $this->report_source_valid($row->source) && permission::can_edit_report($this->get_report_from_row($row));
})
);
// Delete action.
$this->add_action((new action(
new moodle_url('#'),
......
......@@ -43,17 +43,24 @@ class custom_report implements renderable, templatable {
/** @var bool $showeditbutton */
protected $showeditbutton;
/** @var string $download */
protected $download;
/**
* Class constructor
*
* @param report $reportpersistent
* @param bool $editmode
* @param bool $showeditbutton
* @param string $download
*/
public function __construct(report $reportpersistent, bool $editmode = true, bool $showeditbutton = true) {
public function __construct(report $reportpersistent, bool $editmode = true, bool $showeditbutton = true,
string $download = '') {
$this->persistent = $reportpersistent;
$this->editmode = $editmode;
$this->showeditbutton = $showeditbutton;
$this->download = $download;
}
/**
......@@ -63,7 +70,7 @@ class custom_report implements renderable, templatable {
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$exporter = new custom_report_exporter($this->persistent, [], $this->editmode, $this->showeditbutton);
$exporter = new custom_report_exporter($this->persistent, [], $this->editmode, $this->showeditbutton, $this->download);
return $exporter->export($output);
}
......
......@@ -21,6 +21,7 @@ namespace core_reportbuilder\output;
use html_writer;
use plugin_renderer_base;
use core_reportbuilder\table\custom_report_table;
use core_reportbuilder\table\custom_report_table_view;
use core_reportbuilder\table\system_report_table;
/**
......@@ -59,6 +60,18 @@ class renderer extends plugin_renderer_base {
return $output;
}
/**
* Render a custom report
*
* @param custom_report $report
* @return string
*/
protected function render_custom_report(custom_report $report): string {
$context = $report->export_for_template($this);
return $this->render_from_template('core_reportbuilder/custom_report', $context);
}
/**
* Render a custom report table
*
......@@ -74,6 +87,21 @@ class renderer extends plugin_renderer_base {
return $output;
}
/**
* Render a custom report table (view only mode)
*
* @param custom_report_table_view $table
* @return string
*/
protected function render_custom_report_table_view(custom_report_table_view $table): string {
ob_start();
$table->out($table->get_default_per_page(), false);
$output = ob_get_contents();
ob_end_clean();
return $output;
}
/**
* Renders the New report button
*
......
......@@ -53,6 +53,34 @@ class permission {
return has_capability('moodle/reportbuilder:view', context_system::instance(), $userid);
}
/**
* Require given user can view report
*
* @param report $report
* @param int|null $userid User ID to check, or the current user if omitted
* @throws report_access_exception
*/
public static function require_can_view_report(report $report, ?int $userid = null): void {
if (!static::can_view_report($report, $userid)) {
throw new report_access_exception('errorreportview');
}
}
/**
* Whether given user can view report
*
* @param report $report
* @param int|null $userid User ID to check, or the current user if omitted
* @return bool
*/
public static function can_view_report(report $report, ?int $userid = null): bool {
if (!static::can_view_reports_list()) {
return false;
}
return true; // TODO: Audience.
}
/**
* Require given user can edit report
*
......
......@@ -31,8 +31,10 @@ class report_access_exception extends moodle_exception {
/**
* Constructor
*
* @param string $errorcode
*/
public function __construct() {
parent::__construct('errorreportaccess', 'reportbuilder');
public function __construct(string $errorcode = 'errorreportview') {
parent::__construct($errorcode, 'reportbuilder');
}
}
......@@ -22,6 +22,7 @@ use context;
use moodle_url;
use renderable;
use table_sql;
use html_writer;
use core_table\dynamic;
use core_reportbuilder\local\helpers\database;
use core_reportbuilder\local\filters\base;
......@@ -197,8 +198,24 @@ abstract class base_report_table extends table_sql implements dynamic, renderabl
echo $this->get_dynamic_table_html_start();
echo $this->render_reset_button();
echo $OUTPUT->render(new notification(get_string('nothingtodisplay'), notification::NOTIFY_INFO));
echo $OUTPUT->render(new notification(get_string('nothingtodisplay'), notification::NOTIFY_INFO, false));
echo $this->get_dynamic_table_html_end();
}
/**
* Override start of HTML to remove top pagination.
*/
public function start_html() {
// Render the dynamic table header.
echo $this->get_dynamic_table_html_start();
// Render button to allow user to reset table preferences.
echo $this->render_reset_button();
$this->wrap_html_start();
echo html_writer::start_tag('div', ['class' => 'no-overflow']);
echo html_writer::start_tag('table', $this->attributes);
}
}
......@@ -50,9 +50,10 @@ class custom_report_table extends base_report_table {
* dynamic updates continue to load the same report
*
* @param string $uniqueid
* @param string $download
* @throws moodle_exception For invalid unique ID
*/
public function __construct(string $uniqueid) {
public function __construct(string $uniqueid, string $download = '') {
if (!preg_match('/^' . self::UNIQUEID_PREFIX . '(?<id>\d+)$/', $uniqueid, $matches)) {
throw new moodle_exception('invalidcustomreportid', 'core_reportbuilder', '', null, $uniqueid);
}
......@@ -74,6 +75,10 @@ class custom_report_table extends base_report_table {
$this->set_attribute('data-region', 'reportbuilder-table');
$this->set_attribute('class', $this->attributes['class'] . ' reportbuilder-table');
// Download options.
$this->showdownloadbuttonsat = [TABLE_P_BOTTOM];
$this->is_downloading($download ?? null, $this->persistent->get_formatted_name());
// Retrieve all report columns, exit early if there are none.
$columns = $this->get_active_columns();
if (empty($columns)) {
......@@ -126,10 +131,11 @@ class custom_report_table extends base_report_table {
* Return a new instance of the class for given report ID
*
* @param int $reportid
* @param string $download
* @return static
*/
public static function create(int $reportid): self {
return new static(self::UNIQUEID_PREFIX . $reportid);
public static function create(int $reportid, string $download = ''): self {
return new static(self::UNIQUEID_PREFIX . $reportid, $download);
}
/**
......@@ -176,12 +182,11 @@ class custom_report_table extends base_report_table {
}
/**
* Get the html for the download buttons
* Download is disabled when editing the report
*
* @return string
*/
public function download_buttons(): string {
// TODO.
return '';
}
......@@ -254,22 +259,6 @@ class custom_report_table extends base_report_table {
echo html_writer::end_tag('thead');
}
/**
* Override start of HTML to remove top pagination
*/
public function start_html() {
// Render the dynamic table header.
echo $this->get_dynamic_table_html_start();
// Render button to allow user to reset table preferences.
echo $this->render_reset_button();
$this->wrap_html_start();