Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  declare(strict_types=1);
  18  
  19  namespace core_reportbuilder\local\helpers;
  20  
  21  use context_user;
  22  use core_user;
  23  use invalid_parameter_exception;
  24  use stdClass;
  25  use stored_file;
  26  use table_dataformat_export_format;
  27  use core\message\message;
  28  use core\plugininfo\dataformat;
  29  use core_reportbuilder\local\models\audience as audience_model;
  30  use core_reportbuilder\local\models\schedule as model;
  31  use core_reportbuilder\table\custom_report_table_view;
  32  
  33  /**
  34   * Helper class for report schedule related methods
  35   *
  36   * @package     core_reportbuilder
  37   * @copyright   2021 Paul Holden <paulh@moodle.com>
  38   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class schedule {
  41  
  42      /**
  43       * Create report schedule, calculate when it should be next sent
  44       *
  45       * @param stdClass $data
  46       * @param int|null $timenow Time to use as comparison against current date (defaults to current time)
  47       * @return model
  48       */
  49      public static function create_schedule(stdClass $data, ?int $timenow = null): model {
  50          $data->name = trim($data->name);
  51  
  52          $schedule = (new model(0, $data));
  53          $schedule->set('timenextsend', self::calculate_next_send_time($schedule, $timenow));
  54  
  55          return $schedule->create();
  56      }
  57  
  58      /**
  59       * Update report schedule
  60       *
  61       * @param stdClass $data
  62       * @return model
  63       * @throws invalid_parameter_exception
  64       */
  65      public static function update_schedule(stdClass $data): model {
  66          $schedule = model::get_record(['id' => $data->id, 'reportid' => $data->reportid]);
  67          if ($schedule === false) {
  68              throw new invalid_parameter_exception('Invalid schedule');
  69          }
  70  
  71          // Normalize model properties.
  72          $data = array_intersect_key((array) $data, model::properties_definition());
  73          if (array_key_exists('name', $data)) {
  74              $data['name'] = trim($data['name']);
  75          }
  76  
  77          $schedule->set_many($data);
  78          $schedule->set('timenextsend', self::calculate_next_send_time($schedule))
  79              ->update();
  80  
  81          return $schedule;
  82      }
  83  
  84      /**
  85       * Toggle report schedule enabled
  86       *
  87       * @param int $reportid
  88       * @param int $scheduleid
  89       * @param bool $enabled
  90       * @return bool
  91       * @throws invalid_parameter_exception
  92       */
  93      public static function toggle_schedule(int $reportid, int $scheduleid, bool $enabled): bool {
  94          $schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid]);
  95          if ($schedule === false) {
  96              throw new invalid_parameter_exception('Invalid schedule');
  97          }
  98  
  99          return $schedule->set('enabled', $enabled)->update();
 100      }
 101  
 102      /**
 103       * Return array of users who match the audience records added to the given schedule
 104       *
 105       * @param model $schedule
 106       * @return stdClass[]
 107       */
 108      public static function get_schedule_report_users(model $schedule): array {
 109          global $DB;
 110  
 111          $audienceids = (array) json_decode($schedule->get('audiences'));
 112  
 113          // Retrieve all selected audience records for the schedule.
 114          [$audienceselect, $audienceparams] = $DB->get_in_or_equal($audienceids, SQL_PARAMS_NAMED, 'aid', true, null);
 115          $audiences = audience_model::get_records_select("id {$audienceselect}", $audienceparams);
 116  
 117          // Now convert audiences to SQL for user retrieval.
 118          [$wheres, $params] = audience::user_audience_sql($audiences);
 119          if (count($wheres) === 0) {
 120              return [];
 121          }
 122  
 123          [$userorder] = users_order_by_sql('u');
 124  
 125          $sql = 'SELECT u.*
 126                    FROM {user} u
 127                   WHERE (' . implode(' OR ', $wheres) . ')
 128                     AND u.deleted = 0
 129                ORDER BY ' . $userorder;
 130  
 131          return $DB->get_records_sql($sql, $params);
 132      }
 133  
 134      /**
 135       * Return count of schedule report rows
 136       *
 137       * @param model $schedule
 138       * @return int
 139       */
 140      public static function get_schedule_report_count(model $schedule): int {
 141          global $DB;
 142  
 143          $table = custom_report_table_view::create($schedule->get('reportid'));
 144          $table->setup();
 145  
 146          return $DB->count_records_sql($table->countsql, $table->countparams);
 147      }
 148  
 149      /**
 150       * Generate stored file instance for given schedule, in user draft
 151       *
 152       * @param model $schedule
 153       * @return stored_file
 154       */
 155      public static function get_schedule_report_file(model $schedule): stored_file {
 156          global $CFG, $USER;
 157  
 158          require_once("{$CFG->libdir}/filelib.php");
 159  
 160          $table = custom_report_table_view::create($schedule->get('reportid'));
 161  
 162          $table->setup();
 163          $table->query_db(0, false);
 164  
 165          // Set up table as if it were being downloaded, retrieve appropriate export class (ensure output buffer is
 166          // cleaned in order to instantiate export class without exception).
 167          ob_start();
 168          $table->download = $schedule->get('format');
 169          $exportclass = new table_dataformat_export_format($table, $table->download);
 170          ob_end_clean();
 171  
 172          // Create our schedule report stored file temporarily in user draft.
 173          $filerecord = [
 174              'contextid' => context_user::instance($USER->id)->id,
 175              'component' => 'user',
 176              'filearea' => 'draft',
 177              'itemid' => file_get_unused_draft_itemid(),
 178              'filepath' => '/',
 179              'filename' => clean_filename($schedule->get_formatted_name()),
 180          ];
 181  
 182          $storedfile = \core\dataformat::write_data_to_filearea(
 183              $filerecord,
 184              $table->download,
 185              $exportclass->format_data($table->headers),
 186              $table->rawdata,
 187              static function(stdClass $record, bool $supportshtml) use ($table, $exportclass): array {
 188                  $record = $table->format_row($record);
 189                  if (!$supportshtml) {
 190                      $record = $exportclass->format_data($record);
 191                  }
 192                  return $record;
 193              }
 194          );
 195  
 196          $table->close_recordset();
 197  
 198          return $storedfile;
 199      }
 200  
 201      /**
 202       * Check whether given schedule needs to be sent
 203       *
 204       * @param model $schedule
 205       * @return bool
 206       */
 207      public static function should_send_schedule(model $schedule): bool {
 208          if (!$schedule->get('enabled')) {
 209              return false;
 210          }
 211  
 212          $timenow = time();
 213  
 214          // Ensure we've reached the initial scheduled start time.
 215          $timescheduled = $schedule->get('timescheduled');
 216          if ($timescheduled > $timenow) {
 217              return false;
 218          }
 219  
 220          // If there's no recurrence, check whether it's been sent since initial scheduled start time. This ensures that even if
 221          // the schedule was manually sent beforehand, it'll still be automatically sent once the start time is first reached.
 222          if ($schedule->get('recurrence') === model::RECURRENCE_NONE) {
 223              return $schedule->get('timelastsent') < $timescheduled;
 224          }
 225  
 226          return $schedule->get('timenextsend') <= $timenow;
 227      }
 228  
 229      /**
 230       * Calculate the next time a schedule should be sent, based on it's recurrence and when it was initially scheduled. Ensures
 231       * returned value is after the current date
 232       *
 233       * @param model $schedule
 234       * @param int|null $timenow Time to use as comparison against current date (defaults to current time)
 235       * @return int
 236       */
 237      public static function calculate_next_send_time(model $schedule, ?int $timenow = null): int {
 238          global $CFG;
 239  
 240          $timenow = $timenow ?? time();
 241  
 242          $recurrence = $schedule->get('recurrence');
 243          $timescheduled = $schedule->get('timescheduled');
 244  
 245          // If no recurrence is set or we haven't reached last sent date, return early.
 246          if ($recurrence === model::RECURRENCE_NONE || $timescheduled > $timenow) {
 247              return $timescheduled;
 248          }
 249  
 250          // Extract attributes from date (year, month, day, hours, minutes).
 251          [
 252              'year' => $year,
 253              'mon' => $month,
 254              'mday' => $day,
 255              'wday' => $dayofweek,
 256              'hours' => $hour,
 257              'minutes' => $minute,
 258          ] = usergetdate($timescheduled, $CFG->timezone);
 259  
 260          switch ($recurrence) {
 261              case model::RECURRENCE_DAILY:
 262                  $day += 1;
 263              break;
 264              case model::RECURRENCE_WEEKDAYS:
 265                  $day += 1;
 266  
 267                  $calendar = \core_calendar\type_factory::get_calendar_instance();
 268                  $weekend = get_config('core', 'calendar_weekend');
 269  
 270                  // Increment day until day of week falls on a weekday.
 271                  while ((bool) ($weekend & (1 << (++$dayofweek % $calendar->get_num_weekdays())))) {
 272                      $day++;
 273                  }
 274              break;
 275              case model::RECURRENCE_WEEKLY:
 276                  $day += 7;
 277              break;
 278              case model::RECURRENCE_MONTHLY:
 279                  $month += 1;
 280              break;
 281              case model::RECURRENCE_ANNUALLY:
 282                  $year += 1;
 283              break;
 284          }
 285  
 286          // We need to recursively increment the timestamp until we get one after the current time.
 287          $timestamp = make_timestamp($year, $month, $day, $hour, $minute, 0, $CFG->timezone);
 288          if ($timestamp < $timenow) {
 289              // Ensure we don't modify anything in the original model.
 290              $scheduleclone = new model(0, $schedule->to_record());
 291  
 292              return self::calculate_next_send_time(
 293                  $scheduleclone->set('timescheduled', $timestamp), $timenow);
 294          } else {
 295              return $timestamp;
 296          }
 297      }
 298  
 299      /**
 300       * Send schedule message to user
 301       *
 302       * @param model $schedule
 303       * @param stdClass $user
 304       * @param stored_file $attachment
 305       * @return bool
 306       */
 307      public static function send_schedule_message(model $schedule, stdClass $user, stored_file $attachment): bool {
 308          $message = new message();
 309          $message->component = 'moodle';
 310          $message->name = 'reportbuilderschedule';
 311          $message->courseid = SITEID;
 312          $message->userfrom = core_user::get_noreply_user();
 313          $message->userto = $user;
 314          $message->subject = $schedule->get('subject');
 315          $message->fullmessage = $schedule->get('message');
 316          $message->fullmessageformat = $schedule->get('messageformat');
 317          $message->fullmessagehtml = $message->fullmessage;
 318          $message->smallmessage = $message->fullmessage;
 319  
 320          // Attach report to outgoing message.
 321          $message->attachment = $attachment;
 322          $message->attachname = $attachment->get_filename();
 323  
 324          return (bool) message_send($message);
 325      }
 326  
 327      /**
 328       * Delete report schedule
 329       *
 330       * @param int $reportid
 331       * @param int $scheduleid
 332       * @return bool
 333       * @throws invalid_parameter_exception
 334       */
 335      public static function delete_schedule(int $reportid, int $scheduleid): bool {
 336          $schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid]);
 337          if ($schedule === false) {
 338              throw new invalid_parameter_exception('Invalid schedule');
 339          }
 340  
 341          return $schedule->delete();
 342      }
 343  
 344      /**
 345       * Return list of available data formats
 346       *
 347       * @return string[]
 348       */
 349      public static function get_format_options(): array {
 350          $dataformats = dataformat::get_enabled_plugins();
 351  
 352          return array_map(static function(string $pluginname): string {
 353              return get_string('dataformat', 'dataformat_' . $pluginname);
 354          }, $dataformats);
 355      }
 356  
 357      /**
 358       * Return list of available view as user options
 359       *
 360       * @return string[]
 361       */
 362      public static function get_viewas_options(): array {
 363          return [
 364              model::REPORT_VIEWAS_CREATOR => get_string('scheduleviewascreator', 'core_reportbuilder'),
 365              model::REPORT_VIEWAS_RECIPIENT => get_string('scheduleviewasrecipient', 'core_reportbuilder'),
 366              model::REPORT_VIEWAS_USER => get_string('userselect', 'core_reportbuilder'),
 367          ];
 368      }
 369  
 370      /**
 371       * Return list of recurrence options
 372       *
 373       * @return string[]
 374       */
 375      public static function get_recurrence_options(): array {
 376          return [
 377              model::RECURRENCE_NONE => get_string('none'),
 378              model::RECURRENCE_DAILY => get_string('recurrencedaily', 'core_reportbuilder'),
 379              model::RECURRENCE_WEEKDAYS => get_string('recurrenceweekdays', 'core_reportbuilder'),
 380              model::RECURRENCE_WEEKLY => get_string('recurrenceweekly', 'core_reportbuilder'),
 381              model::RECURRENCE_MONTHLY => get_string('recurrencemonthly', 'core_reportbuilder'),
 382              model::RECURRENCE_ANNUALLY => get_string('recurrenceannually', 'core_reportbuilder'),
 383          ];
 384      }
 385  
 386      /**
 387       * Return list of options for when report is empty
 388       *
 389       * @return string[]
 390       */
 391      public static function get_report_empty_options(): array {
 392          return [
 393              model::REPORT_EMPTY_SEND_EMPTY => get_string('scheduleemptysendwithattachment', 'core_reportbuilder'),
 394              model::REPORT_EMPTY_SEND_WITHOUT => get_string('scheduleemptysendwithoutattachment', 'core_reportbuilder'),
 395              model::REPORT_EMPTY_DONT_SEND => get_string('scheduleemptydontsend', 'core_reportbuilder'),
 396          ];
 397      }
 398  }