" . get_string('subscriptionupdated', 'calendar', $sub->name) . "
" . - 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 .= '' . 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 .= "
\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, [ + 'subscriptionname' => s($subscription->name), + ]) + ), \core\notification::INFO); + + return $updateresult; + } + + \external_api::validate_context(context_system::instance()); +} diff --git a/calendar/managesubscriptions.php b/calendar/managesubscriptions.php index 1410bd3e651..316b1f6b395 100644 --- a/calendar/managesubscriptions.php +++ b/calendar/managesubscriptions.php @@ -30,10 +30,6 @@ require_once($CFG->dirroot.'/calendar/lib.php'); // Required use. $courseid = optional_param('course', null, PARAM_INT); $categoryid = optional_param('category', null, PARAM_INT); -// Used for processing subscription actions. -$subscriptionid = optional_param('id', 0, PARAM_INT); -$pollinterval = optional_param('pollinterval', 0, PARAM_INT); -$action = optional_param('action', '', PARAM_INT); $url = new moodle_url('/calendar/managesubscriptions.php'); if ($courseid != SITEID && !empty($courseid)) { @@ -74,22 +70,6 @@ if (!calendar_user_can_add_event($course)) { $PAGE->navbar->add(get_string('managesubscriptions', 'calendar')); -if (!empty($subscriptionid)) { - // The user is wanting to perform an action upon an existing subscription. - require_sesskey(); // Must have sesskey for all actions. - if (calendar_can_edit_subscription($subscriptionid)) { - try { - $importresults = calendar_process_subscription_row($subscriptionid, $pollinterval, $action); - redirect($PAGE->url, $importresults); - } catch (moodle_exception $e) { - // If exception caught, then user should be redirected to page where he/she came from. - print_error($e->errorcode, $e->module, $PAGE->url); - } - } else { - print_error('nopermissions', 'error', $PAGE->url, get_string('managesubscriptions', 'calendar')); - } -} - $types = calendar_get_allowed_event_types($courseid); $searches = []; diff --git a/calendar/renderer.php b/calendar/renderer.php index 6c26be2d2e9..5268000dc53 100644 --- a/calendar/renderer.php +++ b/calendar/renderer.php @@ -371,8 +371,6 @@ class core_calendar_renderer extends plugin_renderer_base { get_string('colpoll', 'calendar'), get_string('colactions', 'calendar') ); - $table->align = array('left', 'left', 'left', 'left', 'left'); - $table->width = '100%'; $table->data = array(); $table->id = 'subscription_details_table'; @@ -394,20 +392,29 @@ class core_calendar_renderer extends plugin_renderer_base { } $type = $sub->eventtype . 'events'; + $calendarname = new html_table_cell($label); + $calendarname->header = true; - $table->data[] = new html_table_row(array( - new html_table_cell($label), + $tablerow = new html_table_row(array( + $calendarname, new html_table_cell($lastupdated), new html_table_cell(get_string($type, 'calendar')), new html_table_cell($this->render_subscription_update_interval($sub)), - new html_table_cell($this->subscription_action_form($sub)) + new html_table_cell($this->subscription_action_links()) )); + $tablerow->attributes += [ + 'data-subid' => $sub->id, + 'data-subname' => $sub->name + ]; + $table->data[] = $tablerow; } $out = $this->output->box_start('generalbox calendarsubs'); $out .= html_writer::table($table); $out .= $this->output->box_end(); + + $this->page->requires->js_call_amd('core_calendar/manage_subscriptions', 'init'); return $out; } @@ -418,51 +425,24 @@ class core_calendar_renderer extends plugin_renderer_base { * @return string */ protected function render_subscription_update_interval(stdClass $subscription): string { - // Assemble form for the subscription row. - $output = html_writer::start_tag('form', - ['action' => new moodle_url('/calendar/managesubscriptions.php', calendar_get_export_import_link_params()), - 'method' => 'post']); if (empty($subscription->url)) { - $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'pollinterval', 'value' => '0']); - } else { - // Assemble pollinterval control. - $output .= html_writer::start_div(); - $output .= html_writer::start_tag('select', ['name' => 'pollinterval', 'class' => 'custom-select']); - foreach (calendar_get_pollinterval_choices() as $k => $v) { - $attributes = array(); - if ($k == $subscription->pollinterval) { - $attributes['selected'] = 'selected'; - } - $attributes['value'] = $k; - $output .= html_writer::tag('option', $v, $attributes); - } - $output .= html_writer::end_tag('select'); - $output .= html_writer::end_div(); + return ''; } - return $output; + $tmpl = new \core_calendar\output\refreshintervalcollection($subscription); + return $this->output->render_from_template('core/inplace_editable', $tmpl->export_for_template($this->output)); } /** * Creates a form to perform actions on a given subscription. * - * @param stdClass $subscription * @return string */ - protected function subscription_action_form($subscription) { - $html = html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())); - $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $subscription->id)); - $html .= html_writer::start_tag('div', array('class' => 'btn-group float-left')); - if (!empty($subscription->url)) { - $html .= html_writer::tag('button', get_string('update'), array('type' => 'submit', 'name' => 'action', - 'class' => 'btn btn-link', - 'value' => CALENDAR_SUBSCRIPTION_UPDATE)); - } - $html .= html_writer::tag('button', get_string('remove'), array('type' => 'submit', 'name' => 'action', - 'class' => 'btn btn-link', - 'value' => CALENDAR_SUBSCRIPTION_REMOVE)); + protected function subscription_action_links(): string { + $html = html_writer::start_tag('div', array('class' => 'btn-group float-left')); + $html .= html_writer::span(html_writer::link('#', get_string('delete'), + ['data-action' => 'delete-subscription']), ''); $html .= html_writer::end_tag('div'); - $html .= html_writer::end_tag('form'); return $html; } @@ -477,4 +457,23 @@ class core_calendar_renderer extends plugin_renderer_base { ]; return $this->render_from_template('core_calendar/event_filter', $data); } + + /** + * Render the calendar import result. + * + * @param array $result Import result + * @return string|null + */ + public function render_import_result(array $result): ?string { + $data = [ + 'eventsimported' => $result['eventsimported'], + 'eventsskipped' => $result['eventsskipped'], + 'eventsupdated' => $result['eventsupdated'], + 'eventsdeleted' => $result['eventsdeleted'], + 'haserror' => $result['haserror'], + 'errors' => $result['errors'] + ]; + + return $this->render_from_template('core_calendar/subscription_update_result', $data); + } } diff --git a/calendar/templates/subscription_update_result.mustache b/calendar/templates/subscription_update_result.mustache new file mode 100644 index 00000000000..85085c920e2 --- /dev/null +++ b/calendar/templates/subscription_update_result.mustache @@ -0,0 +1,61 @@ +{{! + 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{{#str}} subscriptionupdated, calendar, {{subscriptionname}} {{/str}}
+{{/subscriptionname}} +{{#str}} erroraddingevent, calendar {{/str}}
+" . get_string('subscriptionupdated', 'calendar', $sub->name) . "
" . + calendar_update_subscription_events($subscriptionid); + case CALENDAR_SUBSCRIPTION_REMOVE: + calendar_delete_subscription($subscriptionid); + return get_string('subscriptionremoved', 'calendar', $sub->name); + break; + default: + break; + } + return ''; +} + +/** + * 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. + */ +function calendar_import_icalendar_events($ical, $unused = null, $subscriptionid = null) { + debugging('calendar_import_icalendar_events() is deprecated. Please use calendar_import_events_from_ical() instead.', + DEBUG_DEVELOPER); + global $DB; + + $return = ''; + $eventcount = 0; + $updatecount = 0; + $skippedcount = 0; + + // Large calendars take a while... + if (!CLI_SCRIPT) { + \core_php_time_limit::raise(300); + } + + // Grab the timezone from the iCalendar file to be used later. + if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) { + $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value; + } else { + $timezone = 'UTC'; + } + + $icaluuids = []; + foreach ($ical->components['VEVENT'] as $event) { + $icaluuids[] = $event->properties['UID'][0]->value; + $res = calendar_add_icalendar_event($event, null, $subscriptionid, $timezone); + switch ($res) { + case CALENDAR_IMPORT_EVENT_UPDATED: + $updatecount++; + break; + case CALENDAR_IMPORT_EVENT_INSERTED: + $eventcount++; + break; + case CALENDAR_IMPORT_EVENT_SKIPPED: + $skippedcount++; + break; + case 0: + $return .= '' . get_string('erroraddingevent', 'calendar') . ': '; + if (empty($event->properties['SUMMARY'])) { + $return .= '(' . get_string('notitle', 'calendar') . ')'; + } else { + $return .= $event->properties['SUMMARY'][0]->value; + } + $return .= "
\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'); + + $icaleventscount = count($icaluuids); + $tobedeleted = []; + if (count($eventsuuids) > $icaleventscount) { + foreach ($eventsuuids as $eventid => $eventuuid) { + if (!in_array($eventuuid, $icaluuids)) { + $tobedeleted[] = $eventid; + } + } + if (!empty($tobedeleted)) { + $DB->delete_records_list('event', 'id', $tobedeleted); + $return .= html_writer::tag('li', get_string('eventsdeleted', 'calendar', 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; +} diff --git a/lib/tests/calendar_cron_task_test.php b/lib/tests/calendar_cron_task_test.php index 8b9c3a177bd..0a2bf8360c0 100644 --- a/lib/tests/calendar_cron_task_test.php +++ b/lib/tests/calendar_cron_task_test.php @@ -58,9 +58,13 @@ class core_calendar_cron_task_testcase extends advanced_testcase { $subscription->lastupdated = 0; calendar_add_subscription($subscription); - $this->expectOutputRegex('/.* events were imported.* events were skipped.* events were updated/'); $task = new \core\task\calendar_cron_task(); + ob_start(); $task->execute(); + $output = ob_get_clean(); + $this->assertStringContainsString('events were imported', $output); + $this->assertStringContainsString('events were skipped', $output); + $this->assertStringContainsString('events were updated', $output); } /** diff --git a/lib/tests/upgradelib_test.php b/lib/tests/upgradelib_test.php index e6bc221f1c7..0e1ce803cd0 100644 --- a/lib/tests/upgradelib_test.php +++ b/lib/tests/upgradelib_test.php @@ -1276,7 +1276,7 @@ class upgradelib_test extends advanced_testcase { $ical->unserialize($calendar); // Import subscription events. - calendar_import_icalendar_events($ical, null, $id); + calendar_import_events_from_ical($ical, $id); // Subscription should have added 18 events. $eventscount = $DB->count_records('event'); diff --git a/version.php b/version.php index 49a05533dac..c8778b1e5b4 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2021101900.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2021101900.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.0dev+ (Build: 20211019)'; // Human-friendly version name