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 401 and 402] [Versions 401 and 403]

   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  namespace mod_data;
  18  
  19  use action_menu;
  20  use action_menu_link_secondary;
  21  use core\output\checkbox_toggleall;
  22  use html_writer;
  23  use mod_data\manager;
  24  use moodle_url;
  25  use pix_icon;
  26  use stdClass;
  27  use user_picture;
  28  use core_user;
  29  use portfolio_add_button;
  30  use data_portfolio_caller;
  31  use comment;
  32  use core_tag_tag;
  33  
  34  /**
  35   * Class template for database activity
  36   *
  37   * @package    mod_data
  38   * @copyright  2022 Ferran Recio <ferran@moodle.com>
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class template {
  42  
  43      /** @var manager the current instance manager. */
  44      private $manager;
  45  
  46      /** @var stdClass the current instance record. */
  47      private $instance;
  48  
  49      /** @var string the template. */
  50      private $templatecontent;
  51  
  52      /** @var string the template name. */
  53      private $templatename;
  54  
  55      /** @var moodle_url the base url. */
  56      private $baseurl;
  57  
  58      /** @var string the current search if any. */
  59      private $search;
  60  
  61      /** @var bool if ratings must be added. */
  62      private $ratings;
  63  
  64      /** @var bool if comments must be added if not present in the template. */
  65      private $forcecomments;
  66  
  67      /** @var bool if show more option must be added. */
  68      private $showmore;
  69  
  70      /** @var bool if the current user can manage entries. */
  71      private $canmanageentries = null;
  72  
  73      /** @var array if icons HTML. */
  74      private $icons = [];
  75  
  76      /** @var array All template tags (calculated in load_template_tags). */
  77      protected $tags = [];
  78  
  79      /** @var array The mod_data fields. */
  80      protected $fields = [];
  81  
  82      /**
  83       * Class contructor.
  84       *
  85       * See the add_options method for the available display options.
  86       *
  87       * @param manager $manager the current instance manager
  88       * @param string $templatecontent the template string to use
  89       * @param array $options an array of extra diplay options
  90       * @param array $fields alternative array of fields (for preview presets)
  91       */
  92      public function __construct(manager $manager, string $templatecontent, array $options = [], array $fields = null) {
  93          $this->manager = $manager;
  94          $this->instance = $manager->get_instance();
  95          $this->templatecontent = $templatecontent;
  96  
  97          $context = $manager->get_context();
  98          $this->canmanageentries = has_capability('mod/data:manageentries', $context);
  99          $this->icons = $this->get_icons();
 100          $this->fields = $fields ?? $manager->get_fields();
 101          $this->add_options($options);
 102          $this->load_template_tags($templatecontent);
 103      }
 104  
 105      /**
 106       * Create a template class with the default template content.
 107       *
 108       * @param manager $manager the current instance manager.
 109       * @param string $templatename the template name.
 110       * @param bool $form whether the fields should be displayed as form instead of data.
 111       * @return self The template with the default content (to be displayed when no template is defined).
 112       */
 113      public static function create_default_template(
 114              manager $manager,
 115              string $templatename,
 116              bool $form = false
 117      ): self {
 118          $renderer = $manager->get_renderer();
 119          $content = '';
 120          switch ($templatename) {
 121              case 'addtemplate':
 122              case 'asearchtemplate':
 123              case 'listtemplate':
 124              case 'rsstemplate':
 125              case 'singletemplate':
 126                  $template = new \mod_data\output\defaulttemplate($manager->get_fields(), $templatename, $form);
 127                  $content = $renderer->render_defaulttemplate($template);
 128          }
 129  
 130          // Some templates have extra options.
 131          $options = self::get_default_display_options($templatename);
 132  
 133          return new self($manager, $content, $options);
 134      }
 135  
 136      /**
 137       * Get default options for templates.
 138       *
 139       * For instance, the list template supports the show more button.
 140       *
 141       * @param string $templatename the template name.
 142       * @return array an array of extra diplay options.
 143       */
 144      public static function get_default_display_options(string $templatename): array {
 145          $options = [];
 146  
 147          if ($templatename === 'singletemplate') {
 148              $options['comments'] = true;
 149              $options['ratings'] = true;
 150          }
 151          if ($templatename === 'listtemplate') {
 152              // The "Show more" button should be only displayed in the listtemplate.
 153              $options['showmore'] = true;
 154          }
 155  
 156          return $options;
 157      }
 158  
 159      /**
 160       * Return the raw template content.
 161       *
 162       * @return string the template content before parsing
 163       */
 164      public function get_template_content(): string {
 165          return $this->templatecontent;
 166      }
 167  
 168      /**
 169       * Add extra display options.
 170       *
 171       * The extra options are:
 172       *  - page: the current pagination page
 173       *  - search: the current search text
 174       *  - baseurl: an alternative entry url (moodle_url)
 175       *  - comments: if comments must be added if not present
 176       *  - ratings: if ratings must be added
 177       *
 178       * @param array $options the array of options.
 179       */
 180      public function add_options(array $options = []) {
 181          $cm = $this->manager->get_coursemodule();
 182          $baseurl = $options['baseurl'] ?? new moodle_url('/mod/data/view.php', ['id' => $cm->id]);
 183          if (isset($options['page'])) {
 184              $baseurl->params([
 185                  'page' => $options['page'],
 186              ]);
 187          }
 188          $this->baseurl = $baseurl;
 189  
 190          // Save options.
 191          $this->search = $options['search'] ?? null;
 192          $this->ratings = $options['ratings'] ?? false;
 193          $this->forcecomments = $options['comments'] ?? false;
 194          $this->showmore = $options['showmore'] ?? false;
 195          $this->templatename = $options['templatename'] ?? 'singletemplate';
 196      }
 197  
 198      /**
 199       * Scan the template tags.
 200       *
 201       * This method detects which tags are used in this template and store them
 202       * in the $this->tags attribute. This attribute will be used to determine
 203       * which replacements needs to be calculated.
 204       *
 205       * @param string $templatecontent the current template
 206       */
 207      protected function load_template_tags(string $templatecontent) {
 208          // Detect action tags.
 209          $pattern = '/##(?P<tags>\w+?)##/';
 210          $matches = [];
 211          preg_match_all($pattern, $templatecontent, $matches);
 212          if (!isset($matches['tags']) || empty($matches['tags'])) {
 213              return;
 214          }
 215          $this->tags = $matches['tags'];
 216      }
 217  
 218      /**
 219       * Check if a tag is present in the template.
 220       *
 221       * @param bool $tagname the tag to check (without ##)
 222       * @return bool if the tag is present
 223       */
 224      public function has_tag(string $tagname): bool {
 225          return in_array($tagname, $this->tags);
 226      }
 227  
 228      /**
 229       * Return the current template name.
 230       *
 231       * @return string the template name
 232       */
 233      public function get_template_name(): string {
 234          return $this->templatename;
 235      }
 236  
 237      /**
 238       * Generate the list of action icons.
 239       *
 240       * @return pix_icon[] icon name => pix_icon
 241       */
 242      protected function get_icons() {
 243          $attrs = ['class' => 'iconsmall dataicon'];
 244          return [
 245              'edit' => new pix_icon('t/editinline', get_string('edit'), '', $attrs),
 246              'delete' => new pix_icon('t/delete', get_string('delete'), '', $attrs),
 247              'more' => new pix_icon('t/preview', get_string('more', 'data'), '', $attrs),
 248              'approve' => new pix_icon('t/approve', get_string('approve', 'data'), '', $attrs),
 249              'disapprove' => new pix_icon('t/block', get_string('disapprove', 'data'), '', $attrs),
 250          ];
 251      }
 252  
 253      /**
 254       * Return the parsed entry using a template.
 255       *
 256       * This method apply a template replacing all necessary tags.
 257       *
 258       * @param array $entries of entres to parse
 259       * @return string the entries outputs using the template
 260       */
 261      public function parse_entries(array $entries): string {
 262          if (empty($entries)) {
 263              return '';
 264          }
 265          $result = '';
 266          foreach ($entries as $entry) {
 267              $result .= $this->parse_entry($entry);
 268          }
 269          return $result;
 270      }
 271  
 272      /**
 273       * Parse a single entry.
 274       *
 275       * @param stdClass $entry the entry to parse
 276       * @return string the parsed entry
 277       */
 278      private function parse_entry(stdClass $entry): string {
 279          if (empty($this->templatecontent)) {
 280              return '';
 281          }
 282          $context = $this->manager->get_context();
 283          $canmanageentry = data_user_can_manage_entry($entry, $this->instance, $context);
 284  
 285          // Load all replacements for the entry.
 286          $fields = $this->get_fields_replacements($entry);
 287          $tags = $this->get_tags_replacements($entry, $canmanageentry);
 288          $replacements = array_merge($fields, $tags);
 289  
 290          $patterns = array_keys($replacements);
 291          $replacement = array_values($replacements);
 292          $result = str_ireplace($patterns, $replacement, $this->templatecontent);
 293  
 294          return $this->post_parse($result, $entry);
 295      }
 296  
 297      /**
 298       * Get all field replacements.
 299       *
 300       * @param stdClass $entry the entry object
 301       * @return array of pattern => replacement
 302       */
 303      private function get_fields_replacements(stdClass $entry): array {
 304          $result = [];
 305          foreach ($this->fields as $field) {
 306              // Field value.
 307              $pattern = '[[' . $field->field->name . ']]';
 308              $result[$pattern] = highlight(
 309                  $this->search,
 310                  $field->display_browse_field($entry->id, $this->templatename)
 311              );
 312              // Field id.
 313              $pattern = '[[' . $field->field->name . '#id]]';
 314              $result[$pattern] = $field->field->id;
 315          }
 316          return $result;
 317      }
 318  
 319      /**
 320       * Get all standard tags replacements.
 321       *
 322       * @param stdClass $entry the entry object
 323       * @param bool $canmanageentry if the current user can manage this entry
 324       * @return array of pattern => replacement
 325       */
 326      private function get_tags_replacements(stdClass $entry, bool $canmanageentry): array {
 327          $result = [];
 328          foreach ($this->tags as $tagname) {
 329              $methodname = "get_tag_{$tagname}_replacement";
 330              if (method_exists($this, $methodname)) {
 331                  $pattern = "##$tagname##";
 332                  $replacement = $this->$methodname($entry, $canmanageentry);
 333                  $result[$pattern] = $replacement;
 334              }
 335          }
 336          return $result;
 337      }
 338  
 339      /**
 340       * Add any extra information to the parsed entry.
 341       *
 342       * @param string $result the parsed template with the entry data
 343       * @param stdClass $entry the entry object
 344       * @return string the final parsed template
 345       */
 346      private function post_parse(string $result, stdClass $entry): string {
 347          if ($this->ratings) {
 348              $result .= data_print_ratings($this->instance, $entry, false);
 349          }
 350          if ($this->forcecomments && strpos($this->templatecontent, '##comments##') === false) {
 351              $result .= $this->get_tag_comments_replacement($entry, false);
 352          }
 353          return $result;
 354      }
 355  
 356      /**
 357       * Returns the ##edit## tag replacement for an entry.
 358       *
 359       * @param stdClass $entry the entry object
 360       * @param bool $canmanageentry if the current user can manage this entry
 361       * @return string the tag replacement
 362       */
 363      protected function get_tag_edit_replacement(stdClass $entry, bool $canmanageentry): string {
 364          global $OUTPUT;
 365          if (!$canmanageentry) {
 366              return '';
 367          }
 368          $backurl = new moodle_url($this->baseurl, [
 369              'rid' => $entry->id,
 370              'mode' => 'single',
 371          ]);
 372          $url = new moodle_url('/mod/data/edit.php', $this->baseurl->params());
 373          $url->params([
 374              'rid' => $entry->id,
 375              'sesskey' => sesskey(),
 376              'backto' => urlencode($backurl->out(false))
 377          ]);
 378          return html_writer::tag(
 379              'span',
 380              $OUTPUT->action_icon($url, $this->icons['edit']),
 381              ['class' => 'edit']
 382          );
 383      }
 384  
 385      /**
 386       * Returns the ##delete## tag replacement for an entry.
 387       *
 388       * @param stdClass $entry the entry object
 389       * @param bool $canmanageentry if the current user can manage this entry
 390       * @return string the tag replacement
 391       */
 392      protected function get_tag_delete_replacement(stdClass $entry, bool $canmanageentry): string {
 393          global $OUTPUT;
 394          if (!$canmanageentry) {
 395              return '';
 396          }
 397          $url = new moodle_url($this->baseurl, [
 398              'delete' => $entry->id,
 399              'sesskey' => sesskey(),
 400              'mode' => 'single',
 401          ]);
 402  
 403          return html_writer::tag(
 404              'span',
 405              $OUTPUT->action_icon($url, $this->icons['delete']),
 406              ['class' => 'delete']
 407          );
 408      }
 409  
 410      /**
 411       * Returns the ##more## tag replacement for an entry.
 412       *
 413       * @param stdClass $entry the entry object
 414       * @param bool $canmanageentry if the current user can manage this entry
 415       * @return string the tag replacement
 416       */
 417      protected function get_tag_more_replacement(stdClass $entry, bool $canmanageentry): string {
 418          global $OUTPUT;
 419  
 420          if (!$this->showmore) {
 421              return '';
 422          }
 423  
 424          $url = new moodle_url($this->baseurl, [
 425              'rid' => $entry->id,
 426              'filter' => 1,
 427          ]);
 428          return html_writer::tag(
 429              'span',
 430              $OUTPUT->action_icon($url, $this->icons['more']),
 431              ['class' => 'more']
 432          );
 433      }
 434  
 435      /**
 436       * Returns the ##moreurl## tag replacement for an entry.
 437       *
 438       * @param stdClass $entry the entry object
 439       * @param bool $canmanageentry if the current user can manage this entry
 440       * @return string the tag replacement
 441       */
 442      protected function get_tag_moreurl_replacement(stdClass $entry, bool $canmanageentry): string {
 443          $url = new moodle_url($this->baseurl, [
 444              'rid' => $entry->id,
 445              'filter' => 1,
 446          ]);
 447          return $url->out(false);
 448      }
 449  
 450      /**
 451       * Returns the ##delcheck## tag replacement for an entry.
 452       *
 453       * @param stdClass $entry the entry object
 454       * @param bool $canmanageentry if the current user can manage this entry
 455       * @return string the tag replacement
 456       */
 457      protected function get_tag_delcheck_replacement(stdClass $entry, bool $canmanageentry): string {
 458          global $OUTPUT;
 459          if (!$this->canmanageentries) {
 460              return '';
 461          }
 462          $checkbox = new checkbox_toggleall('listview-entries', false, [
 463              'id' => "entry_{$entry->id}",
 464              'name' => 'delcheck[]',
 465              'classes' => 'recordcheckbox',
 466              'value' => $entry->id,
 467          ]);
 468          return $OUTPUT->render($checkbox);
 469      }
 470  
 471      /**
 472       * Returns the ##user## tag replacement for an entry.
 473       *
 474       * @param stdClass $entry the entry object
 475       * @param bool $canmanageentry if the current user can manage this entry
 476       * @return string the tag replacement
 477       */
 478      protected function get_tag_user_replacement(stdClass $entry, bool $canmanageentry): string {
 479          $cm = $this->manager->get_coursemodule();
 480          $url = new moodle_url('/user/view.php', [
 481              'id' => $entry->userid,
 482              'course' => $cm->course,
 483          ]);
 484          return html_writer::tag(
 485              'a',
 486              fullname($entry),
 487              ['href' => $url->out(false)]
 488          );
 489      }
 490  
 491      /**
 492       * Returns the ##userpicture## tag replacement for an entry.
 493       *
 494       * @param stdClass $entry the entry object
 495       * @param bool $canmanageentry if the current user can manage this entry
 496       * @return string the tag replacement
 497       */
 498      protected function get_tag_userpicture_replacement(stdClass $entry, bool $canmanageentry): string {
 499          global $OUTPUT;
 500          $cm = $this->manager->get_coursemodule();
 501          $user = user_picture::unalias($entry, null, 'userid');
 502          // If the record didn't come with user data, retrieve the user from database.
 503          if (!isset($user->picture)) {
 504              $user = core_user::get_user($entry->userid);
 505          }
 506          return $OUTPUT->user_picture($user, ['courseid' => $cm->course, 'size' => 64]);
 507      }
 508  
 509      /**
 510       * Returns the ##export## tag replacement for an entry.
 511       *
 512       * @param stdClass $entry the entry object
 513       * @param bool $canmanageentry if the current user can manage this entry
 514       * @return string the tag replacement
 515       */
 516      protected function get_tag_export_replacement(stdClass $entry, bool $canmanageentry): string {
 517          global $CFG;
 518          if (empty($CFG->enableportfolios)) {
 519              return '';
 520          }
 521          // Check the user can export the entry.
 522          $cm = $this->manager->get_coursemodule();
 523          $context = $this->manager->get_context();
 524          $canexportall = has_capability('mod/data:exportentry', $context);
 525          $canexportown = has_capability('mod/data:exportownentry', $context);
 526          if (!$canexportall && !(data_isowner($entry->id) && $canexportown)) {
 527              return '';
 528          }
 529          // Add the portfolio export button.
 530          require_once($CFG->libdir . '/portfoliolib.php');
 531          $button = new portfolio_add_button();
 532          $button->set_callback_options(
 533              'data_portfolio_caller',
 534              ['id' => $cm->id, 'recordid' => $entry->id],
 535              'mod_data'
 536          );
 537          list($formats, $files) = data_portfolio_caller::formats($this->fields, $entry);
 538          $button->set_formats($formats);
 539          $result = $button->to_html(PORTFOLIO_ADD_ICON_LINK);
 540          if (is_null($result)) {
 541              $result = '';
 542          }
 543          return $result;
 544      }
 545  
 546      /**
 547       * Returns the ##timeadded## tag replacement for an entry.
 548       *
 549       * @param stdClass $entry the entry object
 550       * @param bool $canmanageentry if the current user can manage this entry
 551       * @return string the tag replacement
 552       */
 553      protected function get_tag_timeadded_replacement(stdClass $entry, bool $canmanageentry): string {
 554          return html_writer::tag(
 555              'span',
 556              userdate($entry->timecreated, get_string('strftimedatemonthabbr', 'langconfig')),
 557              ['title' => userdate($entry->timecreated)]
 558          );
 559      }
 560  
 561      /**
 562       * Returns the ##timemodified## tag replacement for an entry.
 563       *
 564       * @param stdClass $entry the entry object
 565       * @param bool $canmanageentry if the current user can manage this entry
 566       * @return string the tag replacement
 567       */
 568      protected function get_tag_timemodified_replacement(stdClass $entry, bool $canmanageentry): string {
 569          return html_writer::tag(
 570              'span',
 571              userdate($entry->timemodified, get_string('strftimedatemonthabbr', 'langconfig')),
 572              ['title' => userdate($entry->timemodified)]
 573          );
 574      }
 575  
 576      /**
 577       * Returns the ##approve## tag replacement for an entry.
 578       *
 579       * @param stdClass $entry the entry object
 580       * @param bool $canmanageentry if the current user can manage this entry
 581       * @return string the tag replacement
 582       */
 583      protected function get_tag_approve_replacement(stdClass $entry, bool $canmanageentry): string {
 584          global $OUTPUT;
 585          $context = $this->manager->get_context();
 586          if (!has_capability('mod/data:approve', $context) || !$this->instance->approval || $entry->approved) {
 587              return '';
 588          }
 589          $url = new moodle_url($this->baseurl, [
 590              'approve' => $entry->id,
 591              'sesskey' => sesskey(),
 592          ]);
 593          return html_writer::tag(
 594              'span',
 595              $OUTPUT->action_icon($url, $this->icons['approve']),
 596              ['class' => 'approve']
 597          );
 598      }
 599  
 600      /**
 601       * Returns the ##disapprove## tag replacement for an entry.
 602       *
 603       * @param stdClass $entry the entry object
 604       * @param bool $canmanageentry if the current user can manage this entry
 605       * @return string the tag replacement
 606       */
 607      protected function get_tag_disapprove_replacement(stdClass $entry, bool $canmanageentry): string {
 608          global $OUTPUT;
 609          $context = $this->manager->get_context();
 610          if (!has_capability('mod/data:approve', $context) || !$this->instance->approval || !$entry->approved) {
 611              return '';
 612          }
 613          $url = new moodle_url($this->baseurl, [
 614              'disapprove' => $entry->id,
 615              'sesskey' => sesskey(),
 616          ]);
 617          return html_writer::tag(
 618              'span',
 619              $OUTPUT->action_icon($url, $this->icons['disapprove']),
 620              ['class' => 'disapprove']
 621          );
 622      }
 623  
 624      /**
 625       * Returns the ##approvalstatus## tag replacement for an entry.
 626       *
 627       * @param stdClass $entry the entry object
 628       * @param bool $canmanageentry if the current user can manage this entry
 629       * @return string the tag replacement
 630       */
 631      protected function get_tag_approvalstatus_replacement(stdClass $entry, bool $canmanageentry): string {
 632          if (!$this->instance->approval) {
 633              return '';
 634          }
 635          return ($entry->approved) ? '' : html_writer::div(get_string('notapproved', 'data'), 'mod-data-approval-status-badge');
 636      }
 637  
 638      /**
 639       * Returns the ##approvalstatusclass## tag replacement for an entry.
 640       *
 641       * @param stdClass $entry the entry object
 642       * @param bool $canmanageentry if the current user can manage this entry
 643       * @return string the tag replacement
 644       */
 645      protected function get_tag_approvalstatusclass_replacement(stdClass $entry, bool $canmanageentry): string {
 646          if (!$this->instance->approval) {
 647              return '';
 648          }
 649          return ($entry->approved) ? 'approved' : 'notapproved';
 650      }
 651  
 652      /**
 653       * Returns the ##comments## tag replacement for an entry.
 654       *
 655       * @param stdClass $entry the entry object
 656       * @param bool $canmanageentry if the current user can manage this entry
 657       * @return string the tag replacement
 658       */
 659      protected function get_tag_comments_replacement(stdClass $entry, bool $canmanageentry): string {
 660          global $CFG;
 661          if (empty($CFG->usecomments) || empty($this->instance->comments)) {
 662              return '';
 663          }
 664          $context = $this->manager->get_context();
 665          require_once($CFG->dirroot  . '/comment/lib.php');
 666          list($context, $course, $cm) = get_context_info_array($context->id);
 667          $cmdata = (object)[
 668              'context' => $context,
 669              'course' => $course,
 670              'cm' => $cm,
 671              'area' => 'database_entry',
 672              'itemid' => $entry->id,
 673              'showcount' => true,
 674              'component' => 'mod_data',
 675          ];
 676          $comment = new comment($cmdata);
 677          return $comment->output(true);
 678      }
 679  
 680      /**
 681       * Returns the ##tags## tag replacement for an entry.
 682       *
 683       * @param stdClass $entry the entry object
 684       * @param bool $canmanageentry if the current user can manage this entry
 685       * @return string the tag replacement
 686       */
 687      protected function get_tag_tags_replacement(stdClass $entry, bool $canmanageentry): string {
 688          global $OUTPUT;
 689          if (!core_tag_tag::is_enabled('mod_data', 'data_records')) {
 690              return '';
 691          }
 692          return $OUTPUT->tag_list(
 693              core_tag_tag::get_item_tags('mod_data', 'data_records', $entry->id),
 694              '',
 695              'data-tags'
 696          );
 697      }
 698  
 699      /**
 700       * Returns the ##id## tag replacement for an entry.
 701       *
 702       * @param stdClass $entry the entry object
 703       * @param bool $canmanageentry if the current user can manage this entry
 704       * @return string the tag replacement
 705       */
 706      protected function get_tag_id_replacement(stdClass $entry, bool $canmanageentry): string {
 707          return (string) $entry->id;
 708      }
 709  
 710      /**
 711       * Returns the ##actionsmenu## tag replacement for an entry.
 712       *
 713       * @param stdClass $entry the entry object
 714       * @param bool $canmanageentry if the current user can manage this entry
 715       * @return string the tag replacement
 716       */
 717      protected function get_tag_actionsmenu_replacement(stdClass $entry, bool $canmanageentry): string {
 718          global $OUTPUT, $CFG;
 719  
 720          $actionmenu = new action_menu();
 721          $icon = $OUTPUT->pix_icon('i/menu', get_string('actions'));
 722          $actionmenu->set_menu_trigger($icon, 'btn btn-icon d-flex align-items-center justify-content-center');
 723          $actionmenu->set_action_label(get_string('actions'));
 724          $actionmenu->attributes['class'] .= ' entry-actionsmenu';
 725  
 726          // Show more.
 727          if ($this->showmore) {
 728              $showmoreurl = new moodle_url($this->baseurl, [
 729                  'rid' => $entry->id,
 730                  'filter' => 1,
 731              ]);
 732              $actionmenu->add(new action_menu_link_secondary(
 733                  $showmoreurl,
 734                  null,
 735                  get_string('showmore', 'mod_data')
 736              ));
 737          }
 738  
 739          if ($canmanageentry) {
 740              // Edit entry.
 741              $backurl = new moodle_url($this->baseurl, [
 742                  'rid' => $entry->id,
 743                  'mode' => 'single',
 744              ]);
 745              $editurl = new moodle_url('/mod/data/edit.php', $this->baseurl->params());
 746              $editurl->params([
 747                  'rid' => $entry->id,
 748                  'sesskey' => sesskey(),
 749                  'backto' => urlencode($backurl->out(false))
 750              ]);
 751  
 752              $actionmenu->add(new action_menu_link_secondary(
 753                  $editurl,
 754                  null,
 755                  get_string('edit')
 756              ));
 757  
 758              // Delete entry.
 759              $deleteurl = new moodle_url($this->baseurl, [
 760                  'delete' => $entry->id,
 761                  'sesskey' => sesskey(),
 762                  'mode' => 'single',
 763              ]);
 764  
 765              $actionmenu->add(new action_menu_link_secondary(
 766                  $deleteurl,
 767                  null,
 768                  get_string('delete')
 769              ));
 770          }
 771  
 772          // Approve/disapprove entry.
 773          $context = $this->manager->get_context();
 774          if (has_capability('mod/data:approve', $context) && $this->instance->approval) {
 775              if ($entry->approved) {
 776                  $disapproveurl = new moodle_url($this->baseurl, [
 777                      'disapprove' => $entry->id,
 778                      'sesskey' => sesskey(),
 779                  ]);
 780                  $actionmenu->add(new action_menu_link_secondary(
 781                      $disapproveurl,
 782                      null,
 783                      get_string('disapprove', 'mod_data')
 784                  ));
 785              } else {
 786                  $approveurl = new moodle_url($this->baseurl, [
 787                      'approve' => $entry->id,
 788                      'sesskey' => sesskey(),
 789                  ]);
 790                  $actionmenu->add(new action_menu_link_secondary(
 791                      $approveurl,
 792                      null,
 793                      get_string('approve', 'mod_data')
 794                  ));
 795              }
 796          }
 797  
 798          // Export entry to portfolio.
 799          if (!empty($CFG->enableportfolios)) {
 800              // Check the user can export the entry.
 801              $cm = $this->manager->get_coursemodule();
 802              $canexportall = has_capability('mod/data:exportentry', $context);
 803              $canexportown = has_capability('mod/data:exportownentry', $context);
 804              if ($canexportall || (data_isowner($entry->id) && $canexportown)) {
 805                  // Add the portfolio export button.
 806                  require_once($CFG->libdir . '/portfoliolib.php');
 807                  $button = new portfolio_add_button();
 808                  $button->set_callback_options(
 809                      'data_portfolio_caller',
 810                      ['id' => $cm->id, 'recordid' => $entry->id],
 811                      'mod_data'
 812                  );
 813                  $fields = $this->manager->get_fields();
 814                  list($formats, $files) = data_portfolio_caller::formats($fields, $entry);
 815                  $button->set_formats($formats);
 816                  $exporturl = $button->to_html(PORTFOLIO_ADD_MOODLE_URL);
 817                  if (!is_null($exporturl)) {
 818                      $actionmenu->add(new action_menu_link_secondary(
 819                          $exporturl,
 820                          null,
 821                          get_string('addtoportfolio', 'portfolio')
 822                      ));
 823                  }
 824              }
 825          }
 826  
 827          return $OUTPUT->render($actionmenu);
 828      }
 829  
 830      /**
 831       * Parse the template as if it was for add entry.
 832       *
 833       * This method is similar to the parse_entry but it uses the display_add_field method
 834       * instead of the display_browse_field.
 835       *
 836       * @param stdClass|null $processeddata the previous process data information.
 837       * @param int|null $entryid the possible entry id
 838       * @param stdClass|null $entrydata the entry data from a previous form or from a real entry
 839       * @return string the add entry HTML content
 840       */
 841      public function parse_add_entry(
 842          ?stdClass $processeddata = null,
 843          ?int $entryid = null,
 844          ?stdClass $entrydata = null
 845      ): string {
 846          $manager = $this->manager;
 847          $renderer = $manager->get_renderer();
 848          $templatecontent = $this->templatecontent;
 849  
 850          if (!$processeddata) {
 851              $processeddata = (object)[
 852                  'generalnotifications' => [],
 853                  'fieldnotifications' => [],
 854              ];
 855          }
 856  
 857          $result = '';
 858  
 859          foreach ($processeddata->generalnotifications as $notification) {
 860              $result .= $renderer->notification($notification);
 861          }
 862  
 863          $possiblefields = $manager->get_fields();
 864          $patterns = [];
 865          $replacements = [];
 866  
 867          // Then we generate strings to replace.
 868          foreach ($possiblefields as $field) {
 869              // To skip unnecessary calls to display_add_field().
 870              if (strpos($templatecontent, "[[" . $field->field->name . "]]") !== false) {
 871                  // Replace the field tag.
 872                  $patterns[] = "[[" . $field->field->name . "]]";
 873                  $errors = '';
 874                  $fieldnotifications = $processeddata->fieldnotifications[$field->field->name] ?? [];
 875                  if (!empty($fieldnotifications)) {
 876                      foreach ($fieldnotifications as $notification) {
 877                          $errors .= $renderer->notification($notification);
 878                      }
 879                  }
 880                  $fielddisplay = '';
 881                  if ($field->type === 'unknown') {
 882                      if ($this->canmanageentries) { // Display notification for users that can manage entries.
 883                          $errors .= $renderer->notification(get_string('missingfieldtype', 'data',
 884                          (object)['name' => $field->field->name]));
 885                      }
 886                  } else {
 887                      $fielddisplay = $field->display_add_field($entryid, $entrydata);
 888                  }
 889  
 890                  $replacements[] = $errors . $fielddisplay;
 891              }
 892  
 893              // Replace the field id tag.
 894              $patterns[] = "[[" . $field->field->name . "#id]]";
 895              $replacements[] = 'field_' . $field->field->id;
 896          }
 897  
 898          if (core_tag_tag::is_enabled('mod_data', 'data_records')) {
 899              $patterns[] = "##tags##";
 900              $replacements[] = data_generate_tag_form($entryid);
 901          }
 902  
 903          $result .= str_ireplace($patterns, $replacements, $templatecontent);
 904          return $result;
 905      }
 906  }