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.

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