This commit is contained in:
Jun Pataleta 2019-11-04 10:41:39 +08:00
commit fcc1facdca
23 changed files with 784 additions and 52 deletions

View File

@ -1371,4 +1371,82 @@ class core_calendar_external extends external_api {
]
);
}
/**
* Convert the specified dates into unix timestamps.
*
* @param array $datetimes Array of arrays containing date time details, each in the format:
* ['year' => a, 'month' => b, 'day' => c,
* 'hour' => d (optional), 'minute' => e (optional), 'key' => 'x' (optional)]
* @return array Provided array of dates converted to unix timestamps
* @throws moodle_exception If one or more of the dates provided does not convert to a valid timestamp.
*/
public static function get_timestamps($datetimes) {
$params = self::validate_parameters(self::get_timestamps_parameters(), ['data' => $datetimes]);
$type = \core_calendar\type_factory::get_calendar_instance();
$timestamps = ['timestamps' => []];
foreach ($params['data'] as $key => $datetime) {
$hour = $datetime['hour'] ?? 0;
$minute = $datetime['minute'] ?? 0;
try {
$timestamp = $type->convert_to_timestamp(
$datetime['year'], $datetime['month'], $datetime['day'], $hour, $minute);
$timestamps['timestamps'][] = [
'key' => $datetime['key'] ?? $key,
'timestamp' => $timestamp,
];
} catch (Exception $e) {
throw new moodle_exception('One or more of the dates provided were invalid');
}
}
return $timestamps;
}
/**
* Describes the parameters for get_timestamps.
*
* @return external_function_parameters
*/
public static function get_timestamps_parameters() {
return new external_function_parameters ([
'data' => new external_multiple_structure(
new external_single_structure(
[
'key' => new external_value(PARAM_ALPHANUMEXT, 'key', VALUE_OPTIONAL),
'year' => new external_value(PARAM_INT, 'year'),
'month' => new external_value(PARAM_INT, 'month'),
'day' => new external_value(PARAM_INT, 'day'),
'hour' => new external_value(PARAM_INT, 'hour', VALUE_OPTIONAL),
'minute' => new external_value(PARAM_INT, 'minute', VALUE_OPTIONAL),
]
)
)
]);
}
/**
* Describes the timestamps return format.
*
* @return external_single_structure
*/
public static function get_timestamps_returns() {
return new external_single_structure(
[
'timestamps' => new external_multiple_structure(
new external_single_structure(
[
'key' => new external_value(PARAM_ALPHANUMEXT, 'Timestamp key'),
'timestamp' => new external_value(PARAM_INT, 'Unix timestamp'),
]
)
)
]
);
}
}

View File

@ -2696,4 +2696,87 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
);
$this->assertEquals(['user', 'course', 'group'], $data['allowedeventtypes']);
}
/**
* Test get_timestamps with string keys, with and without optional hour/minute values.
*/
public function test_get_timestamps_string_keys() {
$this->resetAfterTest(true);
$this->setAdminUser();
$time1 = new DateTime('2018-12-30 00:00:00');
$time2 = new DateTime('2019-03-27 23:59:00');
$dates = [
[
'key' => 'from',
'year' => $time1->format('Y'),
'month' => $time1->format('m'),
'day' => $time1->format('d'),
],
[
'key' => 'to',
'year' => $time2->format('Y'),
'month' => (int) $time2->format('m'),
'day' => $time2->format('d'),
'hour' => $time2->format('H'),
'minute' => $time2->format('i'),
],
];
$expectedtimestamps = [
'from' => $time1->getTimestamp(),
'to' => $time2->getTimestamp(),
];
$result = core_calendar_external::get_timestamps($dates);
$this->assertEquals(['timestamps'], array_keys($result));
$this->assertEquals(2, count($result['timestamps']));
foreach ($result['timestamps'] as $data) {
$this->assertTrue(in_array($data['key'], ['from', 'to']));
$this->assertEquals($expectedtimestamps[$data['key']], $data['timestamp']);
}
}
/**
* Test get_timestamps with no keys specified, with and without optional hour/minute values.
*/
public function test_get_timestamps_no_keys() {
$this->resetAfterTest(true);
$this->setAdminUser();
$time1 = new DateTime('2018-12-30 00:00:00');
$time2 = new DateTime('2019-03-27 23:59:00');
$dates = [
[
'year' => $time1->format('Y'),
'month' => $time1->format('m'),
'day' => $time1->format('d'),
],
[
'year' => $time2->format('Y'),
'month' => (int) $time2->format('m'),
'day' => $time2->format('d'),
'hour' => $time2->format('H'),
'minute' => $time2->format('i'),
],
];
$expectedtimestamps = [
0 => $time1->getTimestamp(),
1 => $time2->getTimestamp(),
];
$result = core_calendar_external::get_timestamps($dates);
$this->assertEquals(['timestamps'], array_keys($result));
$this->assertEquals(2, count($result['timestamps']));
foreach ($result['timestamps'] as $data) {
$this->assertEquals($expectedtimestamps[$data['key']], $data['timestamp']);
}
}
}

View File

@ -9,6 +9,8 @@ information provided here is intended especially for developers.
* calendar_cron()
* calendar_get_mini()
* calendar_get_upcoming()
* Added core_calendar_external::get_timestamps(), which allows an array containing an arbitrary number of arrays of
date/time data to be converted and returned as timestamps, along with an optional key.
=== 3.6 ===
* calendar_get_default_courses() function now has optional $userid parameter.

View File

@ -261,6 +261,14 @@ $functions = array(
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_calendar_get_timestamps' => [
'classname' => 'core_calendar_external',
'methodname' => 'get_timestamps',
'description' => 'Fetch unix timestamps for given date times.',
'classpath' => 'calendar/externallib.php',
'type' => 'read',
'ajax' => true,
],
'core_cohort_add_cohort_members' => array(
'classname' => 'core_cohort_external',
'methodname' => 'add_cohort_members',

View File

@ -1,2 +1,2 @@
define ("forumreport_summary/filters",["exports","jquery","core/popper","core/custom_interaction_events","forumreport_summary/selectors"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=f(b);c=f(c);d=f(d);e=f(e);function f(a){return a&&a.__esModule?a:{default:a}}var g=function(a){var f=(0,b.default)(a);(0,b.default)(document).ready(function(){(0,b.default)(".loading-icon").hide();(0,b.default)("#summaryreport").removeClass("hidden")});var g=function(a){var c=(0,b.default)("#filtersform").attr("action");if(a){a.preventDefault();var d=a.target.search.substr(1);c+="&"+d}(0,b.default)("#filtersform").attr("action",c);(0,b.default)("#filtersform").submit()};(0,b.default)(".resettable").on("click","a",function(a){g(a)});(0,b.default)("thead").on("click","a",function(a){g(a)});(0,b.default)(".pagination").on("click","a",function(a){g(a)});var h=function(a){(0,b.default)(a).addClass("hidden");g(!1)};f.on(d.default.events.activate,e.default.filters.group.selectall,function(){var b=a.querySelectorAll(e.default.filters.group.checkbox+":not(:checked)");b.forEach(function(a){a.checked=!0})});f.on(d.default.events.activate,e.default.filters.group.clear,function(){var b=a.querySelectorAll(e.default.filters.group.checkbox+":checked");b.forEach(function(a){a.checked=!1})});f.on(d.default.events.activate,e.default.filters.group.trigger,function(){var b=a.querySelector(e.default.filters.group.trigger),d=a.querySelector(e.default.filters.group.popover);new c.default(b,d,{placement:"bottom"});d.classList.remove("hidden");b.classList.add("btn-outline-primary");b.classList.remove("btn-primary");b.setAttribute("aria-expanded",!0)});f.on(d.default.events.activate,e.default.filters.group.save,function(){h("#filter-groups-popover")})};a.init=g});
define ("forumreport_summary/filters",["exports","jquery","core/popper","core/custom_interaction_events","forumreport_summary/selectors","core/yui","core/ajax"],function(a,b,c,d,e,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=h(b);c=h(c);d=h(d);e=h(e);f=h(f);g=h(g);function h(a){return a&&a.__esModule?a:{default:a}}var i=function(a){var h=(0,b.default)(a);(0,b.default)(document).ready(function(){(0,b.default)(".loading-icon").hide();(0,b.default)("#summaryreport").removeClass("hidden")});var i=function(a){var c=(0,b.default)("#filtersform").attr("action");if(a){a.preventDefault();var d=a.target.search.substr(1);c+="&"+d}(0,b.default)("#filtersform").attr("action",c);(0,b.default)("#filtersform").submit()};(0,b.default)(".resettable").on("click","a",function(a){i(a)});(0,b.default)("thead").on("click","a",function(a){i(a)});(0,b.default)(".pagination").on("click","a",function(a){i(a)});var j=function(a){(0,b.default)(a).addClass("hidden");i(!1)},k=function(a){var b=document.querySelector(a),d=document.querySelector(e.default.filters.date.calendar);d.style.removeProperty("z-index");new c.default(b,d,{placement:"bottom"})},l=function(a){if(document.querySelector("[data-openfilter=\"true\"]")){return!1}a.target.setAttribute("data-openfilter","true");return!0};h.on(d.default.events.activate,e.default.filters.group.selectall,function(){var b=a.querySelectorAll(e.default.filters.group.checkbox+":not(:checked)");b.forEach(function(a){a.checked=!0})});h.on(d.default.events.activate,e.default.filters.group.clear,function(){var b=a.querySelectorAll(e.default.filters.group.checkbox+":checked");b.forEach(function(a){a.checked=!1})});h.on(d.default.events.activate,e.default.filters.group.trigger,function(b){if(!l(b)){return!1}var d=a.querySelector(e.default.filters.group.trigger),f=a.querySelector(e.default.filters.group.popover);new c.default(d,f,{placement:"bottom"});f.classList.remove("hidden");d.classList.add("btn-outline-primary");d.classList.remove("btn-primary");d.setAttribute("aria-expanded",!0);return!0});h.on(d.default.events.activate,e.default.filters.group.save,function(){j("#filter-groups-popover")});h.on(d.default.events.activate,e.default.filters.date.trigger,function(b){if(!l(b)){return!1}var d=a.querySelector(e.default.filters.date.trigger),f=a.querySelector(e.default.filters.date.popover);new c.default(d,f,{placement:"bottom"});f.classList.remove("hidden");f.querySelector("[name=\"filterdatefrompopover[enabled]\"]").focus();d.classList.add("btn-outline-primary");d.classList.remove("btn-primary");d.setAttribute("aria-expanded",!0);return!0});h.on(d.default.events.activate,e.default.filters.date.save,function(){var b=document.forms.filtersform,c=a.querySelector(e.default.filters.date.popover),d=c.querySelector("[name=\"filterdatefrompopover[enabled]\"]").checked?1:0,h=c.querySelector("[name=\"filterdatetopopover[enabled]\"]").checked?1:0;f.default.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()});if(!d&&!h){b.elements["datefrom[timestamp]"].value=0;b.elements["datefrom[enabled]"].value=d;b.elements["dateto[timestamp]"].value=0;b.elements["dateto[enabled]"].value=h;j("#filter-dates-popover")}else{var i={data:[]};if(d){i.data.push({key:"from",year:c.querySelector("[name=\"filterdatefrompopover[year]\"]").value,month:c.querySelector("[name=\"filterdatefrompopover[month]\"]").value,day:c.querySelector("[name=\"filterdatefrompopover[day]\"]").value,hour:0,minute:0})}if(h){i.data.push({key:"to",year:c.querySelector("[name=\"filterdatetopopover[year]\"]").value,month:c.querySelector("[name=\"filterdatetopopover[month]\"]").value,day:c.querySelector("[name=\"filterdatetopopover[day]\"]").value,hour:23,minute:59})}g.default.call([{methodname:"core_calendar_get_timestamps",args:i}])[0].done(function(a){var c=0,e=0;a.timestamps.forEach(function(a){if("from"===a.key){c=a.timestamp}else if("to"===a.key){e=a.timestamp}});if(0<e&&c>e){var f=document.getElementById("dates-filter-warning");f.classList.remove("hidden");f.classList.add("d-block")}else{b.elements["datefrom[timestamp]"].value=c;b.elements["datefrom[enabled]"].value=d;b.elements["dateto[timestamp]"].value=e;b.elements["dateto[enabled]"].value=h;j("#filter-dates-popover")}})}});h.on(d.default.events.activate,e.default.filters.date.calendariconfrom,function(){k(e.default.filters.date.calendariconfrom)});h.on(d.default.events.activate,e.default.filters.date.calendariconto,function(){k(e.default.filters.date.calendariconto)})};a.init=i});
//# sourceMappingURL=filters.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
define ("forumreport_summary/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={filters:{group:{checkbox:"[data-region=\"filter-groups\"] input[type=\"checkbox\"]",clear:"[data-region=\"filter-groups\"] .filter-clear",popover:"#filter-groups-popover",save:"[data-region=\"filter-groups\"] .filter-save",selectall:"[data-region=\"filter-groups\"] .select-all",trigger:"#filter-groups-button"}}};return a.default});
define ("forumreport_summary/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={filters:{group:{checkbox:"[data-region=\"filter-groups\"] input[type=\"checkbox\"]",clear:"[data-region=\"filter-groups\"] .filter-clear",popover:"#filter-groups-popover",save:"[data-region=\"filter-groups\"] .filter-save",selectall:"[data-region=\"filter-groups\"] .select-all",trigger:"#filter-groups-button"},date:{calendar:"#dateselector-calendar-panel",calendariconfrom:"#id_filterdatefrompopover_calendar",calendariconto:"#id_filterdatetopopover_calendar",popover:"#filter-dates-popover",save:"[data-region=\"filter-dates\"] .filter-save",trigger:"#filter-dates-button"}}};return a.default});
//# sourceMappingURL=selectors.min.js.map

View File

@ -1 +1 @@
{"version":3,"sources":["../src/selectors.js"],"names":["filters","group","checkbox","clear","popover","save","selectall","trigger"],"mappings":"yJAwBe,CACXA,OAAO,CAAE,CACLC,KAAK,CAAE,CACHC,QAAQ,CAAE,0DADP,CAEHC,KAAK,CAAE,+CAFJ,CAGHC,OAAO,CAAE,wBAHN,CAIHC,IAAI,CAAE,8CAJH,CAKHC,SAAS,CAAE,6CALR,CAMHC,OAAO,CAAE,uBANN,CADF,CADE,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for the forum summary report.\n *\n * @module forumreport_summary/selectors\n * @package forumreport_summary\n * @copyright 2019 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n filters: {\n group: {\n checkbox: '[data-region=\"filter-groups\"] input[type=\"checkbox\"]',\n clear: '[data-region=\"filter-groups\"] .filter-clear',\n popover: '#filter-groups-popover',\n save: '[data-region=\"filter-groups\"] .filter-save',\n selectall: '[data-region=\"filter-groups\"] .select-all',\n trigger: '#filter-groups-button',\n }\n }\n};\n"],"file":"selectors.min.js"}
{"version":3,"sources":["../src/selectors.js"],"names":["filters","group","checkbox","clear","popover","save","selectall","trigger","date","calendar","calendariconfrom","calendariconto"],"mappings":"yJAwBe,CACXA,OAAO,CAAE,CACLC,KAAK,CAAE,CACHC,QAAQ,CAAE,0DADP,CAEHC,KAAK,CAAE,+CAFJ,CAGHC,OAAO,CAAE,wBAHN,CAIHC,IAAI,CAAE,8CAJH,CAKHC,SAAS,CAAE,6CALR,CAMHC,OAAO,CAAE,uBANN,CADF,CASLC,IAAI,CAAE,CACFC,QAAQ,CAAE,8BADR,CAEFC,gBAAgB,CAAE,oCAFhB,CAGFC,cAAc,CAAE,kCAHd,CAIFP,OAAO,CAAE,uBAJP,CAKFC,IAAI,CAAE,6CALJ,CAMFE,OAAO,CAAE,sBANP,CATD,CADE,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for the forum summary report.\n *\n * @module forumreport_summary/selectors\n * @package forumreport_summary\n * @copyright 2019 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n filters: {\n group: {\n checkbox: '[data-region=\"filter-groups\"] input[type=\"checkbox\"]',\n clear: '[data-region=\"filter-groups\"] .filter-clear',\n popover: '#filter-groups-popover',\n save: '[data-region=\"filter-groups\"] .filter-save',\n selectall: '[data-region=\"filter-groups\"] .select-all',\n trigger: '#filter-groups-button',\n },\n date: {\n calendar: '#dateselector-calendar-panel',\n calendariconfrom: '#id_filterdatefrompopover_calendar',\n calendariconto: '#id_filterdatetopopover_calendar',\n popover: '#filter-dates-popover',\n save: '[data-region=\"filter-dates\"] .filter-save',\n trigger: '#filter-dates-button',\n }\n }\n};\n"],"file":"selectors.min.js"}

View File

@ -26,6 +26,8 @@ import $ from 'jquery';
import Popper from 'core/popper';
import CustomEvents from 'core/custom_interaction_events';
import Selectors from 'forumreport_summary/selectors';
import Y from 'core/yui';
import Ajax from 'core/ajax';
export const init = (root) => {
let jqRoot = $(root);
@ -40,8 +42,8 @@ export const init = (root) => {
// Generic filter handlers.
// Called to override click event to trigger a proper generate request with filtering.
var generateWithFilters = (event) => {
var newLink = $('#filtersform').attr('action');
const generateWithFilters = (event) => {
let newLink = $('#filtersform').attr('action');
if (event) {
event.preventDefault();
@ -70,7 +72,7 @@ export const init = (root) => {
});
// Submit report via filter
var submitWithFilter = (containerelement) => {
const submitWithFilter = (containerelement) => {
// Close the container (eg popover).
$(containerelement).addClass('hidden');
@ -78,6 +80,25 @@ export const init = (root) => {
generateWithFilters(false);
};
// Use popper to override date mform calendar position.
const updateCalendarPosition = (referenceid) => {
let referenceElement = document.querySelector(referenceid),
popperContent = document.querySelector(Selectors.filters.date.calendar);
popperContent.style.removeProperty("z-index");
new Popper(referenceElement, popperContent, {placement: 'bottom'});
};
// Call when opening filter to ensure only one can be activated.
const canOpenFilter = (event) => {
if (document.querySelector('[data-openfilter="true"]')) {
return false;
}
event.target.setAttribute('data-openfilter', "true");
return true;
};
// Groups filter specific handlers.
// Event handler for clicking select all groups.
@ -98,9 +119,13 @@ export const init = (root) => {
});
// Event handler for showing groups filter popover.
jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function() {
jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function(event) {
if (!canOpenFilter(event)) {
return false;
}
// Create popover.
var referenceElement = root.querySelector(Selectors.filters.group.trigger),
let referenceElement = root.querySelector(Selectors.filters.group.trigger),
popperContent = root.querySelector(Selectors.filters.group.popover);
new Popper(referenceElement, popperContent, {placement: 'bottom'});
@ -114,10 +139,128 @@ export const init = (root) => {
// Let screen readers know that it's now expanded.
referenceElement.setAttribute('aria-expanded', true);
return true;
});
// Event handler to click save groups filter.
jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.save, function() {
submitWithFilter('#filter-groups-popover');
});
// Dates filter specific handlers.
// Event handler for showing dates filter popover.
jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.trigger, function(event) {
if (!canOpenFilter(event)) {
return false;
}
// Create popover.
let referenceElement = root.querySelector(Selectors.filters.date.trigger),
popperContent = root.querySelector(Selectors.filters.date.popover);
new Popper(referenceElement, popperContent, {placement: 'bottom'});
// Show popover and move focus.
popperContent.classList.remove('hidden');
popperContent.querySelector('[name="filterdatefrompopover[enabled]"]').focus();
// Change to outlined button.
referenceElement.classList.add('btn-outline-primary');
referenceElement.classList.remove('btn-primary');
// Let screen readers know that it's now expanded.
referenceElement.setAttribute('aria-expanded', true);
return true;
});
// Event handler to save dates filter.
jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.save, function() {
// Populate the hidden form inputs to submit the data.
let filtersForm = document.forms.filtersform;
const datesPopover = root.querySelector(Selectors.filters.date.popover);
const fromEnabled = datesPopover.querySelector('[name="filterdatefrompopover[enabled]"]').checked ? 1 : 0;
const toEnabled = datesPopover.querySelector('[name="filterdatetopopover[enabled]"]').checked ? 1 : 0;
// Disable the mform checker to prevent unsubmitted form warning to the user when closing the popover.
Y.use('moodle-core-formchangechecker', function() {
M.core_formchangechecker.reset_form_dirty_state();
});
if (!fromEnabled && !toEnabled) {
// Update the elements in the filter form.
filtersForm.elements['datefrom[timestamp]'].value = 0;
filtersForm.elements['datefrom[enabled]'].value = fromEnabled;
filtersForm.elements['dateto[timestamp]'].value = 0;
filtersForm.elements['dateto[enabled]'].value = toEnabled;
// Submit the filter values and re-generate report.
submitWithFilter('#filter-dates-popover');
} else {
let args = {data: []};
if (fromEnabled) {
args.data.push({
'key': 'from',
'year': datesPopover.querySelector('[name="filterdatefrompopover[year]"]').value,
'month': datesPopover.querySelector('[name="filterdatefrompopover[month]"]').value,
'day': datesPopover.querySelector('[name="filterdatefrompopover[day]"]').value,
'hour': 0,
'minute': 0
});
}
if (toEnabled) {
args.data.push({
'key': 'to',
'year': datesPopover.querySelector('[name="filterdatetopopover[year]"]').value,
'month': datesPopover.querySelector('[name="filterdatetopopover[month]"]').value,
'day': datesPopover.querySelector('[name="filterdatetopopover[day]"]').value,
'hour': 23,
'minute': 59
});
}
const request = {
methodname: 'core_calendar_get_timestamps',
args: args
};
Ajax.call([request])[0].done(function(result) {
let fromTimestamp = 0,
toTimestamp = 0;
result['timestamps'].forEach(function(data){
if (data.key === 'from') {
fromTimestamp = data.timestamp;
} else if (data.key === 'to') {
toTimestamp = data.timestamp;
}
});
// Display an error if the from date is later than the do date.
if (toTimestamp > 0 && fromTimestamp > toTimestamp) {
const warningdiv = document.getElementById('dates-filter-warning');
warningdiv.classList.remove('hidden');
warningdiv.classList.add('d-block');
} else {
filtersForm.elements['datefrom[timestamp]'].value = fromTimestamp;
filtersForm.elements['datefrom[enabled]'].value = fromEnabled;
filtersForm.elements['dateto[timestamp]'].value = toTimestamp;
filtersForm.elements['dateto[enabled]'].value = toEnabled;
// Submit the filter values and re-generate report.
submitWithFilter('#filter-dates-popover');
}
});
}
});
jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconfrom, function() {
updateCalendarPosition(Selectors.filters.date.calendariconfrom);
});
jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconto, function() {
updateCalendarPosition(Selectors.filters.date.calendariconto);
});
};

View File

@ -31,6 +31,14 @@ export default {
save: '[data-region="filter-groups"] .filter-save',
selectall: '[data-region="filter-groups"] .select-all',
trigger: '#filter-groups-button',
},
date: {
calendar: '#dateselector-calendar-panel',
calendariconfrom: '#id_filterdatefrompopover_calendar',
calendariconto: '#id_filterdatetopopover_calendar',
popover: '#filter-dates-popover',
save: '[data-region="filter-dates"] .filter-save',
trigger: '#filter-dates-button',
}
}
};

View File

@ -0,0 +1,53 @@
<?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/>.
/**
* The mform used by the forum summary report dates filter.
*
* @package forumreport_summary
* @copyright 2019 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace forumreport_summary\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/lib/formslib.php');
/**
* The mform class for creating the forum summary report dates filter.
*
* @copyright 2019 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates_filter_form extends \moodleform {
/**
* The form definition.
*
*/
public function definition() {
$attributes = [
'class' => 'align-items-center',
];
// From date field.
$this->_form->addElement('date_selector', 'filterdatefrompopover', get_string('from'), ['optional' => true], $attributes);
// To date field.
$this->_form->addElement('date_selector', 'filterdatetopopover', get_string('to'), ['optional' => true], $attributes);
}
}

View File

@ -29,6 +29,7 @@ use renderable;
use renderer_base;
use stdClass;
use templatable;
use forumreport_summary;
defined('MOODLE_INTERNAL') || die();
@ -69,6 +70,20 @@ class filters implements renderable, templatable {
*/
protected $groupsselected = [];
/**
* HTML for dates filter.
*
* @var array $datesdata
*/
protected $datesdata = [];
/**
* Text to display on the dates filter button.
*
* @var string $datesbuttontext
*/
protected $datesbuttontext;
/**
* Builds renderable filter data.
*
@ -84,6 +99,11 @@ class filters implements renderable, templatable {
// Prepare groups filter data.
$groupsdata = $filterdata['groups'] ?? [];
$this->prepare_groups_data($groupsdata);
// Prepare dates filter data.
$datefromdata = $filterdata['datefrom'] ?? [];
$datetodata = $filterdata['dateto'] ?? [];
$this->prepare_dates_data($datefromdata, $datetodata);
}
/**
@ -129,6 +149,78 @@ class filters implements renderable, templatable {
$this->groupsselected = $groupsselected;
}
/**
* Prepares from date, to date and button text.
* Empty data will default to a disabled filter with today's date.
*
* @param array $datefromdata From date selected for filtering, and whether the filter is enabled.
* @param array $datetodata To date selected for filtering, and whether the filter is enabled.
* @return void.
*/
private function prepare_dates_data(array $datefromdata, array $datetodata): void {
$timezone = \core_date::get_user_timezone_object();
$calendartype = \core_calendar\type_factory::get_calendar_instance();
$timestamptoday = time();
$datetoday = $calendartype->timestamp_to_date_array($timestamptoday, $timezone);
// Prepare date/enabled data.
if (empty($datefromdata['enabled'])) {
$fromdate = $datetoday;
$fromtimestamp = $timestamptoday;
$fromenabled = false;
} else {
$fromdate = $calendartype->timestamp_to_date_array($datefromdata['timestamp'], $timezone);
$fromtimestamp = $datefromdata['timestamp'];
$fromenabled = true;
}
if (empty($datetodata['enabled'])) {
$todate = $datetoday;
$totimestamp = $timestamptoday;
$toenabled = false;
} else {
$todate = $calendartype->timestamp_to_date_array($datetodata['timestamp'], $timezone);
$totimestamp = $datetodata['timestamp'];
$toenabled = true;
}
$this->datesdata = [
'from' => [
'day' => $fromdate['mday'],
'month' => $fromdate['mon'],
'year' => $fromdate['year'],
'timestamp' => $fromtimestamp,
'enabled' => $fromenabled,
],
'to' => [
'day' => $todate['mday'],
'month' => $todate['mon'],
'year' => $todate['year'],
'timestamp' => $totimestamp,
'enabled' => $toenabled,
],
];
// Prepare button string data.
$displayformat = get_string('strftimedatemonthabbr', 'langconfig');
$fromdatestring = $calendartype->timestamp_to_date_string($fromtimestamp, $displayformat, $timezone, true, true);
$todatestring = $calendartype->timestamp_to_date_string($totimestamp, $displayformat, $timezone, true, true);
if ($fromenabled && $toenabled) {
$datestrings = [
'datefrom' => $fromdatestring,
'dateto' => $todatestring,
];
$this->datesbuttontext = get_string('filter:datesfromto', 'forumreport_summary', $datestrings);
} else if ($fromenabled) {
$this->datesbuttontext = get_string('filter:datesfrom', 'forumreport_summary', $fromdatestring);
} else if ($toenabled) {
$this->datesbuttontext = get_string('filter:datesto', 'forumreport_summary', $todatestring);
} else {
$this->datesbuttontext = get_string('filter:datesname', 'forumreport_summary');
}
}
/**
* Export data for use as the context of a mustache template.
*
@ -171,6 +263,33 @@ class filters implements renderable, templatable {
$output->hasgroups = false;
}
// Set date button and generate dates popover mform.
$datesformdata = [];
if ($this->datesdata['from']['enabled']) {
$datesformdata['filterdatefrompopover'] = $this->datesdata['from'];
}
if ($this->datesdata['to']['enabled']) {
$datesformdata['filterdatetopopover'] = $this->datesdata['to'];
}
$output->filterdatesname = $this->datesbuttontext;
$datesform = new forumreport_summary\form\dates_filter_form();
$datesform->set_data($datesformdata);
$output->filterdatesform = $datesform->render();
// Set dates filter data within filters form.
$disableddate = [
'day' => '',
'month' => '',
'year' => '',
'enabled' => '0',
];
$datefromdata = ['type' => 'from'] + ($this->datesdata['from']['enabled'] ? $this->datesdata['from'] : $disableddate);
$datetodata = ['type' => 'to'] + ($this->datesdata['to']['enabled'] ? $this->datesdata['to'] : $disableddate);
$output->filterdatesdata = [$datefromdata, $datetodata];
return $output;
}
}

View File

@ -44,6 +44,9 @@ class summary_table extends table_sql {
/** Groups filter type */
const FILTER_GROUPS = 2;
/** Dates filter type */
const FILTER_DATES = 3;
/** Table to store summary data extracted from the log table */
const LOG_SUMMARY_TEMP_TABLE = 'forum_report_summary_counts';
@ -146,23 +149,26 @@ class summary_table extends table_sql {
// Define configs.
$this->define_table_configs();
// Apply relevant filters.
$this->define_base_filter_sql();
$this->apply_filters($filters);
// Define the basic SQL data and object format.
$this->define_base_sql();
// Apply relevant filters.
$this->apply_filters($filters);
}
/**
* Provides the string name of each filter type.
* Provides the string name of each filter type, to be used by errors.
* Note: This does not use language strings as the value is injected into error strings.
*
* @param int $filtertype Type of filter
* @return string Name of the filter
*/
public function get_filter_name(int $filtertype): string {
protected function get_filter_name(int $filtertype): string {
$filternames = [
self::FILTER_FORUM => 'Forum',
self::FILTER_GROUPS => 'Groups',
self::FILTER_DATES => 'Dates',
];
return $filternames[$filtertype];
@ -362,6 +368,38 @@ class summary_table extends table_sql {
break;
case self::FILTER_DATES:
if (!isset($values['from']['enabled']) || !isset($values['to']['enabled']) ||
($values['from']['enabled'] && !isset($values['from']['timestamp'])) ||
($values['to']['enabled'] && !isset($values['to']['timestamp']))) {
$paramcounterror = true;
} else {
$this->sql->filterbase['dates'] = '';
$this->sql->filterbase['dateslog'] = '';
$this->sql->filterbase['dateslogparams'] = [];
// From date.
if ($values['from']['enabled']) {
// If the filter was enabled, include the date restriction.
// Needs to form part of the base join to posts, so will be injected by define_base_sql().
$this->sql->filterbase['dates'] .= " AND p.created >= :fromdate";
$this->sql->params['fromdate'] = $values['from']['timestamp'];
$this->sql->filterbase['dateslog'] .= ' AND timecreated >= :fromdate';
$this->sql->filterbase['dateslogparams']['fromdate'] = $values['from']['timestamp'];
}
// To date.
if ($values['to']['enabled']) {
// If the filter was enabled, include the date restriction.
// Needs to form part of the base join to posts, so will be injected by define_base_sql().
$this->sql->filterbase['dates'] .= " AND p.created <= :todate";
$this->sql->params['todate'] = $values['to']['timestamp'];
$this->sql->filterbase['dateslog'] .= ' AND timecreated <= :todate';
$this->sql->filterbase['dateslogparams']['todate'] = $values['to']['timestamp'];
}
}
break;
default:
throw new coding_exception("Report filter type '{$filtertype}' not found.");
break;
@ -385,6 +423,8 @@ class summary_table extends table_sql {
$this->is_downloadable(true);
$this->no_sorting('select');
$this->set_attribute('id', 'forumreport_summary_table');
$this->sql = new \stdClass();
$this->sql->params = [];
}
/**
@ -395,8 +435,6 @@ class summary_table extends table_sql {
protected function define_base_sql(): void {
global $USER;
$this->sql = new \stdClass();
$userfields = get_extra_user_fields($this->context);
$userfieldssql = \user_picture::fields('u', $userfields);
@ -429,7 +467,8 @@ class summary_table extends table_sql {
JOIN {forum_discussions} d ON d.forum = f.id
LEFT JOIN {forum_posts} p ON p.discussion = d.id
AND p.userid = ue.userid
' . $privaterepliessql . '
' . $privaterepliessql
. $this->sql->filterbase['dates'] . '
LEFT JOIN (
SELECT COUNT(fi.id) AS attcount, fi.itemid AS postid, fi.userid
FROM {files} fi
@ -457,7 +496,7 @@ class summary_table extends table_sql {
$this->sql->basefields .= ', SUM(CASE WHEN p.charcount IS NOT NULL THEN p.charcount ELSE 0 END) AS charcount';
}
$this->sql->params = [
$this->sql->params += [
'component' => 'mod_forum',
'courseid' => $this->cm->course,
] + $privaterepliesparams;
@ -467,7 +506,14 @@ class summary_table extends table_sql {
$this->sql->basewhere .= ' AND ue.userid = :userid';
$this->sql->params['userid'] = $this->userid;
}
}
/**
* Instantiate the properties to store filter values.
*
* @return void.
*/
protected function define_base_filter_sql(): void {
// Filter values will be populated separately where required.
$this->sql->filterfields = '';
$this->sql->filterfromjoins = '';
@ -533,6 +579,13 @@ class summary_table extends table_sql {
// Apply groups filter.
$this->add_filter(self::FILTER_GROUPS, $filters['groups']);
// Apply dates filter.
$datevalues = [
'from' => $filters['datefrom'],
'to' => $filters['dateto'],
];
$this->add_filter(self::FILTER_DATES, $datevalues);
}
/**
@ -614,11 +667,17 @@ class summary_table extends table_sql {
$logtable = $this->logreader->get_internal_log_table_name();
$nonanonymous = 'AND anonymous = 0';
}
$params = ['contextid' => $contextid];
// Apply dates filter if applied.
$datewhere = $this->sql->filterbase['dateslog'] ?? '';
$dateparams = $this->sql->filterbase['dateslogparams'] ?? [];
$params = ['contextid' => $contextid] + $dateparams;
$sql = "INSERT INTO {" . self::LOG_SUMMARY_TEMP_TABLE . "} (userid, viewcount)
SELECT userid, COUNT(*) AS viewcount
FROM {" . $logtable . "}
WHERE contextid = :contextid
$datewhere
$nonanonymous
GROUP BY userid";
$DB->execute($sql, $params);

View File

@ -36,6 +36,8 @@ $filters = [];
// Establish filter values.
$filters['forums'] = [$forumid];
$filters['groups'] = optional_param_array('filtergroups', [], PARAM_INT);
$filters['datefrom'] = optional_param_array('datefrom', ['enabled' => 0], PARAM_INT);
$filters['dateto'] = optional_param_array('dateto', ['enabled' => 0], PARAM_INT);
$download = optional_param('download', '', PARAM_ALPHA);

View File

@ -26,6 +26,12 @@ $string['attachmentcount'] = 'Number of attachments';
$string['charcount'] = 'Character count';
$string['viewcount'] = 'Number of views';
$string['earliestpost'] = 'Earliest post';
$string['filter:datesbuttonlabel'] = 'Open the dates filter';
$string['filter:datesname'] = 'Dates';
$string['filter:datesfrom'] = 'From {$a}';
$string['filter:datesfromto'] = '{$a->datefrom} - {$a->dateto}';
$string['filter:datesorderwarning'] = 'Please ensure the "From" date selected is earlier than the "To" date selected.';
$string['filter:datesto'] = 'To {$a}';
$string['filter:groupsbuttonlabel'] = 'Open the groups filter';
$string['filter:groupsname'] = 'Groups';
$string['filter:groupscountall'] = 'Groups (all)';

View File

@ -0,0 +1,50 @@
{{!
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/>.
}}
{{!
@template forumreport_summary/filter_dates
Summary report dates filter.
Example context (json):
{
"filterdatesname": "Dates",
"filterdatesdata": [
{
"type": "from",
"timestamp": "1571377510",
"enabled": "1"
},
{
"type": "to",
"timestamp": "1571377510",
"enabled": "1"
}
]
}
}}
<button type="button" id="filter-dates-button" class="btn btn-primary rounded p-1" aria-expanded="false"
aria-haspopup="true" aria-label="{{# str}} filter:datesbuttonlabel, forumreport_summary {{/ str}}">
{{filterdatesname}}
</button>
{{! Hidden dates fields to populate from visible mform in popover. }}
{{#filterdatesdata}}
<input type="hidden" name="date{{type}}[timestamp]" value="{{timestamp}}">
<input type="hidden" name="date{{type}}[enabled]" value="{{enabled}}">
{{/filterdatesdata}}

View File

@ -0,0 +1,40 @@
{{!
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/>.
}}
{{!
@template forumreport_summary/filter_dates
Summary report dates filter.
Example context (json):
{
"filterdatesform": "<div>HTML for date filters</div>"
}
}}
<div id="filter-dates-popover" class="popover filter-dates-popover mt-3 hidden">
<h3 class="popover-header">{{# str}} filter:datesname, forumreport_summary {{/ str}}</h3>
<div class="popover-body" data-region="filter-dates">
{{{filterdatesform}}}
<div id="dates-filter-warning" class="form-control-feedback text-danger hidden float-right">
{{# str}} filter:datesorderwarning, forumreport_summary {{/ str}}
</div>
<br>&nbsp;
<button type="button" class="filter-save float-right btn btn-link p-0" aria-label="{{# str}} save {{/ str}}">
<strong>{{# str}} save {{/ str}}</strong>
</button>
</div>
</div>

View File

@ -0,0 +1,70 @@
{{!
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/>.
}}
{{!
@template forumreport_summary/filter_groups
Summary report groups filter.
Example context (json):
{
"hasgroups": true,
"filtergroupsname" : "Groups (all)",
"filtergroups": [
{
"groupid": "1",
"groupname": "Group A",
"checked": true
},
{
"groupid": "3",
"groupname": "Group C",
"checked": false
}
]
}
}}
{{#hasgroups}}
<button type="button" id="filter-groups-button" class="btn btn-primary rounded p-1 ml-2" aria-expanded="false"
aria-haspopup="true" aria-label="{{# str}} filter:groupsbuttonlabel, forumreport_summary {{/ str}}">
{{filtergroupsname}}
</button>
{{! Groups filter popover }}
<div id="filter-groups-popover" class="popover m-t-1 hidden">
<h3 class="popover-header">{{# str}} filter:groupsname, forumreport_summary {{/ str}}</h3>
<div class="popover-body" data-region="filter-groups">
<div class="form-check filter-scrollable">
{{#filtergroups}}
<input id="filtergroups{{groupid}}" class="form-check-input" type="checkbox" name="filtergroups[]"
value="{{groupid}}" {{#checked}} checked="checked" {{/checked}}>
<label class="form-check-label pt-1" for="filtergroups{{groupid}}">{{groupname}}</label>
<br>
{{/filtergroups}}
</div>
<div class="filter-actions">
<button type="button" class="select-all btn btn-link p-0 pr-1" aria-label="{{# str}} selectall {{/ str}}">{{# str}} selectall {{/ str}}</button>
<div class="float-right">
<button type="button" class="filter-clear btn btn-link p-0 px-1" aria-label="{{# str}} clear {{/ str}}">{{# str}} clear {{/ str}}</button>
<button type="button" class="filter-save btn btn-link p-0" aria-label="{{# str}} save {{/ str}}">
<strong>{{# str}} save {{/ str}}</strong>
</button>
</div>
</div>
</div>
</div>
{{/hasgroups}}

View File

@ -35,47 +35,36 @@
"groupname": "Group C",
"checked": false
}
],
"filterdatesname": "Dates",
"filterdatesform": "<div>HTML for date filters</div>",
"filterdatesdata": [
{
"type": "from",
"timestamp": "510969600",
"enabled": "1"
},
{
"type": "to",
"timestamp": "725673600",
"enabled": "1"
}
]
}
}}
<div class="p-b-3" data-report-id="{{uniqid}}">
<div class="pb-4" data-report-id="{{uniqid}}">
<form id="filtersform" name="filtersform" method="post" action="{{actionurl}}">
<input type="hidden" name="submitted" value="true">
<div id="filtersbuttons">
{{#hasgroups}}
<button type="button" id="filter-groups-button" class="btn btn-primary rounded p-1" aria-expanded="false"
aria-haspopup="true" aria-label="{{# str}} filter:groupsbuttonlabel, forumreport_summary {{/ str}}">
{{filtergroupsname}}
</button>
{{! Start groups filter popover}}
<div id="filter-groups-popover" class="popover m-t-1 hidden">
<h3 class="popover-header">{{# str}} filter:groupsname, forumreport_summary {{/ str}}</h3>
<div class="popover-body" data-region="filter-groups">
<div class="form-check filter-scrollable">
{{#filtergroups}}
<input id="filtergroups{{groupid}}" class="form-check-input" type="checkbox" name="filtergroups[]"
value="{{groupid}}" {{#checked}} checked="checked" {{/checked}}>
<label class="form-check-label pt-1" for="filtergroups{{groupid}}">{{groupname}}</label>
<br>
{{/filtergroups}}
</div>
<div class="filter-actions">
<button type="button" class="select-all btn btn-link p-0 pr-1" aria-label="{{# str}} selectall {{/ str}}">{{# str}} selectall {{/ str}}</button>
<div class="float-right">
<button type="button" class="filter-clear btn btn-link p-0 px-1" aria-label="{{# str}} clear {{/ str}}">{{# str}} clear {{/ str}}</button>
<button type="button" class="filter-save btn btn-link p-0" aria-label="{{# str}} save {{/ str}}">
<strong>{{# str}} save {{/ str}}</strong>
</button>
</div>
</div>
</div>
</div>
{{! End groups filter popover}}
{{/hasgroups}}
<div id="filtersbuttons">
{{> forumreport_summary/filter_dates}}
{{> forumreport_summary/filter_groups}}
</div>
</form>
{{! Dates filter popover - mform must exist outside of the filtersform }}
{{> forumreport_summary/filter_dates_popover}}
</div>
{{#js}}

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2019090200;
$plugin->version = 2019101800;
$plugin->requires = 2019071900;
$plugin->component = 'forumreport_summary';

View File

@ -416,6 +416,16 @@ $author-image-margin-sm: 8px;
margin-bottom: 1em;
}
// Required to fit a date mform into a filter popover.
.filter-dates-popover {
width: 100%;
max-width: 41.5em;
.mform {
margin-left: -3em;
}
}
// End styling for mod_forum.
.maincalendar .calendarmonth td,

View File

@ -16561,6 +16561,12 @@ select {
max-height: 25em;
margin-bottom: 1em; }
.filter-dates-popover {
width: 100%;
max-width: 41.5em; }
.filter-dates-popover .mform {
margin-left: -3em; }
.maincalendar .calendarmonth td,
.maincalendar .calendarmonth th {
border: 1px dotted #dee2e6; }

View File

@ -16833,6 +16833,12 @@ select {
max-height: 25em;
margin-bottom: 1em; }
.filter-dates-popover {
width: 100%;
max-width: 41.5em; }
.filter-dates-popover .mform {
margin-left: -3em; }
.maincalendar .calendarmonth td,
.maincalendar .calendarmonth th {
border: 1px dotted #dee2e6; }