Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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  /**
  18   * Insights generator.
  19   *
  20   * @package   core_analytics
  21   * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_analytics;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  require_once($CFG->dirroot . '/lib/messagelib.php');
  30  
  31  /**
  32   * Insights generator.
  33   *
  34   * @package   core_analytics
  35   * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
  36   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class insights_generator {
  39  
  40      /**
  41       * @var int
  42       */
  43      private $modelid;
  44  
  45      /**
  46       * @var \core_analytics\local\target\base
  47       */
  48      private $target;
  49  
  50      /**
  51       * @var int[]
  52       */
  53      private $contextcourseids;
  54  
  55      /**
  56       * Constructor.
  57       *
  58       * @param int $modelid
  59       * @param \core_analytics\local\target\base $target
  60       */
  61      public function __construct(int $modelid, \core_analytics\local\target\base $target) {
  62          $this->modelid = $modelid;
  63          $this->target = $target;
  64      }
  65  
  66      /**
  67       * Generates insight notifications.
  68       *
  69       * @param array                         $samplecontexts    The contexts these predictions belong to
  70       * @param \core_analytics\prediction[]  $predictions       The prediction records
  71       * @return  null
  72       */
  73      public function generate($samplecontexts, $predictions) {
  74  
  75          $analyserclass = $this->target->get_analyser_class();
  76  
  77          // We will need to restore it later.
  78          $actuallanguage = current_language();
  79  
  80          if ($analyserclass::one_sample_per_analysable()) {
  81  
  82              // Iterate through the predictions and the users in each prediction (likely to be just one).
  83              foreach ($predictions as $prediction) {
  84  
  85                  $context = $samplecontexts[$prediction->get_prediction_data()->contextid];
  86  
  87                  $users = $this->target->get_insights_users($context);
  88                  foreach ($users as $user) {
  89  
  90                      $this->set_notification_language($user);
  91                      list($insighturl, $fullmessage, $fullmessagehtml) = $this->prediction_info($prediction, $context, $user);
  92                      $this->notification($context, $user, $insighturl, $fullmessage, $fullmessagehtml);
  93                  }
  94              }
  95  
  96          } else {
  97  
  98              // Iterate through the context and the users in each context.
  99              foreach ($samplecontexts as $context) {
 100  
 101                  // Weird to pass both the context and the contextname to a method right, but this way we don't add unnecessary
 102                  // db reads calling get_context_name() multiple times.
 103                  $contextname = $context->get_context_name(false);
 104  
 105                  $users = $this->target->get_insights_users($context);
 106                  foreach ($users as $user) {
 107  
 108                      $this->set_notification_language($user);
 109  
 110                      $insighturl = $this->target->get_insight_context_url($this->modelid, $context);
 111  
 112                      list($fullmessage, $fullmessagehtml) = $this->target->get_insight_body($context, $contextname, $user,
 113                          $insighturl);
 114  
 115                      $this->notification($context, $user, $insighturl, $fullmessage, $fullmessagehtml);
 116                  }
 117              }
 118          }
 119  
 120          force_current_language($actuallanguage);
 121      }
 122  
 123      /**
 124       * Generates a insight notification for the user.
 125       *
 126       * @param  \context    $context
 127       * @param  \stdClass   $user
 128       * @param  \moodle_url $insighturl    The insight URL
 129       * @param  string      $fullmessage
 130       * @param  string      $fullmessagehtml
 131       * @return null
 132       */
 133      private function notification(\context $context, \stdClass $user, \moodle_url $insighturl, string $fullmessage, string $fullmessagehtml) {
 134  
 135          $message = new \core\message\message();
 136          $message->component = 'moodle';
 137          $message->name = 'insights';
 138  
 139          $message->userfrom = \core_user::get_support_user();
 140          $message->userto = $user;
 141  
 142          $message->subject = $this->target->get_insight_subject($this->modelid, $context);
 143  
 144          // Same than the subject.
 145          $message->contexturlname = $message->subject;
 146          $message->courseid = $this->get_context_courseid($context);
 147  
 148          $message->fullmessage = $fullmessage;
 149          $message->fullmessageformat = FORMAT_PLAIN;
 150          $message->fullmessagehtml = $fullmessagehtml;
 151          $message->smallmessage = $fullmessage;
 152          $message->contexturl = $insighturl->out(false);
 153  
 154          message_send($message);
 155      }
 156  
 157      /**
 158       * Returns the course context of the provided context reading an internal cache first.
 159       *
 160       * @param  \context $context
 161       * @return int
 162       */
 163      private function get_context_courseid(\context $context) {
 164  
 165          if (empty($this->contextcourseids[$context->id])) {
 166  
 167              $coursecontext = $context->get_course_context(false);
 168              if (!$coursecontext) {
 169                  // Default to the frontpage course context.
 170                  $coursecontext = \context_course::instance(SITEID);
 171              }
 172              $this->contextcourseids[$context->id] = $coursecontext->instanceid;
 173          }
 174  
 175          return $this->contextcourseids[$context->id];
 176      }
 177  
 178      /**
 179       * Extracts info from the prediction for display purposes.
 180       *
 181       * @param  \core_analytics\prediction   $prediction
 182       * @param  \context                     $context
 183       * @param  \stdClass                    $user
 184       * @return array Three items array with formats [\moodle_url, string, string]
 185       */
 186      private function prediction_info(\core_analytics\prediction $prediction, \context $context, \stdClass $user) {
 187          global $OUTPUT;
 188  
 189          // The prediction actions get passed to the target so that it can show them in its preferred way.
 190          $actions = array_merge(
 191              $this->target->prediction_actions($prediction, true, true),
 192              $this->target->bulk_actions([$prediction])
 193          );
 194          $predictioninfo = $this->target->get_insight_body_for_prediction($context, $user, $prediction, $actions);
 195  
 196          // For FORMAT_PLAIN.
 197          $fullmessageplaintext = '';
 198          if (!empty($predictioninfo[FORMAT_PLAIN])) {
 199              $fullmessageplaintext .= $predictioninfo[FORMAT_PLAIN];
 200          }
 201  
 202          $insighturl = $predictioninfo['url'] ?? null;
 203  
 204          // For FORMAT_HTML.
 205          $messageactions  = [];
 206          foreach ($actions as $action) {
 207              if (!$action->get_url()->get_param('forwardurl')) {
 208  
 209                  $params = ['actionvisiblename' => $action->get_text(), 'target' => '_blank'];
 210                  $actiondoneurl = new \moodle_url('/report/insights/done.php', $params);
 211                  // Set the forward url to the 'done' script.
 212                  $action->get_url()->param('forwardurl', $actiondoneurl->out(false));
 213              }
 214  
 215              if ($action->get_url()->param('predictionid') === null) {
 216                  // Bulk actions do not include the prediction id by default.
 217                  $action->get_url()->param('predictionid', $prediction->get_prediction_data()->id);
 218              }
 219  
 220              if (empty($insighturl)) {
 221                  // Ideally the target provides us with the best URL for the insight, if it doesn't we default
 222                  // to the first actions.
 223                  $insighturl = $action->get_url();
 224              }
 225  
 226              $actiondata = (object)['url' => $action->get_url()->out(false), 'text' => $action->get_text()];
 227  
 228              // Basic message for people who still lives in the 90s.
 229              $fullmessageplaintext .= get_string('insightinfomessageaction', 'analytics', $actiondata) . PHP_EOL;
 230  
 231              // We now process the HTML version actions, with a special treatment for useful/notuseful.
 232              if ($action->get_action_name() === 'useful') {
 233                  $usefulurl = $actiondata->url;
 234              } else if ($action->get_action_name() === 'notuseful') {
 235                  $notusefulurl = $actiondata->url;
 236              } else {
 237                  $messageactions[] = $actiondata;
 238              }
 239          }
 240  
 241          // Extra condition because we don't want to show the yes/no unless we have urls for both of them.
 242          if (!empty($usefulurl) && !empty($notusefulurl)) {
 243              $usefulbuttons = ['usefulurl' => $usefulurl, 'notusefulurl' => $notusefulurl];
 244          }
 245  
 246          $contextinfo = [
 247              'usefulbuttons' => !empty($usefulbuttons) ? $usefulbuttons : false,
 248              'actions' => $messageactions,
 249              'body' => $predictioninfo[FORMAT_HTML] ?? ''
 250          ];
 251          $fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message_prediction', $contextinfo);
 252  
 253          return [$insighturl, $fullmessageplaintext, $fullmessagehtml];
 254      }
 255  
 256      /**
 257       * Sets the session language to the language used by the notification receiver.
 258       *
 259       * @param  \stdClass $user The user who will receive the message
 260       * @return null
 261       */
 262      private function set_notification_language($user) {
 263          global $CFG;
 264  
 265          // Copied from current_language().
 266          if (!empty($user->lang)) {
 267              $lang = $user->lang;
 268          } else if (isset($CFG->lang)) {
 269              $lang = $CFG->lang;
 270          } else {
 271              $lang = 'en';
 272          }
 273          force_current_language($lang);
 274      }
 275  }