Commit bca6b06a authored by Huong Nguyen's avatar Huong Nguyen
Browse files

MDL-71953 calendar: Accessibility improvement for manage subscription

 - Create new web services for manage subscription (Update calendar subscription)
 - Modified delete subscription feature to use Web service.
 - Midified update subscription feature to use in-place editbale
 - Delete subscription feature now have a confirmation box before processing.
 - Fixed some accessibility issues
 - Used 'Delete' instead of 'Remove' for deleting subscriptions
parent 8885e22a
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.
// 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/>.
/**
* A module to handle Delete/Update operations of the manage subscription page.
*
* @module core_calendar/manage_subscriptions
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/
import * as CalendarSelectors from 'core_calendar/selectors';
import * as CalendarRepository from 'core_calendar/repository';
import * as Modal from 'core/modal_factory';
import * as ModalEvents from 'core/modal_events';
import {displayException, addNotification, fetchNotifications} from 'core/notification';
import Prefetch from 'core/prefetch';
import {get_string as getString} from 'core/str';
import {eventTypes} from 'core/local/inplace_editable/events';
/**
* Get subscription id for given element.
*
* @param {HTMLElement} element update/delete link
* @return {Number}
*/
const getSubscriptionId = element => {
return parseInt(element.closest('tr').dataset.subid);
};
/**
* Get subscription name for given element.
*
* @param {HTMLElement} element update/delete link
* @return {String}
*/
const getSubscriptionName = element => {
return element.closest('tr').dataset.subname;
};
/**
* Get subscription table row for subscription id.
*
* @param {string} subscriptionId Subscription id
* @return {Element}
*/
const getSubscriptionRow = subscriptionId => {
return document.querySelector(`tr[data-subid="${subscriptionId}"]`);
};
/**
* Create modal.
*
* @param {HTMLElement} element
* @param {string} messageCode Message code.
* @return {promise} Promise for modal
*/
const createModal = (element, messageCode) => {
const subscriptionName = getSubscriptionName(element);
return Modal.create({
type: Modal.types.SAVE_CANCEL,
title: getString('confirmation', 'admin'),
body: getString(messageCode, 'calendar', subscriptionName),
buttons: {
save: getString('yes')
},
}).then(modal => {
modal.getRoot().on(ModalEvents.hidden, () => {
element.focus();
});
modal.show();
return modal;
});
};
/**
* Response handler for delete action.
*
* @param {HTMLElement} element
* @param {Object} data
* @return {Promise}
*/
const responseHandlerForDelete = async(element, data) => {
const subscriptionName = getSubscriptionName(element);
const message = data.status ? await getString('subscriptionremoved', 'calendar', subscriptionName) : data.warnings[0].message;
const type = data.status ? 'info' : 'error';
return addNotification({message, type});
};
/**
* Register events for update/delete links.
*/
const registerEventListeners = () => {
document.addEventListener('click', e => {
const deleteAction = e.target.closest(CalendarSelectors.actions.deleteSubscription);
if (deleteAction) {
e.preventDefault();
const modalPromise = createModal(deleteAction, 'confirmsubscriptiondelete');
modalPromise.then(modal => {
modal.getRoot().on(ModalEvents.save, () => {
const subscriptionId = getSubscriptionId(deleteAction);
CalendarRepository.deleteSubscription(subscriptionId).then(data => {
const response = responseHandlerForDelete(deleteAction, data);
return response.then(() => {
const subscriptionRow = getSubscriptionRow(subscriptionId);
return subscriptionRow.remove();
});
}).catch(displayException);
});
return modal;
}).catch(displayException);
}
});
document.addEventListener(eventTypes.elementUpdated, e => {
const inplaceEditable = e.target;
if (inplaceEditable.getAttribute('data-component') == 'core_calendar') {
fetchNotifications();
}
});
};
/**
* Initialises.
*/
export const init = () => {
Prefetch.prefetchStrings('moodle', ['yes']);
Prefetch.prefetchStrings('core_admin', ['confirmation']);
Prefetch.prefetchStrings('core_calendar', ['confirmsubscriptiondelete', 'subscriptionremoved']);
registerEventListeners();
};
......@@ -196,3 +196,20 @@ export const getCourseGroupsData = (courseId) => {
return Ajax.call([request])[0];
};
/**
* Delete calendar subscription by id.
*
* @param {Number} subscriptionId The subscription id
* @return {promise}
*/
export const deleteSubscription = (subscriptionId) => {
const request = {
methodname: 'core_calendar_delete_subscription',
args: {
subscriptionid: subscriptionId
}
};
return Ajax.call([request])[0];
};
......@@ -49,6 +49,7 @@ define([], function() {
edit: '[data-action="edit"]',
remove: '[data-action="delete"]',
viewEvent: '[data-action="view-event"]',
deleteSubscription: '[data-action="delete-subscription"]',
},
elements: {
courseSelector: 'select[name="course"]',
......
......@@ -101,19 +101,6 @@ class event_exporter_base extends exporter {
if ($cm = $event->get_course_module()) {
$data->modulename = $cm->get('modname');
$data->instance = $cm->get('id');
$data->activityname = $cm->get('name');
$component = 'mod_' . $data->modulename;
if (!component_callback_exists($component, 'core_calendar_get_event_action_string')) {
$modulename = get_string('modulename', $data->modulename);
$data->activitystr = get_string('requiresaction', 'calendar', $modulename);
} else {
$data->activitystr = component_callback(
$component,
'core_calendar_get_event_action_string',
[$event->get_type()]
);
}
}
parent::__construct($data, $related);
......@@ -188,18 +175,6 @@ class event_exporter_base extends exporter {
'default' => null,
'null' => NULL_ALLOWED
],
'activityname' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'activitystr' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'instance' => [
'type' => PARAM_INT,
'optional' => true,
......
<?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/>.
/**
* Calendar external API for deleting the subscription.
*
* @package core_calendar
* @category external
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_calendar\external\subscription;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/calendar/lib.php');
use external_api;
use external_function_parameters;
use external_single_structure;
use external_value;
use external_warnings;
/**
* Calendar external API for deleting the subscription.
*
* @package core_calendar
* @category external
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delete extends external_api {
/**
* Describes the parameters for deleting the subscription.
*
* @return external_function_parameters
* @since Moodle 4.0
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'subscriptionid' => new external_value(PARAM_INT, 'The id of the subscription', VALUE_REQUIRED)
]);
}
/**
* External function to delete the calendar subscription.
*
* @param int $subscriptionid Subscription id.
* @return array
*/
public static function execute(int $subscriptionid): array {
[
'subscriptionid' => $subscriptionid
] = self::validate_parameters(self::execute_parameters(), [
'subscriptionid' => $subscriptionid
]);
$status = false;
$warnings = [];
if (calendar_can_edit_subscription($subscriptionid)) {
// Fetch the subscription from the database making sure it exists.
$sub = calendar_get_subscription($subscriptionid);
calendar_delete_subscription($subscriptionid);
$status = true;
} else {
$warnings = [
'item' => $subscriptionid,
'warningcode' => 'errordeletingsubscription',
'message' => get_string('nopermissions', 'error')
];
}
return [
'status' => $status,
'warnings' => $warnings
];
}
/**
* Describes the data returned from the external function.
*
* @return external_single_structure
* @since Moodle 4.0
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'warnings' => new external_warnings()
]);
}
}
<?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/>.
/**
* Class to display collection select for the refresh interval.
*
* @package core_calendar
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_calendar\output;
use core\output\inplace_editable;
class refreshintervalcollection extends inplace_editable {
/**
* Constructor.
*
* @param \stdClass $subscription Subscription object
*/
public function __construct(\stdClass $subscription) {
$collection = calendar_get_pollinterval_choices();
parent::__construct('core_calendar', 'refreshinterval', $subscription->id, true, null, $subscription->pollinterval, null,
get_string('pollinterval', 'calendar'));
$this->set_type_select($collection);
}
public static function update(int $subscriptionid, int $pollinterval) {
if (calendar_can_edit_subscription($subscriptionid)) {
$subscription = calendar_get_subscription($subscriptionid);
$subscription->pollinterval = $pollinterval;
calendar_update_subscription($subscription);
$tmpl = new self($subscription);
return $tmpl;
} else {
throw new \moodle_exception('nopermissions', 'error', '', get_string('managesubscriptions', 'calendar'));
}
}
}
......@@ -90,6 +90,23 @@ if (!empty($groupcourseid)) {
$data['eventtype'] = 'group';
$pageurl->param('groupcourseid', $groupcourseid);
}
if (!empty($category)) {
$pageurl->param('category', $category);
$managesubscriptionsurl->param('category', $category);
$data['category'] = $category;
$data['eventtype'] = 'category';
}
$heading = get_string('importcalendar', 'calendar');
$pagetitle = $course->shortname . ': ' . get_string('calendar', 'calendar') . ': ' . $heading;
$PAGE->set_title($pagetitle);
$PAGE->set_heading($heading);
$PAGE->set_url($pageurl);
$PAGE->set_pagelayout('admin');
$PAGE->navbar->add(get_string('managesubscriptions', 'calendar'), $managesubscriptionsurl);
$PAGE->navbar->add($heading);
$renderer = $PAGE->get_renderer('core_calendar');
$customdata = [
'courseid' => $course->id,
......@@ -109,7 +126,7 @@ if (!empty($formdata)) {
$calendar = $form->get_file_content('importfile');
$ical = new iCalendar();
$ical->unserialize($calendar);
$importresults = calendar_import_icalendar_events($ical, null, $subscriptionid);
$importresults = calendar_import_events_from_ical($ical, $subscriptionid);
} else {
try {
$importresults = calendar_update_subscription_events($subscriptionid);
......@@ -125,11 +142,9 @@ if (!empty($formdata)) {
if (!empty($formdata->categoryid)) {
$managesubscriptionsurl->param('category', $formdata->categoryid);
}
redirect($managesubscriptionsurl, $importresults);
redirect($managesubscriptionsurl, $renderer->render_import_result($importresults));
}
$renderer = $PAGE->get_renderer('core_calendar');
echo $OUTPUT->header();
echo $renderer->start_layout();
echo $OUTPUT->heading($heading);
......
......@@ -2721,12 +2721,12 @@ function calendar_add_event_allowed($event) {
*/
function calendar_get_pollinterval_choices() {
return array(
'0' => new \lang_string('never', 'calendar'),
HOURSECS => new \lang_string('hourly', 'calendar'),
DAYSECS => new \lang_string('daily', 'calendar'),
WEEKSECS => new \lang_string('weekly', 'calendar'),
'2628000' => new \lang_string('monthly', 'calendar'),
YEARSECS => new \lang_string('annually', 'calendar')
'0' => get_string('never', 'calendar'),
HOURSECS => get_string('hourly', 'calendar'),
DAYSECS => get_string('daily', 'calendar'),
WEEKSECS => get_string('weekly', 'calendar'),
'2628000' => get_string('monthly', 'calendar'),
YEARSECS => get_string('annually', 'calendar')
);
}
......@@ -2964,42 +2964,6 @@ function calendar_add_icalendar_event($event, $unused, $subscriptionid, $timezon
}
}
/**
* Update a subscription from the form data in one of the rows in the existing subscriptions table.
*
* @param int $subscriptionid The ID of the subscription we are acting upon.
* @param int $pollinterval The poll interval to use.
* @param int $action The action to be performed. One of update or remove.
* @throws dml_exception if invalid subscriptionid is provided
* @return string A log of the import progress, including errors
*/
function calendar_process_subscription_row($subscriptionid, $pollinterval, $action) {
// Fetch the subscription from the database making sure it exists.
$sub = calendar_get_subscription($subscriptionid);
// Update or remove the subscription, based on action.
switch ($action) {
case CALENDAR_SUBSCRIPTION_UPDATE:
// Skip updating file subscriptions.
if (empty($sub->url)) {
break;
}
$sub->pollinterval = $pollinterval;
calendar_update_subscription($sub);
// Update the events.
return "<p>" . get_string('subscriptionupdated', 'calendar', $sub->name) . "</p>" .
calendar_update_subscription_events($subscriptionid);
case CALENDAR_SUBSCRIPTION_REMOVE:
calendar_delete_subscription($subscriptionid);
return get_string('subscriptionremoved', 'calendar', $sub->name);
break;
default:
break;
}
return '';
}
/**
* Delete subscription and all related events.
*
......@@ -3052,6 +3016,7 @@ function calendar_get_icalendar($url) {
global $CFG;
require_once($CFG->libdir . '/filelib.php');
require_once($CFG->libdir . '/bennu/bennu.inc.php');
$curl = new \curl();
$curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5));
......@@ -3072,17 +3037,17 @@ function calendar_get_icalendar($url) {
* Import events from an iCalendar object into a course calendar.
*
* @param iCalendar $ical The iCalendar object.
* @param int $unused Deprecated
* @param int $subscriptionid The subscription ID.
* @return string A log of the import progress, including errors.
* @param int|null $subscriptionid The subscription ID.
* @return array A log of the import progress, including errors.
*/
function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid = null) {
function calendar_import_events_from_ical(iCalendar $ical, int $subscriptionid = null): array {
global $DB;
$return = '';
$errors = [];
$eventcount = 0;
$updatecount = 0;
$skippedcount = 0;
$deletedcount = 0;
// Large calendars take a while...
if (!CLI_SCRIPT) {
......@@ -3111,18 +3076,15 @@ function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid
$skippedcount++;
break;
case 0:
$return .= '<p>' . get_string('erroraddingevent', 'calendar') . ': ';
if (empty($event->properties['SUMMARY'])) {
$return .= '(' . get_string('notitle', 'calendar') . ')';
$errors[] = '(' . get_string('notitle', 'calendar') . ')';
} else {
$return .= $event->properties['SUMMARY'][0]->value;
$errors[] = $event->properties['SUMMARY'][0]->value;
}
$return .= "</p>\n";
break;
}
}
$return .= html_writer::start_tag('ul');
$existing = $DB->get_field('event_subscriptions', 'lastupdated', ['id' => $subscriptionid]);
if (!empty($existing)) {
$eventsuuids = $DB->get_records_menu('event', ['subscriptionid' => $subscriptionid], '', 'id, uuid');
......@@ -3137,16 +3099,21 @@ function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid
}
if (!empty($tobedeleted)) {
$DB->delete_records_list('event', 'id', $tobedeleted);
$return .= html_writer::tag('li', get_string('eventsdeleted', 'calendar', count($tobedeleted)));
$deletedcount = count($tobedeleted);
}
}
}
$return .= html_writer::tag('li', get_string('eventsimported', 'calendar', $eventcount));
$return .= html_writer::tag('li', get_string('eventsskipped', 'calendar', $skippedcount));
$return .= html_writer::tag('li', get_string('eventsupdated', 'calendar', $updatecount));
$return .= html_writer::end_tag('ul');
return $return;
$result = [
'eventsimported' => $eventcount,
'eventsskipped' => $skippedcount,
'eventsupdated' => $updatecount,
'eventsdeleted' => $deletedcount,
'haserror' => !empty($errors),
'errors' => $errors,
];
return $result;
}
/**
......@@ -3164,7 +3131,7 @@ function calendar_update_subscription_events($subscriptionid) {
}
$ical = calendar_get_icalendar($sub->url);
$return = calendar_import_icalendar_events($ical, null, $subscriptionid);
$return = calendar_import_events_from_ical($ical, $subscriptionid);
$sub->lastupdated = time();
calendar_update_subscription($sub);
......@@ -3979,3 +3946,36 @@ function calendar_get_export_import_link_params(): array {
return $params;
}
/**
* Implements the inplace editable feature.
*
* @param string $itemtype Type of the inplace editable element
* @param int $itemid Id of the item to edit
* @param int $newvalue New value of the item
* @return \core\output\inplace_editable
*/
function calendar_inplace_editable(string $itemtype, int $itemid, int $newvalue): \core\output\inplace_editable {
global $OUTPUT;
if ($itemtype === 'refreshinterval') {
$subscription = calendar_get_subscription($itemid);
$context = calendar_get_calendar_context($subscription);
\external_api::validate_context($context);
$updateresult = \core_calendar\output\refreshintervalcollection::update($itemid, $newvalue);
$refreshresults = calendar_update_subscription_events($itemid);
\core\notification::add($OUTPUT->render_from_template(
'core_calendar/subscription_update_result',
array_merge($refreshresults, [