Merge branch 'MDL-62384-33' of git://github.com/andrewnicols/moodle into MOODLE_33_STABLE
[moodle.git] / mod / glossary / locallib.php
blob4496f3ce993e0628ccec7746c51fc931ef7b5969
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Library of functions and constants for module glossary
20 * outside of what is required for the core moodle api
22 * @package mod_glossary
23 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 require_once($CFG->libdir . '/portfolio/caller.php');
28 require_once($CFG->libdir . '/filelib.php');
30 /**
31 * class to handle exporting an entire glossary database
33 class glossary_full_portfolio_caller extends portfolio_module_caller_base {
35 private $glossary;
36 private $exportdata;
37 private $keyedfiles = array(); // keyed on entry
39 /**
40 * return array of expected call back arguments
41 * and whether they are required or not
43 * @return array
45 public static function expected_callbackargs() {
46 return array(
47 'id' => true,
51 /**
52 * load up all data required for this export.
54 * @return void
56 public function load_data() {
57 global $DB;
58 if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) {
59 throw new portfolio_caller_exception('invalidid', 'glossary');
61 if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) {
62 throw new portfolio_caller_exception('invalidid', 'glossary');
64 $entries = $DB->get_records('glossary_entries', array('glossaryid' => $this->glossary->id));
65 list($where, $params) = $DB->get_in_or_equal(array_keys($entries));
67 $aliases = $DB->get_records_select('glossary_alias', 'entryid ' . $where, $params);
68 $categoryentries = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec
69 JOIN {glossary_categories} c
70 ON c.id = ec.categoryid
71 WHERE ec.entryid ' . $where, $params);
73 $this->exportdata = array('entries' => $entries, 'aliases' => $aliases, 'categoryentries' => $categoryentries);
74 $fs = get_file_storage();
75 $context = context_module::instance($this->cm->id);
76 $this->multifiles = array();
77 foreach (array_keys($entries) as $entry) {
78 $this->keyedfiles[$entry] = array_merge(
79 $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry, "timemodified", false),
80 $fs->get_area_files($context->id, 'mod_glossary', 'entry', $entry, "timemodified", false)
82 $this->multifiles = array_merge($this->multifiles, $this->keyedfiles[$entry]);
86 /**
87 * how long might we expect this export to take
89 * @return constant one of PORTFOLIO_TIME_XX
91 public function expected_time() {
92 $filetime = portfolio_expected_time_file($this->multifiles);
93 $dbtime = portfolio_expected_time_db(count($this->exportdata['entries']));
94 return ($filetime > $dbtime) ? $filetime : $dbtime;
97 /**
98 * return the sha1 of this content
100 * @return string
102 public function get_sha1() {
103 $file = '';
104 if ($this->multifiles) {
105 $file = $this->get_sha1_file();
107 return sha1(serialize($this->exportdata) . $file);
111 * prepare the package ready to be passed off to the portfolio plugin
113 * @return void
115 public function prepare_package() {
116 $entries = $this->exportdata['entries'];
117 $aliases = array();
118 $categories = array();
119 if (is_array($this->exportdata['aliases'])) {
120 foreach ($this->exportdata['aliases'] as $alias) {
121 if (!array_key_exists($alias->entryid, $aliases)) {
122 $aliases[$alias->entryid] = array();
124 $aliases[$alias->entryid][] = $alias->alias;
127 if (is_array($this->exportdata['categoryentries'])) {
128 foreach ($this->exportdata['categoryentries'] as $cat) {
129 if (!array_key_exists($cat->entryid, $categories)) {
130 $categories[$cat->entryid] = array();
132 $categories[$cat->entryid][] = $cat->name;
135 if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_SPREADSHEET) {
136 $csv = glossary_generate_export_csv($entries, $aliases, $categories);
137 $this->exporter->write_new_file($csv, clean_filename($this->cm->name) . '.csv', false);
138 return;
139 } else if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
140 $ids = array(); // keep track of these to make into a selection later
141 global $USER, $DB;
142 $writer = $this->get('exporter')->get('format')->leap2a_writer($USER);
143 $format = $this->exporter->get('format');
144 $filename = $this->get('exporter')->get('format')->manifest_name();
145 foreach ($entries as $e) {
146 $content = glossary_entry_portfolio_caller::entry_content(
147 $this->course,
148 $this->cm,
149 $this->glossary,
151 (array_key_exists($e->id, $aliases) ? $aliases[$e->id] : array()),
152 $format
154 $entry = new portfolio_format_leap2a_entry('glossaryentry' . $e->id, $e->concept, 'entry', $content);
155 $entry->author = $DB->get_record('user', array('id' => $e->userid), 'id,firstname,lastname,email');
156 $entry->published = $e->timecreated;
157 $entry->updated = $e->timemodified;
158 if (!empty($this->keyedfiles[$e->id])) {
159 $writer->link_files($entry, $this->keyedfiles[$e->id], 'glossaryentry' . $e->id . 'file');
160 foreach ($this->keyedfiles[$e->id] as $file) {
161 $this->exporter->copy_existing_file($file);
164 if (!empty($categories[$e->id])) {
165 foreach ($categories[$e->id] as $cat) {
166 // this essentially treats them as plain tags
167 // leap has the idea of category schemes
168 // but I think this is overkill here
169 $entry->add_category($cat);
172 $writer->add_entry($entry);
173 $ids[] = $entry->id;
175 $selection = new portfolio_format_leap2a_entry('wholeglossary' . $this->glossary->id, get_string('modulename', 'glossary'), 'selection');
176 $writer->add_entry($selection);
177 $writer->make_selection($selection, $ids, 'Grouping');
178 $content = $writer->to_xml();
180 $this->exporter->write_new_file($content, $filename, true);
184 * make sure that the current user is allowed to do this
186 * @return boolean
188 public function check_permissions() {
189 return has_capability('mod/glossary:export', context_module::instance($this->cm->id));
193 * return a nice name to be displayed about this export location
195 * @return string
197 public static function display_name() {
198 return get_string('modulename', 'glossary');
202 * what formats this function *generally* supports
204 * @return array
206 public static function base_supported_formats() {
207 return array(PORTFOLIO_FORMAT_SPREADSHEET, PORTFOLIO_FORMAT_LEAP2A);
212 * class to export a single glossary entry
214 * @package mod_glossary
215 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
216 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
218 class glossary_entry_portfolio_caller extends portfolio_module_caller_base {
220 private $glossary;
221 private $entry;
222 protected $entryid;
224 * @return array
226 public static function expected_callbackargs() {
227 return array(
228 'entryid' => true,
229 'id' => true,
234 * load up all data required for this export.
236 * @return void
238 public function load_data() {
239 global $DB;
240 if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) {
241 throw new portfolio_caller_exception('invalidid', 'glossary');
243 if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) {
244 throw new portfolio_caller_exception('invalidid', 'glossary');
246 if ($this->entryid) {
247 if (!$this->entry = $DB->get_record('glossary_entries', array('id' => $this->entryid))) {
248 throw new portfolio_caller_exception('noentry', 'glossary');
250 // in case we don't have USER this will make the entry be printed
251 $this->entry->approved = true;
253 $this->categories = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec
254 JOIN {glossary_categories} c
255 ON c.id = ec.categoryid
256 WHERE ec.entryid = ?', array($this->entryid));
257 $context = context_module::instance($this->cm->id);
258 if ($this->entry->sourceglossaryid == $this->cm->instance) {
259 if ($maincm = get_coursemodule_from_instance('glossary', $this->entry->glossaryid)) {
260 $context = context_module::instance($maincm->id);
263 $this->aliases = $DB->get_record('glossary_alias', array('entryid'=>$this->entryid));
264 $fs = get_file_storage();
265 $this->multifiles = array_merge(
266 $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $this->entry->id, "timemodified", false),
267 $fs->get_area_files($context->id, 'mod_glossary', 'entry', $this->entry->id, "timemodified", false)
270 if (!empty($this->multifiles)) {
271 $this->add_format(PORTFOLIO_FORMAT_RICHHTML);
272 } else {
273 $this->add_format(PORTFOLIO_FORMAT_PLAINHTML);
278 * how long might we expect this export to take
280 * @return constant one of PORTFOLIO_TIME_XX
282 public function expected_time() {
283 return PORTFOLIO_TIME_LOW;
287 * make sure that the current user is allowed to do this
289 * @return boolean
291 public function check_permissions() {
292 $context = context_module::instance($this->cm->id);
293 return has_capability('mod/glossary:exportentry', $context)
294 || ($this->entry->userid == $this->user->id && has_capability('mod/glossary:exportownentry', $context));
298 * return a nice name to be displayed about this export location
300 * @return string
302 public static function display_name() {
303 return get_string('modulename', 'glossary');
307 * prepare the package ready to be passed off to the portfolio plugin
309 * @return void
311 public function prepare_package() {
312 global $DB;
314 $format = $this->exporter->get('format');
315 $content = self::entry_content($this->course, $this->cm, $this->glossary, $this->entry, $this->aliases, $format);
317 if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_PLAINHTML) {
318 $filename = clean_filename($this->entry->concept) . '.html';
319 $this->exporter->write_new_file($content, $filename);
321 } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_RICHHTML) {
322 if ($this->multifiles) {
323 foreach ($this->multifiles as $file) {
324 $this->exporter->copy_existing_file($file);
327 $filename = clean_filename($this->entry->concept) . '.html';
328 $this->exporter->write_new_file($content, $filename);
330 } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_LEAP2A) {
331 $writer = $this->get('exporter')->get('format')->leap2a_writer();
332 $entry = new portfolio_format_leap2a_entry('glossaryentry' . $this->entry->id, $this->entry->concept, 'entry', $content);
333 $entry->author = $DB->get_record('user', array('id' => $this->entry->userid), 'id,firstname,lastname,email');
334 $entry->published = $this->entry->timecreated;
335 $entry->updated = $this->entry->timemodified;
336 if ($this->multifiles) {
337 $writer->link_files($entry, $this->multifiles);
338 foreach ($this->multifiles as $file) {
339 $this->exporter->copy_existing_file($file);
342 if ($this->categories) {
343 foreach ($this->categories as $cat) {
344 // this essentially treats them as plain tags
345 // leap has the idea of category schemes
346 // but I think this is overkill here
347 $entry->add_category($cat->name);
350 $writer->add_entry($entry);
351 $content = $writer->to_xml();
352 $filename = $this->get('exporter')->get('format')->manifest_name();
353 $this->exporter->write_new_file($content, $filename);
355 } else {
356 throw new portfolio_caller_exception('unexpected_format_class', 'glossary');
361 * return the sha1 of this content
363 * @return string
365 public function get_sha1() {
366 if ($this->multifiles) {
367 return sha1(serialize($this->entry) . $this->get_sha1_file());
369 return sha1(serialize($this->entry));
373 * what formats this function *generally* supports
375 * @return array
377 public static function base_supported_formats() {
378 return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A);
382 * helper function to get the html content of an entry
383 * for both this class and the full glossary exporter
384 * this is a very simplified version of the dictionary format output,
385 * but with its 500 levels of indirection removed
386 * and file rewriting handled by the portfolio export format.
388 * @param stdclass $course
389 * @param stdclass $cm
390 * @param stdclass $glossary
391 * @param stdclass $entry
393 * @return string
395 public static function entry_content($course, $cm, $glossary, $entry, $aliases, $format) {
396 global $OUTPUT, $DB;
397 $entry = clone $entry;
398 $context = context_module::instance($cm->id);
399 $options = portfolio_format_text_options();
400 $options->trusted = $entry->definitiontrust;
401 $options->context = $context;
403 $output = '<table class="glossarypost dictionary" cellspacing="0">' . "\n";
404 $output .= '<tr valign="top">' . "\n";
405 $output .= '<td class="entry">' . "\n";
407 $output .= '<div class="concept">';
408 $output .= format_text($OUTPUT->heading($entry->concept, 3), FORMAT_MOODLE, $options);
409 $output .= '</div> ' . "\n";
411 $entry->definition = format_text($entry->definition, $entry->definitionformat, $options);
412 $output .= portfolio_rewrite_pluginfile_urls($entry->definition, $context->id, 'mod_glossary', 'entry', $entry->id, $format);
414 if (isset($entry->footer)) {
415 $output .= $entry->footer;
418 $output .= '</td></tr>' . "\n";
420 if (!empty($aliases)) {
421 $aliases = explode(',', $aliases->alias);
422 $output .= '<tr valign="top"><td class="entrylowersection">';
423 $key = (count($aliases) == 1) ? 'alias' : 'aliases';
424 $output .= get_string($key, 'glossary') . ': ';
425 foreach ($aliases as $alias) {
426 $output .= s($alias) . ',';
428 $output = substr($output, 0, -1);
429 $output .= '</td></tr>' . "\n";
432 if ($entry->sourceglossaryid == $cm->instance) {
433 if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) {
434 return '';
436 $filecontext = context_module::instance($maincm->id);
438 } else {
439 $filecontext = $context;
441 $fs = get_file_storage();
442 if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) {
443 $output .= '<table border="0" width="100%"><tr><td>' . "\n";
445 foreach ($files as $file) {
446 $output .= $format->file_output($file);
448 $output .= '</td></tr></table>' . "\n";
451 $output .= '</table>' . "\n";
453 return $output;
459 * Class representing the virtual node with all itemids in the file browser
461 * @category files
462 * @copyright 2012 David Mudrak <david@moodle.com>
463 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
465 class glossary_file_info_container extends file_info {
466 /** @var file_browser */
467 protected $browser;
468 /** @var stdClass */
469 protected $course;
470 /** @var stdClass */
471 protected $cm;
472 /** @var string */
473 protected $component;
474 /** @var stdClass */
475 protected $context;
476 /** @var array */
477 protected $areas;
478 /** @var string */
479 protected $filearea;
482 * Constructor (in case you did not realize it ;-)
484 * @param file_browser $browser
485 * @param stdClass $course
486 * @param stdClass $cm
487 * @param stdClass $context
488 * @param array $areas
489 * @param string $filearea
491 public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
492 parent::__construct($browser, $context);
493 $this->browser = $browser;
494 $this->course = $course;
495 $this->cm = $cm;
496 $this->component = 'mod_glossary';
497 $this->context = $context;
498 $this->areas = $areas;
499 $this->filearea = $filearea;
503 * @return array with keys contextid, filearea, itemid, filepath and filename
505 public function get_params() {
506 return array(
507 'contextid' => $this->context->id,
508 'component' => $this->component,
509 'filearea' => $this->filearea,
510 'itemid' => null,
511 'filepath' => null,
512 'filename' => null,
517 * Can new files or directories be added via the file browser
519 * @return bool
521 public function is_writable() {
522 return false;
526 * Should this node be considered as a folder in the file browser
528 * @return bool
530 public function is_directory() {
531 return true;
535 * Returns localised visible name of this node
537 * @return string
539 public function get_visible_name() {
540 return $this->areas[$this->filearea];
544 * Returns list of children nodes
546 * @return array of file_info instances
548 public function get_children() {
549 return $this->get_filtered_children('*', false, true);
553 * Help function to return files matching extensions or their count
555 * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
556 * @param bool|int $countonly if false returns the children, if an int returns just the
557 * count of children but stops counting when $countonly number of children is reached
558 * @param bool $returnemptyfolders if true returns items that don't have matching files inside
559 * @return array|int array of file_info instances or the count
561 private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
562 global $DB;
563 $sql = 'SELECT DISTINCT f.itemid, ge.concept
564 FROM {files} f
565 JOIN {modules} m ON (m.name = :modulename AND m.visible = 1)
566 JOIN {course_modules} cm ON (cm.module = m.id AND cm.id = :instanceid)
567 JOIN {glossary} g ON g.id = cm.instance
568 JOIN {glossary_entries} ge ON (ge.glossaryid = g.id AND ge.id = f.itemid)
569 WHERE f.contextid = :contextid
570 AND f.component = :component
571 AND f.filearea = :filearea';
572 $params = array(
573 'modulename' => 'glossary',
574 'instanceid' => $this->context->instanceid,
575 'contextid' => $this->context->id,
576 'component' => $this->component,
577 'filearea' => $this->filearea);
578 if (!$returnemptyfolders) {
579 $sql .= ' AND f.filename <> :emptyfilename';
580 $params['emptyfilename'] = '.';
582 list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f');
583 $sql .= ' '.$sql2;
584 $params = array_merge($params, $params2);
585 if ($countonly !== false) {
586 $sql .= ' ORDER BY ge.concept, f.itemid';
589 $rs = $DB->get_recordset_sql($sql, $params);
590 $children = array();
591 foreach ($rs as $record) {
592 if ($child = $this->browser->get_file_info($this->context, 'mod_glossary', $this->filearea, $record->itemid)) {
593 $children[] = $child;
595 if ($countonly !== false && count($children) >= $countonly) {
596 break;
599 $rs->close();
600 if ($countonly !== false) {
601 return count($children);
603 return $children;
607 * Returns list of children which are either files matching the specified extensions
608 * or folders that contain at least one such file.
610 * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
611 * @return array of file_info instances
613 public function get_non_empty_children($extensions = '*') {
614 return $this->get_filtered_children($extensions, false);
618 * Returns the number of children which are either files matching the specified extensions
619 * or folders containing at least one such file.
621 * @param string|array $extensions, for example '*' or array('.gif','.jpg')
622 * @param int $limit stop counting after at least $limit non-empty children are found
623 * @return int
625 public function count_non_empty_children($extensions = '*', $limit = 1) {
626 return $this->get_filtered_children($extensions, $limit);
630 * Returns parent file_info instance
632 * @return file_info or null for root
634 public function get_parent() {
635 return $this->browser->get_file_info($this->context);
640 * Returns glossary entries tagged with a specified tag.
642 * This is a callback used by the tag area mod_glossary/glossary_entries to search for glossary entries
643 * tagged with a specific tag.
645 * @param core_tag_tag $tag
646 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
647 * are displayed on the page and the per-page limit may be bigger
648 * @param int $fromctx context id where the link was displayed, may be used by callbacks
649 * to display items in the same context first
650 * @param int $ctx context id where to search for records
651 * @param bool $rec search in subcontexts as well
652 * @param int $page 0-based number of page being displayed
653 * @return \core_tag\output\tagindex
655 function mod_glossary_get_tagged_entries($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
656 global $OUTPUT;
657 $perpage = $exclusivemode ? 20 : 5;
659 // Build the SQL query.
660 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
661 $query = "SELECT ge.id, ge.concept, ge.glossaryid, ge.approved, ge.userid,
662 cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
663 FROM {glossary_entries} ge
664 JOIN {glossary} g ON g.id = ge.glossaryid
665 JOIN {modules} m ON m.name='glossary'
666 JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = g.id
667 JOIN {tag_instance} tt ON ge.id = tt.itemid
668 JOIN {course} c ON cm.course = c.id
669 JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
670 WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
671 AND cm.deletioninprogress = 0
672 AND ge.id %ITEMFILTER% AND c.id %COURSEFILTER%";
674 $params = array('itemtype' => 'glossary_entries', 'tagid' => $tag->id, 'component' => 'mod_glossary',
675 'coursemodulecontextlevel' => CONTEXT_MODULE);
677 if ($ctx) {
678 $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
679 $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
680 $params['contextid'] = $context->id;
681 $params['path'] = $context->path.'/%';
684 $query .= " ORDER BY ";
685 if ($fromctx) {
686 // In order-clause specify that modules from inside "fromctx" context should be returned first.
687 $fromcontext = context::instance_by_id($fromctx);
688 $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
689 $params['fromcontextid'] = $fromcontext->id;
690 $params['frompath'] = $fromcontext->path.'/%';
692 $query .= ' c.sortorder, cm.id, ge.id';
694 $totalpages = $page + 1;
696 // Use core_tag_index_builder to build and filter the list of items.
697 $builder = new core_tag_index_builder('mod_glossary', 'glossary_entries', $query, $params, $page * $perpage, $perpage + 1);
698 while ($item = $builder->has_item_that_needs_access_check()) {
699 context_helper::preload_from_record($item);
700 $courseid = $item->courseid;
701 if (!$builder->can_access_course($courseid)) {
702 $builder->set_accessible($item, false);
703 continue;
705 $modinfo = get_fast_modinfo($builder->get_course($courseid));
706 // Set accessibility of this item and all other items in the same course.
707 $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
708 global $USER;
709 if ($taggeditem->courseid == $courseid) {
710 $accessible = false;
711 if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) {
712 if ($taggeditem->approved) {
713 $accessible = true;
714 } else if ($taggeditem->userid == $USER->id) {
715 $accessible = true;
716 } else {
717 $accessible = has_capability('mod/glossary:approve', context_module::instance($cm->id));
720 $builder->set_accessible($taggeditem, $accessible);
725 $items = $builder->get_items();
726 if (count($items) > $perpage) {
727 $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
728 array_pop($items);
731 // Build the display contents.
732 if ($items) {
733 $tagfeed = new core_tag\output\tagfeed();
734 foreach ($items as $item) {
735 context_helper::preload_from_record($item);
736 $modinfo = get_fast_modinfo($item->courseid);
737 $cm = $modinfo->get_cm($item->cmid);
738 $pageurl = new moodle_url('/mod/glossary/showentry.php', array('eid' => $item->id, 'displayformat' => 'dictionary'));
739 $pagename = format_string($item->concept, true, array('context' => context_module::instance($item->cmid)));
740 $pagename = html_writer::link($pageurl, $pagename);
741 $courseurl = course_get_url($item->courseid, $cm->sectionnum);
742 $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
743 $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
744 $coursename = html_writer::link($courseurl, $coursename);
745 $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
746 $tagfeed->add($icon, $pagename, $cmname.'<br>'.$coursename);
749 $content = $OUTPUT->render_from_template('core_tag/tagfeed',
750 $tagfeed->export_for_template($OUTPUT));
752 return new core_tag\output\tagindex($tag, 'mod_glossary', 'glossary_entries', $content,
753 $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);