Commit ab596ec1 authored by Paul Holden's avatar Paul Holden
Browse files

MDL-70795 reportbuilder: custom report condition editor.

Report conditions allow report creators to limit which data
is displayed when the report is viewed. Users viewing the
report cannot override these condition values.
parent 8ca9e04f
......@@ -25,10 +25,19 @@
$string['actions'] = 'Actions';
$string['addcolumn'] = 'Add column \'{$a}\'';
$string['apply'] = 'Apply';
$string['conditionadded'] = 'Added condition \'{$a}\'';
$string['conditiondeleted'] = 'Deleted condition \'{$a}\'';
$string['conditionmoved'] = 'Moved condition \'{$a}\'';
$string['conditions'] = 'Conditions';
$string['conditions_help'] = 'Report conditions allow you to limit which data is displayed when the report is viewed. Users viewing the report cannot override these condition values.';
$string['conditionsapplied'] = 'Conditions applied';
$string['conditionsreset'] = 'Conditions reset';
$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['deletecondition'] = 'Delete condition \'{$a}\'';
$string['deleteconditionconfirm'] = 'Are you sure you want to delete the condition \'{$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';
......@@ -80,7 +89,9 @@ $string['filterstartswith'] = 'Starts with';
$string['includedefaultsetup'] = 'Include default setup';
$string['includedefaultsetup_help'] = 'Populate report with default layout as defined by the selected source. These include pre-defined columns, filters and conditions.';
$string['movecolumn'] = 'Move column \'{$a}\'';
$string['movecondition'] = 'Move condition \'{$a}\'';
$string['newreport'] = 'New report';
$string['noconditions'] = 'There are no conditions selected';
$string['privacy:metadata:column'] = 'Report column definitions';
$string['privacy:metadata:column:uniqueidentifier'] = 'Unique identifier of the column';
$string['privacy:metadata:column:usercreated'] = 'The ID of the user who created the column';
......@@ -103,6 +114,7 @@ $string['reportsource'] = 'Report source';
$string['reportsource_help'] = 'The report source defines where the data for the report will come from';
$string['reportupdated'] = 'Report updated';
$string['resetall'] = 'Reset all';
$string['selectacondition'] = 'Select a condition';
$string['selectareportsource'] = 'Select a report source';
$string['selectcourses'] = 'Select courses';
$string['showhide'] = 'Show/hide \'{$a}\'';
......
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.
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.
......@@ -30,6 +30,7 @@ import Pending from 'core/pending';
import Templates from 'core/templates';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {init as columnsEditorInit} from 'core_reportbuilder/local/editor/columns';
import {init as conditionsEditorInit} from 'core_reportbuilder/local/editor/conditions';
import {getReport} from 'core_reportbuilder/local/repository/reports';
let initialized = false;
......@@ -41,6 +42,7 @@ export const init = () => {
const reportElement = document.querySelector(reportSelectors.regions.report);
columnsEditorInit(reportElement, initialized);
conditionsEditorInit(reportElement, initialized);
// Ensure we only add our listeners once (can be called multiple times by mustache template).
if (initialized) {
......
// 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/>.
/**
* Report builder conditions editor
*
* @module core_reportbuilder/local/editor/conditions
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
"use strict";
import $ from 'jquery';
import {dispatchEvent} from 'core/event_dispatcher';
import 'core/inplace_editable';
import Notification from 'core/notification';
import Pending from 'core/pending';
import SortableList from 'core/sortable_list';
import {get_string as getString, get_strings as getStrings} from 'core/str';
import Templates from 'core/templates';
import {add as addToast} from 'core/toast';
import DynamicForm from 'core_form/dynamicform';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {addCondition, deleteCondition, reorderCondition, resetConditions} from 'core_reportbuilder/local/repository/conditions';
/**
* Reload conditions settings region
*
* @param {Element} reportElement
* @param {Object} templateContext
* @return {Promise}
*/
const reloadSettingsConditionsRegion = (reportElement, templateContext) => {
const pendingPromise = new Pending('core_reportbuilder/conditions:reload');
const settingsConditionsRegion = reportElement.querySelector(reportSelectors.regions.settingsConditions);
return Templates.renderForPromise('core_reportbuilder/local/settings/conditions', {conditions: templateContext})
.then(({html, js}) => {
Templates.replaceNode(settingsConditionsRegion, html, js + templateContext.javascript);
initConditionsForm(reportElement);
return pendingPromise.resolve();
});
};
/**
* Initialise conditions form, must be called on each init because the form container is re-created when switching editor modes
*
* @param {Element} reportElement
*/
const initConditionsForm = reportElement => {
// Handle dynamic conditions form.
const conditionFormContainer = reportElement.querySelector(reportSelectors.regions.settingsConditions);
const conditionForm = new DynamicForm(conditionFormContainer, '\\core_reportbuilder\\form\\condition');
// Submit report conditions.
conditionForm.addEventListener(conditionForm.events.FORM_SUBMITTED, event => {
event.preventDefault();
getString('conditionsapplied', 'core_reportbuilder')
.then(addToast)
.catch(Notification.exception);
// After the form has been submitted, we should trigger report table reload.
dispatchEvent(reportEvents.tableReload, {}, reportElement);
});
// Reset report conditions.
conditionForm.addEventListener(conditionForm.events.NOSUBMIT_BUTTON_PRESSED, event => {
event.preventDefault();
const pendingPromise = new Pending('core_reportbuilder/conditions:reset');
resetConditions(reportElement.dataset.reportId)
.then(data => reloadSettingsConditionsRegion(reportElement, data))
.then(() => getString('conditionsreset', 'core_reportbuilder'))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
});
};
/**
* Initialise module
*
* @param {Element} reportElement
* @param {Boolean} initialized Ensure we only add our listeners once
*/
export const init = (reportElement, initialized) => {
initConditionsForm(reportElement);
if (initialized) {
return;
}
reportElement.addEventListener('click', event => {
// Add condition to report.
const reportAddCondition = event.target.closest(reportSelectors.actions.reportAddCondition);
if (reportAddCondition) {
event.preventDefault();
// Check if dropdown is closed with no condition selected.
if (reportAddCondition.value === '0') {
return;
}
const pendingPromise = new Pending('core_reportbuilder/conditions:add');
addCondition(reportElement.dataset.reportId, reportAddCondition.value)
.then(data => reloadSettingsConditionsRegion(reportElement, data))
.then(() => getString('conditionadded', 'core_reportbuilder',
reportAddCondition.options[reportAddCondition.selectedIndex].text))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
// Remove condition from report.
const reportRemoveCondition = event.target.closest(reportSelectors.actions.reportRemoveCondition);
if (reportRemoveCondition) {
event.preventDefault();
const conditionContainer = reportRemoveCondition.closest(reportSelectors.regions.activeCondition);
const conditionName = conditionContainer.dataset.conditionName;
getStrings([
{key: 'deletecondition', component: 'core_reportbuilder', param: conditionName},
{key: 'deleteconditionconfirm', component: 'core_reportbuilder', param: conditionName},
{key: 'delete', component: 'moodle'},
]).then(([confirmTitle, confirmText, confirmButton]) => {
Notification.confirm(confirmTitle, confirmText, confirmButton, null, () => {
const pendingPromise = new Pending('core_reportbuilder/conditions:remove');
deleteCondition(reportElement.dataset.reportId, conditionContainer.dataset.conditionId)
.then(data => reloadSettingsConditionsRegion(reportElement, data))
.then(() => getString('conditiondeleted', 'core_reportbuilder', conditionName))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
});
return;
}).catch(Notification.exception);
}
});
// Initialize sortable list to handle active conditions moving (note JQuery dependency, see MDL-72293 for resolution).
var activeConditionsSortableList = new SortableList(`${reportSelectors.regions.activeConditions}`,
{isHorizontal: false});
activeConditionsSortableList.getElementName = element => Promise.resolve(element.data('conditionName'));
$(reportElement).on(SortableList.EVENTS.DROP, reportSelectors.regions.activeCondition, (event, info) => {
if (info.positionChanged) {
const pendingPromise = new Pending('core_reportbuilder/conditions:reorder');
const conditionId = info.element.data('conditionId');
const conditionPosition = info.element.data('conditionPosition');
// Select target position, if moving to the end then count number of element siblings.
let targetConditionPosition = info.targetNextElement.data('conditionPosition') || info.element.siblings().length + 2;
if (targetConditionPosition > conditionPosition) {
targetConditionPosition--;
}
reorderCondition(reportElement.dataset.reportId, conditionId, targetConditionPosition)
.then(data => reloadSettingsConditionsRegion(reportElement, data))
.then(() => getString('conditionmoved', 'core_reportbuilder', info.element.data('conditionName')))
.then(addToast)
.then(() => {
dispatchEvent(reportEvents.tableReload, {}, reportElement);
return pendingPromise.resolve();
})
.catch(Notification.exception);
}
});
};
// 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/>.
/**
* Module to handle condition AJAX requests
*
* @module core_reportbuilder/local/repository/conditions
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Reset all conditions for given report
*
* @param {Number} reportId
* @return {Promise}
*/
export const resetConditions = reportId => {
const request = {
methodname: 'core_reportbuilder_conditions_reset',
args: {reportid: reportId}
};
return Ajax.call([request])[0];
};
/**
* Add condition to given report
*
* @param {Number} reportId
* @param {String} uniqueIdentifier
* @return {Promise}
*/
export const addCondition = (reportId, uniqueIdentifier) => {
const request = {
methodname: 'core_reportbuilder_conditions_add',
args: {reportid: reportId, uniqueidentifier: uniqueIdentifier}
};
return Ajax.call([request])[0];
};
/**
* Remove condition from given report
*
* @param {Number} reportId
* @param {Number} conditionId
* @return {Promise}
*/
export const deleteCondition = (reportId, conditionId) => {
const request = {
methodname: 'core_reportbuilder_conditions_delete',
args: {reportid: reportId, conditionid: conditionId}
};
return Ajax.call([request])[0];
};
/**
* Reorder a condition in a given report
*
* @param {Number} reportId
* @param {Number} conditionId
* @param {Number} position
* @return {Promise}
*/
export const reorderCondition = (reportId, conditionId, position) => {
const request = {
methodname: 'core_reportbuilder_conditions_reorder',
args: {reportid: reportId, conditionid: conditionId, position: position}
};
return Ajax.call([request])[0];
};
......@@ -38,6 +38,9 @@ const SELECTORS = {
filtersForm: '[data-region="filters-form"]',
sidebarMenu: '[data-region="sidebar-menu"]',
sidebarCard: '[data-region="sidebar-card"]',
settingsConditions: '[data-region="settings-conditions"]',
activeConditions: '[data-region="active-conditions"]',
activeCondition: '[data-region="active-condition"]',
},
actions: {
reportActionPopup: '[data-action="report-action-popup"]',
......@@ -46,6 +49,8 @@ const SELECTORS = {
reportDelete: '[data-action="report-delete"]',
reportAddColumn: '[data-action="report-add-column"]',
reportRemoveColumn: '[data-action="report-remove-column"]',
reportAddCondition: '[data-action="report-add-condition"]',
reportRemoveCondition: '[data-action="report-remove-condition"]',
sidebarSearch: '[data-action="sidebar-search"]',
},
};
......
......@@ -94,4 +94,77 @@ abstract class datasource extends base {
* @return string[]
*/
abstract public function get_default_columns(): array;
/**
* Add conditions from the given entity name to be available to use in a custom report
*
* @param string $entityname
* @param array $include Include only these conditions, if omitted then include all
* @param array $exclude Exclude these conditions, if omitted then exclude none
* @throws coding_exception If both $include and $exclude are non-empty
*/
final protected function add_conditions_from_entity(string $entityname, array $include = [], array $exclude = []): void {
if (!empty($include) && !empty($exclude)) {
throw new coding_exception('Cannot specify conditions to include and exclude simultaneously');
}
$entity = $this->get_entity($entityname);
// Retrieve filtered conditions from entity, respecting given $include/$exclude parameters.
$conditions = array_filter($entity->get_conditions(), static function(filter $condition) use ($include, $exclude): bool {
if (!empty($include)) {
return in_array($condition->get_name(), $include);
}
if (!empty($exclude)) {
return !in_array($condition->get_name(), $exclude);
}
return true;
});
foreach ($conditions as $condition) {
$this->add_condition($condition);
}
}
/**
* Add default datasource conditions to the report
*
* This method is optional and can be called when the report is created to add the default conditions defined in the
* selected datasource.
*/
public function add_default_conditions(): void {
$reportid = $this->get_report_persistent()->get('id');
$conditionidentifiers = $this->get_default_conditions();
foreach ($conditionidentifiers as $uniqueidentifier) {
report::add_report_condition($reportid, $uniqueidentifier);
}
}
/**
* Return the conditions that will be added to the report once is created
*
* @return string[]
*/
abstract public function get_default_conditions(): array;
/**
* Return all configured report conditions
*
* @return filter[]
*/
public function get_active_conditions(): array {
$conditions = [];
$activeconditions = filter_model::get_condition_records($this->get_report_persistent()->get('id'), 'filterorder');
foreach ($activeconditions as $condition) {
$instance = $this->get_condition($condition->get('uniqueidentifier'));
if ($instance !== null && $instance->get_is_available()) {
$conditions[$instance->get_unique_identifier()] = $instance;
}
}
return $conditions;
}
}
<?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\conditions;
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
use core_reportbuilder\manager;
use core_reportbuilder\permission;
use core_reportbuilder\external\custom_report_conditions_exporter;
use core_reportbuilder\local\helpers\report;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("{$CFG->libdir}/externallib.php");
/**
* External method for adding report conditions
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class add 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'),
'uniqueidentifier' => new external_value(PARAM_RAW, 'Unique identifier of the condition'),
]);
}
/**
* External method execution
*
* @param int $reportid
* @param string $uniqueidentifier
* @return array
*/
public static function execute(int $reportid, string $uniqueidentifier): array {
global $PAGE, $OUTPUT;
[
'reportid' => $reportid,
'uniqueidentifier' => $uniqueidentifier,
] = self::validate_parameters(self::execute_parameters(), [
'reportid' => $reportid,
'uniqueidentifier' => $uniqueidentifier,
]);
$report = manager::get_report_from_id($reportid);
self::validate_context($report->get_context());
permission::require_can_edit_report($report->get_report_persistent());
report::add_report_condition($reportid, $uniqueidentifier);
// Set current URL and force bootstrap_renderer to initiate moodle page.
$PAGE->set_url('/');
$OUTPUT->header();
$PAGE->start_collecting_javascript_requirements();
$exporter = new custom_report_conditions_exporter(null, ['report' => $report]);
$export = $exporter->export($PAGE->get_renderer('core'));
$export->javascript = $PAGE->requires->get_end_code();
return (array) $export;
}
/**
* External method return value
*
* @return external_value
*/
public static function execute_returns(): external_single_structure {
return custom_report_conditions_exporter::get_read_structure();
}
}
<?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\conditions;
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
use core_reportbuilder\manager;
use core_reportbuilder\permission;
use core_reportbuilder\external\custom_report_conditions_exporter;
use core_reportbuilder\local\helpers\report;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("{$CFG->libdir}/externallib.php");
/**
* External method for deleting report conditions
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/