MDL-75275 mod_data: Add ##actionsmenu## action tag
[moodle.git] / mod / data / classes / template.php
blobeb5db80f834aa4f44474aba69d3011bbcb781339
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.
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/>.
17 namespace mod_data;
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;
34 /**
35 * Class template for database activity
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
41 class template {
43 /** @var manager the current instance manager. */
44 private $manager;
46 /** @var stdClass the current instance record. */
47 private $instance;
49 /** @var string the template. */
50 private $templatecontent;
52 /** @var moodle_url the base url. */
53 private $baseurl;
55 /** @var string the current search if any. */
56 private $search;
58 /** @var bool if ratings must be added. */
59 private $ratings;
61 /** @var bool if comments must be added if not present in the template. */
62 private $forcecomments;
64 /** @var bool if show more option must be added. */
65 private $showmore;
67 /** @var bool if the current user can manage entries. */
68 private $canmanageentries = null;
70 /** @var array if icons HTML. */
71 private $icons = [];
73 /** @var array All template tags (calculated in load_template_tags). */
74 protected $tags = [];
76 /**
77 * Class contructor.
79 * See the add_options method for the available display options.
81 * @param manager $manager the current instance manager
82 * @param string $templatecontent the template string to use
83 * @param array $options an array of extra diplay options
85 public function __construct(manager $manager, string $templatecontent, array $options = []) {
86 $this->manager = $manager;
87 $this->instance = $manager->get_instance();
88 $this->templatecontent = $templatecontent;
90 $context = $manager->get_context();
91 $this->canmanageentries = has_capability('mod/data:manageentries', $context);
92 $this->icons = $this->get_icons();
93 $this->add_options($options);
94 $this->load_template_tags($templatecontent);
97 /**
98 * Add extra display options.
100 * The extra options are:
101 * - page: the current pagination page
102 * - search: the current search text
103 * - baseurl: an alternative entry url (moodle_url)
104 * - comments: if comments must be added if not present
105 * - ratings: if ratings must be added
107 * @param array $options the array of options.
109 public function add_options(array $options = []) {
110 $cm = $this->manager->get_coursemodule();
111 $baseurl = $options['baseurl'] ?? new moodle_url('/mod/data/view.php', ['id' => $cm->id]);
112 if (isset($options['page'])) {
113 $baseurl->params([
114 'page' => $options['page'],
117 $this->baseurl = $baseurl;
119 // Save options.
120 $this->search = $options['search'] ?? null;
121 $this->ratings = $options['ratings'] ?? false;
122 $this->forcecomments = $options['comments'] ?? false;
123 $this->showmore = $options['showmore'] ?? false;
127 * Scan the template tags.
129 * This method detects which tags are used in this template and store them
130 * in the $this->tags attribute. This attribute will be used to determine
131 * which replacements needs to be calculated.
133 * @param string $templatecontent the current template
135 protected function load_template_tags(string $templatecontent) {
136 // Detect action tags.
137 $pattern = '/##(?P<tags>\w+?)##/';
138 $matches = [];
139 preg_match_all($pattern, $templatecontent, $matches);
140 if (!isset($matches['tags']) || empty($matches['tags'])) {
141 return;
143 $this->tags = $matches['tags'];
147 * Generate the list of action icons.
149 * @return pix_icon[] icon name => pix_icon
151 protected function get_icons() {
152 $attrs = ['class' => 'iconsmall'];
153 return [
154 'edit' => new pix_icon('t/edit', get_string('edit'), '', $attrs),
155 'delete' => new pix_icon('t/delete', get_string('delete'), '', $attrs),
156 'more' => new pix_icon('t/preview', get_string('more', 'data'), '', $attrs),
157 'approve' => new pix_icon('t/approve', get_string('approve', 'data'), '', $attrs),
158 'disapprove' => new pix_icon('t/block', get_string('disapprove', 'data'), '', $attrs),
163 * Return the parsed entry using a template.
165 * This method apply a template replacing all necessary tags.
167 * @param array $entries of entres to parse
168 * @return string the entries outputs using the template
170 public function parse_entries(array $entries): string {
171 if (empty($entries)) {
172 return '';
174 $result = '';
175 foreach ($entries as $entry) {
176 $result .= $this->parse_entry($entry);
178 return $result;
182 * Parse a single entry.
184 * @param stdClass $entry the entry to parse
185 * @return string the parsed entry
187 private function parse_entry(stdClass $entry): string {
188 if (empty($this->templatecontent)) {
189 return '';
191 $context = $this->manager->get_context();
192 $canmanageentry = data_user_can_manage_entry($entry, $this->instance, $context);
194 // Load all replacements for the entry.
195 $fields = $this->get_fields_replacements($entry);
196 $tags = $this->get_tags_replacements($entry, $canmanageentry);
197 $replacements = array_merge($fields, $tags);
199 $patterns = array_keys($replacements);
200 $replacement = array_values($replacements);
201 $result = str_ireplace($patterns, $replacement, $this->templatecontent);
203 return $this->post_parse($result, $entry);
207 * Get all field replacements.
209 * @param stdClass $entry the entry object
210 * @return array of pattern => replacement
212 private function get_fields_replacements(stdClass $entry): array {
213 $result = [];
214 $fields = $this->manager->get_fields();
215 foreach ($fields as $field) {
216 // Field value.
217 $pattern = '[[' . $field->field->name . ']]';
218 $result[$pattern] = highlight(
219 $this->search,
220 $field->display_browse_field($entry->id, $this->templatecontent)
222 // Field id.
223 $pattern = '[[' . $field->field->name . '#id]]';
224 $result[$pattern] = $field->field->id;
226 return $result;
230 * Get all standard tags replacements.
232 * @param stdClass $entry the entry object
233 * @param bool $canmanageentry if the current user can manage this entry
234 * @return array of pattern => replacement
236 private function get_tags_replacements(stdClass $entry, bool $canmanageentry): array {
237 $result = [];
238 foreach ($this->tags as $tagname) {
239 $methodname = "get_tag_{$tagname}_replacement";
240 if (method_exists($this, $methodname)) {
241 $pattern = "##$tagname##";
242 $replacement = $this->$methodname($entry, $canmanageentry);
243 $result[$pattern] = $replacement;
246 return $result;
250 * Add any extra information to the parsed entry.
252 * @param string $result the parsed template with the entry data
253 * @param stdClass $entry the entry object
254 * @return string the final parsed template
256 private function post_parse(string $result, stdClass $entry): string {
257 if ($this->ratings) {
258 $result .= data_print_ratings($this->instance, $entry, false);
260 if ($this->forcecomments && strpos($this->templatecontent, '##comments##') === false) {
261 $result .= $this->get_tag_comments_replacement($entry, false);
263 return $result;
267 * Returns the ##edit## tag replacement for an entry.
269 * @param stdClass $entry the entry object
270 * @param bool $canmanageentry if the current user can manage this entry
271 * @return string the tag replacement
273 protected function get_tag_edit_replacement(stdClass $entry, bool $canmanageentry): string {
274 global $OUTPUT;
275 if (!$canmanageentry) {
276 return '';
278 $backurl = new moodle_url($this->baseurl, [
279 'rid' => $entry->id,
280 'mode' => 'single',
282 $url = new moodle_url('/mod/data/edit.php', $this->baseurl->params());
283 $url->params([
284 'rid' => $entry->id,
285 'sesskey' => sesskey(),
286 'backto' => urlencode($backurl->out(false))
288 return html_writer::tag(
289 'span',
290 $OUTPUT->action_icon($url, $this->icons['edit']),
291 ['class' => 'edit']
296 * Returns the ##delete## tag replacement for an entry.
298 * @param stdClass $entry the entry object
299 * @param bool $canmanageentry if the current user can manage this entry
300 * @return string the tag replacement
302 protected function get_tag_delete_replacement(stdClass $entry, bool $canmanageentry): string {
303 global $OUTPUT;
304 if (!$canmanageentry) {
305 return '';
307 $url = new moodle_url($this->baseurl, [
308 'delete' => $entry->id,
309 'sesskey' => sesskey(),
310 'mode' => 'single',
313 return html_writer::tag(
314 'span',
315 $OUTPUT->action_icon($url, $this->icons['delete']),
316 ['class' => 'delete']
321 * Returns the ##more## tag replacement for an entry.
323 * @param stdClass $entry the entry object
324 * @param bool $canmanageentry if the current user can manage this entry
325 * @return string the tag replacement
327 protected function get_tag_more_replacement(stdClass $entry, bool $canmanageentry): string {
328 global $OUTPUT;
330 if (!$this->showmore) {
331 return '';
334 $url = new moodle_url($this->baseurl, [
335 'rid' => $entry->id,
336 'filter' => 1,
338 return html_writer::tag(
339 'span',
340 $OUTPUT->action_icon($url, $this->icons['more']),
341 ['class' => 'more']
346 * Returns the ##moreurl## tag replacement for an entry.
348 * @param stdClass $entry the entry object
349 * @param bool $canmanageentry if the current user can manage this entry
350 * @return string the tag replacement
352 protected function get_tag_moreurl_replacement(stdClass $entry, bool $canmanageentry): string {
353 $url = new moodle_url($this->baseurl, [
354 'rid' => $entry->id,
355 'filter' => 1,
357 return $url->out(false);
361 * Returns the ##delcheck## tag replacement for an entry.
363 * @param stdClass $entry the entry object
364 * @param bool $canmanageentry if the current user can manage this entry
365 * @return string the tag replacement
367 protected function get_tag_delcheck_replacement(stdClass $entry, bool $canmanageentry): string {
368 global $OUTPUT;
369 if (!$this->canmanageentries) {
370 return '';
372 $checkbox = new checkbox_toggleall('listview-entries', false, [
373 'id' => "entry_{$entry->id}",
374 'name' => 'delcheck[]',
375 'classes' => 'recordcheckbox',
376 'value' => $entry->id,
378 return $OUTPUT->render($checkbox);
382 * Returns the ##user## tag replacement for an entry.
384 * @param stdClass $entry the entry object
385 * @param bool $canmanageentry if the current user can manage this entry
386 * @return string the tag replacement
388 protected function get_tag_user_replacement(stdClass $entry, bool $canmanageentry): string {
389 $cm = $this->manager->get_coursemodule();
390 $url = new moodle_url('/user/view.php', [
391 'id' => $entry->userid,
392 'course' => $cm->course,
394 return html_writer::tag(
395 'a',
396 fullname($entry),
397 ['href' => $url->out(false)]
402 * Returns the ##userpicture## tag replacement for an entry.
404 * @param stdClass $entry the entry object
405 * @param bool $canmanageentry if the current user can manage this entry
406 * @return string the tag replacement
408 protected function get_tag_userpicture_replacement(stdClass $entry, bool $canmanageentry): string {
409 global $OUTPUT;
410 $cm = $this->manager->get_coursemodule();
411 $user = user_picture::unalias($entry, null, 'userid');
412 // If the record didn't come with user data, retrieve the user from database.
413 if (!isset($user->picture)) {
414 $user = core_user::get_user($entry->userid);
416 return $OUTPUT->user_picture($user, ['courseid' => $cm->course]);
420 * Returns the ##export## tag replacement for an entry.
422 * @param stdClass $entry the entry object
423 * @param bool $canmanageentry if the current user can manage this entry
424 * @return string the tag replacement
426 protected function get_tag_export_replacement(stdClass $entry, bool $canmanageentry): string {
427 global $CFG;
428 if (empty($CFG->enableportfolios)) {
429 return '';
431 // Check the user can export the entry.
432 $cm = $this->manager->get_coursemodule();
433 $context = $this->manager->get_context();
434 $canexportall = has_capability('mod/data:exportentry', $context);
435 $canexportown = has_capability('mod/data:exportownentry', $context);
436 if (!$canexportall && !(data_isowner($entry->id) && $canexportown)) {
437 return '';
439 // Add the portfolio export button.
440 require_once($CFG->libdir . '/portfoliolib.php');
441 $button = new portfolio_add_button();
442 $button->set_callback_options(
443 'data_portfolio_caller',
444 ['id' => $cm->id, 'recordid' => $entry->id],
445 'mod_data'
447 $fields = $this->manager->get_fields();
448 list($formats, $files) = data_portfolio_caller::formats($fields, $entry);
449 $button->set_formats($formats);
450 $result = $button->to_html(PORTFOLIO_ADD_ICON_LINK);
451 if (is_null($result)) {
452 $result = '';
454 return $result;
458 * Returns the ##timeadded## tag replacement for an entry.
460 * @param stdClass $entry the entry object
461 * @param bool $canmanageentry if the current user can manage this entry
462 * @return string the tag replacement
464 protected function get_tag_timeadded_replacement(stdClass $entry, bool $canmanageentry): string {
465 return userdate($entry->timecreated);
469 * Returns the ##timemodified## tag replacement for an entry.
471 * @param stdClass $entry the entry object
472 * @param bool $canmanageentry if the current user can manage this entry
473 * @return string the tag replacement
475 protected function get_tag_timemodified_replacement(stdClass $entry, bool $canmanageentry): string {
476 return userdate($entry->timemodified);
480 * Returns the ##approve## tag replacement for an entry.
482 * @param stdClass $entry the entry object
483 * @param bool $canmanageentry if the current user can manage this entry
484 * @return string the tag replacement
486 protected function get_tag_approve_replacement(stdClass $entry, bool $canmanageentry): string {
487 global $OUTPUT;
488 $context = $this->manager->get_context();
489 if (!has_capability('mod/data:approve', $context) || !$this->instance->approval || $entry->approved) {
490 return '';
492 $url = new moodle_url($this->baseurl, [
493 'approve' => $entry->id,
494 'sesskey' => sesskey(),
496 return html_writer::tag(
497 'span',
498 $OUTPUT->action_icon($url, $this->icons['approve']),
499 ['class' => 'approve']
504 * Returns the ##disapprove## tag replacement for an entry.
506 * @param stdClass $entry the entry object
507 * @param bool $canmanageentry if the current user can manage this entry
508 * @return string the tag replacement
510 protected function get_tag_disapprove_replacement(stdClass $entry, bool $canmanageentry): string {
511 global $OUTPUT;
512 $context = $this->manager->get_context();
513 if (!has_capability('mod/data:approve', $context) || !$this->instance->approval || !$entry->approved) {
514 return '';
516 $url = new moodle_url($this->baseurl, [
517 'disapprove' => $entry->id,
518 'sesskey' => sesskey(),
520 return html_writer::tag(
521 'span',
522 $OUTPUT->action_icon($url, $this->icons['disapprove']),
523 ['class' => 'disapprove']
528 * Returns the ##approvalstatus## tag replacement for an entry.
530 * @param stdClass $entry the entry object
531 * @param bool $canmanageentry if the current user can manage this entry
532 * @return string the tag replacement
534 protected function get_tag_approvalstatus_replacement(stdClass $entry, bool $canmanageentry): string {
535 if (!$this->instance->approval) {
536 return '';
538 return ($entry->approved) ? get_string('approved', 'data') : get_string('notapproved', 'data');
542 * Returns the ##approvalstatusclass## tag replacement for an entry.
544 * @param stdClass $entry the entry object
545 * @param bool $canmanageentry if the current user can manage this entry
546 * @return string the tag replacement
548 protected function get_tag_approvalstatusclass_replacement(stdClass $entry, bool $canmanageentry): string {
549 if (!$this->instance->approval) {
550 return '';
552 return ($entry->approved) ? 'approved' : 'notapproved';
556 * Returns the ##comments## tag replacement for an entry.
558 * @param stdClass $entry the entry object
559 * @param bool $canmanageentry if the current user can manage this entry
560 * @return string the tag replacement
562 protected function get_tag_comments_replacement(stdClass $entry, bool $canmanageentry): string {
563 global $CFG;
564 if (empty($CFG->usecomments) || empty($this->instance->comments)) {
565 return '';
567 $context = $this->manager->get_context();
568 require_once($CFG->dirroot . '/comment/lib.php');
569 list($context, $course, $cm) = get_context_info_array($context->id);
570 $cmdata = (object)[
571 'context' => $context,
572 'course' => $course,
573 'cm' => $cm,
574 'area' => 'database_entry',
575 'itemid' => $entry->id,
576 'showcount' => true,
577 'component' => 'mod_data',
579 $comment = new comment($cmdata);
580 return $comment->output(true);
584 * Returns the ##tags## tag replacement for an entry.
586 * @param stdClass $entry the entry object
587 * @param bool $canmanageentry if the current user can manage this entry
588 * @return string the tag replacement
590 protected function get_tag_tags_replacement(stdClass $entry, bool $canmanageentry): string {
591 global $OUTPUT;
592 if (!core_tag_tag::is_enabled('mod_data', 'data_records')) {
593 return '';
595 return $OUTPUT->tag_list(
596 core_tag_tag::get_item_tags('mod_data', 'data_records', $entry->id),
598 'data-tags'
603 * Returns the ##id## tag replacement for an entry.
605 * @param stdClass $entry the entry object
606 * @param bool $canmanageentry if the current user can manage this entry
607 * @return string the tag replacement
609 protected function get_tag_id_replacement(stdClass $entry, bool $canmanageentry): string {
610 return (string) $entry->id;
614 * Returns the ##actionsmenu## tag replacement for an entry.
616 * @param stdClass $entry the entry object
617 * @param bool $canmanageentry if the current user can manage this entry
618 * @return string the tag replacement
620 protected function get_tag_actionsmenu_replacement(stdClass $entry, bool $canmanageentry): string {
621 global $OUTPUT, $CFG;
623 $actionmenu = new action_menu();
624 $icon = $OUTPUT->pix_icon('i/menu', get_string('actions'));
625 $actionmenu->set_menu_trigger($icon, 'btn btn-icon d-flex align-items-center justify-content-center');
626 $actionmenu->set_action_label(get_string('actions'));
627 $actionmenu->attributes['class'] .= ' entry-actionsmenu';
629 // Show more.
630 if ($this->showmore) {
631 $showmoreurl = new moodle_url($this->baseurl, [
632 'rid' => $entry->id,
633 'filter' => 1,
635 $actionmenu->add(new action_menu_link_secondary(
636 $showmoreurl,
637 null,
638 get_string('showmore', 'mod_data')
642 if ($canmanageentry) {
643 // Edit entry.
644 $backurl = new moodle_url($this->baseurl, [
645 'rid' => $entry->id,
646 'mode' => 'single',
648 $editurl = new moodle_url('/mod/data/edit.php', $this->baseurl->params());
649 $editurl->params([
650 'rid' => $entry->id,
651 'sesskey' => sesskey(),
652 'backto' => urlencode($backurl->out(false))
655 $actionmenu->add(new action_menu_link_secondary(
656 $editurl,
657 null,
658 get_string('edit')
661 // Delete entry.
662 $deleteurl = new moodle_url($this->baseurl, [
663 'delete' => $entry->id,
664 'sesskey' => sesskey(),
665 'mode' => 'single',
668 $actionmenu->add(new action_menu_link_secondary(
669 $deleteurl,
670 null,
671 get_string('delete')
675 // Approve/disapprove entry.
676 $context = $this->manager->get_context();
677 if (has_capability('mod/data:approve', $context) && $this->instance->approval) {
678 if ($entry->approved) {
679 $disapproveurl = new moodle_url($this->baseurl, [
680 'disapprove' => $entry->id,
681 'sesskey' => sesskey(),
683 $actionmenu->add(new action_menu_link_secondary(
684 $disapproveurl,
685 null,
686 get_string('disapprove', 'mod_data')
688 } else {
689 $approveurl = new moodle_url($this->baseurl, [
690 'approve' => $entry->id,
691 'sesskey' => sesskey(),
693 $actionmenu->add(new action_menu_link_secondary(
694 $approveurl,
695 null,
696 get_string('approve', 'mod_data')
701 // Export entry to portfolio.
702 if (!empty($CFG->enableportfolios)) {
703 // Check the user can export the entry.
704 $cm = $this->manager->get_coursemodule();
705 $canexportall = has_capability('mod/data:exportentry', $context);
706 $canexportown = has_capability('mod/data:exportownentry', $context);
707 if ($canexportall || (data_isowner($entry->id) && $canexportown)) {
708 // Add the portfolio export button.
709 require_once($CFG->libdir . '/portfoliolib.php');
710 $button = new portfolio_add_button();
711 $button->set_callback_options(
712 'data_portfolio_caller',
713 ['id' => $cm->id, 'recordid' => $entry->id],
714 'mod_data'
716 $fields = $this->manager->get_fields();
717 list($formats, $files) = data_portfolio_caller::formats($fields, $entry);
718 $button->set_formats($formats);
719 $exporturl = $button->to_html(PORTFOLIO_ADD_MOODLE_URL);
720 if (!is_null($exporturl)) {
721 $actionmenu->add(new action_menu_link_secondary(
722 $exporturl,
723 null,
724 get_string('addtoportfolio', 'portfolio')
730 return $OUTPUT->render($actionmenu);