ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç deleteentry.php000064400000005576152265517410007626 0ustar00$id,'prevmode'=>$prevmode)); if ($confirm !== 0) { $url->param('confirm', $confirm); } if ($entry !== 0) { $url->param('entry', $entry); } if ($hook !== '') { $url->param('hook', $hook); } $PAGE->set_url($url); $strglossary = get_string("modulename", "glossary"); $strglossaries = get_string("modulenameplural", "glossary"); $stredit = get_string("edit"); $entrydeleted = get_string("entrydeleted","glossary"); if (! $cm = get_coursemodule_from_id('glossary', $id)) { throw new \moodle_exception("invalidcoursemodule"); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $entry = $DB->get_record("glossary_entries", array("id"=>$entry))) { throw new \moodle_exception('invalidentry'); } // Permission checks are based on the course module instance so make sure it is correct. if ($cm->instance != $entry->glossaryid) { throw new \moodle_exception('invalidentry'); } require_login($course, false, $cm); $context = context_module::instance($cm->id); if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidid', 'glossary'); } // Throws an exception if the user cannot delete the entry. mod_glossary_can_delete_entry($entry, $glossary, $context, false); /// If data submitted, then process and store. if ($confirm and confirm_sesskey()) { // the operation was confirmed. mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course, $hook, $prevmode); redirect("view.php?id=$cm->id&mode=$prevmode&hook=$hook"); } else { // the operation has not been confirmed yet so ask the user to do so $strareyousuredelete = get_string("areyousuredelete", "glossary"); $PAGE->navbar->add(get_string('delete')); $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); $PAGE->activityheader->disable(); echo $OUTPUT->header(); $areyousure = "".format_string($entry->concept)."

$strareyousuredelete

"; $linkyes = 'deleteentry.php'; $linkno = 'view.php'; $optionsyes = array('id'=>$cm->id, 'entry'=>$entry->id, 'confirm'=>1, 'sesskey'=>sesskey(), 'prevmode'=>$prevmode, 'hook'=>$hook); $optionsno = array('id'=>$cm->id, 'mode'=>$prevmode, 'hook'=>$hook); echo $OUTPUT->confirm($areyousure, new moodle_url($linkyes, $optionsyes), new moodle_url($linkno, $optionsno)); echo $OUTPUT->footer(); } styles.css000064400000005157152265517410006621 0ustar00/** General Styles **/ .path-mod-glossary .glossarypost { width: 95%; border-collapse: separate; margin: 0 auto; text-align: left; } .path-mod-glossary .glossarypost.entrylist { border-width: 0; } .path-mod-glossary .glossarypost.continuous .concept { display: inline; } .path-mod-glossary .glossarypost .commands { width: 200px; white-space: nowrap; } .path-mod-glossary .glossarypost td.picture { width: 35px; } .path-mod-glossary .glossarypost .entrylowersection .icons { text-align: right; padding-right: 5px; } .path-mod-glossary .glossarypost .entrylowersection .ratings { text-align: right; padding-right: 5px; padding-bottom: 2px; } .path-mod-glossary .glossarypost .glossary-hidden-note { margin: 0 .45em; } .path-mod-glossary .glossarydisplay { margin-left: auto; margin-right: auto; } .path-mod-glossary .glossarydisplay .tabs { width: 100%; margin-bottom: 0; } .path-mod-glossary .glossarydisplay .tabs .side { border-style: none; border-width: 0; width: auto; } .path-mod-glossary .glossarydisplay .separator { width: 4px; } .path-mod-glossary table.glossarypopup { width: 95%; } .path-mod-glossary .entrybox, .path-mod-glossary table.glossaryapproval, .path-mod-glossary .glossarypost .entrylowersection table { width: 100%; margin-bottom: 0; } .glossary-activity-picture { float: left; } .glossary-activity-content { margin-left: 40px; } /** Page specific styles **/ #page-mod-glossary-view .glossarycontrol { float: right; text-align: right; white-space: nowrap; margin: 5px 0; } #page-mod-glossary-view table.glossarycategoryheader, #page-mod-glossary-import table.glossaryimportexport { margin-left: auto; margin-right: auto; } #page-mod-glossary-view table.glossarycategoryheader { margin-bottom: 0; } #page-mod-glossary-view table.glossarycategoryheader th { padding: 0; } #page-mod-glossary-view td.glossarysearchbox label { display: inline-block; } @media (min-width: 768px) { /* Only apply this rule for larger devices (tablets, 768px and up). */ #page-mod-glossary-showentry #page-content { min-width: 600px; } } #page-mod-glossary-print .mod-glossary-entrylist .mod-glossary-entry { vertical-align: top; } #page-mod-glossary-print .displayprinticon, #page-mod-glossary-print .displaydate { text-align: right; font-size: 0.75em; } #page-mod-glossary-print .strong { font-weight: bold; } .path-mod-glossary .printicon { background: url([[pix:t/print]]) no-repeat scroll 2px center transparent; /*rtl:ignore*/ padding-left: 20px; } db/tag.php000064400000002231152265517410006423 0ustar00. /** * Tag areas in component mod_glossary * * @package mod_glossary * @copyright 2017 Andrew Hancox * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tagareas = array( array( 'itemtype' => 'glossary_entries', 'component' => 'mod_glossary', 'callback' => 'mod_glossary_get_tagged_entries', 'callbackfile' => '/mod/glossary/locallib.php', ), ); db/services.php000064400000016531152265517410007503 0ustar00. /** * Glossary module external functions. * * @package mod_glossary * @category external * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $functions = array( 'mod_glossary_get_glossaries_by_courses' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_glossaries_by_courses', 'description' => 'Retrieve a list of glossaries from several courses.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_view_glossary' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'view_glossary', 'description' => 'Notify the glossary as being viewed.', 'type' => 'write', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_view_entry' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'view_entry', 'description' => 'Notify a glossary entry as being viewed.', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_by_letter' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_by_letter', 'description' => 'Browse entries by letter.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_by_date' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_by_date', 'description' => 'Browse entries by date.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_categories' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_categories', 'description' => 'Get the categories.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_by_category' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_by_category', 'description' => 'Browse entries by category.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_authors' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_authors', 'description' => 'Get the authors.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_by_author' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_by_author', 'description' => 'Browse entries by author.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_by_author_id' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_by_author_id', 'description' => 'Browse entries by author ID.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_by_search' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_by_search', 'description' => 'Browse entries by search query.', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_by_term' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_by_term', 'description' => 'Browse entries by term (concept or alias).', 'type' => 'read', 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entries_to_approve' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entries_to_approve', 'description' => 'Browse entries to be approved.', 'type' => 'read', 'capabilities' => 'mod/glossary:approve', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_get_entry_by_id' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'get_entry_by_id', 'description' => 'Get an entry by ID', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/glossary:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_add_entry' => array( 'classname' => 'mod_glossary_external', 'methodname' => 'add_entry', 'description' => 'Add a new entry to a given glossary', 'type' => 'write', 'capabilities' => 'mod/glossary:write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_glossary_delete_entry' => [ 'classname' => 'mod_glossary\external\delete_entry', 'classpath' => '', 'description' => 'Delete the given entry from the glossary.', 'type' => 'write', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] ], 'mod_glossary_update_entry' => [ 'classname' => 'mod_glossary\external\update_entry', 'classpath' => '', 'description' => 'Updates the given glossary entry.', 'type' => 'write', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] ], 'mod_glossary_prepare_entry_for_edition' => [ 'classname' => 'mod_glossary\external\prepare_entry', 'classpath' => '', 'description' => 'Prepares the given entry for edition returning draft item areas and file areas information.', 'type' => 'read', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] ], ); db/log.php000064400000004112152265517410006431 0ustar00. /** * Definition of log events * * @package mod_glossary * @category log * @copyright 2010 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $logs = array( array('module'=>'glossary', 'action'=>'add', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'update', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'view', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'view all', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'add entry', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'update entry', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'add category', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'update category', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'delete category', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'approve entry', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'disapprove entry', 'mtable'=>'glossary', 'field'=>'name'), array('module'=>'glossary', 'action'=>'view entry', 'mtable'=>'glossary_entries', 'field'=>'concept'), );db/access.php000064400000013751152265517410007122 0ustar00. /** * Plugin capabilities * * @package mod_glossary * @copyright 2006 Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'mod/glossary:addinstance' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/course:manageactivities' ), 'mod/glossary:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'frontpage' => CAP_ALLOW, 'guest' => CAP_ALLOW, 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:write' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:manageentries' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:managecategories' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:comment' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:managecomments' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:import' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:export' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:approve' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:rate' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:viewrating' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:viewanyrating' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'mod/glossary:viewrating' ), 'mod/glossary:viewallratings' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'mod/glossary:viewrating' ), 'mod/glossary:exportentry' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/glossary:exportownentry' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW, 'student' => CAP_ALLOW, ) ), ); db/upgrade.php000064400000004132152265517410007301 0ustar00. /** * This file keeps track of upgrades to the glossary module * * Sometimes, changes between versions involve * alterations to database structures and other * major things that may break installations. * * The upgrade function in this file will attempt * to perform all the necessary actions to upgrade * your older installation to the current version. * * If there's something it cannot do itself, it * will tell you what you need to do. * * The commands in here will all be database-neutral, * using the methods of database_manager class * * Please do not forget to use upgrade_set_timeout() * before any action that may take longer time to finish. * * @package mod_glossary * @copyright 2006 Eloy Lafuente (stronk7) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ function xmldb_glossary_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } db/install.xml000064400000020754152265517410007341 0ustar00
db/events.php000064400000002060152265517410007154 0ustar00. /** * Glossary event observer. * * @package mod_glossary * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $observers = array( array ( 'eventname' => '\core\event\course_module_updated', 'callback' => '\mod_glossary\local\concept_cache::cm_updated', ), );db/caches.php000064400000002311152265517410007075 0ustar00. /** * Glossary cache definitions. * * @package mod_glossary * @category cache * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $definitions = array( // This MUST NOT be a local cache, sorry cluster lovers. 'concepts' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, // The course id or 0 for global. 'simpledata' => false, 'staticacceleration' => true, 'staticaccelerationsize' => 30, ), ); lang/en/deprecated.txt000064400000000031152265517410010732 0ustar00tagsdeleted,mod_glossary lang/en/glossary.php000064400000055412152265517410010462 0ustar00. /** * Strings for component 'mod_glossary', language 'en', branch 'MOODLE_20_STABLE' * * @package mod_glossary * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['addcomment'] = 'Add comment'; $string['addcategory'] = 'Add category'; $string['addentry'] = 'Add a new entry'; $string['addsingleentry'] = 'Add entry'; $string['addingcomment'] = 'Add a comment'; $string['alias'] = 'Keyword'; $string['aliases'] = 'Keyword(s)'; $string['aliases_help'] = 'Each glossary entry can have an associated list of keywords (or aliases). If the entry is auto-linked, then any keywords will also be auto-linked. Enter each keyword on a new line (not separated by commas).'; $string['allcategories'] = 'All categories'; $string['allentries'] = 'ALL'; $string['allowcomments'] = 'Allow comments on entries'; $string['allowcomments_help'] = 'If enabled, all participants with permission to create comments will be able to add comments to glossary entries.'; $string['allowduplicatedentries'] = 'Duplicate entries allowed'; $string['allowduplicatedentries_help'] = 'If enabled, multiple entries can have the same concept name.'; $string['allowprintview'] = 'Allow print view'; $string['allowprintview_help'] = 'If enabled, students are provided with a link to a printer-friendly version of the glossary. The link is always available to teachers.'; $string['andmorenewentries'] = 'and {$a} more new entries.'; $string['answer'] = 'Answer'; $string['approve'] = 'Approve'; $string['approvaldisplayformat'] = 'Approval display format'; $string['approvaldisplayformat_help'] = 'When approving glossary items you may wish to use a different display format'; $string['areaattachment'] = 'Attachments'; $string['areaentry'] = 'Definitions'; $string['areyousuredelete'] = 'Are you sure you want to delete this entry?'; $string['areyousuredeletecomment'] = 'Are you sure you want to delete this comment?'; $string['areyousureexport'] = 'Are you sure you want to export this entry to'; $string['ascending'] = 'ascending'; $string['attachment'] = 'Attachment'; $string['attachment_help'] = 'You can optionally attach one or more files to a glossary entry.'; $string['author'] = 'author'; $string['authorview'] = 'Browse by Author'; $string['back'] = 'Back'; $string['cachedef_concepts'] = 'Concept linking'; $string['cantinsertcat'] = 'Can\'t insert category'; $string['cantinsertrec'] = 'Can\'t insert record'; $string['cantinsertrel'] = 'Can\'t insert relation category-entry'; $string['casesensitive'] = 'This entry is case sensitive'; $string['casesensitive_help'] = 'This setting specifies whether matching exact upper and lower case is necessary when auto-linking to an entry.'; $string['cat'] = 'cat'; $string['categories'] = 'Categories'; $string['category'] = 'Category'; $string['categorydeleted'] = 'Category deleted'; $string['categoryview'] = 'Browse by category'; $string['cnfallowcomments'] = 'Define if a glossary will accept comments on entries by default'; $string['cnfallowdupentries'] = 'Define if a glossary will allows duplicated entries by default'; $string['cnfapprovalstatus'] = 'Define the approval status by default of an entry posted by a student'; $string['cnfcasesensitive'] = 'Define if an entry, when linked, is case sensitive by default'; $string['cnfdefaulthook'] = 'Select the default selection to show when the glossary is first viewed'; $string['cnfdefaultmode'] = 'Select the default frame to show when the glossary is first viewed.'; $string['cnffullmatch'] = 'Define if an entry, when linked, should match whole words (and not words inside other words) by default.'; $string['cnflinkentry'] = 'Define if an entry should be automatically linked by default'; $string['cnflinkglossaries'] = 'Define if a glossary should be automatically linked by default'; $string['cnfrelatedview'] = 'Select the display format to be used for automatic linking and entry view.'; $string['cnfshowgroup'] = 'Specify if the group break should be shown or not.'; $string['cnfsortkey'] = 'Select the sorting key by default.'; $string['cnfsortorder'] = 'Select the sorting order by default.'; $string['cnfstudentcanpost'] = 'Define if the students can or cannot post entries by default'; $string['cnftabs'] = 'Select visible tabs for this glossary format'; $string['comment'] = 'Comment'; $string['commentdeleted'] = 'The comment has been deleted.'; $string['comments'] = 'Comments'; $string['commentson'] = 'Comments on'; $string['commentupdated'] = 'The comment has been updated.'; $string['completiondetail:entries'] = 'Make entries: {$a}'; $string['completionentries'] = 'Add entries'; $string['completionentriesdesc'] = 'Student must create at least {$a} entry/entries'; $string['concept'] = 'Concept'; $string['concepts'] = 'Concepts'; $string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all glossaries. You will still need to turn feeds on manually in the settings for each glossary.'; $string['current'] = 'Currently sorted {$a}'; $string['currentglossary'] = 'Current glossary'; $string['date'] = 'date'; $string['dateview'] = 'Browse by date'; $string['defaultapproval'] = 'Approved by default'; $string['defaultapproval_help'] = 'If set to no, entries require approving by a teacher before they are viewable by everyone.'; $string['defaulthook'] = 'Default hook'; $string['defaultmode'] = 'Default mode'; $string['defaultsortkey'] = 'Default sort key'; $string['defaultsortorder'] = 'Default sort order'; $string['definition'] = 'Definition'; $string['definitions'] = 'Definitions'; $string['deleteentry'] = 'Delete entry'; $string['deletecategory'] = 'Delete category'; $string['deleteentrya'] = 'Delete entry: {$a}'; $string['deletenotenrolled'] = 'Entries by users not enrolled'; $string['deletingcomment'] = 'Deleting comment'; $string['deletingnoneemptycategory'] = 'Deleting this category will not delete the entries it contains - they will be marked as uncategorised.'; $string['descending'] = 'descending'; $string['destination'] = 'Destination of imported entries'; $string['destination_help'] = 'Entries can either be imported and added to the current glossary or to a new glossary, in which case a new glossary will be created based on information in the XML file.'; $string['disapprove'] = 'Undo approval'; $string['displayformat'] = 'Display format'; $string['displayformat_help'] = 'There are 7 display formats: * Simple, dictionary style - No authors are displayed and attachments are shown as links * Continuous without author - Entries are displayed one after another without any separation apart from the editing icons * Full with author - A forum-like display format showing the author\'s data and with attachments shown as links * Full without author - A forum-like display format without authors and with attachments shown as links * Encyclopedia - As for "Full with author" but attached images are shown inline * Entry list - Concepts are listed as links * FAQ - The words QUESTION and ANSWER are appended to the concept and definition respectively'; $string['displayformatcontinuous'] = 'Continuous without author'; $string['displayformatdefault'] = 'Default to same as display format'; $string['displayformatdictionary'] = 'Simple, dictionary style'; $string['displayformatencyclopedia'] = 'Encyclopedia'; $string['displayformatentrylist'] = 'Entry list'; $string['displayformatfaq'] = 'FAQ'; $string['displayformatfullwithauthor'] = 'Full with author'; $string['displayformatfullwithoutauthor'] = 'Full without author'; $string['displayformats'] = 'Display formats'; $string['displayformatssetup'] = 'Display formats setup'; $string['duplicatecategory'] = 'Duplicate category'; $string['duplicateentry'] = 'Duplicate entry'; $string['editalways'] = 'Always allow editing'; $string['editalways_help'] = 'This setting specifies whether entries are always editable or whether students can only edit their entries during a configured editing time (usually 30 minutes).'; $string['editcategories'] = 'Edit categories'; $string['editcategory'] = 'Edit category'; $string['editentry'] = 'Edit entry'; $string['editentrya'] = 'Edit entry: {$a}'; $string['editingcomment'] = 'Editing comment'; $string['entbypage'] = 'Entries shown per page'; $string['entries'] = 'Entries'; $string['entrieswithoutcategory'] = 'Entries without category'; $string['entry'] = 'Entry'; $string['entryalreadyexist'] = 'Entry already exists'; $string['entryapproved'] = 'This entry has been approved'; $string['entrydeleted'] = 'Entry deleted'; $string['entryexported'] = 'Entry successfully exported'; $string['entryishidden'] = '(this entry is currently hidden)'; $string['entryleveldefaultsettings'] = 'Entry level default settings'; $string['entrylink'] = 'Entry link: {$a}'; $string['entrysaved'] = 'This entry has been saved'; $string['entryupdated'] = 'This entry has been updated'; $string['entryusedynalink'] = 'This entry should be automatically linked'; $string['entryusedynalink_help'] = 'If site-wide glossary auto-linking has been enabled by an administrator and this checkbox is ticked, the entry will be automatically linked wherever the concept words and phrases appear throughout the rest of the course.'; $string['errcannoteditothers'] = 'You cannot edit other people\'s entries.'; $string['errconceptalreadyexists'] = 'This concept already exists. No duplicates allowed in this glossary.'; $string['errdeltimeexpired'] = 'You can\'t delete this. Time expired!'; $string['erredittimeexpired'] = 'The editing time for this entry has expired.'; $string['errorparsingxml'] = 'Errors occurred while parsing the file. Make sure it is valid XML syntax.'; $string['errreservedkeywords'] = 'One or more keywords contain a special character which cannot be used.'; $string['eventcategorycreated'] = 'Category has been created'; $string['eventcategorydeleted'] = 'Category has been deleted'; $string['eventcategoryupdated'] = 'Category has been updated'; $string['evententryapproved'] = 'Entry has been approved'; $string['evententrycreated'] = 'Entry has been created'; $string['evententrydeleted'] = 'Entry has been deleted'; $string['evententrydisapproved'] = 'Entry has been disapproved'; $string['evententryviewed'] = 'Entry has been viewed'; $string['evententryupdated'] = 'Entry has been updated'; $string['explainaddentry'] = 'Add a new entry to the current glossary.
Concept and definition are mandatory fields.'; $string['explainall'] = 'Shows ALL entries on one page'; $string['explainalphabet'] = 'Browse the glossary using this index'; $string['explainexport'] = 'Click on the button below to export glossary entries.
You can import it anytime you wish in this or other course.

Please note that attachments (e.g. images) and authors are not exported.

'; $string['explainimport'] = 'You must specify the file to import and define the criteria of the process.

Submit your request and review the results.

'; $string['explainspecial'] = 'Shows entries that do not begin with a letter'; $string['export'] = 'Export'; $string['exportedentry'] = 'Exported entry'; $string['exportentries'] = 'Export entries'; $string['exportentriestoxml'] = 'Export entries to XML file'; $string['exportfile'] = 'Export entries to file'; $string['exportglossary'] = 'Export glossary'; $string['exporttomainglossary'] = 'Export to main glossary'; $string['filetoimport'] = 'File to import'; $string['filetoimport_help'] = 'Browse for and select the XML file on your computer which contains the entries to import.'; $string['fillfields'] = 'Concept and definition are mandatory fields.'; $string['filtername'] = 'Glossary auto-linking'; $string['fullmatch'] = 'Match whole words only'; $string['fullmatch_help'] = 'This setting specifies whether only whole words will be linked, for example, a glossary entry named "construct" will not create a link inside the word "constructivism".'; $string['glossary:addinstance'] = 'Add a new glossary'; $string['glossary:approve'] = 'Approve and undo approved entries'; $string['glossary:comment'] = 'Create comments'; $string['glossary:export'] = 'Export entries'; $string['glossary:exportentry'] = 'Export single entry'; $string['glossary:exportownentry'] = 'Export single entry of yours'; $string['glossary:import'] = 'Import entries'; $string['glossaryleveldefaultsettings'] = 'Glossary level default settings'; $string['glossary:managecategories'] = 'Manage categories'; $string['glossary:managecomments'] = 'Manage comments'; $string['glossary:manageentries'] = 'Manage entries'; $string['glossary:rate'] = 'Rate entries'; $string['glossary:view'] = 'View glossary'; $string['glossary:viewallratings'] = 'View all raw ratings given by individuals'; $string['glossary:viewanyrating'] = 'View total ratings that anyone received'; $string['glossary:viewrating'] = 'View the total rating you received'; $string['glossary:write'] = 'Create new entries'; $string['glossarytype'] = 'Glossary type'; $string['glossarytype_help'] = 'A main glossary is a glossary in which entries from secondary glossaries can be imported. There can only be one main glossary in a course. If glossary entry import is not required, all glossaries in the course can be secondary glossaries.'; $string['guestnoedit'] = 'Guests are not allowed to edit glossaries'; $string['changeto'] = 'change to {$a}'; $string['importcategories'] = 'Import categories'; $string['importedcategories'] = 'Imported categories'; $string['importedentries'] = 'Imported entries'; $string['importentries'] = 'Import entries'; $string['importentriesfromxml'] = 'Import entries from XML file'; $string['includegroupbreaks'] = 'Include group breaks'; $string['indicator:cognitivedepth'] = 'Glossary cognitive'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a Glossary activity.'; $string['indicator:cognitivedepthdef'] = 'Glossary cognitive'; $string['indicator:cognitivedepthdef_help'] = 'The participant has reached this percentage of the cognitive engagement offered by the Glossary activities during this analysis interval (Levels = No view, View, Submit)'; $string['indicator:cognitivedepthdef_link'] = 'Learning_analytics_indicators#Cognitive_depth'; $string['indicator:socialbreadth'] = 'Glossary social'; $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a Glossary activity.'; $string['indicator:socialbreadthdef'] = 'Glossary social'; $string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Glossary activities during this analysis interval (Levels = No participation, Participant alone)'; $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; $string['isglobal'] = 'Is this glossary global?'; $string['isglobal_help'] = 'A global glossary has entries which are linked to from throughout the site, rather than only in the course that the glossary is in. Only administrators can set a glossary as global.'; $string['letter'] = 'letter'; $string['linkcategory'] = 'Automatically link this category'; $string['linkcategory_help'] = 'If glossary auto-linking has been enabled and this setting is enabled, the category name will be automatically linked wherever it appears throughout the rest of the course. When a participant follows a category name link, they will be taken to the "Browse by category" page of the glossary.'; $string['linking'] = 'Auto-linking'; $string['mainglossary'] = 'Main glossary'; $string['maxtimehaspassed'] = 'Sorry, but the maximum time for editing this comment ({$a}) has passed!'; $string['modulename'] = 'Glossary'; $string['modulename_help'] = 'The glossary activity module enables participants to create and maintain a list of definitions, like a dictionary, or to collect and organise resources or information. A teacher can allow files to be attached to glossary entries. Attached images are displayed in the entry. Entries can be searched or browsed alphabetically or by category, date or author. Entries can be approved by default or require approval by a teacher before they are viewable by everyone. If the glossary auto-linking filter is enabled, entries will be automatically linked where the concept words and/or phrases appear within the course. A teacher can allow comments on entries. Entries can also be rated by teachers or students (peer evaluation). Ratings can be aggregated to form a final grade which is recorded in the gradebook. Glossaries have many uses, such as * A collaborative bank of key terms * A ‘getting to know you’ space where new students add their name and personal details * A ‘handy tips’ resource of best practice in a practical subject * A sharing area of useful videos, images or sound files * A revision resource of facts to remember'; $string['modulename_link'] = 'mod/glossary/view'; $string['modulenameplural'] = 'Glossaries'; $string['newentries'] = 'New glossary entries'; $string['newglossary'] = 'New glossary'; $string['newglossarycreated'] = 'New glossary created.'; $string['newglossaryentries'] = 'New glossary entries:'; $string['nocomment'] = 'No comment found'; $string['nocomments'] = '(No comments found on this entry)'; $string['noconceptfound'] = 'No concept or definition found.'; $string['noentries'] = 'No entries found in this section'; $string['noentry'] = 'No entry found.'; $string['nopermissiontodelcomment'] = 'You can\'t delete other people\'s comments!'; $string['nopermissiontodelinglossary'] = 'You can\'t comments in this glossary!'; $string['nopermissiontoviewresult'] = 'You can only look at results for your own entries'; $string['notcategorised'] = 'Not categorised'; $string['notapproved'] = 'glossary entry is not approved yet.'; $string['entrynotapproved'] = 'Entry not approved'; $string['numberofentries'] = 'Number of entries'; $string['onebyline'] = '(one per line)'; $string['page-mod-glossary-x'] = 'Any glossary module page'; $string['page-mod-glossary-edit'] = 'Glossary add/edit entry page'; $string['page-mod-glossary-view'] = 'View glossary edit page'; $string['pendingapproval'] = 'Pending approval'; $string['pendingapprovalcount'] = 'Pending approval ({$a})'; $string['pluginadministration'] = 'Glossary administration'; $string['pluginname'] = 'Glossary'; $string['popupformat'] = 'Popup format'; $string['print'] = 'Print'; $string['printerfriendly'] = 'Printer-friendly version'; $string['printviewnotallowed'] = 'Print view isn\'t allowed'; $string['privacy'] = 'Privacy of results'; $string['privacy:metadata:core_comments'] = 'Comments made on glossary definitions are stored using core_comment system'; $string['privacy:metadata:core_files'] = 'Files linked to glossary definitions are stored using the core_files system'; $string['privacy:metadata:core_rating'] = 'Ratings added to glossary entries are stored using core_rating system'; $string['privacy:metadata:core_tag'] = 'Tags added to glossary definitions are stored using core_tag system '; $string['privacy:metadata:glossary_entries'] = 'Information about the user\'s entries for a given glossary activity'; $string['privacy:metadata:glossary_entries:attachment'] = 'The attachment of the entry the user added'; $string['privacy:metadata:glossary_entries:concept'] = 'The concept of the entry the user added'; $string['privacy:metadata:glossary_entries:definition'] = 'The definition of the entry the user added'; $string['privacy:metadata:glossary_entries:glossaryid'] = 'The ID of the glossary activity'; $string['privacy:metadata:glossary_entries:userid'] = 'The ID of the user that is adding this glossary entry'; $string['privacy:metadata:glossary_entries:timemodified'] = 'The timestamp indicating when the glossary entry was modified by the user'; $string['question'] = 'Question'; $string['rejectedentries'] = 'Rejected entries'; $string['rejectionrpt'] = 'Rejection report'; $string['removeallglossarytags'] = 'All glossary tags'; $string['resetglossaries'] = 'Delete entries from'; $string['resetglossariesall'] = 'Entries from all glossaries'; $string['rssarticles'] = 'Number of RSS recent articles'; $string['rssarticles_help'] = 'This setting specifies the number of glossary entry concepts to include in the RSS feed. Between 5 and 20 generally acceptable.'; $string['rsssubscriberss'] = 'Display the RSS feed for \'{$a}\' concepts'; $string['rssfeed'] = 'RSS feed'; $string['rsstype'] = 'RSS feed for this activity'; $string['rsstype_help'] = 'To enable the RSS feed for this activity, select either concepts with author or concepts without author to be included in the feed.'; $string['search:activity'] = 'Glossary - activity information'; $string['search:entry'] = 'Glossary - entries'; $string['searchindefinition'] = 'Search full text'; $string['secondaryglossary'] = 'Secondary glossary'; $string['showall'] = 'Show \'ALL\' link'; $string['showall_help'] = 'If enabled, participants can browse all entries at once.'; $string['showalphabet'] = 'Show alphabet links'; $string['showalphabet_help'] = 'If enabled, participants can browse the glossary by letters of the alphabet.'; $string['showspecial'] = 'Show \'Special\' link'; $string['showspecial_help'] = 'If enabled, participants can browse the glossary by special characters, such as @ and #.'; $string['sortby'] = 'Sort by'; $string['sortbycreation'] = 'By creation date'; $string['sortbylastupdate'] = 'By last update'; $string['sortchronogically'] = 'Sort chronologically'; $string['special'] = 'Special'; $string['standardview'] = 'Browse by alphabet'; $string['studentcanpost'] = 'Students can add entries'; $string['tagarea_glossary_entries'] = 'Glossary entries'; $string['totalentries'] = 'Total entries'; $string['usedynalink'] = 'Automatically link glossary entries'; $string['usedynalink_help'] = 'If site-wide glossary auto-linking has been enabled by an administrator and this setting is enabled, the "Add a new entry" form includes the option to automatically link the entry wherever the concept words and phrases appear throughout the rest of the course.'; $string['visibletabs'] = 'Visible tabs'; $string['warningstudentcapost'] = '(Applies only if the glossary is not the main one)'; $string['withauthor'] = 'Concepts with author'; $string['withoutauthor'] = 'Concepts without author'; $string['writtenby'] = 'by'; $string['youarenottheauthor'] = 'You are not the author of this comment, so you are not allowed to edit it.'; // Deprecated since 4.5. $string['tagsdeleted'] = 'Glossary tags have been deleted'; backup/moodle2/backup_glossary_stepslib.php000064400000014151152265517410015172 0ustar00. /** * @package mod_glossary * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the backup steps that will be used by the backup_glossary_activity_task */ /** * Define the complete glossary structure for backup, with file and id annotations */ class backup_glossary_activity_structure_step extends backup_activity_structure_step { protected function define_structure() { // To know if we are including userinfo $userinfo = $this->get_setting_value('userinfo'); // Define each element separated $glossary = new backup_nested_element('glossary', array('id'), array( 'name', 'intro', 'introformat', 'allowduplicatedentries', 'displayformat', 'mainglossary', 'showspecial', 'showalphabet', 'showall', 'allowcomments', 'allowprintview', 'usedynalink', 'defaultapproval', 'globalglossary', 'entbypage', 'editalways', 'rsstype', 'rssarticles', 'assessed', 'assesstimestart', 'assesstimefinish', 'scale', 'timecreated', 'timemodified', 'completionentries')); $entries = new backup_nested_element('entries'); $entry = new backup_nested_element('entry', array('id'), array( 'userid', 'concept', 'definition', 'definitionformat', 'definitiontrust', 'attachment', 'timecreated', 'timemodified', 'teacherentry', 'sourceglossaryid', 'usedynalink', 'casesensitive', 'fullmatch', 'approved')); $tags = new backup_nested_element('entriestags'); $tag = new backup_nested_element('tag', array('id'), array('itemid', 'rawname')); $aliases = new backup_nested_element('aliases'); $alias = new backup_nested_element('alias', array('id'), array( 'alias_text')); $ratings = new backup_nested_element('ratings'); $rating = new backup_nested_element('rating', array('id'), array( 'component', 'ratingarea', 'scaleid', 'value', 'userid', 'timecreated', 'timemodified')); $categories = new backup_nested_element('categories'); $category = new backup_nested_element('category', array('id'), array( 'name', 'usedynalink')); $categoryentries = new backup_nested_element('category_entries'); $categoryentry = new backup_nested_element('category_entry', array('id'), array( 'entryid')); // Build the tree $glossary->add_child($entries); $entries->add_child($entry); $glossary->add_child($tags); $tags->add_child($tag); $entry->add_child($aliases); $aliases->add_child($alias); $entry->add_child($ratings); $ratings->add_child($rating); $glossary->add_child($categories); $categories->add_child($category); $category->add_child($categoryentries); $categoryentries->add_child($categoryentry); // Define sources $glossary->set_source_table('glossary', array('id' => backup::VAR_ACTIVITYID)); $category->set_source_table('glossary_categories', array('glossaryid' => backup::VAR_PARENTID)); // All the rest of elements only happen if we are including user info if ($userinfo) { $entry->set_source_table('glossary_entries', array('glossaryid' => backup::VAR_PARENTID)); $alias->set_source_table('glossary_alias', array('entryid' => backup::VAR_PARENTID)); $alias->set_source_alias('alias', 'alias_text'); $rating->set_source_table('rating', array('contextid' => backup::VAR_CONTEXTID, 'itemid' => backup::VAR_PARENTID, 'component' => backup_helper::is_sqlparam('mod_glossary'), 'ratingarea' => backup_helper::is_sqlparam('entry'))); $rating->set_source_alias('rating', 'value'); $categoryentry->set_source_table('glossary_entries_categories', array('categoryid' => backup::VAR_PARENTID)); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { $tag->set_source_sql('SELECT t.id, ti.itemid, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = ? AND ti.component = ? AND ti.contextid = ?', array( backup_helper::is_sqlparam('glossary_entries'), backup_helper::is_sqlparam('mod_glossary'), backup::VAR_CONTEXTID)); } } // Define id annotations $glossary->annotate_ids('scale', 'scale'); $entry->annotate_ids('user', 'userid'); $rating->annotate_ids('scale', 'scaleid'); $rating->annotate_ids('user', 'userid'); // Define file annotations $glossary->annotate_files('mod_glossary', 'intro', null); // This file area hasn't itemid $entry->annotate_files('mod_glossary', 'entry', 'id'); $entry->annotate_files('mod_glossary', 'attachment', 'id'); // Return the root element (glossary), wrapped into standard activity structure return $this->prepare_activity_structure($glossary); } } backup/moodle2/restore_glossary_stepslib.php000064400000017052152265517410015413 0ustar00. /** * @package mod_glossary * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the restore steps that will be used by the restore_glossary_activity_task */ /** * Structure step to restore one glossary activity */ class restore_glossary_activity_structure_step extends restore_activity_structure_step { protected function define_structure() { $paths = array(); $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('glossary', '/activity/glossary'); $paths[] = new restore_path_element('glossary_category', '/activity/glossary/categories/category'); if ($userinfo) { $paths[] = new restore_path_element('glossary_entry', '/activity/glossary/entries/entry'); $paths[] = new restore_path_element('glossary_entry_tag', '/activity/glossary/entriestags/tag'); $paths[] = new restore_path_element('glossary_alias', '/activity/glossary/entries/entry/aliases/alias'); $paths[] = new restore_path_element('glossary_rating', '/activity/glossary/entries/entry/ratings/rating'); $paths[] = new restore_path_element('glossary_category_entry', '/activity/glossary/categories/category/category_entries/category_entry'); } // Return the paths wrapped into standard activity structure return $this->prepare_activity_structure($paths); } protected function process_glossary($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. $data->assesstimestart = $this->apply_date_offset($data->assesstimestart); $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish); if ($data->scale < 0) { // scale found, get mapping $data->scale = -($this->get_mappingid('scale', abs($data->scale))); } $formats = get_list_of_plugins('mod/glossary/formats'); // Check format if (!in_array($data->displayformat, $formats)) { $data->displayformat = 'dictionary'; } if (!empty($data->globalglossary) && !has_capability('mod/glossary:manageentries', context_system::instance())) { $data->globalglossary = 0; } if (!empty($data->mainglossary) and $data->mainglossary == 1 and $DB->record_exists('glossary', array('mainglossary' => 1, 'course' => $this->get_courseid()))) { // Only allow one main glossary in the course $data->mainglossary = 0; } // insert the glossary record $newitemid = $DB->insert_record('glossary', $data); $this->apply_activity_instance($newitemid); } protected function process_glossary_entry($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->glossaryid = $this->get_new_parentid('glossary'); $data->userid = $this->get_mappingid('user', $data->userid); $data->sourceglossaryid = $this->get_mappingid('glossary', $data->sourceglossaryid); $context = context_module::instance($this->task->get_moduleid()); if (!empty($data->definitiontrust) && !trusttext_trusted($context)) { $data->definitiontrust = 0; } // insert the entry record $newitemid = $DB->insert_record('glossary_entries', $data); $this->set_mapping('glossary_entry', $oldid, $newitemid, true); // childs and files by itemname } protected function process_glossary_alias($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->entryid = $this->get_new_parentid('glossary_entry'); $data->alias = $data->alias_text; $newitemid = $DB->insert_record('glossary_alias', $data); } protected function process_glossary_rating($data) { global $DB; $data = (object)$data; // Cannot use ratings API, cause, it's missing the ability to specify times (modified/created) $data->contextid = $this->task->get_contextid(); $data->itemid = $this->get_new_parentid('glossary_entry'); if ($data->scaleid < 0) { // scale found, get mapping $data->scaleid = -($this->get_mappingid('scale', abs($data->scaleid))); } $data->rating = $data->value; $data->userid = $this->get_mappingid('user', $data->userid); // Make sure that we have both component and ratingarea set. These were added in 2.1. // Prior to that all ratings were for entries so we know what to set them too. if (empty($data->component)) { $data->component = 'mod_glossary'; } if (empty($data->ratingarea)) { $data->ratingarea = 'entry'; } $newitemid = $DB->insert_record('rating', $data); } protected function process_glossary_entry_tag($data) { $data = (object)$data; if (!core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { // Tags disabled in server, nothing to process. return; } $tag = $data->rawname; if (!$itemid = $this->get_mappingid('glossary_entry', $data->itemid)) { // Some orphaned tag, we could not find the glossary entry for it - ignore. return; } $context = context_module::instance($this->task->get_moduleid()); core_tag_tag::add_item_tag('mod_glossary', 'glossary_entries', $itemid, $context, $tag); } protected function process_glossary_category($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->glossaryid = $this->get_new_parentid('glossary'); $newitemid = $DB->insert_record('glossary_categories', $data); $this->set_mapping('glossary_category', $oldid, $newitemid); } protected function process_glossary_category_entry($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->categoryid = $this->get_new_parentid('glossary_category'); $data->entryid = $this->get_mappingid('glossary_entry', $data->entryid); $newitemid = $DB->insert_record('glossary_entries_categories', $data); } protected function after_execute() { // Add glossary related files, no need to match by itemname (just internally handled context) $this->add_related_files('mod_glossary', 'intro', null); // Add entries related files, matching by itemname (glossary_entry) $this->add_related_files('mod_glossary', 'entry', 'glossary_entry'); $this->add_related_files('mod_glossary', 'attachment', 'glossary_entry'); } } backup/moodle2/restore_glossary_activity_task.class.php000064400000012416152265517410017547 0ustar00. /** * @package mod_glossary * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/glossary/backup/moodle2/restore_glossary_stepslib.php'); // Because it exists (must) /** * glossary restore task that provides all the settings and steps to perform one * complete restore of the activity */ class restore_glossary_activity_task extends restore_activity_task { /** * Define (add) particular settings this activity can have */ protected function define_my_settings() { // No particular settings for this activity } /** * Define (add) particular steps this activity can have */ protected function define_my_steps() { // Choice only has one structure step $this->add_step(new restore_glossary_activity_structure_step('glossary_structure', 'glossary.xml')); } /** * Define the contents in the activity that must be * processed by the link decoder */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('glossary', array('intro'), 'glossary'); $contents[] = new restore_decode_content('glossary_entries', array('definition'), 'glossary_entry'); return $contents; } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ public static function define_decode_rules() { $rules = array(); $rules[] = new restore_decode_rule('GLOSSARYVIEWBYID', '/mod/glossary/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('GLOSSARYINDEX', '/mod/glossary/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('GLOSSARYSHOWENTRY', '/mod/glossary/showentry.php?courseid=$1&eid=$2', array('course', 'glossary_entry')); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * glossary logs. It must return one array * of {@link restore_log_rule} objects */ public static function define_restore_log_rules() { $rules = array(); $rules[] = new restore_log_rule('glossary', 'add', 'view.php?id={course_module}', '{glossary}'); $rules[] = new restore_log_rule('glossary', 'update', 'view.php?id={course_module}', '{glossary}'); $rules[] = new restore_log_rule('glossary', 'view', 'view.php?id={course_module}', '{glossary}'); $rules[] = new restore_log_rule('glossary', 'add category', 'editcategories.php?id={course_module}', '{glossary_category}'); $rules[] = new restore_log_rule('glossary', 'edit category', 'editcategories.php?id={course_module}', '{glossary_category}'); $rules[] = new restore_log_rule('glossary', 'delete category', 'editcategories.php?id={course_module}', '{glossary_category}'); $rules[] = new restore_log_rule('glossary', 'add entry', 'view.php?id={course_module}&mode=entry&hook={glossary_entry}', '{glossary_entry}'); $rules[] = new restore_log_rule('glossary', 'update entry', 'view.php?id={course_module}&mode=entry&hook={glossary_entry}', '{glossary_entry}'); $rules[] = new restore_log_rule('glossary', 'delete entry', 'view.php?id={course_module}&mode=entry&hook={glossary_entry}', '{glossary_entry}'); $rules[] = new restore_log_rule('glossary', 'approve entry', 'showentry.php?id={course_module}&eid={glossary_entry}', '{glossary_entry}'); $rules[] = new restore_log_rule('glossary', 'disapprove entry', 'showentry.php?id={course_module}&eid={glossary_entry}', '{glossary_entry}'); $rules[] = new restore_log_rule('glossary', 'view entry', 'showentry.php?eid={glossary_entry}', '{glossary_entry}'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * course logs. It must return one array * of {@link restore_log_rule} objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { $rules = array(); $rules[] = new restore_log_rule('glossary', 'view all', 'index.php?id={course}', null); return $rules; } } backup/moodle2/backup_glossary_activity_task.class.php000064400000005101152265517410017322 0ustar00. /** * Defines backup_glossary_activity_task class * * @package mod_glossary * @category backup * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/glossary/backup/moodle2/backup_glossary_stepslib.php'); /** * Provides the steps to perform one complete backup of the Glossary instance */ class backup_glossary_activity_task extends backup_activity_task { /** * No specific settings for this activity */ protected function define_my_settings() { } /** * Defines a backup step to store the instance data in the glossary.xml file */ protected function define_my_steps() { $this->add_step(new backup_glossary_activity_structure_step('glossary_structure', 'glossary.xml')); } /** * Encodes URLs to the index.php and view.php scripts * * @param string $content some HTML text that eventually contains URLs to the activity instance scripts * @return string the content with the URLs encoded */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot,"/"); // Link to the list of glossaries $search="/(".$base."\/mod\/glossary\/index.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@GLOSSARYINDEX*$2@$', $content); // Link to glossary view by moduleid $search="/(".$base."\/mod\/glossary\/view.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@GLOSSARYVIEWBYID*$2@$', $content); // Link to glossary entry $search="/(".$base."\/mod\/glossary\/showentry.php\?courseid=)([0-9]+)(&|&)eid=([0-9]+)/"; $content = preg_replace($search, '$@GLOSSARYSHOWENTRY*$2*$4@$', $content); return $content; } } backup/moodle1/lib.php000064400000013052152265517410010641 0ustar00. /** * Provides support for the conversion of moodle1 backup to the moodle2 format * * @package mod_glossary * @copyright 2011 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Glossary conversion handler */ class moodle1_mod_glossary_handler extends moodle1_mod_handler { /** @var moodle1_file_manager */ protected $fileman = null; /** @var int cmid */ protected $moduleid = null; /** * Declare the paths in moodle.xml we are able to convert * * The method returns list of {@link convert_path} instances. * For each path returned, the corresponding conversion method must be * defined. * * Note that the path /MOODLE_BACKUP/COURSE/MODULES/MOD/GLOSSARY does not * actually exist in the file. The last element with the module name was * appended by the moodle1_converter class. * * @return array of {@link convert_path} instances */ public function get_paths() { return array( new convert_path( 'glossary', '/MOODLE_BACKUP/COURSE/MODULES/MOD/GLOSSARY', array( 'newfields' => array( 'introformat' => FORMAT_MOODLE, 'completionentries' => 0, ), ) ), new convert_path('glossary_categories', '/MOODLE_BACKUP/COURSE/MODULES/MOD/GLOSSARY/CATEGORIES'), new convert_path( 'glossary_category', '/MOODLE_BACKUP/COURSE/MODULES/MOD/GLOSSARY/CATEGORIES/CATEGORY', array( 'dropfields' => array( 'glossaryid' ) ) ) ); } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/GLOSSARY * data available */ public function process_glossary($data) { global $CFG; // get the course module id and context id $instanceid = $data['id']; $cminfo = $this->get_cminfo($instanceid); $this->moduleid = $cminfo['id']; $contextid = $this->converter->get_contextid(CONTEXT_MODULE, $this->moduleid); // replay the upgrade step 2009042006 if ($CFG->texteditors !== 'textarea') { $data['intro'] = text_to_html($data['intro'], false, false, true); $data['introformat'] = FORMAT_HTML; } // get a fresh new file manager for this instance $this->fileman = $this->converter->get_file_manager($contextid, 'mod_glossary'); // convert course files embedded into the intro $this->fileman->filearea = 'intro'; $this->fileman->itemid = 0; $data['intro'] = moodle1_converter::migrate_referenced_files($data['intro'], $this->fileman); // start writing glossary.xml $this->open_xml_writer("activities/glossary_{$this->moduleid}/glossary.xml"); $this->xmlwriter->begin_tag('activity', array('id' => $instanceid, 'moduleid' => $this->moduleid, 'modulename' => 'glossary', 'contextid' => $contextid)); $this->xmlwriter->begin_tag('glossary', array('id' => $instanceid)); foreach ($data as $field => $value) { if ($field <> 'id') { $this->xmlwriter->full_tag($field, $value); } } return $data; } /** * This is executed when the parser reaches the opening element */ public function on_glossary_categories_start() { $this->xmlwriter->begin_tag('categories'); } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/GLOSSARY/CATEGORIES/CATEGORY * data available */ public function process_glossary_category($data) { $this->write_xml('category', $data, array('/category/id')); } /** * This is executed when the parser reaches the closing element */ public function on_glossary_categories_end() { $this->xmlwriter->end_tag('categories'); } /** * This is executed when we reach the closing tag of our 'glossary' path */ public function on_glossary_end() { // finalize glossary.xml $this->xmlwriter->end_tag('glossary'); $this->xmlwriter->end_tag('activity'); $this->close_xml_writer(); // write inforef.xml $this->open_xml_writer("activities/glossary_{$this->moduleid}/inforef.xml"); $this->xmlwriter->begin_tag('inforef'); $this->xmlwriter->begin_tag('fileref'); foreach ($this->fileman->get_fileids() as $fileid) { $this->write_xml('file', array('id' => $fileid)); } $this->xmlwriter->end_tag('fileref'); $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); } } view.php000064400000042667152265517410006256 0ustar00libdir . '/completionlib.php'); require_once("$CFG->libdir/rsslib.php"); $id = optional_param('id', 0, PARAM_INT); // Course Module ID $g = optional_param('g', 0, PARAM_INT); // Glossary ID $tab = optional_param('tab', GLOSSARY_NO_VIEW, PARAM_ALPHA); // browsing entries by categories? $displayformat = optional_param('displayformat',-1, PARAM_INT); // override of the glossary display format $mode = optional_param('mode', '', PARAM_ALPHA); // term entry cat date letter search author approval $hook = optional_param('hook', '', PARAM_CLEAN); // the term, entry, cat, etc... to look for based on mode $fullsearch = optional_param('fullsearch', 0,PARAM_INT); // full search (concept and definition) when searching? $sortkey = optional_param('sortkey', '', PARAM_ALPHA);// Sorted view: CREATION | UPDATE | FIRSTNAME | LASTNAME... $sortorder = optional_param('sortorder', 'ASC', PARAM_ALPHA); // it defines the order of the sorting (ASC or DESC) $offset = optional_param('offset', 0,PARAM_INT); // entries to bypass (for paging purposes) $page = optional_param('page', 0,PARAM_INT); // Page to show (for paging purposes) $show = optional_param('show', '', PARAM_ALPHA); // [ concept | alias ] => mode=term hook=$show if (!empty($id)) { if (! $cm = get_coursemodule_from_id('glossary', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidid', 'glossary'); } } else if (!empty($g)) { if (! $glossary = $DB->get_record("glossary", array("id"=>$g))) { throw new \moodle_exception('invalidid', 'glossary'); } if (! $course = $DB->get_record("course", array("id"=>$glossary->course))) { throw new \moodle_exception('invalidcourseid'); } if (!$cm = get_coursemodule_from_instance("glossary", $glossary->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } $id = $cm->id; } else { throw new \moodle_exception('invalidid', 'glossary'); } $cm = cm_info::create($cm); require_course_login($course->id, true, $cm); $context = context_module::instance($cm->id); require_capability('mod/glossary:view', $context); $hassecondary = $PAGE->has_secondary_navigation(); // Prepare format_string/text options $fmtoptions = array( 'context' => $context); require_once($CFG->dirroot . '/comment/lib.php'); comment::init(); /// redirecting if adding a new entry if ($tab == GLOSSARY_ADDENTRY_VIEW ) { redirect("edit.php?cmid=$cm->id&mode=$mode"); } /// setting the defaut number of entries per page if not set if ( !$entriesbypage = $glossary->entbypage ) { $entriesbypage = $CFG->glossary_entbypage; } // If we have received a page, recalculate offset and page size. $pagelimit = $entriesbypage; if ($page > 0 && $offset == 0) { $offset = $page * $entriesbypage; } else if ($page < 0) { $offset = 0; $pagelimit = 0; } /// setting the default values for the display mode of the current glossary /// only if the glossary is viewed by the first time if ( $dp = $DB->get_record('glossary_formats', array('name'=>$glossary->displayformat)) ) { /// Based on format->defaultmode, we build the defaulttab to be showed sometimes $showtabs = glossary_get_visible_tabs($dp); switch ($dp->defaultmode) { case 'cat': $defaulttab = GLOSSARY_CATEGORY_VIEW; // Handle defaultmode if 'category' tab is disabled. Fallback to 'standard' tab. if (!in_array(GLOSSARY_CATEGORY, $showtabs)) { $defaulttab = GLOSSARY_STANDARD_VIEW; } break; case 'date': $defaulttab = GLOSSARY_DATE_VIEW; // Handle defaultmode if 'date' tab is disabled. Fallback to 'standard' tab. if (!in_array(GLOSSARY_DATE, $showtabs)) { $defaulttab = GLOSSARY_STANDARD_VIEW; } break; case 'author': $defaulttab = GLOSSARY_AUTHOR_VIEW; // Handle defaultmode if 'author' tab is disabled. Fallback to 'standard' tab. if (!in_array(GLOSSARY_AUTHOR, $showtabs)) { $defaulttab = GLOSSARY_STANDARD_VIEW; } break; default: $defaulttab = GLOSSARY_STANDARD_VIEW; } /// Fetch the rest of variables $printpivot = $dp->showgroup; if ( $mode == '' and $hook == '' and $show == '') { $mode = $dp->defaultmode; $hook = $dp->defaulthook; $sortkey = $dp->sortkey; $sortorder = $dp->sortorder; } } else { $defaulttab = GLOSSARY_STANDARD_VIEW; $showtabs = array($defaulttab); $printpivot = 1; if ( $mode == '' and $hook == '' and $show == '') { $mode = 'letter'; $hook = 'ALL'; } } if ( $displayformat == -1 ) { $displayformat = $glossary->displayformat; } if ( $show ) { $mode = 'term'; $hook = $show; $show = ''; } /// stablishing flag variables if ( $sortorder = strtolower($sortorder) ) { if ($sortorder != 'asc' and $sortorder != 'desc') { $sortorder = ''; } } if ( $sortkey = strtoupper($sortkey) ) { if ($sortkey != 'CREATION' and $sortkey != 'UPDATE' and $sortkey != 'FIRSTNAME' and $sortkey != 'LASTNAME' ) { $sortkey = ''; } } switch ( $mode = strtolower($mode) ) { case 'search': /// looking for terms containing certain word(s) $tab = GLOSSARY_STANDARD_VIEW; //Clean a bit the search string $hook = trim(strip_tags($hook)); break; case 'entry': /// Looking for a certain entry id $tab = GLOSSARY_STANDARD_VIEW; if ( $dp = $DB->get_record("glossary_formats", array("name"=>$glossary->displayformat)) ) { $displayformat = $dp->popupformatname; } break; case 'cat': /// Looking for a certain cat $tab = GLOSSARY_CATEGORY_VIEW; // Validation - we don't want to display 'category' tab if it is disabled. if (!in_array(GLOSSARY_CATEGORY, $showtabs)) { $tab = GLOSSARY_STANDARD_VIEW; } if ( $hook > 0 ) { $category = $DB->get_record("glossary_categories", array("id"=>$hook)); } break; case 'approval': /// Looking for entries waiting for approval $tab = GLOSSARY_APPROVAL_VIEW; // Override the display format with the approvaldisplayformat if ($glossary->approvaldisplayformat !== 'default' && ($df = $DB->get_record("glossary_formats", array("name" => $glossary->approvaldisplayformat)))) { $displayformat = $df->popupformatname; } if ( !$hook and !$sortkey and !$sortorder) { $hook = 'ALL'; } break; case 'term': /// Looking for entries that include certain term in its concept, definition or aliases $tab = GLOSSARY_STANDARD_VIEW; break; case 'date': $tab = GLOSSARY_DATE_VIEW; // Validation - we dont want to display 'date' tab if it is disabled. if (!in_array(GLOSSARY_DATE, $showtabs)) { $tab = GLOSSARY_STANDARD_VIEW; } if ( !$sortkey ) { $sortkey = 'UPDATE'; } if ( !$sortorder ) { $sortorder = 'desc'; } break; case 'author': /// Looking for entries, browsed by author $tab = GLOSSARY_AUTHOR_VIEW; // Validation - we dont want to display 'author' tab if it is disabled. if (!in_array(GLOSSARY_AUTHOR, $showtabs)) { $tab = GLOSSARY_STANDARD_VIEW; } if ( !$hook ) { $hook = 'ALL'; } if ( !$sortkey ) { $sortkey = 'FIRSTNAME'; } if ( !$sortorder ) { $sortorder = 'asc'; } break; case 'letter': /// Looking for entries that begin with a certain letter, ALL or SPECIAL characters default: $tab = GLOSSARY_STANDARD_VIEW; if ( !$hook ) { $hook = 'ALL'; } break; } switch ( $tab ) { case GLOSSARY_IMPORT_VIEW: case GLOSSARY_EXPORT_VIEW: case GLOSSARY_APPROVAL_VIEW: $showcommonelements = 0; break; default: $showcommonelements = 1; break; } // Trigger module viewed event. glossary_view($glossary, $course, $cm, $context, $mode); /// Printing the heading $strglossaries = get_string("modulenameplural", "glossary"); $strglossary = get_string("modulename", "glossary"); $strallcategories = get_string("allcategories", "glossary"); $straddentry = get_string("addentry", "glossary"); $strnoentries = get_string("noentries", "glossary"); $strsearchindefinition = get_string("searchindefinition", "glossary"); $strsearch = get_string("search"); $strwaitingapproval = get_string('pendingapproval', 'glossary'); /// If we are in approval mode, prit special header $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); $url = new moodle_url('/mod/glossary/view.php', array('id'=>$cm->id)); if (isset($mode) && $mode) { $url->param('mode', $mode); } $PAGE->set_url($url); $renderer = $PAGE->get_renderer('mod_glossary'); $actionbar = new \mod_glossary\output\standard_action_bar($cm, $glossary, $dp, $mode, $hook, $sortkey, $sortorder, $offset, $pagelimit, $fullsearch, $tab, $defaulttab); $PAGE->force_settings_menu(); if (!empty($CFG->enablerssfeeds) && !empty($CFG->glossary_enablerssfeeds) && $glossary->rsstype && $glossary->rssarticles) { $rsstitle = format_string($course->shortname, true, array('context' => context_course::instance($course->id))) . ': '. format_string($glossary->name); rss_add_http_header($context, 'mod_glossary', $glossary, $rsstitle); } if ($tab == GLOSSARY_APPROVAL_VIEW) { require_capability('mod/glossary:approve', $context); } $hassecondary = $PAGE->has_secondary_navigation(); if ($tab == GLOSSARY_APPROVAL_VIEW && !$hassecondary && $PAGE->activityheader->is_title_allowed()) { $PAGE->activityheader->set_title( $OUTPUT->heading($strwaitingapproval) . $OUTPUT->heading(format_string($glossary->name)) ); } if ($tab == GLOSSARY_APPROVAL_VIEW || !($glossary->intro && $showcommonelements)) { $PAGE->activityheader->set_description(''); } echo $OUTPUT->header(); if ($showcommonelements) { echo $renderer->main_action_bar($actionbar); } /// All this depends if whe have $showcommonelements if ($showcommonelements) { /// To calculate available options $availableoptions = ''; /// Decide about to print the import link /*if (has_capability('mod/glossary:import', $context)) { $availableoptions = '' . '' . get_string('importentries', 'glossary') . '' . ''; } /// Decide about to print the export link if (has_capability('mod/glossary:export', $context)) { if ($availableoptions) { $availableoptions .= ' / '; } $availableoptions .='' . '' . get_string('exportentries', 'glossary') . '' . ''; }*/ /// Decide about to print the approval link if (has_capability('mod/glossary:approve', $context) && !$hassecondary) { /// Check we have pending entries if ($hiddenentries = $DB->count_records('glossary_entries', array('glossaryid'=>$glossary->id, 'approved'=>0))) { if ($availableoptions) { $availableoptions .= '
'; } $availableoptions .='' . '' . $strwaitingapproval . ' ('.$hiddenentries.')' . ''; } } /// Start to print glossary controls // print_box_start('glossarycontrol clearfix'); echo '
'; echo $availableoptions; /// End glossary controls // print_box_end(); /// glossarycontrol echo '

'; // print_box(' ', 'clearer'); } require("tabs.php"); require("sql.php"); /// printing the entries $entriesshown = 0; $currentpivot = ''; $paging = NULL; if ($allentries) { //Decide if we must show the ALL link in the pagebar $specialtext = ''; if ($glossary->showall) { $specialtext = get_string("allentries","glossary"); } //Build paging bar $baseurl = new moodle_url('/mod/glossary/view.php', ['id' => $id, 'mode' => $mode, 'hook' => $hook, 'sortkey' => $sortkey, 'sortorder' => $sortorder, 'fullsearch' => $fullsearch]); $paging = glossary_get_paging_bar($count, $page, $entriesbypage, $baseurl->out() . '&', 9999, 10, '  ', $specialtext, -1); echo '
'; echo $paging; echo '
'; //load ratings require_once($CFG->dirroot.'/rating/lib.php'); if ($glossary->assessed != RATING_AGGREGATE_NONE) { $ratingoptions = new stdClass; $ratingoptions->context = $context; $ratingoptions->component = 'mod_glossary'; $ratingoptions->ratingarea = 'entry'; $ratingoptions->items = $allentries; $ratingoptions->aggregate = $glossary->assessed;//the aggregation method $ratingoptions->scaleid = $glossary->scale; $ratingoptions->userid = $USER->id; $ratingoptions->returnurl = $CFG->wwwroot.'/mod/glossary/view.php?id='.$cm->id; $ratingoptions->assesstimestart = $glossary->assesstimestart; $ratingoptions->assesstimefinish = $glossary->assesstimefinish; $rm = new rating_manager(); $allentries = $rm->get_ratings($ratingoptions); } foreach ($allentries as $entry) { // Setting the pivot for the current entry if ($printpivot) { $pivot = format_string($entry->{$pivotkey}, false, ["context" => $context]); $upperpivot = core_text::strtoupper($pivot); $pivottoshow = core_text::strtoupper(format_string($pivot, true, $fmtoptions)); // Reduce pivot to 1cc if necessary. if (!$fullpivot) { $upperpivot = core_text::substr($upperpivot, 0, 1); $pivottoshow = core_text::substr($pivottoshow, 0, 1); } // If there's a group break. if ($currentpivot != $upperpivot) { $currentpivot = $upperpivot; // print the group break if apply echo '
'; echo ''; echo ''; if ($userispivot) { // printing the user icon if defined (only when browsing authors) echo '
'; $user = mod_glossary_entry_query_builder::get_user_from_record($entry); echo $OUTPUT->user_picture($user, array('courseid'=>$course->id)); $pivottoshow = fullname($user, has_capability('moodle/site:viewfullnames', context_course::instance($course->id))); } else { echo ''; } echo $OUTPUT->heading($pivottoshow, 3); echo "
\n"; } } /// highlight the term if necessary if ($mode == 'search') { //We have to strip any word starting by + and take out words starting by - //to make highlight works properly $searchterms = explode(' ', $hook); // Search for words independently foreach ($searchterms as $key => $searchterm) { if (preg_match('/^\-/',$searchterm)) { unset($searchterms[$key]); } else { $searchterms[$key] = preg_replace('/^\+/','',$searchterm); } //Avoid highlight of <2 len strings. It's a well known hilight limitation. if (strlen($searchterm) < 2) { unset($searchterms[$key]); } } $strippedsearch = implode(' ', $searchterms); // Rebuild the string $entry->highlight = $strippedsearch; } /// and finally print the entry. glossary_print_entry($course, $cm, $glossary, $entry, $mode, $hook,1,$displayformat); $entriesshown++; } // The all entries value may be a recordset or an array. if ($allentries instanceof moodle_recordset) { $allentries->close(); } } if ( !$entriesshown ) { echo $OUTPUT->box(get_string("noentries","glossary"), "generalbox boxaligncenter boxwidthwide"); } if (!empty($formsent)) { // close the form properly if used echo ""; echo ""; } if ( $paging ) { echo '
'; echo '
'; echo $paging; echo '
'; } echo '
'; /// Finish the page echo $OUTPUT->footer(); rsslib.php000064400000017550152265517410006573 0ustar00. /** * This file adds support to rss feeds generation * * @package mod_glossary * @category rss * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Returns the path to the cached rss feed contents. Creates/updates the cache if necessary. * * @param stdClass $context the context * @param array $args the arguments received in the url * @return string the full path to the cached RSS feed directory. Null if there is a problem. */ function glossary_rss_get_feed($context, $args) { global $CFG, $DB, $COURSE, $USER; $status = true; if (empty($CFG->glossary_enablerssfeeds)) { debugging("DISABLED (module configuration)"); return null; } $glossaryid = clean_param($args[3], PARAM_INT); $cm = get_coursemodule_from_instance('glossary', $glossaryid, 0, false, MUST_EXIST); $modcontext = context_module::instance($cm->id); if ($COURSE->id == $cm->course) { $course = $COURSE; } else { $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); } //context id from db should match the submitted one if ($context->id != $modcontext->id || !has_capability('mod/glossary:view', $modcontext)) { return null; } $glossary = $DB->get_record('glossary', array('id' => $glossaryid), '*', MUST_EXIST); if (!rss_enabled_for_mod('glossary', $glossary)) { return null; } $sql = glossary_rss_get_sql($glossary); //get the cache file info $filename = rss_get_file_name($glossary, $sql); $cachedfilepath = rss_get_file_full_name('mod_glossary', $filename); //Is the cache out of date? $cachedfilelastmodified = 0; if (file_exists($cachedfilepath)) { $cachedfilelastmodified = filemtime($cachedfilepath); } //if the cache is more than 60 seconds old and there's new stuff $dontrecheckcutoff = time()-60; if ( $dontrecheckcutoff > $cachedfilelastmodified && glossary_rss_newstuff($glossary, $cachedfilelastmodified)) { if (!$recs = $DB->get_records_sql($sql, array(), 0, $glossary->rssarticles)) { return null; } $items = array(); foreach ($recs as $rec) { $item = new stdClass(); $item->title = $rec->entryconcept; if ($glossary->rsstype == 1) {//With author $item->author = fullname($rec); } $item->pubdate = $rec->entrytimecreated; $item->link = $CFG->wwwroot."/mod/glossary/showentry.php?courseid=".$glossary->course."&eid=".$rec->entryid; $definition = file_rewrite_pluginfile_urls($rec->entrydefinition, 'pluginfile.php', $modcontext->id, 'mod_glossary', 'entry', $rec->entryid); $item->description = format_text($definition, $rec->entryformat, [ 'context' => $modcontext, 'trusted' => true, ]); $items[] = $item; } //First all rss feeds common headers $header = rss_standard_header(format_string($glossary->name,true), $CFG->wwwroot."/mod/glossary/view.php?g=".$glossary->id, format_string($glossary->intro,true)); //Now all the rss items if (!empty($header)) { $articles = rss_add_items($items); } //Now all rss feeds common footers if (!empty($header) && !empty($articles)) { $footer = rss_standard_footer(); } //Now, if everything is ok, concatenate it if (!empty($header) && !empty($articles) && !empty($footer)) { $rss = $header.$articles.$footer; //Save the XML contents to file. $status = rss_save_file('mod_glossary', $filename, $rss); } } if (!$status) { $cachedfilepath = null; } return $cachedfilepath; } /** * The appropriate SQL query for the glossary items to go into the RSS feed * * @param stdClass $glossary the glossary object * @param int $time check for items since this epoch timestamp * @return string the SQL query to be used to get the entried from the glossary table of the database */ function glossary_rss_get_sql($glossary, $time=0) { //do we only want new items? if ($time) { $time = "AND e.timecreated > $time"; } else { $time = ""; } if ($glossary->rsstype == 1) {//With author $userfieldsapi = \core_user\fields::for_name(); $allnamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT e.id AS entryid, e.concept AS entryconcept, e.definition AS entrydefinition, e.definitionformat AS entryformat, e.definitiontrust AS entrytrust, e.timecreated AS entrytimecreated, u.id AS userid, $allnamefields FROM {glossary_entries} e, {user} u WHERE e.glossaryid = {$glossary->id} AND u.id = e.userid AND e.approved = 1 $time ORDER BY e.timecreated desc"; } else {//Without author $sql = "SELECT e.id AS entryid, e.concept AS entryconcept, e.definition AS entrydefinition, e.definitionformat AS entryformat, e.definitiontrust AS entrytrust, e.timecreated AS entrytimecreated, u.id AS userid FROM {glossary_entries} e, {user} u WHERE e.glossaryid = {$glossary->id} AND u.id = e.userid AND e.approved = 1 $time ORDER BY e.timecreated desc"; } return $sql; } /** * If there is new stuff in since $time this returns true * Otherwise it returns false. * * @param stdClass $glossary the glossary activity object * @param int $time epoch timestamp to compare new items against, 0 for everyting * @return bool true if there are new items */ function glossary_rss_newstuff($glossary, $time) { global $DB; $sql = glossary_rss_get_sql($glossary, $time); $recs = $DB->get_records_sql($sql, null, 0, 1);//limit of 1. If we get even 1 back we have new stuff return ($recs && !empty($recs)); } /** * Given a glossary object, deletes all cached RSS files associated with it. * * @param stdClass $glossary */ function glossary_rss_delete_file($glossary) { global $CFG; require_once("$CFG->libdir/rsslib.php"); rss_delete_file('mod_glossary', $glossary); } edit_form.php000064400000016175152265517410007247 0ustar00dirroot.'/lib/formslib.php'); class mod_glossary_entry_form extends moodleform { function definition() { global $CFG, $DB; $mform = $this->_form; $currententry = $this->_customdata['current']; $glossary = $this->_customdata['glossary']; $cm = $this->_customdata['cm']; $definitionoptions = $this->_customdata['definitionoptions']; $attachmentoptions = $this->_customdata['attachmentoptions']; $context = context_module::instance($cm->id); // Prepare format_string/text options $fmtoptions = array( 'context' => $context); //------------------------------------------------------------------------------- $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text', 'concept', get_string('concept', 'glossary')); $mform->setType('concept', PARAM_TEXT); $mform->addRule('concept', null, 'required', null, 'client'); $mform->addElement('editor', 'definition_editor', get_string('definition', 'glossary'), null, $definitionoptions); $mform->setType('definition_editor', PARAM_RAW); $mform->addRule('definition_editor', get_string('required'), 'required', null, 'client'); if ($categories = $DB->get_records_menu('glossary_categories', array('glossaryid'=>$glossary->id), 'name ASC', 'id, name')){ foreach ($categories as $id => $name) { $categories[$id] = format_string($name, true, $fmtoptions); } $categories = array(0 => get_string('notcategorised', 'glossary')) + $categories; $categoriesEl = $mform->addElement('select', 'categories', get_string('categories', 'glossary'), $categories); $categoriesEl->setMultiple(true); $categoriesEl->setSize(5); } $mform->addElement('textarea', 'aliases', get_string('aliases', 'glossary'), 'rows="2" cols="40"'); $mform->setType('aliases', PARAM_TEXT); $mform->addHelpButton('aliases', 'aliases', 'glossary'); $mform->addElement('filemanager', 'attachment_filemanager', get_string('attachment', 'glossary'), null, $attachmentoptions); $mform->addHelpButton('attachment_filemanager', 'attachment', 'glossary'); if (!$glossary->usedynalink) { $mform->addElement('hidden', 'usedynalink', $CFG->glossary_linkentries); $mform->setType('usedynalink', PARAM_INT); $mform->addElement('hidden', 'casesensitive', $CFG->glossary_casesensitive); $mform->setType('casesensitive', PARAM_INT); $mform->addElement('hidden', 'fullmatch', $CFG->glossary_fullmatch); $mform->setType('fullmatch', PARAM_INT); } else { //------------------------------------------------------------------------------- $mform->addElement('header', 'linkinghdr', get_string('linking', 'glossary')); $mform->addElement('checkbox', 'usedynalink', get_string('entryusedynalink', 'glossary')); $mform->addHelpButton('usedynalink', 'entryusedynalink', 'glossary'); $mform->setDefault('usedynalink', $CFG->glossary_linkentries); $mform->addElement('checkbox', 'casesensitive', get_string('casesensitive', 'glossary')); $mform->addHelpButton('casesensitive', 'casesensitive', 'glossary'); $mform->hideIf('casesensitive', 'usedynalink'); $mform->setDefault('casesensitive', $CFG->glossary_casesensitive); $mform->addElement('checkbox', 'fullmatch', get_string('fullmatch', 'glossary')); $mform->addHelpButton('fullmatch', 'fullmatch', 'glossary'); $mform->hideIf('fullmatch', 'usedynalink'); $mform->setDefault('fullmatch', $CFG->glossary_fullmatch); } if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { $mform->addElement('header', 'tagshdr', get_string('tags', 'tag')); $mform->addElement('tags', 'tags', get_string('tags'), array('itemtype' => 'glossary_entries', 'component' => 'mod_glossary')); } $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'cmid'); $mform->setType('cmid', PARAM_INT); //------------------------------------------------------------------------------- $this->add_action_buttons(); //------------------------------------------------------------------------------- $this->set_data($currententry); } function validation($data, $files) { global $CFG, $USER, $DB; $errors = parent::validation($data, $files); $glossary = $this->_customdata['glossary']; $cm = $this->_customdata['cm']; $context = context_module::instance($cm->id); $id = (int)$data['id']; $data['concept'] = trim($data['concept']); $aliases = explode("\r\n", trim($data['aliases'])); foreach ($aliases as $alias) { // Check if the alias is just a single character and that it doesn't match reserved symbols. if (strlen(trim($alias)) == 1 && preg_match('/[$-\/:-?{-~!"^_`\[\]]/', trim($alias))) { $errors['aliases'] = get_string('errreservedkeywords', 'glossary'); } } if ($id) { //We are updating an entry, so we compare current session user with //existing entry user to avoid some potential problems if secureforms=off //Perhaps too much security? Anyway thanks to skodak (Bug 1823) $old = $DB->get_record('glossary_entries', array('id'=>$id)); $ineditperiod = ((time() - $old->timecreated < $CFG->maxeditingtime) || $glossary->editalways); if ((!$ineditperiod || $USER->id != $old->userid) and !has_capability('mod/glossary:manageentries', $context)) { if ($USER->id != $old->userid) { $errors['concept'] = get_string('errcannoteditothers', 'glossary'); } elseif (!$ineditperiod) { $errors['concept'] = get_string('erredittimeexpired', 'glossary'); } } if (!$glossary->allowduplicatedentries) { if ($DB->record_exists_select('glossary_entries', 'glossaryid = :glossaryid AND LOWER(concept) = :concept AND id != :id', array( 'glossaryid' => $glossary->id, 'concept' => core_text::strtolower($data['concept']), 'id' => $id))) { $errors['concept'] = get_string('errconceptalreadyexists', 'glossary'); } } } else { if (!$glossary->allowduplicatedentries) { if (glossary_concept_exists($glossary, $data['concept'])) { $errors['concept'] = get_string('errconceptalreadyexists', 'glossary'); } } } return $errors; } } print.php000064400000015677152265517410006441 0ustar00$id)); if ($sortorder !== 'asc') { $url->param('sortorder', $sortorder); } if ($offset !== 0) { $url->param('offset', $offset); } if ($displayformat !== -1) { $url->param('displayformat', $displayformat); } if ($sortkey !== 'UPDATE') { $url->param('sortkey', $sortkey); } if ($mode !== 'letter') { $url->param('mode', $mode); } if ($hook !== 'ALL') { $url->param('hook', $hook); } $PAGE->set_url($url); if (! $cm = get_coursemodule_from_id('glossary', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidid', 'glossary'); } if ($pagelimit < 0) { $pagelimit = 0; } require_course_login($course, true, $cm); $context = context_module::instance($cm->id); // Prepare format_string/text options $fmtoptions = array( 'context' => $context); $PAGE->set_pagelayout('print'); $PAGE->set_title(get_string("modulenameplural", "glossary")); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); if (!has_capability('mod/glossary:manageentries', $context) and !$glossary->allowprintview) { notice(get_string('printviewnotallowed', 'glossary')); } /// setting the default values for the display mode of the current glossary /// only if the glossary is viewed by the first time if ( $dp = $DB->get_record('glossary_formats', array('name'=>$glossary->displayformat)) ) { $printpivot = $dp->showgroup; if ( $mode == '' and $hook == '' and $show == '') { $mode = $dp->defaultmode; $hook = $dp->defaulthook; $sortkey = $dp->sortkey; $sortorder = $dp->sortorder; } } else { $printpivot = 1; if ( $mode == '' and $hook == '' and $show == '') { $mode = 'letter'; $hook = 'ALL'; } } if ( $displayformat == -1 ) { $displayformat = $glossary->displayformat; } /// stablishing flag variables if ( $sortorder = strtolower($sortorder) ) { if ($sortorder != 'asc' and $sortorder != 'desc') { $sortorder = ''; } } if ( $sortkey = strtoupper($sortkey) ) { if ($sortkey != 'CREATION' and $sortkey != 'UPDATE' and $sortkey != 'FIRSTNAME' and $sortkey != 'LASTNAME' ) { $sortkey = ''; } } switch ( $mode = strtolower($mode) ) { case 'entry': /// Looking for a certain entry id $tab = GLOSSARY_STANDARD_VIEW; break; case 'cat': /// Looking for a certain cat $tab = GLOSSARY_CATEGORY_VIEW; if ( $hook > 0 ) { $category = $DB->get_record("glossary_categories", array("id"=>$hook)); } break; case 'approval': /// Looking for entries waiting for approval $tab = GLOSSARY_APPROVAL_VIEW; if ( !$hook and !$sortkey and !$sortorder) { $hook = 'ALL'; } break; case 'term': /// Looking for entries that include certain term in its concept, definition or aliases $tab = GLOSSARY_STANDARD_VIEW; break; case 'date': $tab = GLOSSARY_DATE_VIEW; if ( !$sortkey ) { $sortkey = 'UPDATE'; } if ( !$sortorder ) { $sortorder = 'desc'; } break; case 'author': /// Looking for entries, browsed by author $tab = GLOSSARY_AUTHOR_VIEW; if ( !$hook ) { $hook = 'ALL'; } if ( !$sortkey ) { $sortkey = 'FIRSTNAME'; } if ( !$sortorder ) { $sortorder = 'asc'; } break; case 'letter': /// Looking for entries that begin with a certain letter, ALL or SPECIAL characters default: $tab = GLOSSARY_STANDARD_VIEW; if ( !$hook ) { $hook = 'ALL'; } break; } include_once("sql.php"); $entriesshown = 0; $currentpivot = ''; $site = $DB->get_record("course", array("id"=>1)); // Print dialog link. $printtext = get_string('print', 'glossary'); $printlinkatt = array('onclick' => 'window.print();return false;', 'class' => 'glossary_no_print printicon'); $printiconlink = html_writer::link('#', $printtext, $printlinkatt); echo html_writer::tag('div', $printiconlink, array('class' => 'displayprinticon')); echo html_writer::tag('div', userdate(time()), array('class' => 'displaydate')); $sitename = get_string("site") . ': ' . format_string($site->fullname) . ''; echo html_writer::tag('div', $sitename, array('class' => 'sitename')); $coursename = get_string("course") . ': ' . format_string($course->fullname) . ' ('. format_string($course->shortname) . ')'; echo html_writer::tag('div', $coursename, array('class' => 'coursename')); $modname = get_string("modulename","glossary") . ': ' . format_string($glossary->name, true) . ''; echo html_writer::tag('div', $modname, array('class' => 'modname')); if ( $allentries ) { foreach ($allentries as $entry) { // Setting the pivot for the current entry. if ($printpivot) { $pivot = $entry->{$pivotkey}; $upperpivot = core_text::strtoupper($pivot); $pivottoshow = core_text::strtoupper(format_string($pivot, true, $fmtoptions)); // Reduce pivot to 1cc if necessary. if (!$fullpivot) { $upperpivot = core_text::substr($upperpivot, 0, 1); $pivottoshow = core_text::substr($pivottoshow, 0, 1); } // If there's a group break. if ($currentpivot != $upperpivot) { $currentpivot = $upperpivot; if ($userispivot) { // Printing the user icon if defined (only when browsing authors). $user = mod_glossary_entry_query_builder::get_user_from_record($entry); $pivottoshow = fullname($user); } echo html_writer::tag('div', clean_text($pivottoshow), array('class' => 'mdl-align strong')); } } glossary_print_entry($course, $cm, $glossary, $entry, $mode, $hook, 1, $displayformat, true); } // The all entries value may be a recordset or an array. if ($allentries instanceof moodle_recordset) { $allentries->close(); } } echo $OUTPUT->footer(); pix/export.svg000064400000001025152265517410007414 0ustar00 pix/desc.svg000064400000000646152265517410007021 0ustar00 pix/monologo.svg000064400000003764152265517410007740 0ustar00 pix/asc.svg000064400000000647152265517410006652 0ustar00 pix/minus.svg000064400000000560152265517410007231 0ustar00 lib.php000064400000527506152265517410006052 0ustar00. /** * Library of functions and constants for module glossary * (replace glossary with the name of your module and delete this line) * * @package mod_glossary * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->libdir . '/completionlib.php'); define("GLOSSARY_SHOW_ALL_CATEGORIES", 0); define("GLOSSARY_SHOW_NOT_CATEGORISED", -1); define("GLOSSARY_NO_VIEW", -1); define("GLOSSARY_STANDARD_VIEW", 0); define("GLOSSARY_CATEGORY_VIEW", 1); define("GLOSSARY_DATE_VIEW", 2); define("GLOSSARY_AUTHOR_VIEW", 3); define("GLOSSARY_ADDENTRY_VIEW", 4); define("GLOSSARY_IMPORT_VIEW", 5); define("GLOSSARY_EXPORT_VIEW", 6); define("GLOSSARY_APPROVAL_VIEW", 7); // Glossary tabs. define('GLOSSARY_STANDARD', 'standard'); define('GLOSSARY_AUTHOR', 'author'); define('GLOSSARY_CATEGORY', 'category'); define('GLOSSARY_DATE', 'date'); // Glossary displayformats. define('GLOSSARY_CONTINUOUS', 'continuous'); define('GLOSSARY_DICTIONARY', 'dictionary'); define('GLOSSARY_FULLWITHOUTAUTHOR', 'fullwithoutauthor'); /// STANDARD FUNCTIONS /////////////////////////////////////////////////////////// /** * @global object * @param object $glossary * @return int */ function glossary_add_instance($glossary) { global $DB; /// Given an object containing all the necessary data, /// (defined by the form in mod_form.php) this function /// will create a new instance and return the id number /// of the new instance. if (empty($glossary->ratingtime) or empty($glossary->assessed)) { $glossary->assesstimestart = 0; $glossary->assesstimefinish = 0; } if (empty($glossary->globalglossary) ) { $glossary->globalglossary = 0; } if (!has_capability('mod/glossary:manageentries', context_system::instance())) { $glossary->globalglossary = 0; } $glossary->timecreated = time(); $glossary->timemodified = $glossary->timecreated; //Check displayformat is a valid one $formats = get_list_of_plugins('mod/glossary/formats','TEMPLATE'); if (!in_array($glossary->displayformat, $formats)) { throw new \moodle_exception('unknowformat', '', '', $glossary->displayformat); } $returnid = $DB->insert_record("glossary", $glossary); $glossary->id = $returnid; glossary_grade_item_update($glossary); $completiontimeexpected = !empty($glossary->completionexpected) ? $glossary->completionexpected : null; \core_completion\api::update_completion_date_event($glossary->coursemodule, 'glossary', $glossary->id, $completiontimeexpected); return $returnid; } /** * Given an object containing all the necessary data, * (defined by the form in mod_form.php) this function * will update an existing instance with new data. * * @global object * @global object * @param object $glossary * @return bool */ function glossary_update_instance($glossary) { global $CFG, $DB; if (empty($glossary->globalglossary)) { $glossary->globalglossary = 0; } if (!has_capability('mod/glossary:manageentries', context_system::instance())) { // keep previous unset($glossary->globalglossary); } $glossary->timemodified = time(); $glossary->id = $glossary->instance; if (empty($glossary->ratingtime) or empty($glossary->assessed)) { $glossary->assesstimestart = 0; $glossary->assesstimefinish = 0; } //Check displayformat is a valid one $formats = get_list_of_plugins('mod/glossary/formats','TEMPLATE'); if (!in_array($glossary->displayformat, $formats)) { throw new \moodle_exception('unknowformat', '', '', $glossary->displayformat); } $DB->update_record("glossary", $glossary); if ($glossary->defaultapproval) { $DB->execute("UPDATE {glossary_entries} SET approved = 1 where approved <> 1 and glossaryid = ?", array($glossary->id)); } glossary_grade_item_update($glossary); $completiontimeexpected = !empty($glossary->completionexpected) ? $glossary->completionexpected : null; \core_completion\api::update_completion_date_event($glossary->coursemodule, 'glossary', $glossary->id, $completiontimeexpected); return true; } /** * Given an ID of an instance of this module, * this function will permanently delete the instance * and any data that depends on it. * * @global object * @param int $id glossary id * @return bool success */ function glossary_delete_instance($id) { global $DB, $CFG; if (!$glossary = $DB->get_record('glossary', array('id'=>$id))) { return false; } if (!$cm = get_coursemodule_from_instance('glossary', $id)) { return false; } if (!$context = context_module::instance($cm->id, IGNORE_MISSING)) { return false; } $fs = get_file_storage(); if ($glossary->mainglossary) { // unexport entries $sql = "SELECT ge.id, ge.sourceglossaryid, cm.id AS sourcecmid FROM {glossary_entries} ge JOIN {modules} m ON m.name = 'glossary' JOIN {course_modules} cm ON (cm.module = m.id AND cm.instance = ge.sourceglossaryid) WHERE ge.glossaryid = ? AND ge.sourceglossaryid > 0"; if ($exported = $DB->get_records_sql($sql, array($id))) { foreach ($exported as $entry) { $entry->glossaryid = $entry->sourceglossaryid; $entry->sourceglossaryid = 0; $newcontext = context_module::instance($entry->sourcecmid); if ($oldfiles = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id)) { foreach ($oldfiles as $oldfile) { $file_record = new stdClass(); $file_record->contextid = $newcontext->id; $fs->create_file_from_storedfile($file_record, $oldfile); } $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id); $entry->attachment = '1'; } else { $entry->attachment = '0'; } $DB->update_record('glossary_entries', $entry); } } } else { // move exported entries to main glossary $sql = "UPDATE {glossary_entries} SET sourceglossaryid = 0 WHERE sourceglossaryid = ?"; $DB->execute($sql, array($id)); } // Delete any dependent records $entry_select = "SELECT id FROM {glossary_entries} WHERE glossaryid = ?"; $DB->delete_records_select('comments', "contextid=? AND commentarea=? AND itemid IN ($entry_select)", array($id, 'glossary_entry', $context->id)); $DB->delete_records_select('glossary_alias', "entryid IN ($entry_select)", array($id)); $category_select = "SELECT id FROM {glossary_categories} WHERE glossaryid = ?"; $DB->delete_records_select('glossary_entries_categories', "categoryid IN ($category_select)", array($id)); $DB->delete_records('glossary_categories', array('glossaryid'=>$id)); $DB->delete_records('glossary_entries', array('glossaryid'=>$id)); // delete all files $fs->delete_area_files($context->id); glossary_grade_item_delete($glossary); \core_completion\api::update_completion_date_event($cm->id, 'glossary', $glossary->id, null); $DB->delete_records('glossary', array('id'=>$id)); // Reset caches. \mod_glossary\local\concept_cache::reset_glossary($glossary); return true; } /** * Return a small object with summary information about what a * user has done with a given particular instance of this module * Used for user activity reports. * $return->time = the time they did it * $return->info = a short text description * * @param object $course * @param object $user * @param object $mod * @param object $glossary * @return object|null */ function glossary_user_outline($course, $user, $mod, $glossary) { global $CFG; require_once("$CFG->libdir/gradelib.php"); $grades = grade_get_grades($course->id, 'mod', 'glossary', $glossary->id, $user->id); if (empty($grades->items[0]->grades)) { $grade = false; } else { $grade = reset($grades->items[0]->grades); } if ($entries = glossary_get_user_entries($glossary->id, $user->id)) { $result = new stdClass(); $result->info = count($entries) . ' ' . get_string("entries", "glossary"); $lastentry = array_pop($entries); $result->time = $lastentry->timemodified; if ($grade) { if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $result->info .= ', ' . get_string('gradenoun') . ': ' . $grade->str_long_grade; } else { $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); } } return $result; } else if ($grade) { $result = (object) [ 'time' => grade_get_date_for_user_grade($grade, $user), ]; if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $result->info = get_string('gradenoun') . ': ' . $grade->str_long_grade; } else { $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); } return $result; } return NULL; } /** * @global object * @param int $glossaryid * @param int $userid * @return array */ function glossary_get_user_entries($glossaryid, $userid) { /// Get all the entries for a user in a glossary global $DB; return $DB->get_records_sql("SELECT e.*, u.firstname, u.lastname, u.email, u.picture FROM {glossary} g, {glossary_entries} e, {user} u WHERE g.id = ? AND e.glossaryid = g.id AND e.userid = ? AND e.userid = u.id ORDER BY e.timemodified ASC", array($glossaryid, $userid)); } /** * Print a detailed representation of what a user has done with * a given particular instance of this module, for user activity reports. * * @global object * @param object $course * @param object $user * @param object $mod * @param object $glossary */ function glossary_user_complete($course, $user, $mod, $glossary) { global $CFG, $OUTPUT; require_once("$CFG->libdir/gradelib.php"); $grades = grade_get_grades($course->id, 'mod', 'glossary', $glossary->id, $user->id); if (!empty($grades->items[0]->grades)) { $grade = reset($grades->items[0]->grades); if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { echo $OUTPUT->container(get_string('gradenoun') . ': ' . $grade->str_long_grade); if ($grade->str_feedback) { echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); } } else { echo $OUTPUT->container(get_string('gradenoun') . ': ' . get_string('hidden', 'grades')); } } if ($entries = glossary_get_user_entries($glossary->id, $user->id)) { echo '
'; foreach ($entries as $entry) { $cm = get_coursemodule_from_instance("glossary", $glossary->id, $course->id); glossary_print_entry($course, $cm, $glossary, $entry,"","",0); echo '

'; } echo '

'; } } /** * Returns all glossary entries since a given time for specified glossary * * @param array $activities sequentially indexed array of objects * @param int $index * @param int $timestart * @param int $courseid * @param int $cmid * @param int $userid defaults to 0 * @param int $groupid defaults to 0 * @return void adds items into $activities and increases $index */ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid = 0, $groupid = 0) { global $COURSE, $USER, $DB; if ($COURSE->id == $courseid) { $course = $COURSE; } else { $course = $DB->get_record('course', array('id' => $courseid)); } $modinfo = get_fast_modinfo($course); $cm = $modinfo->cms[$cmid]; $context = context_module::instance($cm->id); if (!$cm->uservisible) { return; } $viewfullnames = has_capability('moodle/site:viewfullnames', $context); // Groups are not yet supported for glossary. See MDL-10728 . /* $accessallgroups = has_capability('moodle/site:accessallgroups', $context); $groupmode = groups_get_activity_groupmode($cm, $course); */ $params['timestart'] = $timestart; if ($userid) { $userselect = "AND u.id = :userid"; $params['userid'] = $userid; } else { $userselect = ''; } if ($groupid) { $groupselect = 'AND gm.groupid = :groupid'; $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; $params['groupid'] = $groupid; } else { $groupselect = ''; $groupjoin = ''; } $approvedselect = ""; if (!has_capability('mod/glossary:approve', $context)) { $approvedselect = " AND ge.approved = 1 "; } $params['timestart'] = $timestart; $params['glossaryid'] = $cm->instance; $userfieldsapi = \core_user\fields::for_userpic(); $ufields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; $entries = $DB->get_records_sql(" SELECT ge.id AS entryid, ge.glossaryid, ge.concept, ge.definition, ge.approved, ge.timemodified, $ufields FROM {glossary_entries} ge JOIN {user} u ON u.id = ge.userid $groupjoin WHERE ge.timemodified > :timestart AND ge.glossaryid = :glossaryid $approvedselect $userselect $groupselect ORDER BY ge.timemodified ASC", $params); if (!$entries) { return; } foreach ($entries as $entry) { // Groups are not yet supported for glossary. See MDL-10728 . /* $usersgroups = null; if ($entry->userid != $USER->id) { if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { if (is_null($usersgroups)) { $usersgroups = groups_get_all_groups($course->id, $entry->userid, $cm->groupingid); if (is_array($usersgroups)) { $usersgroups = array_keys($usersgroups); } else { $usersgroups = array(); } } if (!array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid))) { continue; } } } */ $tmpactivity = new stdClass(); $tmpactivity->user = user_picture::unalias($entry, null, 'userid'); $tmpactivity->user->fullname = fullname($tmpactivity->user, $viewfullnames); $tmpactivity->type = 'glossary'; $tmpactivity->cmid = $cm->id; $tmpactivity->glossaryid = $entry->glossaryid; $tmpactivity->name = format_string($cm->name, true); $tmpactivity->sectionnum = $cm->sectionnum; $tmpactivity->timestamp = $entry->timemodified; $tmpactivity->content = new stdClass(); $tmpactivity->content->entryid = $entry->entryid; $tmpactivity->content->concept = $entry->concept; $tmpactivity->content->definition = $entry->definition; $tmpactivity->content->approved = $entry->approved; $activities[$index++] = $tmpactivity; } return true; } /** * Outputs the glossary entry indicated by $activity * * @param object $activity the activity object the glossary resides in * @param int $courseid the id of the course the glossary resides in * @param bool $detail not used, but required for compatibilty with other modules * @param int $modnames not used, but required for compatibilty with other modules * @param bool $viewfullnames not used, but required for compatibilty with other modules * @return void */ function glossary_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) { global $OUTPUT; echo html_writer::start_tag('div', array('class'=>'glossary-activity clearfix')); if (!empty($activity->user)) { echo html_writer::tag('div', $OUTPUT->user_picture($activity->user, array('courseid'=>$courseid)), array('class' => 'glossary-activity-picture')); } echo html_writer::start_tag('div', array('class'=>'glossary-activity-content')); echo html_writer::start_tag('div', array('class'=>'glossary-activity-entry')); if (isset($activity->content->approved) && !$activity->content->approved) { $urlparams = array('g' => $activity->glossaryid, 'mode' => 'approval', 'hook' => $activity->content->concept); $class = array('class' => 'dimmed_text'); } else { $urlparams = array('g' => $activity->glossaryid, 'mode' => 'entry', 'hook' => $activity->content->entryid); $class = array(); } echo html_writer::link(new moodle_url('/mod/glossary/view.php', $urlparams), strip_tags($activity->content->concept), $class); echo html_writer::end_tag('div'); $url = new moodle_url('/user/view.php', array('course'=>$courseid, 'id'=>$activity->user->id)); $name = $activity->user->fullname; $link = html_writer::link($url, $name, $class); echo html_writer::start_tag('div', array('class'=>'user')); echo $link .' - '. userdate($activity->timestamp); echo html_writer::end_tag('div'); echo html_writer::end_tag('div'); echo html_writer::end_tag('div'); return; } /** * Given a course and a time, this module should find recent activity * that has occurred in glossary activities and print it out. * Return true if there was output, or false is there was none. * * @global object * @global object * @global object * @param object $course * @param object $viewfullnames * @param int $timestart * @return bool */ function glossary_print_recent_activity($course, $viewfullnames, $timestart) { global $CFG, $USER, $DB, $OUTPUT, $PAGE; //TODO: use timestamp in approved field instead of changing timemodified when approving in 2.0 if (!defined('GLOSSARY_RECENT_ACTIVITY_LIMIT')) { define('GLOSSARY_RECENT_ACTIVITY_LIMIT', 50); } $modinfo = get_fast_modinfo($course); $ids = array(); foreach ($modinfo->cms as $cm) { if ($cm->modname != 'glossary') { continue; } if (!$cm->uservisible) { continue; } $ids[$cm->instance] = $cm->id; } if (!$ids) { return false; } // generate list of approval capabilities for all glossaries in the course. $approvals = array(); foreach ($ids as $glinstanceid => $glcmid) { $context = context_module::instance($glcmid); if (has_capability('mod/glossary:view', $context)) { // get records glossary entries that are approved if user has no capability to approve entries. if (has_capability('mod/glossary:approve', $context)) { $approvals[] = ' ge.glossaryid = :glsid'.$glinstanceid.' '; } else { $approvals[] = ' (ge.approved = 1 AND ge.glossaryid = :glsid'.$glinstanceid.') '; } $params['glsid'.$glinstanceid] = $glinstanceid; } } if (count($approvals) == 0) { return false; } $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; $selectsql = 'SELECT ge.id, ge.concept, ge.approved, ge.timemodified, ge.glossaryid, ' . $userfields; $countsql = 'SELECT COUNT(*)'; $joins = array(' FROM {glossary_entries} ge '); $joins[] = 'JOIN {user} u ON u.id = ge.userid '; $fromsql = implode("\n", $joins); $params['timestart'] = $timestart; $clausesql = ' WHERE ge.timemodified > :timestart '; if (count($approvals) > 0) { $approvalsql = 'AND ('. implode(' OR ', $approvals) .') '; } else { $approvalsql = ''; } $ordersql = 'ORDER BY ge.timemodified ASC'; $entries = $DB->get_records_sql($selectsql.$fromsql.$clausesql.$approvalsql.$ordersql, $params, 0, (GLOSSARY_RECENT_ACTIVITY_LIMIT+1)); if (empty($entries)) { return false; } echo $OUTPUT->heading(get_string('newentries', 'glossary') . ':', 6); $strftimerecent = get_string('strftimerecent'); $entrycount = 0; foreach ($entries as $entry) { if ($entrycount < GLOSSARY_RECENT_ACTIVITY_LIMIT) { if ($entry->approved) { $dimmed = ''; $urlparams = array('g' => $entry->glossaryid, 'mode' => 'entry', 'hook' => $entry->id); } else { $dimmed = ' dimmed_text'; $urlparams = array('id' => $ids[$entry->glossaryid], 'mode' => 'approval', 'hook' => format_text($entry->concept, true)); } $link = new moodle_url($CFG->wwwroot.'/mod/glossary/view.php' , $urlparams); echo '
'; echo '
'.userdate($entry->timemodified, $strftimerecent).'
'; echo '
'.fullname($entry, $viewfullnames).'
'; echo '
'; echo ''; $entrycount += 1; } else { $numnewentries = $DB->count_records_sql($countsql.$joins[0].$clausesql.$approvalsql, $params); echo '
'.get_string('andmorenewentries', 'glossary', $numnewentries - GLOSSARY_RECENT_ACTIVITY_LIMIT).'
'; break; } } return true; } /** * @global object * @param object $log */ function glossary_log_info($log) { global $DB; return $DB->get_record_sql("SELECT e.*, u.firstname, u.lastname FROM {glossary_entries} e, {user} u WHERE e.id = ? AND u.id = ?", array($log->info, $log->userid)); } /** * Function to be run periodically according to the moodle cron * This function searches for things that need to be done, such * as sending out mail, toggling flags etc ... * @return bool */ function glossary_cron () { return true; } /** * Return grade for given user or all users. * * @param stdClass $glossary A glossary instance * @param int $userid Optional user id, 0 means all users * @return array An array of grades, false if none */ function glossary_get_user_grades($glossary, $userid=0) { global $CFG; require_once($CFG->dirroot.'/rating/lib.php'); $ratingoptions = new stdClass; //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance? $ratingoptions->modulename = 'glossary'; $ratingoptions->moduleid = $glossary->id; $ratingoptions->component = 'mod_glossary'; $ratingoptions->ratingarea = 'entry'; $ratingoptions->userid = $userid; $ratingoptions->aggregationmethod = $glossary->assessed; $ratingoptions->scaleid = $glossary->scale; $ratingoptions->itemtable = 'glossary_entries'; $ratingoptions->itemtableusercolumn = 'userid'; $rm = new rating_manager(); return $rm->get_user_grades($ratingoptions); } /** * Return rating related permissions * * @param int $contextid the context id * @param string $component The component we want to get permissions for * @param string $ratingarea The ratingarea that we want to get permissions for * @return array an associative array of the user's rating permissions */ function glossary_rating_permissions($contextid, $component, $ratingarea) { if ($component != 'mod_glossary' || $ratingarea != 'entry') { // We don't know about this component/ratingarea so just return null to get the // default restrictive permissions. return null; } $context = context::instance_by_id($contextid); return array( 'view' => has_capability('mod/glossary:viewrating', $context), 'viewany' => has_capability('mod/glossary:viewanyrating', $context), 'viewall' => has_capability('mod/glossary:viewallratings', $context), 'rate' => has_capability('mod/glossary:rate', $context) ); } /** * Validates a submitted rating * @param array $params submitted data * context => object the context in which the rated items exists [required] * component => The component for this module - should always be mod_forum [required] * ratingarea => object the context in which the rated items exists [required] * itemid => int the ID of the object being rated [required] * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required] * rating => int the submitted rating * rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required] * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional] * @return boolean true if the rating is valid. Will throw rating_exception if not */ function glossary_rating_validate($params) { global $DB, $USER; // Check the component is mod_forum if ($params['component'] != 'mod_glossary') { throw new rating_exception('invalidcomponent'); } // Check the ratingarea is post (the only rating area in forum) if ($params['ratingarea'] != 'entry') { throw new rating_exception('invalidratingarea'); } // Check the rateduserid is not the current user .. you can't rate your own posts if ($params['rateduserid'] == $USER->id) { throw new rating_exception('nopermissiontorate'); } $glossarysql = "SELECT g.id as glossaryid, g.scale, g.course, e.userid as userid, e.approved, e.timecreated, g.assesstimestart, g.assesstimefinish FROM {glossary_entries} e JOIN {glossary} g ON e.glossaryid = g.id WHERE e.id = :itemid"; $glossaryparams = array('itemid' => $params['itemid']); $info = $DB->get_record_sql($glossarysql, $glossaryparams); if (!$info) { //item doesn't exist throw new rating_exception('invaliditemid'); } if ($info->scale != $params['scaleid']) { //the scale being submitted doesnt match the one in the database throw new rating_exception('invalidscaleid'); } //check that the submitted rating is valid for the scale // lower limit if ($params['rating'] < 0 && $params['rating'] != RATING_UNSET_RATING) { throw new rating_exception('invalidnum'); } // upper limit if ($info->scale < 0) { //its a custom scale $scalerecord = $DB->get_record('scale', array('id' => -$info->scale)); if ($scalerecord) { $scalearray = explode(',', $scalerecord->scale); if ($params['rating'] > count($scalearray)) { throw new rating_exception('invalidnum'); } } else { throw new rating_exception('invalidscaleid'); } } else if ($params['rating'] > $info->scale) { //if its numeric and submitted rating is above maximum throw new rating_exception('invalidnum'); } //check the item we're rating was created in the assessable time window if (!empty($info->assesstimestart) && !empty($info->assesstimefinish)) { if ($info->timecreated < $info->assesstimestart || $info->timecreated > $info->assesstimefinish) { throw new rating_exception('notavailable'); } } $cm = get_coursemodule_from_instance('glossary', $info->glossaryid, $info->course, false, MUST_EXIST); $context = context_module::instance($cm->id, MUST_EXIST); // if the supplied context doesnt match the item's context if ($context->id != $params['context']->id) { throw new rating_exception('invalidcontext'); } return true; } /** * Update activity grades * * @category grade * @param stdClass $glossary Null means all glossaries (with extra cmidnumber property) * @param int $userid specific user only, 0 means all * @param bool $nullifnone If true and the user has no grade then a grade item with rawgrade == null will be inserted */ function glossary_update_grades($glossary=null, $userid=0, $nullifnone=true) { global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); if (!$glossary->assessed) { glossary_grade_item_update($glossary); } else if ($grades = glossary_get_user_grades($glossary, $userid)) { glossary_grade_item_update($glossary, $grades); } else if ($userid and $nullifnone) { $grade = new stdClass(); $grade->userid = $userid; $grade->rawgrade = NULL; glossary_grade_item_update($glossary, $grade); } else { glossary_grade_item_update($glossary); } } /** * Create/update grade item for given glossary * * @category grade * @param stdClass $glossary object with extra cmidnumber * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int, 0 if ok, error code otherwise */ function glossary_grade_item_update($glossary, $grades=NULL) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); $params = array('itemname'=>$glossary->name, 'idnumber'=>$glossary->cmidnumber); if (!$glossary->assessed or $glossary->scale == 0) { $params['gradetype'] = GRADE_TYPE_NONE; } else if ($glossary->scale > 0) { $params['gradetype'] = GRADE_TYPE_VALUE; $params['grademax'] = $glossary->scale; $params['grademin'] = 0; } else if ($glossary->scale < 0) { $params['gradetype'] = GRADE_TYPE_SCALE; $params['scaleid'] = -$glossary->scale; } if ($grades === 'reset') { $params['reset'] = true; $grades = NULL; } return grade_update('mod/glossary', $glossary->course, 'mod', 'glossary', $glossary->id, 0, $grades, $params); } /** * Delete grade item for given glossary * * @category grade * @param object $glossary object */ function glossary_grade_item_delete($glossary) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); return grade_update('mod/glossary', $glossary->course, 'mod', 'glossary', $glossary->id, 0, NULL, array('deleted'=>1)); } /** * Checks if scale is being used by any instance of glossary * * This is used to find out if scale used anywhere * * @global object * @param int $scaleid * @return boolean True if the scale is used by any glossary */ function glossary_scale_used_anywhere($scaleid) { global $DB; if ($scaleid and $DB->record_exists_select('glossary', "scale = ? and assessed > 0", [-$scaleid])) { return true; } else { return false; } } ////////////////////////////////////////////////////////////////////////////////////// /// Any other glossary functions go here. Each of them must have a name that /// starts with glossary_ /** * This function return an array of valid glossary_formats records * Everytime it's called, every existing format is checked, new formats * are included if detected and old formats are deleted and any glossary * using an invalid format is updated to the default (dictionary). * * @global object * @global object * @return array */ function glossary_get_available_formats() { global $CFG, $DB; // Get available formats (plugin) and insert them (if necessary) into glossary_formats. $formats = get_list_of_plugins('mod/glossary/formats', 'TEMPLATE'); $pluginformats = array(); $formatrecords = $DB->get_records("glossary_formats"); foreach ($formats as $format) { // If the format file exists. if (file_exists($CFG->dirroot.'/mod/glossary/formats/'.$format.'/'.$format.'_format.php')) { include_once($CFG->dirroot.'/mod/glossary/formats/'.$format.'/'.$format.'_format.php'); //If the function exists if (function_exists('glossary_show_entry_'.$format)) { // Acummulate it as a valid format. $pluginformats[] = $format; // Check if the format exists in the table. $rec = null; foreach ($formatrecords as $record) { if ($record->name == $format) { $rec = $record; break; } } if (!$rec) { // Insert the record in glossary_formats. $gf = new stdClass(); $gf->name = $format; $gf->popupformatname = $format; $gf->visible = 1; $id = $DB->insert_record('glossary_formats', $gf); $rec = $DB->get_record('glossary_formats', array('id' => $id)); array_push($formatrecords, $rec); } if (empty($rec->showtabs)) { glossary_set_default_visible_tabs($rec); } } } } // Delete non_existent formats from glossary_formats table. foreach ($formatrecords as $record) { $todelete = false; // If the format in DB isn't a valid previously detected format then delete the record. if (!in_array($record->name, $pluginformats)) { $todelete = true; } if ($todelete) { // Delete the format. $DB->delete_records('glossary_formats', array('id' => $record->id)); unset($formatrecords[$record->id]); // Reassign existing glossaries to default (dictionary) format. if ($glossaries = $DB->get_records('glossary', array('displayformat' => $record->name))) { foreach($glossaries as $glossary) { $DB->set_field('glossary', 'displayformat', 'dictionary', array('id' => $glossary->id)); } } } } return $formatrecords; } /** * @param bool $debug * @param string $text * @param int $br */ function glossary_debug($debug,$text,$br=1) { if ( $debug ) { echo '' . $text . ''; if ( $br ) { echo '
'; } } } /** * * @global object * @param int $glossaryid * @param string $entrylist * @param string $pivot * @return array */ function glossary_get_entries($glossaryid, $entrylist, $pivot = "") { global $DB; if ($pivot) { $pivot .= ","; } return $DB->get_records_sql("SELECT $pivot id,userid,concept,definition,format FROM {glossary_entries} WHERE glossaryid = ? AND id IN ($entrylist)", array($glossaryid)); } /** * @global object * @global object * @param string $concept * @param int $courseid * @return array */ function glossary_get_entries_search($concept, $courseid) { global $DB; //Check if the user is an admin $bypassadmin = 1; //This means NO (by default) if (has_capability('moodle/course:viewhiddenactivities', context_system::instance())) { $bypassadmin = 0; //This means YES } //Check if the user is a teacher $bypassteacher = 1; //This means NO (by default) if (has_capability('mod/glossary:manageentries', context_course::instance($courseid))) { $bypassteacher = 0; //This means YES } $conceptlower = core_text::strtolower(trim($concept)); $params = array('courseid1'=>$courseid, 'courseid2'=>$courseid, 'conceptlower'=>$conceptlower, 'concept'=>$concept); $sensitiveconceptsql = $DB->sql_equal('concept', ':concept'); return $DB->get_records_sql("SELECT e.*, g.name as glossaryname, cm.id as cmid, cm.course as courseid FROM {glossary_entries} e, {glossary} g, {course_modules} cm, {modules} m WHERE m.name = 'glossary' AND cm.module = m.id AND (cm.visible = 1 OR cm.visible = $bypassadmin OR (cm.course = :courseid1 AND cm.visible = $bypassteacher)) AND g.id = cm.instance AND e.glossaryid = g.id AND ( (e.casesensitive != 1 AND LOWER(concept) = :conceptlower) OR (e.casesensitive = 1 and $sensitiveconceptsql)) AND (g.course = :courseid2 OR g.globalglossary = 1) AND e.usedynalink != 0 AND g.usedynalink != 0", $params); } /** * @global object * @global object * @param object $course * @param object $course * @param object $glossary * @param object $entry * @param string $mode * @param string $hook * @param int $printicons * @param int $displayformat * @param bool $printview * @return mixed */ function glossary_print_entry($course, $cm, $glossary, $entry, $mode='',$hook='',$printicons = 1, $displayformat = -1, $printview = false) { global $USER, $CFG; $return = false; if ( $displayformat < 0 ) { $displayformat = $glossary->displayformat; } if ($entry->approved or ($USER->id == $entry->userid) or ($mode == 'approval' and !$entry->approved) ) { $formatfile = $CFG->dirroot.'/mod/glossary/formats/'.$displayformat.'/'.$displayformat.'_format.php'; if ($printview) { $functionname = 'glossary_print_entry_'.$displayformat; } else { $functionname = 'glossary_show_entry_'.$displayformat; } if (file_exists($formatfile)) { include_once($formatfile); if (function_exists($functionname)) { $return = $functionname($course, $cm, $glossary, $entry,$mode,$hook,$printicons); } else if ($printview) { //If the glossary_print_entry_XXXX function doesn't exist, print default (old) print format $return = glossary_print_entry_default($entry, $glossary, $cm); } } } return $return; } /** * Default (old) print format used if custom function doesn't exist in format * * @param object $entry * @param object $glossary * @param object $cm * @return void Output is echo'd */ function glossary_print_entry_default ($entry, $glossary, $cm) { global $CFG; require_once($CFG->libdir . '/filelib.php'); echo $OUTPUT->heading(strip_tags($entry->concept), 4); $definition = $entry->definition; $definition = '' . strip_tags($definition) . ''; $context = context_module::instance($cm->id); $definition = file_rewrite_pluginfile_urls($definition, 'pluginfile.php', $context->id, 'mod_glossary', 'entry', $entry->id); $options = new stdClass(); $options->para = false; $options->trusted = $entry->definitiontrust; $options->context = $context; $options->overflowdiv = true; $definition = format_text($definition, $entry->definitionformat, $options); echo ($definition); echo '

'; } /** * Print glossary concept/term as a heading <h4> * @param object $entry */ function glossary_print_entry_concept($entry, $return=false) { global $OUTPUT; $text = $OUTPUT->heading(format_string($entry->concept), 4); if (!empty($entry->highlight)) { $text = highlight($entry->highlight, $text); } if ($return) { return $text; } else { echo $text; } } /** * * @global moodle_database DB * @param object $entry * @param object $glossary * @param object $cm */ function glossary_print_entry_definition($entry, $glossary, $cm) { global $GLOSSARY_EXCLUDEENTRY; $definition = $entry->definition; // Do not link self. $GLOSSARY_EXCLUDEENTRY = $entry->id; $context = context_module::instance($cm->id); $definition = file_rewrite_pluginfile_urls($definition, 'pluginfile.php', $context->id, 'mod_glossary', 'entry', $entry->id); $options = new stdClass(); $options->para = false; $options->trusted = $entry->definitiontrust; $options->context = $context; $options->overflowdiv = true; $text = format_text($definition, $entry->definitionformat, $options); // Stop excluding concepts from autolinking unset($GLOSSARY_EXCLUDEENTRY); if (!empty($entry->highlight)) { $text = highlight($entry->highlight, $text); } if (isset($entry->footer)) { // Unparsed footer info $text .= $entry->footer; } echo $text; } /** * * @global object * @param object $course * @param object $cm * @param object $glossary * @param object $entry * @param string $mode * @param string $hook * @param string $type * @return string|void */ function glossary_print_entry_aliases($course, $cm, $glossary, $entry,$mode='',$hook='', $type = 'print') { global $DB; $return = ''; if ($aliases = $DB->get_fieldset_select('glossary_alias', 'alias', 'entryid = :entryid', ['entryid' => $entry->id])) { $id = "keyword-{$entry->id}"; $return = html_writer::select($aliases, $id, '', false, ['id' => $id]); } if ($type == 'print') { echo $return; } else { return $return; } } /** * * @global object * @global object * @global object * @param object $course * @param object $cm * @param object $glossary * @param object $entry * @param string $mode * @param string $hook * @param string $type * @return string|void */ function glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode='',$hook='', $type = 'print') { global $USER, $CFG, $DB, $OUTPUT; $context = context_module::instance($cm->id); $output = false; // To decide if we must really return text in "return". Activate when needed only! $importedentry = ($entry->sourceglossaryid == $glossary->id); $ismainglossary = $glossary->mainglossary; $return = ''; // Differentiate links for each entry. $altsuffix = strip_tags(format_text($entry->concept)); if (!$entry->approved) { $output = true; $return .= html_writer::tag('span', get_string('entryishidden','glossary'), array('class' => 'glossary-hidden-note')); } if ($entry->approved || has_capability('mod/glossary:approve', $context)) { $output = true; $return .= \html_writer::link( new \moodle_url('/mod/glossary/showentry.php', ['eid' => $entry->id]), $OUTPUT->pix_icon('fp/link', get_string('entrylink', 'glossary', $altsuffix), 'theme'), ['title' => get_string('entrylink', 'glossary', $altsuffix), 'class' => 'icon'] ); } if (has_capability('mod/glossary:approve', $context) && !$glossary->defaultapproval && $entry->approved) { $output = true; $return .= '' . $OUTPUT->pix_icon('t/block', get_string('disapprove', 'glossary')) . ''; } $iscurrentuser = ($entry->userid == $USER->id); if (has_capability('mod/glossary:manageentries', $context) or (isloggedin() and has_capability('mod/glossary:write', $context) and $iscurrentuser)) { // only teachers can export entries so check it out if (has_capability('mod/glossary:export', $context) and !$ismainglossary and !$importedentry) { $mainglossary = $DB->get_record('glossary', array('mainglossary'=>1,'course'=>$course->id)); if ( $mainglossary ) { // if there is a main glossary defined, allow to export the current entry $output = true; $return .= '' . $OUTPUT->pix_icon('export', get_string('exporttomainglossary', 'glossary'), 'glossary') . ''; } } $icon = 't/delete'; $iconcomponent = 'moodle'; if ( $entry->sourceglossaryid ) { $icon = 'minus'; // graphical metaphor (minus) for deleting an imported entry $iconcomponent = 'glossary'; } //Decide if an entry is editable: // -It isn't a imported entry (so nobody can edit a imported (from secondary to main) entry)) and // -The user is teacher or he is a student with time permissions (edit period or editalways defined). $ineditperiod = ((time() - $entry->timecreated < $CFG->maxeditingtime) || $glossary->editalways); if ( !$importedentry and (has_capability('mod/glossary:manageentries', $context) or ($entry->userid == $USER->id and ($ineditperiod and has_capability('mod/glossary:write', $context))))) { $output = true; $url = "deleteentry.php?id=$cm->id&mode=delete&entry=$entry->id&prevmode=$mode&hook=".urlencode($hook); $return .= "" . $OUTPUT->pix_icon($icon, get_string('deleteentrya', 'mod_glossary', $altsuffix), $iconcomponent) . ''; $url = "edit.php?cmid=$cm->id&id=$entry->id&mode=$mode&hook=".urlencode($hook); $return .= "" . $OUTPUT->pix_icon('i/edit', get_string('editentrya', 'mod_glossary', $altsuffix)) . ''; } elseif ( $importedentry ) { $return .= "" . get_string("exportedentry","glossary") . ""; } } if (!empty($CFG->enableportfolios) && (has_capability('mod/glossary:exportentry', $context) || ($iscurrentuser && has_capability('mod/glossary:exportownentry', $context)))) { require_once($CFG->libdir . '/portfoliolib.php'); $button = new portfolio_add_button(); $button->set_callback_options('glossary_entry_portfolio_caller', array('id' => $cm->id, 'entryid' => $entry->id), 'mod_glossary'); $filecontext = $context; if ($entry->sourceglossaryid == $cm->instance) { if ($maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { $filecontext = context_module::instance($maincm->id); } } $fs = get_file_storage(); if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false) || $files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'entry', $entry->id, "timemodified", false)) { $button->set_formats(PORTFOLIO_FORMAT_RICHHTML); } else { $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML); } $return .= $button->to_html(PORTFOLIO_ADD_ICON_LINK); } $return .= ''; if (!empty($CFG->usecomments) && has_capability('mod/glossary:comment', $context) and $glossary->allowcomments) { require_once($CFG->dirroot . '/comment/lib.php'); $cmt = new stdClass(); $cmt->component = 'mod_glossary'; $cmt->context = $context; $cmt->course = $course; $cmt->cm = $cm; $cmt->area = 'glossary_entry'; $cmt->itemid = $entry->id; $cmt->showcount = true; $comment = new comment($cmt); $return .= '
'.$comment->output(true).'
'; $output = true; } //If we haven't calculated any REAL thing, delete result ($return) if (!$output) { $return = ''; } //Print or get if ($type == 'print') { echo $return; } else { return $return; } } /** * @param object $course * @param object $cm * @param object $glossary * @param object $entry * @param string $mode * @param object $hook * @param bool $printicons * @param bool $aliases * @param bool $printseparator Whether to print a thematic break (separator) at the end of the lower section. * @return void */ function glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, $printicons, $aliases = true, $printseparator = true) { if ($aliases) { $aliases = glossary_print_entry_aliases($course, $cm, $glossary, $entry, $mode, $hook,'html'); } $icons = ''; if ($printicons) { $icons = glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode, $hook,'html'); } if ($aliases || $icons || !empty($entry->rating)) { echo ''; if ( $aliases ) { $id = "keyword-{$entry->id}"; echo ''; } if ($icons) { echo ''; } if (!empty($entry->rating)) { echo ''; } echo '
' . '' . $aliases . '
'.$icons.'
'; glossary_print_entry_ratings($course, $entry); echo '
'; if ($printseparator) { echo "
\n"; } } } /** * Print the list of attachments for this glossary entry * * @param object $entry * @param object $cm The coursemodule * @param string $format The format for this view (html, or text) * @param string $unused1 This parameter is no longer used * @param string $unused2 This parameter is no longer used */ function glossary_print_entry_attachment($entry, $cm, $format = null, $unused1 = null, $unused2 = null) { // Valid format values: html: The HTML link for the attachment is an icon; and // text: The HTML link for the attachment is text. if ($entry->attachment) { echo '
'; echo glossary_print_attachments($entry, $cm, $format); echo '
'; } if ($unused1) { debugging('The align parameter is deprecated, please use appropriate CSS instead', DEBUG_DEVELOPER); } if ($unused2 !== null) { debugging('The insidetable parameter is deprecated, please use appropriate CSS instead', DEBUG_DEVELOPER); } } /** * @global object * @param object $cm * @param object $entry * @param string $mode * @param string $align * @param bool $insidetable */ function glossary_print_entry_approval($cm, $entry, $mode, $align="right", $insidetable=true) { global $CFG, $OUTPUT; if ($mode == 'approval' and !$entry->approved) { if ($insidetable) { echo '
'; } echo $OUTPUT->action_icon( new moodle_url('approve.php', array('eid' => $entry->id, 'mode' => $mode, 'sesskey' => sesskey())), new pix_icon('t/approve', get_string('approve','glossary'), '', array('class' => 'iconsmall', 'align' => $align)) ); if ($insidetable) { echo '
'; } } } /** * It returns all entries from all glossaries that matches the specified criteria * within a given $course. It performs an $extended search if necessary. * It restrict the search to only one $glossary if the $glossary parameter is set. * * @global object * @global object * @param object $course * @param array $searchterms * @param int $extended * @param object $glossary * @return array */ function glossary_search($course, $searchterms, $extended = 0, $glossary = NULL) { global $CFG, $DB; if ( !$glossary ) { if ( $glossaries = $DB->get_records("glossary", array("course"=>$course->id)) ) { $glos = ""; foreach ( $glossaries as $glossary ) { $glos .= "$glossary->id,"; } $glos = substr($glos,0,-1); } } else { $glos = $glossary->id; } if (!has_capability('mod/glossary:manageentries', context_course::instance($glossary->course))) { $glossarymodule = $DB->get_record("modules", array("name"=>"glossary")); $onlyvisible = " AND g.id = cm.instance AND cm.visible = 1 AND cm.module = $glossarymodule->id"; $onlyvisibletable = ", {course_modules} cm"; } else { $onlyvisible = ""; $onlyvisibletable = ""; } if ($DB->sql_regex_supported()) { $REGEXP = $DB->sql_regex(true); $NOTREGEXP = $DB->sql_regex(false); } $searchcond = array(); $params = array(); $i = 0; $concat = $DB->sql_concat('e.concept', "' '", 'e.definition'); foreach ($searchterms as $searchterm) { $i++; // Initially we aren't going to perform NOT LIKE searches, only MSSQL // will use it to simulate the "-" operator with LIKE clause. $NOT = false; // Under MSSQL, trim the + and - operators and perform // simpler LIKE (or NOT LIKE) queries if (!$DB->sql_regex_supported()) { if (substr($searchterm, 0, 1) == '-') { $NOT = true; } $searchterm = trim($searchterm, '+-'); } // TODO: +- may not work for non latin languages if (substr($searchterm,0,1) == '+') { $searchterm = trim($searchterm, '+-'); $searchterm = preg_quote($searchterm, '|'); $searchcond[] = "$concat $REGEXP :ss$i"; $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)"; } else if (substr($searchterm,0,1) == "-") { $searchterm = trim($searchterm, '+-'); $searchterm = preg_quote($searchterm, '|'); $searchcond[] = "$concat $NOTREGEXP :ss$i"; $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)"; } else { $searchcond[] = $DB->sql_like($concat, ":ss$i", false, true, $NOT); $params['ss'.$i] = "%$searchterm%"; } } if (empty($searchcond)) { $totalcount = 0; return array(); } $searchcond = implode(" AND ", $searchcond); $sql = "SELECT e.* FROM {glossary_entries} e, {glossary} g $onlyvisibletable WHERE $searchcond AND (e.glossaryid = g.id or e.sourceglossaryid = g.id) $onlyvisible AND g.id IN ($glos) AND e.approved <> 0"; return $DB->get_records_sql($sql, $params); } /** * @global object * @param array $searchterms * @param object $glossary * @param bool $extended * @return array */ function glossary_search_entries($searchterms, $glossary, $extended) { global $DB; $course = $DB->get_record("course", array("id"=>$glossary->course)); return glossary_search($course,$searchterms,$extended,$glossary); } /** * if return=html, then return a html string. * if return=text, then return a text-only string. * otherwise, print HTML for non-images, and return image HTML * if attachment is an image, $align set its aligment. * * @global object * @global object * @param object $entry * @param object $cm * @param string $type html, txt, empty * @param string $unused This parameter is no longer used * @return string image string or nothing depending on $type param */ function glossary_print_attachments($entry, $cm, $type=NULL, $unused = null) { global $CFG, $DB, $OUTPUT; if (!$context = context_module::instance($cm->id, IGNORE_MISSING)) { return ''; } if ($entry->sourceglossaryid == $cm->instance) { if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { return ''; } $filecontext = context_module::instance($maincm->id); } else { $filecontext = $context; } $strattachment = get_string('attachment', 'glossary'); $fs = get_file_storage(); $imagereturn = ''; $output = ''; if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) { foreach ($files as $file) { $filename = $file->get_filename(); $mimetype = $file->get_mimetype(); $iconimage = $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file), 'moodle', array('class' => 'icon')); $path = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$context->id.'/mod_glossary/attachment/'.$entry->id.'/'.$filename); if ($type == 'html') { $output .= "$iconimage "; $output .= "".s($filename).""; $output .= "
"; } else if ($type == 'text') { $output .= "$strattachment ".s($filename).":\n$path\n"; } else { if (in_array($mimetype, array('image/gif', 'image/jpeg', 'image/png'))) { // Image attachments don't get printed as links $imagereturn .= "
\"\""; } else { $output .= "$iconimage "; $output .= format_text("".s($filename)."", FORMAT_HTML, array('context'=>$context)); $output .= '
'; } } } } if ($type) { return $output; } else { echo $output; return $imagereturn; } } //////////////////////////////////////////////////////////////////////////////// // File API // //////////////////////////////////////////////////////////////////////////////// /** * Lists all browsable file areas * * @package mod_glossary * @category files * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @return array */ function glossary_get_file_areas($course, $cm, $context) { return array( 'attachment' => get_string('areaattachment', 'mod_glossary'), 'entry' => get_string('areaentry', 'mod_glossary'), ); } /** * File browsing support for glossary module. * * @param file_browser $browser * @param array $areas * @param stdClass $course * @param cm_info $cm * @param context $context * @param string $filearea * @param int $itemid * @param string $filepath * @param string $filename * @return file_info_stored file_info_stored instance or null if not found */ function glossary_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { global $CFG, $DB, $USER; if ($context->contextlevel != CONTEXT_MODULE) { return null; } if (!isset($areas[$filearea])) { return null; } if (is_null($itemid)) { require_once($CFG->dirroot.'/mod/glossary/locallib.php'); return new glossary_file_info_container($browser, $course, $cm, $context, $areas, $filearea); } if (!$entry = $DB->get_record('glossary_entries', array('id' => $itemid))) { return null; } if (!$glossary = $DB->get_record('glossary', array('id' => $cm->instance))) { return null; } if ($glossary->defaultapproval and !$entry->approved and !has_capability('mod/glossary:approve', $context)) { return null; } // this trickery here is because we need to support source glossary access if ($entry->glossaryid == $cm->instance) { $filecontext = $context; } else if ($entry->sourceglossaryid == $cm->instance) { if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { return null; } $filecontext = context_module::instance($maincm->id); } else { return null; } $fs = get_file_storage(); $filepath = is_null($filepath) ? '/' : $filepath; $filename = is_null($filename) ? '.' : $filename; if (!($storedfile = $fs->get_file($filecontext->id, 'mod_glossary', $filearea, $itemid, $filepath, $filename))) { return null; } // Checks to see if the user can manage files or is the owner. // TODO MDL-33805 - Do not use userid here and move the capability check above. if (!has_capability('moodle/course:managefiles', $context) && $storedfile->get_userid() != $USER->id) { return null; } $urlbase = $CFG->wwwroot.'/pluginfile.php'; return new file_info_stored($browser, $filecontext, $storedfile, $urlbase, s($entry->concept), true, true, false, false); } /** * Serves the glossary attachments. Implements needed access control ;-) * * @package mod_glossary * @category files * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClsss $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - justsend the file */ function glossary_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { global $CFG, $DB; if ($context->contextlevel != CONTEXT_MODULE) { return false; } require_course_login($course, true, $cm); if ($filearea === 'attachment' or $filearea === 'entry') { $entryid = (int)array_shift($args); require_course_login($course, true, $cm); if (!$entry = $DB->get_record('glossary_entries', array('id'=>$entryid))) { return false; } if (!$glossary = $DB->get_record('glossary', array('id'=>$cm->instance))) { return false; } if ($glossary->defaultapproval and !$entry->approved and !has_capability('mod/glossary:approve', $context)) { return false; } // this trickery here is because we need to support source glossary access if ($entry->glossaryid == $cm->instance) { $filecontext = $context; } else if ($entry->sourceglossaryid == $cm->instance) { if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { return false; } $filecontext = context_module::instance($maincm->id); } else { return false; } $relativepath = implode('/', $args); $fullpath = "/$filecontext->id/mod_glossary/$filearea/$entryid/$relativepath"; $fs = get_file_storage(); if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { return false; } // finally send the file send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security! } else if ($filearea === 'export') { require_login($course, false, $cm); require_capability('mod/glossary:export', $context); if (!$glossary = $DB->get_record('glossary', array('id'=>$cm->instance))) { return false; } $cat = array_shift($args); $cat = clean_param($cat, PARAM_ALPHANUM); $filename = clean_filename(strip_tags(format_string($glossary->name)).'.xml'); $content = glossary_generate_export_file($glossary, NULL, $cat); send_file($content, $filename, 0, 0, true, true); } return false; } /** * */ function glossary_print_tabbed_table_end() { echo ""; } /** * @param object $cm * @param object $glossary * @param string $mode * @param string $hook * @param string $sortkey * @param string $sortorder */ function glossary_print_approval_menu($cm, $glossary,$mode, $hook, $sortkey = '', $sortorder = '') { if ($glossary->showalphabet) { echo '
' . get_string("explainalphabet","glossary") . '

'; } glossary_print_special_links($cm, $glossary, $mode, $hook); glossary_print_alphabet_links($cm, $glossary, $mode, $hook,$sortkey, $sortorder); glossary_print_all_links($cm, $glossary, $mode, $hook); glossary_print_sorting_links($cm, $mode, $sortkey, $sortorder); } /** * @param object $cm * @param object $glossary * @param string $hook * @param string $sortkey * @param string $sortorder */ function glossary_print_import_menu($cm, $glossary, $mode, $hook, $sortkey='', $sortorder = '') { echo '
' . get_string("explainimport","glossary") . '
'; } /** * @param object $cm * @param object $glossary * @param string $hook * @param string $sortkey * @param string $sortorder */ function glossary_print_export_menu($cm, $glossary, $mode, $hook, $sortkey='', $sortorder = '') { echo '
' . get_string("explainexport","glossary") . '
'; } /** * @param object $cm * @param object $glossary * @param string $hook * @param string $sortkey * @param string $sortorder */ function glossary_print_alphabet_menu($cm, $glossary, $mode, $hook, $sortkey='', $sortorder = '') { if ( $mode != 'date' ) { if ($glossary->showalphabet) { echo '
' . get_string("explainalphabet","glossary") . '

'; } glossary_print_special_links($cm, $glossary, $mode, $hook); glossary_print_alphabet_links($cm, $glossary, $mode, $hook, $sortkey, $sortorder); glossary_print_all_links($cm, $glossary, $mode, $hook); } else { glossary_print_sorting_links($cm, $mode, $sortkey,$sortorder); } } /** * @param object $cm * @param object $glossary * @param string $hook * @param string $sortkey * @param string $sortorder */ function glossary_print_author_menu($cm, $glossary,$mode, $hook, $sortkey = '', $sortorder = '') { if ($glossary->showalphabet) { echo '
' . get_string("explainalphabet","glossary") . '

'; } glossary_print_alphabet_links($cm, $glossary, $mode, $hook, $sortkey, $sortorder); glossary_print_all_links($cm, $glossary, $mode, $hook); glossary_print_sorting_links($cm, $mode, $sortkey,$sortorder); } /** * @global object * @global object * @param object $cm * @param object $glossary * @param string $hook * @param object $category */ function glossary_print_categories_menu($cm, $glossary, $hook, $category) { global $CFG, $DB, $OUTPUT; $context = context_module::instance($cm->id); // Prepare format_string/text options $fmtoptions = array( 'context' => $context); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
'; if (has_capability('mod/glossary:managecategories', $context)) { $options['id'] = $cm->id; $options['mode'] = 'cat'; $options['hook'] = $hook; echo $OUTPUT->single_button(new moodle_url("editcategories.php", $options), get_string("editcategories","glossary"), "get"); } echo ''; echo ''; $menu = array(); $menu[GLOSSARY_SHOW_ALL_CATEGORIES] = get_string("allcategories","glossary"); $menu[GLOSSARY_SHOW_NOT_CATEGORISED] = get_string("notcategorised","glossary"); $categories = $DB->get_records("glossary_categories", array("glossaryid"=>$glossary->id), "name ASC"); $selected = ''; if ( $categories ) { foreach ($categories as $currentcategory) { $url = $currentcategory->id; if ( $category ) { if ($currentcategory->id == $category->id) { $selected = $url; } } $menu[$url] = format_string($currentcategory->name, true, $fmtoptions); } } if ( !$selected ) { $selected = GLOSSARY_SHOW_NOT_CATEGORISED; } if ( $category ) { echo format_string($category->name, true, $fmtoptions); } else { if ( $hook == GLOSSARY_SHOW_NOT_CATEGORISED ) { echo get_string("entrieswithoutcategory","glossary"); $selected = GLOSSARY_SHOW_NOT_CATEGORISED; } else if ( empty($hook) ) { echo get_string("allcategories","glossary"); $selected = GLOSSARY_SHOW_ALL_CATEGORIES; } } echo ''; $select = new single_select(new moodle_url("/mod/glossary/view.php", array('id'=>$cm->id, 'mode'=>'cat')), 'hook', $menu, $selected, null, "catmenu"); $select->set_label(get_string('categories', 'glossary'), array('class' => 'accesshide')); echo $OUTPUT->render($select); echo '
'; } /** * @global object * @param object $cm * @param object $glossary * @param string $mode * @param string $hook */ function glossary_print_all_links($cm, $glossary, $mode, $hook) { global $CFG; if ( $glossary->showall) { $strallentries = get_string("allentries", "glossary"); if ( $hook == 'ALL' ) { echo "$strallentries"; } else { $strexplainall = strip_tags(get_string("explainall","glossary")); echo "wwwroot/mod/glossary/view.php?id=$cm->id&mode=$mode&hook=ALL\">$strallentries"; } } } /** * @global object * @param object $cm * @param object $glossary * @param string $mode * @param string $hook */ function glossary_print_special_links($cm, $glossary, $mode, $hook) { global $CFG; if ( $glossary->showspecial) { $strspecial = get_string("special", "glossary"); if ( $hook == 'SPECIAL' ) { echo "$strspecial | "; } else { $strexplainspecial = strip_tags(get_string("explainspecial","glossary")); echo "wwwroot/mod/glossary/view.php?id=$cm->id&mode=$mode&hook=SPECIAL\">$strspecial | "; } } } /** * @global object * @param object $glossary * @param string $mode * @param string $hook * @param string $sortkey * @param string $sortorder */ function glossary_print_alphabet_links($cm, $glossary, $mode, $hook, $sortkey, $sortorder) { global $CFG; if ( $glossary->showalphabet) { $alphabet = explode(",", get_string('alphabet', 'langconfig')); for ($i = 0; $i < count($alphabet); $i++) { if ( $hook == $alphabet[$i] and $hook) { echo "$alphabet[$i]"; } else { echo "wwwroot/mod/glossary/view.php?id=$cm->id&mode=$mode&hook=".urlencode($alphabet[$i])."&sortkey=$sortkey&sortorder=$sortorder\">$alphabet[$i]"; } echo ' | '; } } } /** * @global object * @param object $cm * @param string $mode * @param string $sortkey * @param string $sortorder */ function glossary_print_sorting_links($cm, $mode, $sortkey = '',$sortorder = '') { global $CFG, $OUTPUT; $asc = get_string("ascending","glossary"); $desc = get_string("descending","glossary"); $bopen = ''; $bclose = ''; $neworder = ''; $currentorder = ''; $currentsort = ''; if ($sortkey === '') { $sortkey = 'CREATION'; } if ( $sortorder ) { if ( $sortorder == 'asc' ) { $currentorder = $asc; $neworder = '&sortorder=desc'; $newordertitle = get_string('changeto', 'glossary', $desc); } else { $currentorder = $desc; $neworder = '&sortorder=asc'; $newordertitle = get_string('changeto', 'glossary', $asc); } $icon = " " . $OUTPUT->pix_icon($sortorder, $newordertitle, 'glossary'); } else { if ( $sortkey != 'CREATION' and $sortkey != 'UPDATE' and $sortkey != 'FIRSTNAME' and $sortkey != 'LASTNAME' ) { $icon = ""; $newordertitle = $asc; } else { $newordertitle = $desc; $neworder = '&sortorder=desc'; $icon = " " . $OUTPUT->pix_icon('asc', $newordertitle, 'glossary'); } } $ficon = ''; $fneworder = ''; $sicon = ''; $sneworder = ''; $sbtag = ''; $fbtag = ''; $fendbtag = ''; $sendbtag = ''; if ( $sortkey == 'CREATION' or $sortkey == 'FIRSTNAME' ) { $ficon = $icon; $fneworder = $neworder; $fordertitle = $newordertitle; $sordertitle = $asc; $fbtag = $bopen; $fendbtag = $bclose; } elseif ($sortkey == 'UPDATE' or $sortkey == 'LASTNAME') { $sicon = $icon; $sneworder = $neworder; $fordertitle = $asc; $sordertitle = $newordertitle; $sbtag = $bopen; $sendbtag = $bclose; } else { $fordertitle = $asc; $sordertitle = $asc; } if ( $sortkey == 'CREATION' or $sortkey == 'UPDATE' ) { $forder = 'CREATION'; $sorder = 'UPDATE'; $fsort = get_string("sortbycreation", "glossary"); $ssort = get_string("sortbylastupdate", "glossary"); $currentsort = $fsort; if ($sortkey == 'UPDATE') { $currentsort = $ssort; } $sort = get_string("sortchronogically", "glossary"); } elseif ( $sortkey == 'FIRSTNAME' or $sortkey == 'LASTNAME') { $forder = 'FIRSTNAME'; $sorder = 'LASTNAME'; $fsort = get_string("firstname"); $ssort = get_string("lastname"); $currentsort = $fsort; if ($sortkey == 'LASTNAME') { $currentsort = $ssort; } $sort = get_string("sortby", "glossary"); } $current = ''.get_string('current', 'glossary', "$currentsort $currentorder").''; echo "
$current $sort: $sbtagwwwroot/mod/glossary/view.php?id=$cm->id&sortkey=$sorder$sneworder&mode=$mode\">$ssort$sicon$sendbtag | ". "$fbtagwwwroot/mod/glossary/view.php?id=$cm->id&sortkey=$forder$fneworder&mode=$mode\">$fsort$ficon$fendbtag
"; } /** * * @param object $entry0 * @param object $entry1 * @return int [-1 | 0 | 1] */ function glossary_sort_entries ( $entry0, $entry1 ) { if ( core_text::strtolower(ltrim($entry0->concept)) < core_text::strtolower(ltrim($entry1->concept)) ) { return -1; } elseif ( core_text::strtolower(ltrim($entry0->concept)) > core_text::strtolower(ltrim($entry1->concept)) ) { return 1; } else { return 0; } } /** * @global object * @global object * @global object * @param object $course * @param object $entry * @return bool */ function glossary_print_entry_ratings($course, $entry) { global $OUTPUT; if( !empty($entry->rating) ){ echo $OUTPUT->render($entry->rating); } } /** * * @global object * @global object * @global object * @param int $courseid * @param array $entries * @param int $displayformat */ function glossary_print_dynaentry($courseid, $entries, $displayformat = -1) { global $USER, $CFG, $DB; echo '
'; echo ''; echo ''; echo '
'; if ( $entries ) { foreach ( $entries as $entry ) { if (! $glossary = $DB->get_record('glossary', array('id'=>$entry->glossaryid))) { throw new \moodle_exception('invalidid', 'glossary'); } if (! $course = $DB->get_record('course', array('id'=>$glossary->course))) { throw new \moodle_exception('coursemisconf'); } if (!$cm = get_coursemodule_from_instance('glossary', $entry->glossaryid, $glossary->course) ) { throw new \moodle_exception('invalidid', 'glossary'); } //If displayformat is present, override glossary->displayformat if ($displayformat < 0) { $dp = $glossary->displayformat; } else { $dp = $displayformat; } //Get popupformatname $format = $DB->get_record('glossary_formats', array('name'=>$dp)); $displayformat = $format->popupformatname; //Check displayformat variable and set to default if necessary if (!$displayformat) { $displayformat = 'dictionary'; } $formatfile = $CFG->dirroot.'/mod/glossary/formats/'.$displayformat.'/'.$displayformat.'_format.php'; $functionname = 'glossary_show_entry_'.$displayformat; if (file_exists($formatfile)) { include_once($formatfile); if (function_exists($functionname)) { $functionname($course, $cm, $glossary, $entry,'','','',''); } } } } echo '
'; } /** * * @global object * @param array $entries * @param array $aliases * @param array $categories * @return string */ function glossary_generate_export_csv($entries, $aliases, $categories) { global $CFG; $csv = ''; $delimiter = ''; require_once($CFG->libdir . '/csvlib.class.php'); $delimiter = csv_import_reader::get_delimiter('comma'); $csventries = array(0 => array(get_string('concept', 'glossary'), get_string('definition', 'glossary'))); $csvaliases = array(0 => array()); $csvcategories = array(0 => array()); $aliascount = 0; $categorycount = 0; foreach ($entries as $entry) { $thisaliasesentry = array(); $thiscategoriesentry = array(); $thiscsventry = array($entry->concept, nl2br($entry->definition)); if (array_key_exists($entry->id, $aliases) && is_array($aliases[$entry->id])) { $thiscount = count($aliases[$entry->id]); if ($thiscount > $aliascount) { $aliascount = $thiscount; } foreach ($aliases[$entry->id] as $alias) { $thisaliasesentry[] = trim($alias); } } if (array_key_exists($entry->id, $categories) && is_array($categories[$entry->id])) { $thiscount = count($categories[$entry->id]); if ($thiscount > $categorycount) { $categorycount = $thiscount; } foreach ($categories[$entry->id] as $catentry) { $thiscategoriesentry[] = trim($catentry); } } $csventries[$entry->id] = $thiscsventry; $csvaliases[$entry->id] = $thisaliasesentry; $csvcategories[$entry->id] = $thiscategoriesentry; } $returnstr = ''; foreach ($csventries as $id => $row) { $aliasstr = ''; $categorystr = ''; if ($id == 0) { $aliasstr = get_string('alias', 'glossary'); $categorystr = get_string('category', 'glossary'); } $row = array_merge($row, array_pad($csvaliases[$id], $aliascount, $aliasstr), array_pad($csvcategories[$id], $categorycount, $categorystr)); $returnstr .= '"' . implode('"' . $delimiter . '"', $row) . '"' . "\n"; } return $returnstr; } /** * * @param object $glossary * @param string $ignored invalid parameter * @param int|string $hook * @return string */ function glossary_generate_export_file($glossary, $ignored = "", $hook = 0) { global $CFG, $DB; // Large exports are likely to take their time and memory. core_php_time_limit::raise(); raise_memory_limit(MEMORY_EXTRA); $cm = get_coursemodule_from_instance('glossary', $glossary->id, $glossary->course); $context = context_module::instance($cm->id); $co = "\n"; $co .= glossary_start_tag("GLOSSARY",0,true); $co .= glossary_start_tag("INFO",1,true); $co .= glossary_full_tag("NAME",2,false,$glossary->name); $co .= glossary_full_tag("INTRO",2,false,$glossary->intro); $co .= glossary_full_tag("INTROFORMAT",2,false,$glossary->introformat); $co .= glossary_full_tag("ALLOWDUPLICATEDENTRIES",2,false,$glossary->allowduplicatedentries); $co .= glossary_full_tag("DISPLAYFORMAT",2,false,$glossary->displayformat); $co .= glossary_full_tag("SHOWSPECIAL",2,false,$glossary->showspecial); $co .= glossary_full_tag("SHOWALPHABET",2,false,$glossary->showalphabet); $co .= glossary_full_tag("SHOWALL",2,false,$glossary->showall); $co .= glossary_full_tag("ALLOWCOMMENTS",2,false,$glossary->allowcomments); $co .= glossary_full_tag("USEDYNALINK",2,false,$glossary->usedynalink); $co .= glossary_full_tag("DEFAULTAPPROVAL",2,false,$glossary->defaultapproval); $co .= glossary_full_tag("GLOBALGLOSSARY",2,false,$glossary->globalglossary); $co .= glossary_full_tag("ENTBYPAGE",2,false,$glossary->entbypage); $co .= glossary_xml_export_files('INTROFILES', 2, $context->id, 'intro', 0); if ( $entries = $DB->get_records("glossary_entries", array("glossaryid"=>$glossary->id))) { $co .= glossary_start_tag("ENTRIES",2,true); foreach ($entries as $entry) { $permissiongranted = 1; if ( $hook ) { switch ( $hook ) { case "ALL": case "SPECIAL": break; default: $permissiongranted = ($entry->concept[ strlen($hook)-1 ] == $hook); break; } } if ( $hook ) { switch ( $hook ) { case GLOSSARY_SHOW_ALL_CATEGORIES: break; case GLOSSARY_SHOW_NOT_CATEGORISED: $permissiongranted = !$DB->record_exists("glossary_entries_categories", array("entryid"=>$entry->id)); break; default: $permissiongranted = $DB->record_exists("glossary_entries_categories", array("entryid"=>$entry->id, "categoryid"=>$hook)); break; } } if ( $entry->approved and $permissiongranted ) { $co .= glossary_start_tag("ENTRY",3,true); $co .= glossary_full_tag("CONCEPT",4,false,trim($entry->concept)); $co .= glossary_full_tag("DEFINITION",4,false,$entry->definition); $co .= glossary_full_tag("FORMAT",4,false,$entry->definitionformat); // note: use old name for BC reasons $co .= glossary_full_tag('DEFINITIONTRUST', 4, false, $entry->definitiontrust); $co .= glossary_full_tag("USEDYNALINK",4,false,$entry->usedynalink); $co .= glossary_full_tag("CASESENSITIVE",4,false,$entry->casesensitive); $co .= glossary_full_tag("FULLMATCH",4,false,$entry->fullmatch); $co .= glossary_full_tag("TEACHERENTRY",4,false,$entry->teacherentry); if ( $aliases = $DB->get_records("glossary_alias", array("entryid"=>$entry->id))) { $co .= glossary_start_tag("ALIASES",4,true); foreach ($aliases as $alias) { $co .= glossary_start_tag("ALIAS",5,true); $co .= glossary_full_tag("NAME",6,false,trim($alias->alias)); $co .= glossary_end_tag("ALIAS",5,true); } $co .= glossary_end_tag("ALIASES",4,true); } if ( $catentries = $DB->get_records("glossary_entries_categories", array("entryid"=>$entry->id))) { $co .= glossary_start_tag("CATEGORIES",4,true); foreach ($catentries as $catentry) { $category = $DB->get_record("glossary_categories", array("id"=>$catentry->categoryid)); $co .= glossary_start_tag("CATEGORY",5,true); $co .= glossary_full_tag("NAME",6,false,$category->name); $co .= glossary_full_tag("USEDYNALINK",6,false,$category->usedynalink); $co .= glossary_end_tag("CATEGORY",5,true); } $co .= glossary_end_tag("CATEGORIES",4,true); } // Export files embedded in entries. $co .= glossary_xml_export_files('ENTRYFILES', 4, $context->id, 'entry', $entry->id); // Export attachments. $co .= glossary_xml_export_files('ATTACHMENTFILES', 4, $context->id, 'attachment', $entry->id); // Export tags. $tags = core_tag_tag::get_item_tags_array('mod_glossary', 'glossary_entries', $entry->id); if (count($tags)) { $co .= glossary_start_tag("TAGS", 4, true); foreach ($tags as $tag) { $co .= glossary_full_tag("TAG", 5, false, $tag); } $co .= glossary_end_tag("TAGS", 4, true); } $co .= glossary_end_tag("ENTRY",3,true); } } $co .= glossary_end_tag("ENTRIES",2,true); } $co .= glossary_end_tag("INFO",1,true); $co .= glossary_end_tag("GLOSSARY",0,true); return $co; } /// Functions designed by Eloy Lafuente /// Functions to create, open and write header of the xml file /** * Read import file and convert to current charset * * @global object * @param string $file * @return string */ function glossary_read_imported_file($file_content) { global $CFG; require_once "../../lib/xmlize.php"; return xmlize($file_content, 0); } /** * Return the xml start tag * * @param string $tag * @param int $level * @param bool $endline * @return string */ function glossary_start_tag($tag,$level=0,$endline=false) { if ($endline) { $endchar = "\n"; } else { $endchar = ""; } return str_repeat(" ",$level*2)."<".strtoupper($tag).">".$endchar; } /** * Return the xml end tag * @param string $tag * @param int $level * @param bool $endline * @return string */ function glossary_end_tag($tag,$level=0,$endline=true) { if ($endline) { $endchar = "\n"; } else { $endchar = ""; } return str_repeat(" ",$level*2)."".$endchar; } /** * Return the start tag, the contents and the end tag * * @global object * @param string $tag * @param int $level * @param bool $endline * @param string $content * @return string */ function glossary_full_tag($tag, $level, $endline, $content) { global $CFG; $st = glossary_start_tag($tag,$level,$endline); $co = preg_replace("/\r\n|\r/", "\n", s($content)); $et = glossary_end_tag($tag,0,true); return $st.$co.$et; } /** * Prepares file area to export as part of XML export * * @param string $tag XML tag to use for the group * @param int $taglevel * @param int $contextid * @param string $filearea * @param int $itemid * @return string */ function glossary_xml_export_files($tag, $taglevel, $contextid, $filearea, $itemid) { $co = ''; $fs = get_file_storage(); if ($files = $fs->get_area_files( $contextid, 'mod_glossary', $filearea, $itemid, 'itemid,filepath,filename', false)) { $co .= glossary_start_tag($tag, $taglevel, true); foreach ($files as $file) { $co .= glossary_start_tag('FILE', $taglevel + 1, true); $co .= glossary_full_tag('FILENAME', $taglevel + 2, false, $file->get_filename()); $co .= glossary_full_tag('FILEPATH', $taglevel + 2, false, $file->get_filepath()); $co .= glossary_full_tag('CONTENTS', $taglevel + 2, false, base64_encode($file->get_content())); $co .= glossary_full_tag('FILEAUTHOR', $taglevel + 2, false, $file->get_author()); $co .= glossary_full_tag('FILELICENSE', $taglevel + 2, false, $file->get_license()); $co .= glossary_end_tag('FILE', $taglevel + 1); } $co .= glossary_end_tag($tag, $taglevel); } return $co; } /** * Parses files from XML import and inserts them into file system * * @param array $xmlparent parent element in parsed XML tree * @param string $tag * @param int $contextid * @param string $filearea * @param int $itemid * @return int */ function glossary_xml_import_files($xmlparent, $tag, $contextid, $filearea, $itemid) { global $USER, $CFG; $count = 0; if (isset($xmlparent[$tag][0]['#']['FILE'])) { $fs = get_file_storage(); $files = $xmlparent[$tag][0]['#']['FILE']; foreach ($files as $file) { $filerecord = array( 'contextid' => $contextid, 'component' => 'mod_glossary', 'filearea' => $filearea, 'itemid' => $itemid, 'filepath' => $file['#']['FILEPATH'][0]['#'], 'filename' => $file['#']['FILENAME'][0]['#'], 'userid' => $USER->id ); if (array_key_exists('FILEAUTHOR', $file['#'])) { $filerecord['author'] = $file['#']['FILEAUTHOR'][0]['#']; } if (array_key_exists('FILELICENSE', $file['#'])) { $license = $file['#']['FILELICENSE'][0]['#']; require_once($CFG->libdir . "/licenselib.php"); if (license_manager::get_license_by_shortname($license)) { $filerecord['license'] = $license; } } $content = $file['#']['CONTENTS'][0]['#']; $fs->create_file_from_string($filerecord, base64_decode($content)); $count++; } } return $count; } /** * How many unrated entries are in the given glossary for a given user? * * @global moodle_database $DB * @param int $glossaryid * @param int $userid * @return int */ function glossary_count_unrated_entries($glossaryid, $userid) { global $DB; $sql = "SELECT COUNT('x') as num FROM {glossary_entries} WHERE glossaryid = :glossaryid AND userid <> :userid"; $params = array('glossaryid' => $glossaryid, 'userid' => $userid); $entries = $DB->count_records_sql($sql, $params); if ($entries) { // We need to get the contextid for the glossaryid we have been given. $sql = "SELECT ctx.id FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid JOIN {modules} m ON m.id = cm.module JOIN {glossary} g ON g.id = cm.instance WHERE ctx.contextlevel = :contextlevel AND m.name = 'glossary' AND g.id = :glossaryid"; $contextid = $DB->get_field_sql($sql, array('glossaryid' => $glossaryid, 'contextlevel' => CONTEXT_MODULE)); // Now we need to count the ratings that this user has made $sql = "SELECT COUNT('x') AS num FROM {glossary_entries} e JOIN {rating} r ON r.itemid = e.id WHERE e.glossaryid = :glossaryid AND r.userid = :userid AND r.component = 'mod_glossary' AND r.ratingarea = 'entry' AND r.contextid = :contextid"; $params = array('glossaryid' => $glossaryid, 'userid' => $userid, 'contextid' => $contextid); $rated = $DB->count_records_sql($sql, $params); if ($rated) { // The number or enties minus the number or rated entries equals the number of unrated // entries if ($entries > $rated) { return $entries - $rated; } else { return 0; // Just in case there was a counting error } } else { return (int)$entries; } } else { return 0; } } /** * * Returns the html code to represent any pagging bar. Paramenters are: * * The function dinamically show the first and last pages, and "scroll" over pages. * Fully compatible with Moodle's print_paging_bar() function. Perhaps some day this * could replace the general one. ;-) * * @param int $totalcount total number of records to be displayed * @param int $page page currently selected (0 based) * @param int $perpage number of records per page * @param string $baseurl url to link in each page, the string 'page=XX' will be added automatically. * * @param int $maxpageallowed Optional maximum number of page allowed. * @param int $maxdisplay Optional maximum number of page links to show in the bar * @param string $separator Optional string to be used between pages in the bar * @param string $specialtext Optional string to be showed as an special link * @param string $specialvalue Optional value (page) to be used in the special link * @param bool $previousandnext Optional to decide if we want the previous and next links * @return string */ function glossary_get_paging_bar($totalcount, $page, $perpage, $baseurl, $maxpageallowed=99999, $maxdisplay=20, $separator=" ", $specialtext="", $specialvalue=-1, $previousandnext = true) { $code = ''; $showspecial = false; $specialselected = false; //Check if we have to show the special link if (!empty($specialtext)) { $showspecial = true; } //Check if we are with the special link selected if ($showspecial && $page == $specialvalue) { $specialselected = true; } //If there are results (more than 1 page) if ($totalcount > $perpage) { $code .= "
"; $code .= "

".get_string("page").":"; $maxpage = (int)(($totalcount-1)/$perpage); //Lower and upper limit of page if ($page < 0) { $page = 0; } if ($page > $maxpageallowed) { $page = $maxpageallowed; } if ($page > $maxpage) { $page = $maxpage; } //Calculate the window of pages $pagefrom = $page - ((int)($maxdisplay / 2)); if ($pagefrom < 0) { $pagefrom = 0; } $pageto = $pagefrom + $maxdisplay - 1; if ($pageto > $maxpageallowed) { $pageto = $maxpageallowed; } if ($pageto > $maxpage) { $pageto = $maxpage; } //Some movements can be necessary if don't see enought pages if ($pageto - $pagefrom < $maxdisplay - 1) { if ($pageto - $maxdisplay + 1 > 0) { $pagefrom = $pageto - $maxdisplay + 1; } } //Calculate first and last if necessary $firstpagecode = ''; $lastpagecode = ''; if ($pagefrom > 0) { $firstpagecode = "$separator1"; if ($pagefrom > 1) { $firstpagecode .= "$separator..."; } } if ($pageto < $maxpage) { if ($pageto < $maxpage -1) { $lastpagecode = "$separator..."; } $lastpagecode .= "$separator".($maxpage+1).""; } //Previous if ($page > 0 && $previousandnext) { $pagenum = $page - 1; $code .= " (".get_string("previous").") "; } //Add first $code .= $firstpagecode; $pagenum = $pagefrom; //List of maxdisplay pages while ($pagenum <= $pageto) { $pagetoshow = $pagenum +1; if ($pagenum == $page && !$specialselected) { $code .= "$separator$pagetoshow"; } else { $code .= "$separator$pagetoshow"; } $pagenum++; } //Add last $code .= $lastpagecode; //Next if ($page < $maxpage && $page < $maxpageallowed && $previousandnext) { $pagenum = $page + 1; $code .= "$separator(".get_string("next").")"; } //Add special if ($showspecial) { $code .= '
'; if ($specialselected) { $code .= "$separator$specialtext"; } else { $code .= "$separator$specialtext"; } } //End html $code .= "

"; $code .= "
"; } return $code; } /** * List the actions that correspond to a view of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = 'r' and edulevel = LEVEL_PARTICIPATING will * be considered as view action. * * @return array */ function glossary_get_view_actions() { return array('view','view all','view entry'); } /** * List the actions that correspond to a post of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING * will be considered as post action. * * @return array */ function glossary_get_post_actions() { return array('add category','add entry','approve entry','delete category','delete entry','edit category','update entry'); } /** * Implementation of the function for printing the form elements that control * whether the course reset functionality affects the glossary. * @param MoodleQuickForm $mform form passed by reference */ function glossary_reset_course_form_definition(&$mform) { $mform->addElement('header', 'glossaryheader', get_string('modulenameplural', 'glossary')); $mform->addElement('static', 'glossarydelete', get_string('delete')); $mform->addElement('checkbox', 'reset_glossary_all', get_string('resetglossariesall','glossary')); $mform->addElement('select', 'reset_glossary_types', get_string('resetglossaries', 'glossary'), array('main'=>get_string('mainglossary', 'glossary'), 'secondary'=>get_string('secondaryglossary', 'glossary')), array('multiple' => 'multiple')); $mform->hideIf('reset_glossary_types', 'reset_glossary_all', 'checked'); $mform->addElement('checkbox', 'reset_glossary_notenrolled', get_string('deletenotenrolled', 'glossary')); $mform->hideIf('reset_glossary_notenrolled', 'reset_glossary_all', 'checked'); $mform->addElement('checkbox', 'reset_glossary_ratings', get_string('deleteallratings')); $mform->hideIf('reset_glossary_ratings', 'reset_glossary_all', 'checked'); $mform->addElement('checkbox', 'reset_glossary_comments', get_string('deleteallcomments')); $mform->hideIf('reset_glossary_comments', 'reset_glossary_all', 'checked'); $mform->addElement('checkbox', 'reset_glossary_tags', get_string('removeallglossarytags', 'glossary')); $mform->hideIf('reset_glossary_tags', 'reset_glossary_all', 'checked'); } /** * Course reset form defaults. * @return array */ function glossary_reset_course_form_defaults($course) { return array('reset_glossary_all'=>0, 'reset_glossary_ratings'=>1, 'reset_glossary_comments'=>1, 'reset_glossary_notenrolled'=>0); } /** * Removes all grades from gradebook * * @param int $courseid The ID of the course to reset * @param string $type The optional type of glossary. 'main', 'secondary' or '' */ function glossary_reset_gradebook($courseid, $type='') { global $DB; switch ($type) { case 'main' : $type = "AND g.mainglossary=1"; break; case 'secondary' : $type = "AND g.mainglossary=0"; break; default : $type = ""; //all } $sql = "SELECT g.*, cm.idnumber as cmidnumber, g.course as courseid FROM {glossary} g, {course_modules} cm, {modules} m WHERE m.name='glossary' AND m.id=cm.module AND cm.instance=g.id AND g.course=? $type"; if ($glossarys = $DB->get_records_sql($sql, array($courseid))) { foreach ($glossarys as $glossary) { glossary_grade_item_update($glossary, 'reset'); } } } /** * Actual implementation of the reset course functionality, delete all the * glossary responses for course $data->courseid. * * @global object * @param $data the data submitted from the reset course. * @return array status array */ function glossary_reset_userdata($data) { global $CFG, $DB; require_once($CFG->dirroot.'/rating/lib.php'); $componentstr = get_string('modulenameplural', 'glossary'); $status = []; $allentriessql = "SELECT e.id FROM {glossary_entries} e JOIN {glossary} g ON e.glossaryid = g.id WHERE g.course = ?"; $allglossariessql = "SELECT g.id FROM {glossary} g WHERE g.course = ?"; $params = [$data->courseid]; $fs = get_file_storage(); $rm = new rating_manager(); $ratingdeloptions = new stdClass; $ratingdeloptions->component = 'mod_glossary'; $ratingdeloptions->ratingarea = 'entry'; // Delete entries if requested. if (!empty($data->reset_glossary_all) or (!empty($data->reset_glossary_types) and in_array('main', $data->reset_glossary_types) and in_array('secondary', $data->reset_glossary_types))) { $params[] = 'glossary_entry'; $DB->delete_records_select('comments', "itemid IN ($allentriessql) AND commentarea=?", $params); $DB->delete_records_select('glossary_alias', "entryid IN ($allentriessql)", $params); $DB->delete_records_select('glossary_entries', "glossaryid IN ($allglossariessql)", $params); // Now get rid of all attachments. if ($glossaries = $DB->get_records_sql($allglossariessql, $params)) { foreach ($glossaries as $glossaryid=>$unused) { if (!$cm = get_coursemodule_from_instance('glossary', $glossaryid)) { continue; } $context = context_module::instance($cm->id); $fs->delete_area_files($context->id, 'mod_glossary', 'attachment'); // Delete ratings. $ratingdeloptions->contextid = $context->id; $rm->delete_ratings($ratingdeloptions); core_tag_tag::delete_instances('mod_glossary', null, $context->id); } } // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { glossary_reset_gradebook($data->courseid); } $status[] = [ 'component' => $componentstr, 'item' => get_string('resetglossariesall', 'glossary'), 'error' => false, ]; } else if (!empty($data->reset_glossary_types)) { $mainentriessql = "$allentriessql AND g.mainglossary=1"; $secondaryentriessql = "$allentriessql AND g.mainglossary=0"; $mainglossariessql = "$allglossariessql AND g.mainglossary=1"; $secondaryglossariessql = "$allglossariessql AND g.mainglossary=0"; if (in_array('main', $data->reset_glossary_types)) { $params[] = 'glossary_entry'; $DB->delete_records_select('comments', "itemid IN ($mainentriessql) AND commentarea=?", $params); $DB->delete_records_select('glossary_entries', "glossaryid IN ($mainglossariessql)", $params); if ($glossaries = $DB->get_records_sql($mainglossariessql, $params)) { foreach ($glossaries as $glossaryid=>$unused) { if (!$cm = get_coursemodule_from_instance('glossary', $glossaryid)) { continue; } $context = context_module::instance($cm->id); $fs->delete_area_files($context->id, 'mod_glossary', 'attachment'); // Delete ratings. $ratingdeloptions->contextid = $context->id; $rm->delete_ratings($ratingdeloptions); core_tag_tag::delete_instances('mod_glossary', null, $context->id); } } // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { glossary_reset_gradebook($data->courseid, 'main'); } $status[] = [ 'component' => $componentstr, 'item' => get_string('resetglossaries', 'glossary').': '.get_string('mainglossary', 'glossary'), 'error' => false, ]; } else if (in_array('secondary', $data->reset_glossary_types)) { $params[] = 'glossary_entry'; $DB->delete_records_select('comments', "itemid IN ($secondaryentriessql) AND commentarea=?", $params); $DB->delete_records_select('glossary_entries', "glossaryid IN ($secondaryglossariessql)", $params); // Remove exported source flag from entries in main glossary. $DB->execute("UPDATE {glossary_entries} SET sourceglossaryid=0 WHERE glossaryid IN ($mainglossariessql)", $params); if ($glossaries = $DB->get_records_sql($secondaryglossariessql, $params)) { foreach ($glossaries as $glossaryid=>$unused) { if (!$cm = get_coursemodule_from_instance('glossary', $glossaryid)) { continue; } $context = context_module::instance($cm->id); $fs->delete_area_files($context->id, 'mod_glossary', 'attachment'); // Delete ratings. $ratingdeloptions->contextid = $context->id; $rm->delete_ratings($ratingdeloptions); core_tag_tag::delete_instances('mod_glossary', null, $context->id); } } // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { glossary_reset_gradebook($data->courseid, 'secondary'); } $status[] = [ 'component' => $componentstr, 'item' => get_string('resetglossaries', 'glossary').': '.get_string('secondaryglossary', 'glossary'), 'error' => false, ]; } } // Remove entries by users not enrolled into course. if (!empty($data->reset_glossary_notenrolled)) { $entriessql = "SELECT e.id, e.userid, e.glossaryid, u.id AS userexists, u.deleted AS userdeleted FROM {glossary_entries} e JOIN {glossary} g ON e.glossaryid = g.id LEFT JOIN {user} u ON e.userid = u.id WHERE g.course = ? AND e.userid > 0"; $coursecontext = context_course::instance($data->courseid); $notenrolled = []; $rs = $DB->get_recordset_sql($entriessql, $params); if ($rs->valid()) { foreach ($rs as $entry) { if (array_key_exists($entry->userid, $notenrolled) || !$entry->userexists || $entry->userdeleted || !is_enrolled($coursecontext , $entry->userid)) { $DB->delete_records('comments', ['commentarea' => 'glossary_entry', 'itemid' => $entry->id]); $DB->delete_records('glossary_entries', ['id' => $entry->id]); if ($cm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { $context = context_module::instance($cm->id); $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id); //delete ratings $ratingdeloptions->contextid = $context->id; $rm->delete_ratings($ratingdeloptions); } } } $status[] = [ 'component' => $componentstr, 'item' => get_string('deletenotenrolled', 'glossary'), 'error' => false, ]; } $rs->close(); } // Remove all ratings. if (!empty($data->reset_glossary_ratings)) { // Remove ratings. if ($glossaries = $DB->get_records_sql($allglossariessql, $params)) { foreach ($glossaries as $glossaryid=>$unused) { if (!$cm = get_coursemodule_from_instance('glossary', $glossaryid)) { continue; } $context = context_module::instance($cm->id); // Delete ratings. $ratingdeloptions->contextid = $context->id; $rm->delete_ratings($ratingdeloptions); } } // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { glossary_reset_gradebook($data->courseid); } $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallratings'), 'error' => false, ]; } // Remove comments. if (!empty($data->reset_glossary_comments)) { $params[] = 'glossary_entry'; $DB->delete_records_select('comments', "itemid IN ($allentriessql) AND commentarea= ? ", $params); $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallcomments'), 'error' => false, ]; } // Remove all the tags. if (!empty($data->reset_glossary_tags)) { if ($glossaries = $DB->get_records_sql($allglossariessql, $params)) { foreach ($glossaries as $glossaryid => $unused) { if (!$cm = get_coursemodule_from_instance('glossary', $glossaryid)) { continue; } $context = context_module::instance($cm->id); core_tag_tag::delete_instances('mod_glossary', null, $context->id); } } $status[] = [ 'component' => $componentstr, 'item' => get_string('removeallglossarytags', 'glossary'), 'error' => false, ]; } // Updating dates - shift may be negative too. if ($data->timeshift) { // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. shift_course_mod_dates('glossary', ['assesstimestart', 'assesstimefinish'], $data->timeshift, $data->courseid); $status[] = [ 'component' => $componentstr, 'item' => get_string('date'), 'error' => false, ]; } return $status; } /** * Returns all other caps used in module * @return array */ function glossary_get_extra_capabilities() { return ['moodle/rating:view', 'moodle/rating:viewany', 'moodle/rating:viewall', 'moodle/rating:rate', 'moodle/comment:view', 'moodle/comment:post', 'moodle/comment:delete']; } /** * @param string $feature FEATURE_xx constant for requested feature * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. */ function glossary_supports($feature) { switch($feature) { case FEATURE_GROUPS: return false; case FEATURE_GROUPINGS: return false; case FEATURE_MOD_INTRO: return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; case FEATURE_COMPLETION_HAS_RULES: return true; case FEATURE_GRADE_HAS_GRADE: return true; case FEATURE_GRADE_OUTCOMES: return true; case FEATURE_RATE: return true; case FEATURE_BACKUP_MOODLE2: return true; case FEATURE_SHOW_DESCRIPTION: return true; case FEATURE_COMMENT: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_COLLABORATION; default: return null; } } function glossary_extend_navigation($navigation, $course, $module, $cm) { global $CFG, $DB; $displayformat = $DB->get_record('glossary_formats', array('name' => $module->displayformat)); // Get visible tabs for the format and check if the menu needs to be displayed. $showtabs = glossary_get_visible_tabs($displayformat); foreach ($showtabs as $showtabkey => $showtabvalue) { switch($showtabvalue) { case GLOSSARY_STANDARD : $navigation->add(get_string('standardview', 'glossary'), new moodle_url('/mod/glossary/view.php', array('id' => $cm->id, 'mode' => 'letter'))); break; case GLOSSARY_CATEGORY : $navigation->add(get_string('categoryview', 'glossary'), new moodle_url('/mod/glossary/view.php', array('id' => $cm->id, 'mode' => 'cat'))); break; case GLOSSARY_DATE : $navigation->add(get_string('dateview', 'glossary'), new moodle_url('/mod/glossary/view.php', array('id' => $cm->id, 'mode' => 'date'))); break; case GLOSSARY_AUTHOR : $navigation->add(get_string('authorview', 'glossary'), new moodle_url('/mod/glossary/view.php', array('id' => $cm->id, 'mode' => 'author'))); break; } } } /** * Adds module specific settings to the settings block * * @param settings_navigation $settings The settings navigation object * @param navigation_node $glossarynode The node to add module settings to */ function glossary_extend_settings_navigation(settings_navigation $settings, navigation_node $glossarynode) { global $DB, $CFG, $USER; $mode = optional_param('mode', '', PARAM_ALPHA); $hook = optional_param('hook', 'ALL', PARAM_CLEAN); if (has_capability('mod/glossary:import', $settings->get_page()->cm->context)) { $node = $glossarynode->add(get_string('importentries', 'glossary'), new moodle_url('/mod/glossary/import.php', ['id' => $settings->get_page()->cm->id])); $node->set_show_in_secondary_navigation(false); } if (has_capability('mod/glossary:export', $settings->get_page()->cm->context)) { $node = $glossarynode->add(get_string('exportentries', 'glossary'), new moodle_url('/mod/glossary/export.php', ['id' => $settings->get_page()->cm->id, 'mode' => $mode, 'hook' => $hook])); $node->set_show_in_secondary_navigation(false); } $glossary = $DB->get_record('glossary', array("id" => $settings->get_page()->cm->instance)); $hiddenentries = $DB->count_records('glossary_entries', ['glossaryid' => $settings->get_page()->cm->instance, 'approved' => 0]); // Safe guard check - Ideally, there shouldn't be any hidden entries if the glossary has 'defaultapproval'. if (has_capability('mod/glossary:approve', $settings->get_page()->cm->context) && (!$glossary->defaultapproval || $hiddenentries)) { $glossarynode->add(get_string('pendingapprovalcount', 'glossary', $hiddenentries), new moodle_url('/mod/glossary/view.php', ['id' => $settings->get_page()->cm->id, 'mode' => 'approval']), navigation_node::TYPE_CUSTOM, null, 'pendingapproval'); } if (has_capability('mod/glossary:write', $settings->get_page()->cm->context)) { $node = $glossarynode->add(get_string('addentry', 'glossary'), new moodle_url('/mod/glossary/edit.php', ['cmid' => $settings->get_page()->cm->id])); $node->set_show_in_secondary_navigation(false); } if (!empty($CFG->enablerssfeeds) && !empty($CFG->glossary_enablerssfeeds) && $glossary->rsstype && $glossary->rssarticles && has_capability('mod/glossary:view', $settings->get_page()->cm->context)) { require_once("$CFG->libdir/rsslib.php"); $string = get_string('rsstype', 'glossary'); $url = new moodle_url(rss_get_url($settings->get_page()->cm->context->id, $USER->id, 'mod_glossary', $glossary->id)); $node = $glossarynode->add($string, $url, settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', '')); $node->set_show_in_secondary_navigation(false); } } /** * Running addtional permission check on plugin, for example, plugins * may have switch to turn on/off comments option, this callback will * affect UI display, not like pluginname_comment_validate only throw * exceptions. * Capability check has been done in comment->check_permissions(), we * don't need to do it again here. * * @package mod_glossary * @category comment * * @param stdClass $comment_param { * context => context the context object * courseid => int course id * cm => stdClass course module object * commentarea => string comment area * itemid => int itemid * } * @return array */ function glossary_comment_permissions($comment_param) { return array('post'=>true, 'view'=>true); } /** * Validate comment parameter before perform other comments actions * * @package mod_glossary * @category comment * * @param stdClass $comment_param { * context => context the context object * courseid => int course id * cm => stdClass course module object * commentarea => string comment area * itemid => int itemid * } * @return boolean */ function glossary_comment_validate($comment_param) { global $DB; // validate comment area if ($comment_param->commentarea != 'glossary_entry') { throw new comment_exception('invalidcommentarea'); } if (!$record = $DB->get_record('glossary_entries', array('id'=>$comment_param->itemid))) { throw new comment_exception('invalidcommentitemid'); } if ($record->sourceglossaryid && $record->sourceglossaryid == $comment_param->cm->instance) { $glossary = $DB->get_record('glossary', array('id'=>$record->sourceglossaryid)); } else { $glossary = $DB->get_record('glossary', array('id'=>$record->glossaryid)); } if (!$glossary) { throw new comment_exception('invalidid', 'data'); } if (!$course = $DB->get_record('course', array('id'=>$glossary->course))) { throw new comment_exception('coursemisconf'); } if (!$cm = get_coursemodule_from_instance('glossary', $glossary->id, $course->id)) { throw new comment_exception('invalidcoursemodule'); } $context = context_module::instance($cm->id); if ($glossary->defaultapproval and !$record->approved and !has_capability('mod/glossary:approve', $context)) { throw new comment_exception('notapproved', 'glossary'); } // validate context id if ($context->id != $comment_param->context->id) { throw new comment_exception('invalidcontext'); } // validation for comment deletion if (!empty($comment_param->commentid)) { if ($comment = $DB->get_record('comments', array('id'=>$comment_param->commentid))) { if ($comment->commentarea != 'glossary_entry') { throw new comment_exception('invalidcommentarea'); } if ($comment->contextid != $comment_param->context->id) { throw new comment_exception('invalidcontext'); } if ($comment->itemid != $comment_param->itemid) { throw new comment_exception('invalidcommentitemid'); } } else { throw new comment_exception('invalidcommentid'); } } return true; } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block */ function glossary_page_type_list($pagetype, $parentcontext, $currentcontext) { $module_pagetype = array( 'mod-glossary-*'=>get_string('page-mod-glossary-x', 'glossary'), 'mod-glossary-view'=>get_string('page-mod-glossary-view', 'glossary'), 'mod-glossary-edit'=>get_string('page-mod-glossary-edit', 'glossary')); return $module_pagetype; } /** * Return list of all glossary tabs. * @throws coding_exception * @return array */ function glossary_get_all_tabs() { return array ( GLOSSARY_AUTHOR => get_string('authorview', 'glossary'), GLOSSARY_CATEGORY => get_string('categoryview', 'glossary'), GLOSSARY_DATE => get_string('dateview', 'glossary') ); } /** * Set 'showtabs' value for glossary formats * @param stdClass $glossaryformat record from 'glossary_formats' table */ function glossary_set_default_visible_tabs($glossaryformat) { global $DB; switch($glossaryformat->name) { case GLOSSARY_CONTINUOUS: $showtabs = 'standard,category,date'; break; case GLOSSARY_DICTIONARY: $showtabs = 'standard'; // Special code for upgraded instances that already had categories set up // in this format - enable "category" tab. // In new instances only 'standard' tab will be visible. if ($DB->record_exists_sql("SELECT 1 FROM {glossary} g, {glossary_categories} gc WHERE g.id = gc.glossaryid and g.displayformat = ?", array(GLOSSARY_DICTIONARY))) { $showtabs .= ',category'; } break; case GLOSSARY_FULLWITHOUTAUTHOR: $showtabs = 'standard,category,date'; break; default: $showtabs = 'standard,category,date,author'; break; } $DB->set_field('glossary_formats', 'showtabs', $showtabs, array('id' => $glossaryformat->id)); $glossaryformat->showtabs = $showtabs; } /** * Convert 'showtabs' string to array * @param stdClass $displayformat record from 'glossary_formats' table * @return array */ function glossary_get_visible_tabs($displayformat) { if (empty($displayformat->showtabs)) { glossary_set_default_visible_tabs($displayformat); } $showtabs = preg_split('/,/', $displayformat->showtabs, -1, PREG_SPLIT_NO_EMPTY); return $showtabs; } /** * Notify that the glossary was viewed. * * This will trigger relevant events and activity completion. * * @param stdClass $glossary The glossary object. * @param stdClass $course The course object. * @param stdClass $cm The course module object. * @param stdClass $context The context object. * @param string $mode The mode in which the glossary was viewed. * @since Moodle 3.1 */ function glossary_view($glossary, $course, $cm, $context, $mode) { // Completion trigger. $completion = new completion_info($course); $completion->set_module_viewed($cm); // Trigger the course module viewed event. $event = \mod_glossary\event\course_module_viewed::create(array( 'objectid' => $glossary->id, 'context' => $context, 'other' => array('mode' => $mode) )); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('glossary', $glossary); $event->trigger(); } /** * Notify that a glossary entry was viewed. * * This will trigger relevant events. * * @param stdClass $entry The entry object. * @param stdClass $context The context object. * @since Moodle 3.1 */ function glossary_entry_view($entry, $context) { // Trigger the entry viewed event. $event = \mod_glossary\event\entry_viewed::create(array( 'objectid' => $entry->id, 'context' => $context )); $event->add_record_snapshot('glossary_entries', $entry); $event->trigger(); } /** * Returns the entries of a glossary by letter. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param string $letter The letter, or ALL, or SPECIAL. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes the non-approved entries created by * the current user. When true, also includes the ones that the user has the permission to approve. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_entries_by_letter($glossary, $context, $letter, $from, $limit, $options = array()) { $qb = new mod_glossary_entry_query_builder($glossary); if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ALL); } else { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_SELF); } $qb->add_field('*', 'entries'); $qb->join_user(); $qb->add_user_fields(); $qb->order_by('concept', 'entries'); $qb->order_by('id', 'entries', 'ASC'); // Sort on ID to avoid random ordering when entries share an ordering value. // Fetching the entries. Those are all entries. $entries = $qb->get_records(); // Now sorting out the array. $filteredentries = []; if ($letter != 'ALL' && $letter != 'SPECIAL' && core_text::strlen($letter)) { // Build a new array with the filtered entries. foreach ($entries as $key => $entry) { if (strtoupper(substr(format_string($entry->concept), 0, 1)) === strtoupper($letter)) { // Add it when starting with the correct letter. $filteredentries[$key] = $entry; } } $entries = $filteredentries; } if ($letter == 'SPECIAL') { // Build a new array with the filtered entries. foreach ($entries as $key => $entry) { if (!ctype_alpha(substr(format_string($entry->concept), 0, 1))) { // Add it when starting with a non-letter character. $filteredentries[$key] = $entry; } } $entries = $filteredentries; } if ($letter == 'ALL') { // No filtering needed. $filteredentries = $entries; } // Now sort the array in regard to the current language. usort($filteredentries, function($a, $b) { return format_string($a->concept) <=> format_string($b->concept); }); // Size of the overall array. $count = count($entries); // Now applying limit. if (isset($limit)) { if (isset($from)) { $entries = array_slice($filteredentries, $from, $limit); } else { $entries = array_slice($filteredentries); } } else { $entries = $filteredentries; } return array($entries, $count); } /** * Returns the entries of a glossary by date. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param string $order The mode of ordering: CREATION or UPDATE. * @param string $sort The direction of the ordering: ASC or DESC. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes the non-approved entries created by * the current user. When true, also includes the ones that the user has the permission to approve. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_entries_by_date($glossary, $context, $order, $sort, $from, $limit, $options = array()) { $qb = new mod_glossary_entry_query_builder($glossary); if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ALL); } else { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_SELF); } $qb->add_field('*', 'entries'); $qb->join_user(); $qb->add_user_fields(); $qb->limit($from, $limit); if ($order == 'CREATION') { $qb->order_by('timecreated', 'entries', $sort); } else { $qb->order_by('timemodified', 'entries', $sort); } $qb->order_by('id', 'entries', $sort); // Sort on ID to avoid random ordering when entries share an ordering value. // Fetching the entries. $count = $qb->count_records(); $entries = $qb->get_records(); return array($entries, $count); } /** * Returns the entries of a glossary by category. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param int $categoryid The category ID, or GLOSSARY_SHOW_* constant. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes the non-approved entries created by * the current user. When true, also includes the ones that the user has the permission to approve. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_entries_by_category($glossary, $context, $categoryid, $from, $limit, $options = array()) { $qb = new mod_glossary_entry_query_builder($glossary); if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ALL); } else { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_SELF); } $qb->join_category($categoryid); $qb->join_user(); // The first field must be the relationship ID when viewing all categories. if ($categoryid === GLOSSARY_SHOW_ALL_CATEGORIES) { $qb->add_field('id', 'entries_categories', 'cid'); } $qb->add_field('*', 'entries'); $qb->add_field('categoryid', 'entries_categories'); $qb->add_user_fields(); if ($categoryid === GLOSSARY_SHOW_ALL_CATEGORIES) { $qb->add_field('name', 'categories', 'categoryname'); $qb->order_by('name', 'categories'); } else if ($categoryid === GLOSSARY_SHOW_NOT_CATEGORISED) { $qb->where('categoryid', 'entries_categories', null); } // Sort on additional fields to avoid random ordering when entries share an ordering value. $qb->order_by('concept', 'entries'); $qb->order_by('id', 'entries', 'ASC'); $qb->limit($from, $limit); // Fetching the entries. $count = $qb->count_records(); $entries = $qb->get_records(); return array($entries, $count); } /** * Returns the entries of a glossary by author. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param string $letter The letter * @param string $field The field to search: FIRSTNAME or LASTNAME. * @param string $sort The sorting: ASC or DESC. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes the non-approved entries created by * the current user. When true, also includes the ones that the user has the permission to approve. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_entries_by_author($glossary, $context, $letter, $field, $sort, $from, $limit, $options = array()) { $firstnamefirst = $field === 'FIRSTNAME'; $qb = new mod_glossary_entry_query_builder($glossary); if ($letter != 'ALL' && $letter != 'SPECIAL' && core_text::strlen($letter)) { $qb->filter_by_author_letter($letter, $firstnamefirst); } if ($letter == 'SPECIAL') { $qb->filter_by_author_non_letter($firstnamefirst); } if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ALL); } else { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_SELF); } $qb->add_field('*', 'entries'); $qb->join_user(true); $qb->add_user_fields(); $qb->order_by_author($firstnamefirst, $sort); $qb->order_by('concept', 'entries'); $qb->order_by('id', 'entries', 'ASC'); // Sort on ID to avoid random ordering when entries share an ordering value. $qb->limit($from, $limit); // Fetching the entries. $count = $qb->count_records(); $entries = $qb->get_records(); return array($entries, $count); } /** * Returns the entries of a glossary by category. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param int $authorid The author ID. * @param string $order The mode of ordering: CONCEPT, CREATION or UPDATE. * @param string $sort The direction of the ordering: ASC or DESC. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes the non-approved entries created by * the current user. When true, also includes the ones that the user has the permission to approve. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_entries_by_author_id($glossary, $context, $authorid, $order, $sort, $from, $limit, $options = array()) { $qb = new mod_glossary_entry_query_builder($glossary); if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ALL); } else { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_SELF); } $qb->add_field('*', 'entries'); $qb->join_user(true); $qb->add_user_fields(); $qb->where('id', 'user', $authorid); if ($order == 'CREATION') { $qb->order_by('timecreated', 'entries', $sort); } else if ($order == 'UPDATE') { $qb->order_by('timemodified', 'entries', $sort); } else { $qb->order_by('concept', 'entries', $sort); } $qb->order_by('id', 'entries', $sort); // Sort on ID to avoid random ordering when entries share an ordering value. $qb->limit($from, $limit); // Fetching the entries. $count = $qb->count_records(); $entries = $qb->get_records(); return array($entries, $count); } /** * Returns the authors in a glossary * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param int $limit Number of records to fetch. * @param int $from Fetch records from. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes self even if all of their entries require approval. * When true, also includes authors only having entries pending approval. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_authors($glossary, $context, $limit, $from, $options = array()) { global $DB, $USER; $params = array(); $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $approvedsql = '(ge.approved <> 0 OR ge.userid = :myid)'; $params['myid'] = $USER->id; if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $approvedsql = '1 = 1'; } $sqlselectcount = "SELECT COUNT(DISTINCT(u.id))"; $sqlselect = "SELECT DISTINCT(u.id) AS userId, $userfields"; $sql = " FROM {user} u JOIN {glossary_entries} ge ON ge.userid = u.id AND (ge.glossaryid = :gid1 OR ge.sourceglossaryid = :gid2) AND $approvedsql"; $ordersql = " ORDER BY u.lastname, u.firstname"; $params['gid1'] = $glossary->id; $params['gid2'] = $glossary->id; $count = $DB->count_records_sql($sqlselectcount . $sql, $params); $users = $DB->get_recordset_sql($sqlselect . $sql . $ordersql, $params, $from, $limit); return array($users, $count); } /** * Returns the categories of a glossary. * * @param object $glossary The glossary. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_categories($glossary, $from, $limit) { global $DB; $count = $DB->count_records('glossary_categories', array('glossaryid' => $glossary->id)); $categories = $DB->get_recordset('glossary_categories', array('glossaryid' => $glossary->id), 'name ASC', '*', $from, $limit); return array($categories, $count); } /** * Get the SQL where clause for searching terms. * * Note that this does not handle invalid or too short terms. * * @param array $terms Array of terms. * @param bool $fullsearch Whether or not full search should be enabled. * @param int $glossaryid The ID of a glossary to reduce the search results. * @return array The first element being the where clause, the second array of parameters. * @since Moodle 3.1 */ function glossary_get_search_terms_sql(array $terms, $fullsearch = true, $glossaryid = null) { global $DB; static $i = 0; if ($DB->sql_regex_supported()) { $regexp = $DB->sql_regex(true); $notregexp = $DB->sql_regex(false); } $params = array(); $conditions = array(); foreach ($terms as $searchterm) { $i++; $not = false; // Initially we aren't going to perform NOT LIKE searches, only MSSQL // will use it to simulate the "-" operator with LIKE clause. if (empty($fullsearch)) { // With fullsearch disabled, look only within concepts and aliases. $concat = $DB->sql_concat('ge.concept', "' '", "COALESCE(al.alias, :emptychar{$i})"); } else { // With fullsearch enabled, look also within definitions. $concat = $DB->sql_concat('ge.concept', "' '", 'ge.definition', "' '", "COALESCE(al.alias, :emptychar{$i})"); } $params['emptychar' . $i] = ''; // Under MSSQL, trim the + and - operators and perform simpler LIKE (or NOT LIKE) queries. if (!$DB->sql_regex_supported()) { if (substr($searchterm, 0, 1) === '-') { $not = true; } $searchterm = trim($searchterm, '+-'); } if (substr($searchterm, 0, 1) === '+') { $searchterm = trim($searchterm, '+-'); $conditions[] = "$concat $regexp :searchterm{$i}"; $params['searchterm' . $i] = '(^|[^a-zA-Z0-9])' . preg_quote($searchterm, '|') . '([^a-zA-Z0-9]|$)'; } else if (substr($searchterm, 0, 1) === "-") { $searchterm = trim($searchterm, '+-'); $conditions[] = "$concat $notregexp :searchterm{$i}"; $params['searchterm' . $i] = '(^|[^a-zA-Z0-9])' . preg_quote($searchterm, '|') . '([^a-zA-Z0-9]|$)'; } else { $conditions[] = $DB->sql_like($concat, ":searchterm{$i}", false, true, $not); $params['searchterm' . $i] = '%' . $DB->sql_like_escape($searchterm) . '%'; } } // Reduce the search results by restricting it to one glossary. if (isset($glossaryid)) { $conditions[] = 'ge.glossaryid = :glossaryid'; $params['glossaryid'] = $glossaryid; } // When there are no conditions we add a negative one to ensure that we don't return anything. if (empty($conditions)) { $conditions[] = '1 = 2'; } $where = implode(' AND ', $conditions); return array($where, $params); } /** * Returns the entries of a glossary by search. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param string $query The search query. * @param bool $fullsearch Whether or not full search is required. * @param string $order The mode of ordering: CONCEPT, CREATION or UPDATE. * @param string $sort The direction of the ordering: ASC or DESC. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes the non-approved entries created by * the current user. When true, also includes the ones that the user has the permission to approve. * @return array The first element being the recordset (taking into account the limit), the second the number of entries the overall * array has. * @since Moodle 3.1 */ function glossary_get_entries_by_search($glossary, $context, $query, $fullsearch, $order, $sort, $from, $limit, $options = array()) { global $DB, $USER; // Clean terms. $terms = explode(' ', $query); foreach ($terms as $key => $term) { if (strlen(trim($term, '+-')) < 1) { unset($terms[$key]); } } list($searchcond, $params) = glossary_get_search_terms_sql($terms, $fullsearch, $glossary->id); $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, 'userdata', 'userdataid', false)->selects; // Need one inner view here to avoid distinct + text. $sqlwrapheader = 'SELECT ge.*, ge.concept AS glossarypivot, ' . $userfields . ' FROM {glossary_entries} ge LEFT JOIN {user} u ON u.id = ge.userid JOIN ( '; $sqlwrapfooter = ' ) gei ON (ge.id = gei.id)'; $sqlselect = "SELECT DISTINCT ge.id"; $sqlfrom = "FROM {glossary_entries} ge LEFT JOIN {glossary_alias} al ON al.entryid = ge.id"; if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $approvedsql = ''; } else { $approvedsql = 'AND (ge.approved <> 0 OR ge.userid = :myid)'; $params['myid'] = $USER->id; } if ($order == 'CREATION') { $sqlorderby = "ORDER BY ge.timecreated $sort"; } else if ($order == 'UPDATE') { $sqlorderby = "ORDER BY ge.timemodified $sort"; } else { $sqlorderby = "ORDER BY ge.concept $sort"; } $sqlorderby .= " , ge.id ASC"; // Sort on ID to avoid random ordering when entries share an ordering value. $sqlwhere = "WHERE ($searchcond) $approvedsql"; // Fetching the entries. $count = $DB->count_records_sql("SELECT COUNT(DISTINCT(ge.id)) $sqlfrom $sqlwhere", $params); $query = "$sqlwrapheader $sqlselect $sqlfrom $sqlwhere $sqlwrapfooter $sqlorderby"; $entries = $DB->get_records_sql($query, $params, $from, $limit); return array($entries, $count); } /** * Returns the entries of a glossary by term. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param string $term The term we are searching for, a concept or alias. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @param array $options Accepts: * - (bool) includenotapproved. When false, includes the non-approved entries created by * the current user. When true, also includes the ones that the user has the permission to approve. * @return array The first element being the recordset, the second the number of entries. * @since Moodle 3.1 */ function glossary_get_entries_by_term($glossary, $context, $term, $from, $limit, $options = array()) { // Build the query. $qb = new mod_glossary_entry_query_builder($glossary); if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ALL); } else { $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_SELF); } $qb->add_field('*', 'entries'); $qb->add_field('alias', 'alias'); $qb->join_alias(); $qb->join_user(); $qb->add_user_fields(); $qb->filter_by_term($term); $qb->order_by('concept', 'entries'); $qb->order_by('id', 'entries'); // Sort on ID to avoid random ordering when entries share an ordering value. // Fetching the entries. Those are all entries. $entries = $qb->get_records(); // Now sorting out the array. $filteredentries = []; // Now sorting out the array. foreach ($entries as $key => $entry) { if (strtoupper(format_string($entry->concept)) === strtoupper($term)) { // Add it when matching concept or alias. $filteredentries[$key] = $entry; } if ((isset($entry->alias)) && (strtoupper(format_string($entry->alias)) === strtoupper($term))) { // Add it when matching concept or alias. $filteredentries[$key] = $entry; } } $entries = $filteredentries; // Check whether concept or alias match the term. // Now sort the array in regard to the current language. usort($filteredentries, function($a, $b) { return format_string($a->concept) <=> format_string($b->concept); }); // Size of the overall array. $count = count($entries); // Now applying limit. if (isset($limit)) { if (isset($from)) { $entries = array_slice($filteredentries, $from, $limit); } else { $entries = array_slice($filteredentries); } } else { $entries = $filteredentries; } return array($entries, $count); } /** * Returns the entries to be approved. * * @param object $glossary The glossary. * @param context $context The context of the glossary. * @param string $letter The letter, or ALL, or SPECIAL. * @param string $order The mode of ordering: CONCEPT, CREATION or UPDATE. * @param string $sort The direction of the ordering: ASC or DESC. * @param int $from Fetch records from. * @param int $limit Number of records to fetch. * @return array The first element being the recordset, the second the number of entries. * @since Moodle 3.1 */ function glossary_get_entries_to_approve($glossary, $context, $letter, $order, $sort, $from, $limit) { $qb = new mod_glossary_entry_query_builder($glossary); $qb->add_field('*', 'entries'); $qb->join_user(); $qb->add_user_fields(); $qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ONLY); // Fetching the entries. Those are all non approved entries. $entries = $qb->get_records(); // Size of the overall array. $count = count($entries); // If a some filter is set, restrict by that filter. $filteredentries = []; if ($letter != 'ALL' && $letter != 'SPECIAL' && core_text::strlen($letter)) { // Build a new array with the filtered entries. foreach ($entries as $key => $entry) { if (strtoupper(substr(format_string($entry->concept), 0, 1)) === strtoupper($letter)) { // Add it when starting with the correct letter. $filteredentries[$key] = $entry; } } } else if ($letter == 'SPECIAL') { // Build a new array with the filtered entries. foreach ($entries as $key => $entry) { if (!ctype_alpha(substr(format_string($entry->concept), 0, 1))) { // Add it when starting with a non-letter character. $filteredentries[$key] = $entry; } } } else { // No filtering needed (This means CONCEPT). $filteredentries = $entries; } // Now sort the array in regard to the current language. if ($order == 'CREATION') { if (strcasecmp($sort, 'DESC') === 0) { usort($filteredentries, function($a, $b) { return $b->timecreated <=> $a->timecreated; }); } else { usort($filteredentries, function($a, $b) { return $a->timecreated <=> $b->timecreated; }); } } else if ($order == 'UPDATE') { if (strcasecmp($sort, 'DESC') === 0) { usort($filteredentries, function($a, $b) { return $b->timemodified <=> $a->timemodified; }); } else { usort($filteredentries, function($a, $b) { return $a->timemodified <=> $b->timemodified; }); } } else { // This means CONCEPT. if (strcasecmp($sort, 'DESC') === 0) { usort($filteredentries, function($a, $b) { return format_string($b->concept) <=> format_string($a->concept); }); } else { usort($filteredentries, function($a, $b) { return format_string($a->concept) <=> format_string($b->concept); }); } } // Now applying limit. if (isset($limit)) { $count = count($filteredentries); if (isset($from)) { $filteredentries = array_slice($filteredentries, $from, $limit); } else { $filteredentries = array_slice($filteredentries, 0, $limit); } } return [$filteredentries, $count]; } /** * Fetch an entry. * * @param int $id The entry ID. * @return object|false The entry, or false when not found. * @since Moodle 3.1 */ function glossary_get_entry_by_id($id) { // Build the query. $qb = new mod_glossary_entry_query_builder(); $qb->add_field('*', 'entries'); $qb->join_user(); $qb->add_user_fields(); $qb->where('id', 'entries', $id); // Fetching the entries. $entries = $qb->get_records(); if (empty($entries)) { return false; } return array_pop($entries); } /** * Checks if the current user can see the glossary entry. * * @since Moodle 3.1 * @param stdClass $entry * @param cm_info $cminfo * @return bool */ function glossary_can_view_entry($entry, $cminfo) { global $USER; $cm = $cminfo->get_course_module_record(); $context = \context_module::instance($cm->id); // Recheck uservisible although it should have already been checked in core_search. if ($cminfo->uservisible === false) { return false; } // Check approval. if (empty($entry->approved) && $entry->userid != $USER->id && !has_capability('mod/glossary:approve', $context)) { return false; } return true; } /** * Check if a concept exists in a glossary. * * @param stdClass $glossary glossary object * @param string $concept the concept to check * @return bool true if exists * @since Moodle 3.2 */ function glossary_concept_exists($glossary, $concept) { global $DB; return $DB->record_exists_select('glossary_entries', 'glossaryid = :glossaryid AND LOWER(concept) = :concept', array( 'glossaryid' => $glossary->id, 'concept' => core_text::strtolower($concept) ) ); } /** * Return the editor and attachment options when editing a glossary entry * * @param stdClass $course course object * @param stdClass $context context object * @param stdClass $entry entry object * @return array array containing the editor and attachment options * @since Moodle 3.2 */ function glossary_get_editor_and_attachment_options($course, $context, $entry) { $maxfiles = 99; // TODO: add some setting. $maxbytes = $course->maxbytes; // TODO: add some setting. $definitionoptions = array('trusttext' => true, 'maxfiles' => $maxfiles, 'maxbytes' => $maxbytes, 'context' => $context, 'subdirs' => file_area_contains_subdirs($context, 'mod_glossary', 'entry', $entry->id)); $attachmentoptions = array('subdirs' => false, 'maxfiles' => $maxfiles, 'maxbytes' => $maxbytes); return array($definitionoptions, $attachmentoptions); } /** * Creates or updates a glossary entry * * @param stdClass $entry entry data * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $glossary glossary object * @param stdClass $context context object * @return stdClass the complete new or updated entry * @since Moodle 3.2 */ function glossary_edit_entry($entry, $course, $cm, $glossary, $context) { global $DB, $USER; list($definitionoptions, $attachmentoptions) = glossary_get_editor_and_attachment_options($course, $context, $entry); $timenow = time(); $categories = empty($entry->categories) ? array() : $entry->categories; unset($entry->categories); $aliases = trim($entry->aliases); unset($entry->aliases); if (empty($entry->id)) { $entry->glossaryid = $glossary->id; $entry->timecreated = $timenow; $entry->userid = $USER->id; $entry->timecreated = $timenow; $entry->sourceglossaryid = 0; $entry->teacherentry = has_capability('mod/glossary:manageentries', $context); $isnewentry = true; } else { $isnewentry = false; } $entry->concept = trim($entry->concept); $entry->definition = ''; // Updated later. $entry->definitionformat = FORMAT_HTML; // Updated later. $entry->definitiontrust = 0; // Updated later. $entry->timemodified = $timenow; $entry->approved = 0; $entry->usedynalink = isset($entry->usedynalink) ? $entry->usedynalink : 0; $entry->casesensitive = isset($entry->casesensitive) ? $entry->casesensitive : 0; $entry->fullmatch = isset($entry->fullmatch) ? $entry->fullmatch : 0; if ($glossary->defaultapproval or has_capability('mod/glossary:approve', $context)) { $entry->approved = 1; } if ($isnewentry) { // Add new entry. $entry->id = $DB->insert_record('glossary_entries', $entry); } else { // Update existing entry. $DB->update_record('glossary_entries', $entry); } // Save and relink embedded images and save attachments. if (!empty($entry->definition_editor)) { $entry = file_postupdate_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_glossary', 'entry', $entry->id); } if (!empty($entry->attachment_filemanager)) { $entry = file_postupdate_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_glossary', 'attachment', $entry->id); } // Store the updated value values. $DB->update_record('glossary_entries', $entry); // Refetch complete entry. $entry = $DB->get_record('glossary_entries', array('id' => $entry->id)); // Update entry categories. $DB->delete_records('glossary_entries_categories', array('entryid' => $entry->id)); // TODO: this deletes cats from both both main and secondary glossary :-(. if (!empty($categories) and array_search(0, $categories) === false) { foreach ($categories as $catid) { $newcategory = new stdClass(); $newcategory->entryid = $entry->id; $newcategory->categoryid = $catid; $DB->insert_record('glossary_entries_categories', $newcategory, false); } } // Update aliases. $DB->delete_records('glossary_alias', array('entryid' => $entry->id)); if ($aliases !== '') { $aliases = explode("\n", $aliases); foreach ($aliases as $alias) { $alias = trim($alias); if ($alias !== '') { $newalias = new stdClass(); $newalias->entryid = $entry->id; $newalias->alias = $alias; $DB->insert_record('glossary_alias', $newalias, false); } } } // Trigger event and update completion (if entry was created). $eventparams = array( 'context' => $context, 'objectid' => $entry->id, 'other' => array('concept' => $entry->concept) ); if ($isnewentry) { $event = \mod_glossary\event\entry_created::create($eventparams); } else { $event = \mod_glossary\event\entry_updated::create($eventparams); } $event->add_record_snapshot('glossary_entries', $entry); $event->trigger(); if ($isnewentry) { // Update completion state. $completion = new completion_info($course); if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $glossary->completionentries && $entry->approved) { $completion->update_state($cm, COMPLETION_COMPLETE); } } // Reset caches. if ($isnewentry) { if ($entry->usedynalink and $entry->approved) { \mod_glossary\local\concept_cache::reset_glossary($glossary); } } else { // So many things may affect the linking, let's just purge the cache always on edit. \mod_glossary\local\concept_cache::reset_glossary($glossary); } return $entry; } /** * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not * @since Moodle 3.2 */ function glossary_check_updates_since(cm_info $cm, $from, $filter = array()) { global $DB; $updates = course_check_module_updates_since($cm, $from, array('attachment', 'entry'), $filter); $updates->entries = (object) array('updated' => false); $select = 'glossaryid = :id AND (timecreated > :since1 OR timemodified > :since2)'; $params = array('id' => $cm->instance, 'since1' => $from, 'since2' => $from); if (!has_capability('mod/glossary:approve', $cm->context)) { $select .= ' AND approved = 1'; } $entries = $DB->get_records_select('glossary_entries', $select, $params, '', 'id'); if (!empty($entries)) { $updates->entries->updated = true; $updates->entries->itemids = array_keys($entries); } return $updates; } /** * Get icon mapping for font-awesome. * * @return array */ function mod_glossary_get_fontawesome_icon_map() { return [ 'mod_glossary:asc' => 'fa-sort-down', 'mod_glossary:desc' => 'fa-sort-up', 'mod_glossary:export' => 'fa-arrow-turn-up', 'mod_glossary:minus' => 'fa-minus', ]; } /** * This function receives a calendar event and returns the action associated with it, or null if there is none. * * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event * is not displayed on the block. * * @param calendar_event $event * @param \core_calendar\action_factory $factory * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return \core_calendar\local\event\entities\action_interface|null */ function mod_glossary_core_calendar_provide_event_action(calendar_event $event, \core_calendar\action_factory $factory, int $userid = 0) { global $USER; if (!$userid) { $userid = $USER->id; } $cm = get_fast_modinfo($event->courseid, $userid)->instances['glossary'][$event->instance]; if (!$cm->uservisible) { // The module is not visible to the user for any reason. return null; } $completion = new \completion_info($cm->get_course()); $completiondata = $completion->get_data($cm, false, $userid); if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { return null; } return $factory->create_instance( get_string('view'), new \moodle_url('/mod/glossary/view.php', ['id' => $cm->id]), 1, true ); } /** * Add a get_coursemodule_info function in case any glossary type wants to add 'extra' information * for the course (see resource). * * Given a course_module object, this function returns any "extra" information that may be needed * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. * * @param stdClass $coursemodule The coursemodule object (record). * @return cached_cm_info An object on information that the courses * will know about (most noticeably, an icon). */ function glossary_get_coursemodule_info($coursemodule) { global $DB; $dbparams = ['id' => $coursemodule->instance]; $fields = 'id, name, intro, introformat, completionentries'; if (!$glossary = $DB->get_record('glossary', $dbparams, $fields)) { return false; } $result = new cached_cm_info(); $result->name = $glossary->name; if ($coursemodule->showdescription) { // Convert intro to html. Do not filter cached version, filters run at display time. $result->content = format_module_intro('glossary', $glossary, $coursemodule->id, false); } // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { $result->customdata['customcompletionrules']['completionentries'] = $glossary->completionentries; } return $result; } /** * Callback which returns human-readable strings describing the active completion custom rules for the module instance. * * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] * @return array $descriptions the array of descriptions for the custom rules. */ function mod_glossary_get_completion_active_rule_descriptions($cm) { // Values will be present in cm_info, and we assume these are up to date. if (empty($cm->customdata['customcompletionrules']) || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { return []; } $descriptions = []; foreach ($cm->customdata['customcompletionrules'] as $key => $val) { switch ($key) { case 'completionentries': if (!empty($val)) { $descriptions[] = get_string('completionentriesdesc', 'glossary', $val); } break; default: break; } } return $descriptions; } /** * Checks if the current user can delete the given glossary entry. * * @since Moodle 3.10 * @param stdClass $entry the entry database object * @param stdClass $glossary the glossary database object * @param stdClass $context the glossary context * @param bool $return Whether to return a boolean value or stop the execution (exception) * @return bool if the user can delete the entry * @throws moodle_exception */ function mod_glossary_can_delete_entry($entry, $glossary, $context, $return = true) { global $USER, $CFG; $manageentries = has_capability('mod/glossary:manageentries', $context); if ($manageentries) { // Users with the capability will always be able to delete entries. return true; } if ($entry->userid != $USER->id) { // Guest id is never matched, no need for special check here. if ($return) { return false; } throw new moodle_exception('nopermissiontodelentry'); } $ineditperiod = ((time() - $entry->timecreated < $CFG->maxeditingtime) || $glossary->editalways); if (!$ineditperiod) { if ($return) { return false; } throw new moodle_exception('errdeltimeexpired', 'glossary'); } return true; } /** * Deletes the given entry, this function does not perform capabilities/permission checks. * * @since Moodle 3.10 * @param stdClass $entry the entry database object * @param stdClass $glossary the glossary database object * @param stdClass $cm the glossary course moduule object * @param stdClass $context the glossary context * @param stdClass $course the glossary course * @param string $hook the hook, usually type of filtering, value * @param string $prevmode the previsualisation mode * @throws moodle_exception */ function mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course, $hook = '', $prevmode = '') { global $CFG, $DB; $origentry = fullclone($entry); // If it is an imported entry, just delete the relation. if ($entry->sourceglossaryid) { if (!$newcm = get_coursemodule_from_instance('glossary', $entry->sourceglossaryid)) { throw new \moodle_exception('invalidcoursemodule'); } $newcontext = context_module::instance($newcm->id); $entry->glossaryid = $entry->sourceglossaryid; $entry->sourceglossaryid = 0; $DB->update_record('glossary_entries', $entry); // Move attachments too. $fs = get_file_storage(); if ($oldfiles = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id)) { foreach ($oldfiles as $oldfile) { $filerecord = new stdClass(); $filerecord->contextid = $newcontext->id; $fs->create_file_from_storedfile($filerecord, $oldfile); } $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id); $entry->attachment = '1'; } else { $entry->attachment = '0'; } $DB->update_record('glossary_entries', $entry); } else { $fs = get_file_storage(); $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id); $DB->delete_records("comments", ['itemid' => $entry->id, 'commentarea' => 'glossary_entry', 'contextid' => $context->id]); $DB->delete_records("glossary_alias", ["entryid" => $entry->id]); $DB->delete_records("glossary_entries", ["id" => $entry->id]); // Update completion state. $completion = new completion_info($course); if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $glossary->completionentries) { $completion->update_state($cm, COMPLETION_INCOMPLETE, $entry->userid); } // Delete glossary entry ratings. require_once($CFG->dirroot.'/rating/lib.php'); $delopt = new stdClass; $delopt->contextid = $context->id; $delopt->component = 'mod_glossary'; $delopt->ratingarea = 'entry'; $delopt->itemid = $entry->id; $rm = new rating_manager(); $rm->delete_ratings($delopt); } // Delete cached RSS feeds. if (!empty($CFG->enablerssfeeds)) { require_once($CFG->dirroot . '/mod/glossary/rsslib.php'); glossary_rss_delete_file($glossary); } core_tag_tag::remove_all_item_tags('mod_glossary', 'glossary_entries', $origentry->id); $event = \mod_glossary\event\entry_deleted::create( [ 'context' => $context, 'objectid' => $origentry->id, 'other' => [ 'mode' => $prevmode, 'hook' => $hook, 'concept' => $origentry->concept ] ] ); $event->add_record_snapshot('glossary_entries', $origentry); $event->trigger(); // Reset caches. if ($entry->usedynalink and $entry->approved) { \mod_glossary\local\concept_cache::reset_glossary($glossary); } } /** * Checks if the current user can update the given glossary entry. * * @since Moodle 3.10 * @param stdClass $entry the entry database object * @param stdClass $glossary the glossary database object * @param stdClass $context the glossary context * @param object $cm the course module object (cm record or cm_info instance) * @param bool $return Whether to return a boolean value or stop the execution (exception) * @return bool if the user can update the entry * @throws moodle_exception */ function mod_glossary_can_update_entry(stdClass $entry, stdClass $glossary, stdClass $context, object $cm, bool $return = true): bool { global $USER, $CFG; $ineditperiod = ((time() - $entry->timecreated < $CFG->maxeditingtime) || $glossary->editalways); if (!has_capability('mod/glossary:manageentries', $context) and !($entry->userid == $USER->id and ($ineditperiod and has_capability('mod/glossary:write', $context)))) { if ($USER->id != $entry->userid) { if ($return) { return false; } throw new moodle_exception('errcannoteditothers', 'glossary', "view.php?id=$cm->id&mode=entry&hook=$entry->id"); } else if (!$ineditperiod) { if ($return) { return false; } throw new moodle_exception('erredittimeexpired', 'glossary', "view.php?id=$cm->id&mode=entry&hook=$entry->id"); } } return true; } /** * Prepares an entry for editing, adding aliases and category information. * * @param stdClass $entry the entry being edited * @return stdClass the entry with the additional data */ function mod_glossary_prepare_entry_for_edition(stdClass $entry): stdClass { global $DB; if ($aliases = $DB->get_records_menu("glossary_alias", ["entryid" => $entry->id], '', 'id, alias')) { $entry->aliases = implode("\n", $aliases) . "\n"; } if ($categoriesarr = $DB->get_records_menu("glossary_entries_categories", ['entryid' => $entry->id], '', 'id, categoryid')) { // TODO: this fetches cats from both main and secondary glossary :-( $entry->categories = array_values($categoriesarr); } return $entry; } /** * Checks whether the current user can see ratings for a given itemid. * * @param array $params submitted data * contextid => int contextid [required] * component => The component for this module - should always be mod_glossary [required] * ratingarea => Should always be entry (the only rating area in glossary) [required] * itemid => int the ID of the entry being rated [required] * @return bool */ function mod_glossary_rating_can_see_item_ratings(array $params): bool { global $DB, $USER; if (!isset($params['component']) || $params['component'] != 'mod_glossary') { throw new rating_exception('invalidcomponent'); } if (!isset($params['ratingarea']) || $params['ratingarea'] != 'entry') { throw new rating_exception('invalidratingarea'); } if (!isset($params['itemid'])) { throw new rating_exception('invaliditemid'); } $entry = $DB->get_record('glossary_entries', ['id' => $params['itemid']], '*', MUST_EXIST); $glossary = $DB->get_record('glossary', ['id' => $entry->glossaryid], '*', MUST_EXIST); $cm = get_coursemodule_from_instance('glossary', $glossary->id, $glossary->course, false, MUST_EXIST); $cminfo = get_fast_modinfo($glossary->course)->instances['glossary'][$cm->instance]; if ($cminfo->context->id != $params['contextid']) { throw new rating_exception('invalidcontext'); } $context = context::instance_by_id($params['contextid']); $ratingpermissions = glossary_rating_permissions($context->id, 'mod_glossary', 'entry'); $requiredpermission = ($entry->userid != $USER->id) ? 'viewall' : 'view'; if (!$ratingpermissions[$requiredpermission]) { return false; } if (!glossary_can_view_entry($entry, $cminfo)) { return false; } return true; } version.php000064400000002312152265517410006750 0ustar00. /** * Glossary module version information * * @package mod_glossary * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'mod_glossary'; // Full name of the plugin (used for diagnostics) $plugin->cron = 0; export.php000064400000005714152265517410006615 0ustar00$id)); if ($cat !== 0) { $url->param('cat', $cat); } if ($mode !== '') { $url->param('mode', $mode); } $PAGE->set_url($url); if (! $cm = get_coursemodule_from_id('glossary', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidid', 'glossary'); } require_login($course, false, $cm); $context = context_module::instance($cm->id); require_capability('mod/glossary:export', $context); $strglossaries = get_string("modulenameplural", "glossary"); $strglossary = get_string("modulename", "glossary"); $strallcategories = get_string("allcategories", "glossary"); $straddentry = get_string("addentry", "glossary"); $strnoentries = get_string("noentries", "glossary"); $strsearchindefinition = get_string("searchindefinition", "glossary"); $strsearch = get_string("search"); $strexportfile = get_string("exportfile", "glossary"); $strexportentries = get_string('exportentriestoxml', 'glossary'); $PAGE->set_url('/mod/glossary/export.php', array('id'=>$cm->id)); $PAGE->navbar->add($strexportentries); $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); echo $OUTPUT->header(); $backlink = html_writer::link(new moodle_url('view.php', ['id' => $cm->id]), get_string('back'), ['class' => 'btn btn-secondary']); echo html_writer::tag('div', $backlink, ['class' => 'tertiary-navigation']); echo $OUTPUT->heading($strexportentries); echo $OUTPUT->box_start('glossarydisplay generalbox'); $exporturl = moodle_url::make_pluginfile_url($context->id, 'mod_glossary', 'export', 0, "/$cat/", 'export.xml', true); ?>
enableportfolios) && $DB->count_records('glossary_entries', array('glossaryid' => $glossary->id))) { require_once($CFG->libdir . '/portfoliolib.php'); $button = new portfolio_add_button(); $button->set_callback_options('glossary_full_portfolio_caller', array('id' => $cm->id), 'mod_glossary'); $button->render(); } echo $OUTPUT->box_end(); echo $OUTPUT->footer(); ?> editcategories.html000064400000006515152265517410010444 0ustar00
" />
" onclick="javascript:history.go(-1);" />
editcategories.php000064400000025775152265517410010300 0ustar00$id)); if ($usedynalink !== 0) { $url->param('usedynalink', $usedynalink); } if ($confirm !== 0) { $url->param('confirm', $confirm); } if ($name !== 'name') { $url->param('name', $name); } if ($action !== 'action') { $url->param('action', $action); } if ($hook !== 'hook') { $url->param('hook', $hook); } if ($mode !== 'mode') { $url->param('mode', $mode); } $PAGE->set_url($url); if (! $cm = get_coursemodule_from_id('glossary', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidcoursemodule'); } if ($hook > 0) { if ($category = $DB->get_record("glossary_categories", array("id"=>$hook))) { //Check it belongs to the same glossary if ($category->glossaryid != $glossary->id) { throw new \moodle_exception('invalidid', 'glossary'); } } else { throw new \moodle_exception('invalidcategoryid'); } } require_login($course, false, $cm); $context = context_module::instance($cm->id); require_capability('mod/glossary:managecategories', $context); $strglossaries = get_string("modulenameplural", "glossary"); $strglossary = get_string("modulename", "glossary"); $PAGE->navbar->add(get_string("categories","glossary"), new moodle_url('/mod/glossary/editcategories.php', array('id' => $cm->id,'mode' => 'cat'))); if (!empty($action)) { $navaction = get_string(core_text::strtolower($action."category"), 'glossary'); $PAGE->navbar->add($navaction); } $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); // Prepare format_string/text options $fmtoptions = array( 'context' => $context); if (right_to_left()) { // RTL table alignment support $rightalignment = 'left'; $leftalignment = 'right'; } else { $rightalignment = 'right'; $leftalignment = 'left'; } if ( $hook >0 ) { if ( $action == "edit" ) { if ( $confirm ) { require_sesskey(); $action = ""; $cat = new stdClass(); $cat->id = $hook; $cat->name = $name; $cat->usedynalink = $usedynalink; $DB->update_record("glossary_categories", $cat); $event = \mod_glossary\event\category_updated::create(array( 'context' => $context, 'objectid' => $hook )); $cat->glossaryid = $glossary->id; $event->add_record_snapshot('glossary_categories', $cat); $event->add_record_snapshot('glossary', $glossary); $event->trigger(); // Reset caches. \mod_glossary\local\concept_cache::reset_glossary($glossary); } else { echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($glossary->name), 2); echo $OUTPUT->heading(format_string(get_string("editcategory", "glossary")), 3); $name = format_string($category->name, true, ['context' => $context]); $usedynalink = $category->usedynalink; require "editcategories.html"; echo $OUTPUT->footer(); die; } } elseif ( $action == "delete" ) { if ( $confirm ) { require_sesskey(); $DB->delete_records("glossary_entries_categories", array("categoryid"=>$hook)); $DB->delete_records("glossary_categories", array("id"=>$hook)); $event = \mod_glossary\event\category_deleted::create(array( 'context' => $context, 'objectid' => $hook )); $event->add_record_snapshot('glossary_categories', $category); $event->add_record_snapshot('glossary', $glossary); $event->trigger(); // Reset caches. \mod_glossary\local\concept_cache::reset_glossary($glossary); redirect("editcategories.php?id=$cm->id", get_string("categorydeleted", "glossary"), 2); } else { echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($glossary->name), 2); echo $OUTPUT->heading(format_string(get_string("deletecategory", "glossary")), 3); echo $OUTPUT->box_start('generalbox boxaligncenter errorboxcontent boxwidthnarrow'); echo "
".format_string($category->name, true, $fmtoptions)."
"; $num_entries = $DB->count_records("glossary_entries_categories", array("categoryid"=>$category->id)); if ( $num_entries ) { print_string("deletingnoneemptycategory","glossary"); } echo "

"; print_string("areyousuredelete","glossary"); echo "

"; ?>
" />
$id); echo $OUTPUT->single_button(new moodle_url("editcategories.php", $options), get_string("no")); echo "
"; echo "
"; echo $OUTPUT->box_end(); } } } elseif ( $action == "add" ) { if ( $confirm ) { require_sesskey(); $dupcategory = $DB->get_records_sql("SELECT * FROM {glossary_categories} WHERE ".$DB->sql_like('name','?', false)." AND glossaryid=?", array($name, $glossary->id)); if ( $dupcategory ) { redirect("editcategories.php?id=$cm->id&action=add&name=$name", get_string("duplicatecategory", "glossary"), 2); } else { $action = ""; $cat = new stdClass(); $cat->name = $name; $cat->usedynalink = $usedynalink; $cat->glossaryid = $glossary->id; $cat->id = $DB->insert_record("glossary_categories", $cat); $event = \mod_glossary\event\category_created::create(array( 'context' => $context, 'objectid' => $cat->id )); $event->add_record_snapshot('glossary_categories', $cat); $event->add_record_snapshot('glossary', $glossary); $event->trigger(); // Reset caches. \mod_glossary\local\concept_cache::reset_glossary($glossary); } } else { echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($glossary->name), 2); echo "

" . get_string("addcategory", "glossary"). "

"; $name=""; require "editcategories.html"; } } if ( $action ) { echo $OUTPUT->footer(); die; } echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($glossary->name), 2); ?>
get_records("glossary_categories", array("glossaryid"=>$glossary->id), "name ASC"); if ( $categories ) { echo ''; foreach ($categories as $category) { $num_entries = $DB->count_records("glossary_entries_categories", array("categoryid"=>$category->id)); ?> '; } ?>
".format_string($category->name, true, $fmtoptions)." ($num_entries " . get_string("entries","glossary") . ")"; ?> id&action=delete&mode=cat&hook=$category->id\">" . $OUTPUT->pix_icon('t/delete', get_string('delete')). " "; echo "id&action=edit&mode=cat&hook=$category->id\">" . $OUTPUT->pix_icon('t/edit', get_string('edit')). " "; ?>
id; $options['action'] = "add"; echo ""; echo "
"; echo $OUTPUT->single_button(new moodle_url("editcategories.php", $options), get_string("addcategory", "glossary")); echo ""; unset($options['action']); $options['mode'] = 'cat'; $options['hook'] = $hook; echo $OUTPUT->single_button(new moodle_url("view.php", $options), get_string("back","glossary")); echo "
"; ?>
footer(); tabs.php000064400000004765152265517410006232 0ustar00. /** * prints the tabbed bar * * @author Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_glossary * @copyright 2021 Peter Dias */ defined('MOODLE_INTERNAL') || die; echo html_writer::start_div('entrybox'); if (!isset($category)) { $category = ""; } switch ($tab) { case GLOSSARY_CATEGORY_VIEW: glossary_print_categories_menu($cm, $glossary, $hook, $category); break; case GLOSSARY_APPROVAL_VIEW: glossary_print_approval_menu($cm, $glossary, $mode, $hook, $sortkey, $sortorder); break; case GLOSSARY_AUTHOR_VIEW: $search = ""; glossary_print_author_menu($cm, $glossary, "author", $hook, $sortkey, $sortorder, 'print'); break; case GLOSSARY_IMPORT_VIEW: $search = ""; $l = ""; glossary_print_import_menu($cm, $glossary, 'import', $hook, $sortkey, $sortorder); break; case GLOSSARY_EXPORT_VIEW: $search = ""; $l = ""; glossary_print_export_menu($cm, $glossary, 'export', $hook, $sortkey, $sortorder); break; case GLOSSARY_DATE_VIEW: if (!$sortkey) { $sortkey = 'UPDATE'; } if (!$sortorder) { $sortorder = 'desc'; } glossary_print_alphabet_menu($cm, $glossary, "date", $hook, $sortkey, $sortorder); break; case GLOSSARY_STANDARD_VIEW: default: glossary_print_alphabet_menu($cm, $glossary, "letter", $hook, $sortkey, $sortorder); if ($mode == 'search' and $hook) { echo html_writer::tag('div', "$strsearch: $hook"); } break; } echo html_writer::empty_tag('hr'); echo html_writer::end_div(); classes/external.php000064400000203161152265517410010547 0ustar00. /** * Glossary module external API. * * @package mod_glossary * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.1 */ use core_course\external\helper_for_get_mods_by_courses; use core_external\external_api; use core_external\external_files; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/glossary/lib.php'); /** * Glossary module external functions. * * @package mod_glossary * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.1 */ class mod_glossary_external extends external_api { /** * Get the browse modes from the display format. * * This returns some of the terms that can be used when reporting a glossary being viewed. * * @param string $format The display format of the glossary. * @return array Containing some of all of the following: letter, cat, date, author. */ protected static function get_browse_modes_from_display_format($format) { global $DB; $formats = array(); $dp = $DB->get_record('glossary_formats', array('name' => $format), '*', IGNORE_MISSING); if ($dp) { $formats = glossary_get_visible_tabs($dp); } // Always add 'letter'. $modes = array('letter'); if (in_array('category', $formats)) { $modes[] = 'cat'; } if (in_array('date', $formats)) { $modes[] = 'date'; } if (in_array('author', $formats)) { $modes[] = 'author'; } return $modes; } /** * Get the return value of an entry. * * @param bool $includecat Whether the definition should include category info. * @return external_definition */ protected static function get_entry_return_structure($includecat = false) { $params = array( 'id' => new external_value(PARAM_INT, 'The entry ID'), 'glossaryid' => new external_value(PARAM_INT, 'The glossary ID'), 'userid' => new external_value(PARAM_INT, 'Author ID'), 'userfullname' => new external_value(PARAM_NOTAGS, 'Author full name'), 'userpictureurl' => new external_value(PARAM_URL, 'Author picture'), 'concept' => new external_value(PARAM_RAW, 'The concept'), 'definition' => new external_value(PARAM_RAW, 'The definition'), 'definitionformat' => new external_format_value('definition'), 'definitiontrust' => new external_value(PARAM_BOOL, 'The definition trust flag'), 'definitioninlinefiles' => new external_files('entry definition inline files', VALUE_OPTIONAL), 'attachment' => new external_value(PARAM_BOOL, 'Whether or not the entry has attachments'), 'attachments' => new external_files('attachments', VALUE_OPTIONAL), 'timecreated' => new external_value(PARAM_INT, 'Time created'), 'timemodified' => new external_value(PARAM_INT, 'Time modified'), 'teacherentry' => new external_value(PARAM_BOOL, 'The entry was created by a teacher, or equivalent.'), 'sourceglossaryid' => new external_value(PARAM_INT, 'The source glossary ID'), 'usedynalink' => new external_value(PARAM_BOOL, 'Whether the concept should be automatically linked'), 'casesensitive' => new external_value(PARAM_BOOL, 'When true, the matching is case sensitive'), 'fullmatch' => new external_value(PARAM_BOOL, 'When true, the matching is done on full words only'), 'approved' => new external_value(PARAM_BOOL, 'Whether the entry was approved'), 'tags' => new external_multiple_structure( \core_tag\external\tag_item_exporter::get_read_structure(), 'Tags', VALUE_OPTIONAL ), ); if ($includecat) { $params['categoryid'] = new external_value(PARAM_INT, 'The category ID. This may be' . ' \''. GLOSSARY_SHOW_NOT_CATEGORISED . '\' when the entry is not categorised', VALUE_DEFAULT, GLOSSARY_SHOW_NOT_CATEGORISED); $params['categoryname'] = new external_value(PARAM_RAW, 'The category name. May be empty when the entry is' . ' not categorised, or the request was limited to one category.', VALUE_DEFAULT, ''); } return new external_single_structure($params); } /** * Fill in an entry object. * * This adds additional required fields for the external function to return. * * @param stdClass $entry The entry. * @param context $context The context the entry belongs to. * @return void */ protected static function fill_entry_details($entry, $context) { global $PAGE; $canviewfullnames = has_capability('moodle/site:viewfullnames', $context); // Format concept and definition. $entry->concept = \core_external\util::format_string($entry->concept, $context); [$entry->definition, $entry->definitionformat] = \core_external\util::format_text( $entry->definition, $entry->definitionformat, $context, 'mod_glossary', 'entry', $entry->id, ['trusted' => true], ); // Author details. $user = mod_glossary_entry_query_builder::get_user_from_record($entry); $userpicture = new user_picture($user); $userpicture->size = 1; $entry->userfullname = fullname($user, $canviewfullnames); $entry->userpictureurl = $userpicture->get_url($PAGE)->out(false); // Fetch attachments. $entry->attachment = !empty($entry->attachment) ? 1 : 0; $entry->attachments = array(); if ($entry->attachment) { $entry->attachments = util::get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id); } $definitioninlinefiles = util::get_area_files($context->id, 'mod_glossary', 'entry', $entry->id); if (!empty($definitioninlinefiles)) { $entry->definitioninlinefiles = $definitioninlinefiles; } $entry->tags = \core_tag\external\util::get_item_tags('mod_glossary', 'glossary_entries', $entry->id); } /** * Validate a glossary via ID. * * @param int $id The glossary ID. * @return array Contains glossary, context, course and cm. */ public static function validate_glossary($id) { global $DB; $glossary = $DB->get_record('glossary', array('id' => $id), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($glossary, 'glossary'); $context = context_module::instance($cm->id); self::validate_context($context); return array($glossary, $context, $course, $cm); } /** * Describes the parameters for get_glossaries_by_courses. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_glossaries_by_courses_parameters() { return new external_function_parameters ( array( 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'course id'), 'Array of course IDs', VALUE_DEFAULT, array() ), ) ); } /** * Returns a list of glossaries in a provided list of courses. * * If no list is provided all glossaries that the user can view will be returned. * * @param array $courseids the course IDs. * @return array of glossaries * @since Moodle 3.1 */ public static function get_glossaries_by_courses($courseids = array()) { global $CFG; $params = self::validate_parameters(self::get_glossaries_by_courses_parameters(), array('courseids' => $courseids)); $warnings = array(); $courses = array(); $courseids = $params['courseids']; if (empty($courseids)) { $courses = enrol_get_my_courses(); $courseids = array_keys($courses); } // Array to store the glossaries to return. $glossaries = array(); $modes = array(); // Ensure there are courseids to loop through. if (!empty($courseids)) { list($courses, $warnings) = util::validate_courses($courseids, $courses); // Get the glossaries in these courses, this function checks users visibility permissions. $glossaries = get_all_instances_in_courses('glossary', $courses); foreach ($glossaries as $glossary) { $context = context_module::instance($glossary->coursemodule); helper_for_get_mods_by_courses::format_name_and_intro($glossary, 'mod_glossary'); // Make sure we have a number of entries per page. if (!$glossary->entbypage) { $glossary->entbypage = $CFG->glossary_entbypage; } // Add the list of browsing modes. if (!isset($modes[$glossary->displayformat])) { $modes[$glossary->displayformat] = self::get_browse_modes_from_display_format($glossary->displayformat); } $glossary->browsemodes = $modes[$glossary->displayformat]; $glossary->canaddentry = has_capability('mod/glossary:write', $context) ? 1 : 0; } } $result = array(); $result['glossaries'] = $glossaries; $result['warnings'] = $warnings; return $result; } /** * Describes the get_glossaries_by_courses return value. * * @return external_single_structure * @since Moodle 3.1 */ public static function get_glossaries_by_courses_returns() { return new external_single_structure(array( 'glossaries' => new external_multiple_structure( new external_single_structure(array_merge( helper_for_get_mods_by_courses::standard_coursemodule_elements_returns(), [ 'allowduplicatedentries' => new external_value(PARAM_INT, 'If enabled, multiple entries can have the' . ' same concept name'), 'displayformat' => new external_value(PARAM_TEXT, 'Display format type'), 'mainglossary' => new external_value(PARAM_INT, 'If enabled this glossary is a main glossary.'), 'showspecial' => new external_value(PARAM_INT, 'If enabled, participants can browse the glossary by' . ' special characters, such as @ and #'), 'showalphabet' => new external_value(PARAM_INT, 'If enabled, participants can browse the glossary by' . ' letters of the alphabet'), 'showall' => new external_value(PARAM_INT, 'If enabled, participants can browse all entries at once'), 'allowcomments' => new external_value(PARAM_INT, 'If enabled, all participants with permission to' . ' create comments will be able to add comments to glossary entries'), 'allowprintview' => new external_value(PARAM_INT, 'If enabled, students are provided with a link to a' . ' printer-friendly version of the glossary. The link is always available to teachers'), 'usedynalink' => new external_value(PARAM_INT, 'If site-wide glossary auto-linking has been enabled' . ' by an administrator and this checkbox is ticked, the entry will be automatically linked' . ' wherever the concept words and phrases appear throughout the rest of the course.'), 'defaultapproval' => new external_value(PARAM_INT, 'If set to no, entries require approving by a' . ' teacher before they are viewable by everyone.'), 'approvaldisplayformat' => new external_value(PARAM_TEXT, 'When approving glossary items you may wish' . ' to use a different display format'), 'globalglossary' => new external_value(PARAM_INT, ''), 'entbypage' => new external_value(PARAM_INT, 'Entries shown per page'), 'editalways' => new external_value(PARAM_INT, 'Always allow editing'), 'rsstype' => new external_value(PARAM_INT, 'To enable the RSS feed for this activity, select either' . ' concepts with author or concepts without author to be included in the feed'), 'rssarticles' => new external_value(PARAM_INT, 'This setting specifies the number of glossary entry' . ' concepts to include in the RSS feed. Between 5 and 20 generally acceptable'), 'assessed' => new external_value(PARAM_INT, 'Aggregate type'), 'assesstimestart' => new external_value(PARAM_INT, 'Restrict rating to items created after this'), 'assesstimefinish' => new external_value(PARAM_INT, 'Restrict rating to items created before this'), 'scale' => new external_value(PARAM_INT, 'Scale ID'), 'timecreated' => new external_value(PARAM_INT, 'Time created'), 'timemodified' => new external_value(PARAM_INT, 'Time modified'), 'completionentries' => new external_value(PARAM_INT, 'Number of entries to complete'), 'browsemodes' => new external_multiple_structure( new external_value(PARAM_ALPHA, 'Modes of browsing allowed') ), 'canaddentry' => new external_value(PARAM_INT, 'Whether the user can add a new entry', VALUE_OPTIONAL), ] ), 'Glossaries') ), 'warnings' => new external_warnings()) ); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function view_glossary_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary instance ID'), 'mode' => new external_value(PARAM_ALPHA, 'The mode in which the glossary is viewed'), )); } /** * Notify that the course module was viewed. * * @param int $id The glossary instance ID. * @param string $mode The view mode. * @return array of warnings and status result * @since Moodle 3.1 * @throws moodle_exception */ public static function view_glossary($id, $mode) { $params = self::validate_parameters(self::view_glossary_parameters(), array( 'id' => $id, 'mode' => $mode )); $id = $params['id']; $mode = $params['mode']; $warnings = array(); // Get and validate the glossary. list($glossary, $context, $course, $cm) = self::validate_glossary($id); // Trigger module viewed event. glossary_view($glossary, $course, $cm, $context, $mode); return array( 'status' => true, 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function view_glossary_returns() { return new external_single_structure(array( 'status' => new external_value(PARAM_BOOL, 'True on success'), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function view_entry_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), )); } /** * Notify that the entry was viewed. * * @param int $id The entry ID. * @return array of warnings and status result * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function view_entry($id) { global $DB, $USER; $params = self::validate_parameters(self::view_entry_parameters(), array('id' => $id)); $id = $params['id']; $warnings = array(); // Get and validate the glossary. $entry = $DB->get_record('glossary_entries', array('id' => $id), '*', MUST_EXIST); list($glossary, $context, $course, $cm) = self::validate_glossary($entry->glossaryid); if (!glossary_can_view_entry($entry, $cm)) { throw new invalid_parameter_exception('invalidentry'); } // Trigger view. glossary_entry_view($entry, $context); return array( 'status' => true, 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function view_entry_returns() { return new external_single_structure(array( 'status' => new external_value(PARAM_BOOL, 'True on success'), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_by_letter_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'letter' => new external_value(PARAM_ALPHA, 'A letter, or either keywords: \'ALL\' or \'SPECIAL\'.'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes the non-approved entries created by' . ' the user. When true, also includes the ones that the user has the permission to approve.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries by letter. * * @param int $id The glossary ID. * @param string $letter A letter, or a special keyword. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function get_entries_by_letter($id, $letter, $from, $limit, $options) { $params = self::validate_parameters(self::get_entries_by_letter_parameters(), array( 'id' => $id, 'letter' => $letter, 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $letter = $params['letter']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Validate the mode. $modes = self::get_browse_modes_from_display_format($glossary->displayformat); if (!in_array('letter', $modes)) { throw new invalid_parameter_exception('invalidbrowsemode'); } $entries = array(); list($records, $count) = glossary_get_entries_by_letter($glossary, $context, $letter, $from, $limit, $options); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_by_letter_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure() ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_by_date_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'order' => new external_value(PARAM_ALPHA, 'Order the records by: \'CREATION\' or \'UPDATE\'.', VALUE_DEFAULT, 'UPDATE'), 'sort' => new external_value(PARAM_ALPHA, 'The direction of the order: \'ASC\' or \'DESC\'', VALUE_DEFAULT, 'DESC'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes the non-approved entries created by' . ' the user. When true, also includes the ones that the user has the permission to approve.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries by date. * * @param int $id The glossary ID. * @param string $order The way to order the records. * @param string $sort The direction of the order. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function get_entries_by_date($id, $order, $sort, $from, $limit, $options) { $params = self::validate_parameters(self::get_entries_by_date_parameters(), array( 'id' => $id, 'order' => core_text::strtoupper($order), 'sort' => core_text::strtoupper($sort), 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $order = $params['order']; $sort = $params['sort']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); if (!in_array($order, array('CREATION', 'UPDATE'))) { throw new invalid_parameter_exception('invalidorder'); } else if (!in_array($sort, array('ASC', 'DESC'))) { throw new invalid_parameter_exception('invalidsort'); } // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Validate the mode. $modes = self::get_browse_modes_from_display_format($glossary->displayformat); if (!in_array('date', $modes)) { throw new invalid_parameter_exception('invalidbrowsemode'); } $entries = array(); list($records, $count) = glossary_get_entries_by_date($glossary, $context, $order, $sort, $from, $limit, $options); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_by_date_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure() ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_categories_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'The glossary ID'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20) )); } /** * Get the categories of a glossary. * * @param int $id The glossary ID. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @return array Containing count, categories and warnings. * @since Moodle 3.1 * @throws moodle_exception */ public static function get_categories($id, $from, $limit) { $params = self::validate_parameters(self::get_categories_parameters(), array( 'id' => $id, 'from' => $from, 'limit' => $limit )); $id = $params['id']; $from = $params['from']; $limit = $params['limit']; $warnings = array(); // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Fetch the categories. $categories = array(); list($records, $count) = glossary_get_categories($glossary, $from, $limit); foreach ($records as $category) { $category->name = \core_external\util::format_string($category->name, $context); $categories[] = $category; } return array( 'count' => $count, 'categories' => $categories, 'warnings' => array(), ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_categories_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records.'), 'categories' => new external_multiple_structure( new external_single_structure(array( 'id' => new external_value(PARAM_INT, 'The category ID'), 'glossaryid' => new external_value(PARAM_INT, 'The glossary ID'), 'name' => new external_value(PARAM_RAW, 'The name of the category'), 'usedynalink' => new external_value(PARAM_BOOL, 'Whether the category is automatically linked'), )) ), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_by_category_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'The glossary ID.'), 'categoryid' => new external_value(PARAM_INT, 'The category ID. Use \'' . GLOSSARY_SHOW_ALL_CATEGORIES . '\' for all' . ' categories, or \'' . GLOSSARY_SHOW_NOT_CATEGORISED . '\' for uncategorised entries.'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes the non-approved entries created by' . ' the user. When true, also includes the ones that the user has the permission to approve.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries by category. * * @param int $id The glossary ID. * @param int $categoryid The category ID. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function get_entries_by_category($id, $categoryid, $from, $limit, $options) { global $DB; $params = self::validate_parameters(self::get_entries_by_category_parameters(), array( 'id' => $id, 'categoryid' => $categoryid, 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $categoryid = $params['categoryid']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Validate the mode. $modes = self::get_browse_modes_from_display_format($glossary->displayformat); if (!in_array('cat', $modes)) { throw new invalid_parameter_exception('invalidbrowsemode'); } // Validate the category. if (in_array($categoryid, array(GLOSSARY_SHOW_ALL_CATEGORIES, GLOSSARY_SHOW_NOT_CATEGORISED))) { // All good. } else if (!$DB->record_exists('glossary_categories', array('id' => $categoryid, 'glossaryid' => $id))) { throw new invalid_parameter_exception('invalidcategory'); } // Fetching the entries. $entries = array(); list($records, $count) = glossary_get_entries_by_category($glossary, $context, $categoryid, $from, $limit, $options); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); if ($record->categoryid === null) { $record->categoryid = GLOSSARY_SHOW_NOT_CATEGORISED; } if (isset($record->categoryname)) { $record->categoryname = \core_external\util::format_string($record->categoryname, $context); } $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_by_category_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure(true) ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_authors_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes self even if all of their entries' . ' require approval. When true, also includes authors only having entries pending approval.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Get the authors of a glossary. * * @param int $id The glossary ID. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, authors and warnings. * @since Moodle 3.1 * @throws moodle_exception */ public static function get_authors($id, $from, $limit, $options) { global $PAGE; $params = self::validate_parameters(self::get_authors_parameters(), array( 'id' => $id, 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Fetching the entries. list($users, $count) = glossary_get_authors($glossary, $context, $limit, $from, $options); $canviewfullnames = has_capability('moodle/site:viewfullnames', $context); foreach ($users as $user) { $userpicture = new user_picture($user); $userpicture->size = 1; $author = new stdClass(); $author->id = $user->id; $author->fullname = fullname($user, $canviewfullnames); $author->pictureurl = $userpicture->get_url($PAGE)->out(false); $authors[] = $author; } $users->close(); return array( 'count' => $count, 'authors' => $authors, 'warnings' => array(), ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_authors_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records.'), 'authors' => new external_multiple_structure( new external_single_structure(array( 'id' => new external_value(PARAM_INT, 'The user ID'), 'fullname' => new external_value(PARAM_NOTAGS, 'The fullname'), 'pictureurl' => new external_value(PARAM_URL, 'The picture URL'), )) ), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_by_author_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'letter' => new external_value(PARAM_ALPHA, 'First letter of firstname or lastname, or either keywords:' . ' \'ALL\' or \'SPECIAL\'.'), 'field' => new external_value(PARAM_ALPHA, 'Search and order using: \'FIRSTNAME\' or \'LASTNAME\'', VALUE_DEFAULT, 'LASTNAME'), 'sort' => new external_value(PARAM_ALPHA, 'The direction of the order: \'ASC\' or \'DESC\'', VALUE_DEFAULT, 'ASC'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes the non-approved entries created by' . ' the user. When true, also includes the ones that the user has the permission to approve.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries by author. * * @param int $id The glossary ID. * @param string $letter A letter, or a special keyword. * @param string $field The field to search from. * @param string $sort The direction of the order. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function get_entries_by_author($id, $letter, $field, $sort, $from, $limit, $options) { $params = self::validate_parameters(self::get_entries_by_author_parameters(), array( 'id' => $id, 'letter' => $letter, 'field' => core_text::strtoupper($field), 'sort' => core_text::strtoupper($sort), 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $letter = $params['letter']; $field = $params['field']; $sort = $params['sort']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); if (!in_array($field, array('FIRSTNAME', 'LASTNAME'))) { throw new invalid_parameter_exception('invalidfield'); } else if (!in_array($sort, array('ASC', 'DESC'))) { throw new invalid_parameter_exception('invalidsort'); } // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Validate the mode. $modes = self::get_browse_modes_from_display_format($glossary->displayformat); if (!in_array('author', $modes)) { throw new invalid_parameter_exception('invalidbrowsemode'); } // Fetching the entries. $entries = array(); list($records, $count) = glossary_get_entries_by_author($glossary, $context, $letter, $field, $sort, $from, $limit, $options); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_by_author_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure() ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_by_author_id_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'authorid' => new external_value(PARAM_INT, 'The author ID'), 'order' => new external_value(PARAM_ALPHA, 'Order by: \'CONCEPT\', \'CREATION\' or \'UPDATE\'', VALUE_DEFAULT, 'CONCEPT'), 'sort' => new external_value(PARAM_ALPHA, 'The direction of the order: \'ASC\' or \'DESC\'', VALUE_DEFAULT, 'ASC'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes the non-approved entries created by' . ' the user. When true, also includes the ones that the user has the permission to approve.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries by author. * * @param int $id The glossary ID. * @param int $authorid The author ID. * @param string $order The way to order the results. * @param string $sort The direction of the order. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function get_entries_by_author_id($id, $authorid, $order, $sort, $from, $limit, $options) { $params = self::validate_parameters(self::get_entries_by_author_id_parameters(), array( 'id' => $id, 'authorid' => $authorid, 'order' => core_text::strtoupper($order), 'sort' => core_text::strtoupper($sort), 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $authorid = $params['authorid']; $order = $params['order']; $sort = $params['sort']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); if (!in_array($order, array('CONCEPT', 'CREATION', 'UPDATE'))) { throw new invalid_parameter_exception('invalidorder'); } else if (!in_array($sort, array('ASC', 'DESC'))) { throw new invalid_parameter_exception('invalidsort'); } // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Validate the mode. $modes = self::get_browse_modes_from_display_format($glossary->displayformat); if (!in_array('author', $modes)) { throw new invalid_parameter_exception('invalidbrowsemode'); } // Fetching the entries. $entries = array(); list($records, $count) = glossary_get_entries_by_author_id($glossary, $context, $authorid, $order, $sort, $from, $limit, $options); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_by_author_id_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure() ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_by_search_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'query' => new external_value(PARAM_NOTAGS, 'The query string'), 'fullsearch' => new external_value(PARAM_BOOL, 'The query', VALUE_DEFAULT, 1), 'order' => new external_value(PARAM_ALPHA, 'Order by: \'CONCEPT\', \'CREATION\' or \'UPDATE\'', VALUE_DEFAULT, 'CONCEPT'), 'sort' => new external_value(PARAM_ALPHA, 'The direction of the order: \'ASC\' or \'DESC\'', VALUE_DEFAULT, 'ASC'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes the non-approved entries created by' . ' the user. When true, also includes the ones that the user has the permission to approve.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries using the search. * * @param int $id The glossary ID. * @param string $query The search query. * @param bool $fullsearch Whether or not full search is required. * @param string $order The way to order the results. * @param string $sort The direction of the order. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function get_entries_by_search($id, $query, $fullsearch, $order, $sort, $from, $limit, $options) { $params = self::validate_parameters(self::get_entries_by_search_parameters(), array( 'id' => $id, 'query' => $query, 'fullsearch' => $fullsearch, 'order' => core_text::strtoupper($order), 'sort' => core_text::strtoupper($sort), 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $query = $params['query']; $fullsearch = $params['fullsearch']; $order = $params['order']; $sort = $params['sort']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); if (!in_array($order, array('CONCEPT', 'CREATION', 'UPDATE'))) { throw new invalid_parameter_exception('invalidorder'); } else if (!in_array($sort, array('ASC', 'DESC'))) { throw new invalid_parameter_exception('invalidsort'); } // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Fetching the entries. $entries = array(); list($records, $count) = glossary_get_entries_by_search($glossary, $context, $query, $fullsearch, $order, $sort, $from, $limit, $options); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_by_search_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure() ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_by_term_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'term' => new external_value(PARAM_NOTAGS, 'The entry concept, or alias'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array( 'includenotapproved' => new external_value(PARAM_BOOL, 'When false, includes the non-approved entries created by' . ' the user. When true, also includes the ones that the user has the permission to approve.', VALUE_DEFAULT, 0) ), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries using a term matching the concept or alias. * * @param int $id The glossary ID. * @param string $term The term. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @param array $options Array of options. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception */ public static function get_entries_by_term($id, $term, $from, $limit, $options) { $params = self::validate_parameters(self::get_entries_by_term_parameters(), array( 'id' => $id, 'term' => $term, 'from' => $from, 'limit' => $limit, 'options' => $options, )); $id = $params['id']; $term = $params['term']; $from = $params['from']; $limit = $params['limit']; $options = $params['options']; $warnings = array(); // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Fetching the entries. $entries = array(); list($records, $count) = glossary_get_entries_by_term($glossary, $context, $term, $from, $limit, $options); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_by_term_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure() ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entries_to_approve_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), 'letter' => new external_value(PARAM_ALPHA, 'A letter, or either keywords: \'ALL\' or \'SPECIAL\'.'), 'order' => new external_value(PARAM_ALPHA, 'Order by: \'CONCEPT\', \'CREATION\' or \'UPDATE\'', VALUE_DEFAULT, 'CONCEPT'), 'sort' => new external_value(PARAM_ALPHA, 'The direction of the order: \'ASC\' or \'DESC\'', VALUE_DEFAULT, 'ASC'), 'from' => new external_value(PARAM_INT, 'Start returning records from here', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'Number of records to return', VALUE_DEFAULT, 20), 'options' => new external_single_structure(array(), 'An array of options', VALUE_DEFAULT, array()) )); } /** * Browse a glossary entries using a term matching the concept or alias. * * @param int $id The glossary ID. * @param string $letter A letter, or a special keyword. * @param string $order The way to order the records. * @param string $sort The direction of the order. * @param int $from Start returning records from here. * @param int $limit Number of records to return. * @return array Containing count, entries and warnings. * @since Moodle 3.1 * @throws moodle_exception */ public static function get_entries_to_approve($id, $letter, $order, $sort, $from, $limit) { $params = self::validate_parameters(self::get_entries_to_approve_parameters(), array( 'id' => $id, 'letter' => $letter, 'order' => $order, 'sort' => $sort, 'from' => $from, 'limit' => $limit )); $id = $params['id']; $letter = $params['letter']; $order = $params['order']; $sort = $params['sort']; $from = $params['from']; $limit = $params['limit']; $warnings = array(); // Get and validate the glossary. list($glossary, $context) = self::validate_glossary($id); // Check the permissions. require_capability('mod/glossary:approve', $context); // Fetching the entries. $entries = array(); list($records, $count) = glossary_get_entries_to_approve($glossary, $context, $letter, $order, $sort, $from, $limit); foreach ($records as $key => $record) { self::fill_entry_details($record, $context); $entries[] = $record; } return array( 'count' => $count, 'entries' => $entries, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', $entries), 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entries_to_approve_returns() { return new external_single_structure(array( 'count' => new external_value(PARAM_INT, 'The total number of records matching the request.'), 'entries' => new external_multiple_structure( self::get_entry_return_structure() ), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_entry_by_id_parameters() { return new external_function_parameters(array( 'id' => new external_value(PARAM_INT, 'Glossary entry ID'), )); } /** * Get an entry. * * @param int $id The entry ID. * @return array Containing entry and warnings. * @since Moodle 3.1 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function get_entry_by_id($id) { global $DB, $USER; $params = self::validate_parameters(self::get_entry_by_id_parameters(), array('id' => $id)); $id = $params['id']; $warnings = array(); // Get and validate the glossary. $entry = $DB->get_record('glossary_entries', array('id' => $id), '*', MUST_EXIST); list($glossary, $context, $course, $cm) = self::validate_glossary($entry->glossaryid); if (empty($entry->approved) && $entry->userid != $USER->id && !has_capability('mod/glossary:approve', $context)) { throw new invalid_parameter_exception('invalidentry'); } $entry = glossary_get_entry_by_id($id); self::fill_entry_details($entry, $context); // Permissions (for entry edition). $permissions = [ 'candelete' => mod_glossary_can_delete_entry($entry, $glossary, $context), 'canupdate' => mod_glossary_can_update_entry($entry, $glossary, $context, $cm), ]; return array( 'entry' => $entry, 'ratinginfo' => \core_rating\external\util::get_rating_info($glossary, $context, 'mod_glossary', 'entry', array($entry)), 'permissions' => $permissions, 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_entry_by_id_returns() { return new external_single_structure(array( 'entry' => self::get_entry_return_structure(), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'permissions' => new external_single_structure( [ 'candelete' => new external_value(PARAM_BOOL, 'Whether the user can delete the entry.'), 'canupdate' => new external_value(PARAM_BOOL, 'Whether the user can update the entry.'), ], 'User permissions for the managing the entry.', VALUE_OPTIONAL ), 'warnings' => new external_warnings() )); } /** * Returns the description of the external function parameters. * * @return external_function_parameters * @since Moodle 3.2 */ public static function add_entry_parameters() { return new external_function_parameters(array( 'glossaryid' => new external_value(PARAM_INT, 'Glossary id'), 'concept' => new external_value(PARAM_TEXT, 'Glossary concept'), 'definition' => new external_value(PARAM_RAW, 'Glossary concept definition'), 'definitionformat' => new external_format_value('definition'), 'options' => new external_multiple_structure ( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUM, 'The allowed keys (value format) are: inlineattachmentsid (int); the draft file area id for inline attachments attachmentsid (int); the draft file area id for attachments categories (comma separated int); comma separated category ids aliases (comma separated str); comma separated aliases usedynalink (bool); whether the entry should be automatically linked. casesensitive (bool); whether the entry is case sensitive. fullmatch (bool); whether to match whole words only.'), 'value' => new external_value(PARAM_RAW, 'the value of the option (validated inside the function)') ) ), 'Optional settings', VALUE_DEFAULT, array() ) )); } /** * Add a new entry to a given glossary. * * @param int $glossaryid the glosary id * @param string $concept the glossary concept * @param string $definition the concept definition * @param int $definitionformat the concept definition format * @param array $options additional settings * @return array Containing entry and warnings. * @since Moodle 3.2 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function add_entry($glossaryid, $concept, $definition, $definitionformat, $options = array()) { global $CFG; $params = self::validate_parameters(self::add_entry_parameters(), array( 'glossaryid' => $glossaryid, 'concept' => $concept, 'definition' => $definition, 'definitionformat' => $definitionformat, 'options' => $options, )); $warnings = array(); // Get and validate the glossary. list($glossary, $context, $course, $cm) = self::validate_glossary($params['glossaryid']); require_capability('mod/glossary:write', $context); if (!$glossary->allowduplicatedentries) { if (glossary_concept_exists($glossary, $params['concept'])) { throw new moodle_exception('errconceptalreadyexists', 'glossary'); } } // Prepare the entry object. $entry = new stdClass; $entry->id = null; $entry->aliases = ''; $entry->usedynalink = $CFG->glossary_linkentries; $entry->casesensitive = $CFG->glossary_casesensitive; $entry->fullmatch = $CFG->glossary_fullmatch; $entry->concept = $params['concept']; $entry->definition_editor = array( 'text' => $params['definition'], 'format' => $params['definitionformat'], ); // Options. foreach ($params['options'] as $option) { $name = trim($option['name']); switch ($name) { case 'inlineattachmentsid': $entry->definition_editor['itemid'] = clean_param($option['value'], PARAM_INT); break; case 'attachmentsid': $entry->attachment_filemanager = clean_param($option['value'], PARAM_INT); break; case 'categories': $entry->categories = clean_param($option['value'], PARAM_SEQUENCE); $entry->categories = explode(',', $entry->categories); break; case 'aliases': $entry->aliases = clean_param($option['value'], PARAM_NOTAGS); // Convert to the expected format. $entry->aliases = str_replace(",", "\n", $entry->aliases); break; case 'usedynalink': case 'casesensitive': case 'fullmatch': // Only allow if linking is enabled. if ($glossary->usedynalink) { $entry->{$name} = clean_param($option['value'], PARAM_BOOL); } break; default: throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); } } $entry = glossary_edit_entry($entry, $course, $cm, $glossary, $context); return array( 'entryid' => $entry->id, 'warnings' => $warnings ); } /** * Returns the description of the external function return value. * * @return \core_external\external_description * @since Moodle 3.2 */ public static function add_entry_returns() { return new external_single_structure(array( 'entryid' => new external_value(PARAM_INT, 'New glossary entry ID'), 'warnings' => new external_warnings() )); } } classes/external/update_entry.php000064400000016041152265517410013251 0ustar00. /** * This is the external method for updating a glossary entry. * * @package mod_glossary * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/glossary/lib.php'); use core_external\external_api; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_text; use moodle_exception; /** * This is the external method for updating a glossary entry. * * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class update_entry extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'entryid' => new external_value(PARAM_INT, 'Glossary entry id to update'), 'concept' => new external_value(PARAM_TEXT, 'Glossary concept'), 'definition' => new external_value(PARAM_RAW, 'Glossary concept definition'), 'definitionformat' => new external_format_value('definition'), 'options' => new external_multiple_structure ( new external_single_structure( [ 'name' => new external_value(PARAM_ALPHANUM, 'The allowed keys (value format) are: inlineattachmentsid (int); the draft file area id for inline attachments attachmentsid (int); the draft file area id for attachments categories (comma separated int); comma separated category ids aliases (comma separated str); comma separated aliases usedynalink (bool); whether the entry should be automatically linked. casesensitive (bool); whether the entry is case sensitive. fullmatch (bool); whether to match whole words only.'), 'value' => new external_value(PARAM_RAW, 'the value of the option (validated inside the function)') ] ), 'Optional settings', VALUE_DEFAULT, [] ) ]); } /** * Update the indicated glossary entry. * * @param int $entryid The entry to update * @param string $concept the glossary concept * @param string $definition the concept definition * @param int $definitionformat the concept definition format * @param array $options additional settings * @return array with result and warnings * @throws moodle_exception */ public static function execute(int $entryid, string $concept, string $definition, int $definitionformat, array $options = []): array { global $DB; $params = self::validate_parameters(self::execute_parameters(), compact('entryid', 'concept', 'definition', 'definitionformat', 'options')); $id = $params['entryid']; // Get and validate the glossary entry. $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST); list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid); // Check if the user can update the entry. mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false); // Check for duplicates if the concept changes. if (!$glossary->allowduplicatedentries && core_text::strtolower($entry->concept) != core_text::strtolower(trim($params['concept']))) { if (glossary_concept_exists($glossary, $params['concept'])) { throw new moodle_exception('errconceptalreadyexists', 'glossary'); } } // Prepare the entry object. $entry->aliases = ''; $entry = mod_glossary_prepare_entry_for_edition($entry); $entry->concept = $params['concept']; $entry->definition_editor = [ 'text' => $params['definition'], 'format' => $params['definitionformat'], ]; // Options. foreach ($params['options'] as $option) { $name = trim($option['name']); switch ($name) { case 'inlineattachmentsid': $entry->definition_editor['itemid'] = clean_param($option['value'], PARAM_INT); break; case 'attachmentsid': $entry->attachment_filemanager = clean_param($option['value'], PARAM_INT); break; case 'categories': $entry->categories = clean_param($option['value'], PARAM_SEQUENCE); $entry->categories = explode(',', $entry->categories); break; case 'aliases': $entry->aliases = clean_param($option['value'], PARAM_NOTAGS); // Convert to the expected format. $entry->aliases = str_replace(",", "\n", $entry->aliases); break; case 'usedynalink': case 'casesensitive': case 'fullmatch': // Only allow if linking is enabled. if ($glossary->usedynalink) { $entry->{$name} = clean_param($option['value'], PARAM_BOOL); } break; default: throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); } } $entry = glossary_edit_entry($entry, $course, $cm, $glossary, $context); return [ 'result' => true, 'warnings' => [], ]; } /** * Return. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The update result'), 'warnings' => new external_warnings() ]); } } classes/external/prepare_entry.php000064400000013425152265517410013430 0ustar00. /** * This is the external method for preparing a entry for edition. * * @package mod_glossary * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/glossary/lib.php'); use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; /** * This is the external method for preparing a entry for edition. * * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class prepare_entry extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'entryid' => new external_value(PARAM_INT, 'Glossary entry id to update'), ]); } /** * Prepare for update the indicated entry from the glossary. * * @param int $entryid The entry to update * @return array with result and warnings * @throws moodle_exception */ public static function execute(int $entryid): array { global $DB; $params = self::validate_parameters(self::execute_parameters(), compact('entryid')); $id = $params['entryid']; // Get and validate the glossary. $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST); list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid); // Check permissions. mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false); list($definitionoptions, $attachmentoptions) = glossary_get_editor_and_attachment_options($course, $context, $entry); $entry->aliases = ''; $entry->categories = []; $entry = mod_glossary_prepare_entry_for_edition($entry); $entry = file_prepare_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_glossary', 'entry', $entry->id); $entry = file_prepare_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_glossary', 'attachment', $entry->id); // Just get a structure compatible with external API. array_walk($definitionoptions, function(&$item, $key) use (&$definitionoptions) { if (!is_scalar($item)) { unset($definitionoptions[$key]); return; } $item = ['name' => $key, 'value' => $item]; }); array_walk($attachmentoptions, function(&$item, $key) use (&$attachmentoptions) { if (!is_scalar($item)) { unset($attachmentoptions[$key]); return; } $item = ['name' => $key, 'value' => $item]; }); return [ 'inlineattachmentsid' => $entry->definition_editor['itemid'], 'attachmentsid' => $entry->attachment_filemanager, 'areas' => [ [ 'area' => 'definition', 'options' => $definitionoptions, ], [ 'area' => 'attachment', 'options' => $attachmentoptions, ], ], 'aliases' => explode("\n", trim($entry->aliases)), 'categories' => $entry->categories, ]; } /** * Return. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'inlineattachmentsid' => new external_value(PARAM_INT, 'Draft item id for the text editor.'), 'attachmentsid' => new external_value(PARAM_INT, 'Draft item id for the file manager.'), 'areas' => new external_multiple_structure( new external_single_structure( [ 'area' => new external_value(PARAM_ALPHA, 'File area name.'), 'options' => new external_multiple_structure( new external_single_structure( [ 'name' => new external_value(PARAM_RAW, 'Name of option.'), 'value' => new external_value(PARAM_RAW, 'Value of option.'), ] ), 'Draft file area options.' ) ] ), 'File areas including options' ), 'aliases' => new external_multiple_structure(new external_value(PARAM_RAW, 'Alias name.')), 'categories' => new external_multiple_structure(new external_value(PARAM_INT, 'Category id')), 'warnings' => new external_warnings(), ]); } } classes/external/delete_entry.php000064400000006036152265517410013234 0ustar00. /** * This is the external method for deleting a content. * * @package mod_glossary * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/glossary/lib.php'); use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; /** * This is the external method for deleting a content. * * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_entry extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'entryid' => new external_value(PARAM_INT, 'Glossary entry id to delete'), ]); } /** * Delete the indicated entry from the glossary. * * @param int $entryid The entry to delete * @return array with result and warnings * @throws moodle_exception */ public static function execute(int $entryid): array { global $DB; $params = self::validate_parameters(self::execute_parameters(), compact('entryid')); $id = $params['entryid']; // Get and validate the glossary. $entry = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST); list($glossary, $context, $course, $cm) = \mod_glossary_external::validate_glossary($entry->glossaryid); // Check and delete. mod_glossary_can_delete_entry($entry, $glossary, $context, false); mod_glossary_delete_entry($entry, $glossary, $cm, $context, $course); return [ 'result' => true, 'warnings' => [], ]; } /** * Return. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } } classes/privacy/provider.php000064400000050772152265517410012244 0ustar00. /** * Privacy Subsystem implementation for mod_glossary. * * @package mod_glossary * @copyright 2018 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\deletion_criteria; use core_privacy\local\request\helper; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; defined('MOODLE_INTERNAL') || die(); /** * Implementation of the privacy subsystem plugin provider for the glossary activity module. * * @copyright 2018 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // This plugin stores personal data. \core_privacy\local\metadata\provider, // This plugin is capable of determining which users have data within it. \core_privacy\local\request\core_userlist_provider, // This plugin is a core_user_data_provider. \core_privacy\local\request\plugin\provider { /** * Return the fields which contain personal data. * * @param collection $items a reference to the collection to use to store the metadata. * @return collection the updated collection of metadata items. */ public static function get_metadata(collection $items): collection { $items->add_database_table( 'glossary_entries', [ 'glossaryid' => 'privacy:metadata:glossary_entries:glossaryid', 'userid' => 'privacy:metadata:glossary_entries:userid', 'concept' => 'privacy:metadata:glossary_entries:concept', 'definition' => 'privacy:metadata:glossary_entries:definition', 'attachment' => 'privacy:metadata:glossary_entries:attachment', 'timemodified' => 'privacy:metadata:glossary_entries:timemodified', ], 'privacy:metadata:glossary_entries' ); $items->add_subsystem_link('core_files', [], 'privacy:metadata:core_files'); $items->add_subsystem_link('core_comment', [], 'privacy:metadata:core_comments'); $items->add_subsystem_link('core_tag', [], 'privacy:metadata:core_tag'); $items->add_subsystem_link('core_rating', [], 'privacy:metadata:core_rating'); return $items; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid the userid. * @return contextlist the list of contexts containing user info for the user. */ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); // Glossary entries. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {glossary} g ON g.id = cm.instance JOIN {glossary_entries} ge ON ge.glossaryid = g.id WHERE ge.userid = :glossaryentryuserid"; $params = [ 'contextlevel' => CONTEXT_MODULE, 'modname' => 'glossary', 'commentarea' => 'glossary_entry', 'glossaryentryuserid' => $userid, ]; $contextlist->add_from_sql($sql, $params); // Where the user has rated something. $ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_glossary', 'entry', 'ge.id', $userid, true); $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {glossary} g ON g.id = cm.instance JOIN {glossary_entries} ge ON ge.glossaryid = g.id {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $params = [ 'contextlevel' => CONTEXT_MODULE, 'modname' => 'glossary', ] + $ratingquery->params; $contextlist->add_from_sql($sql, $params); // Comments. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {glossary} g ON g.id = cm.instance JOIN {glossary_entries} ge ON ge.glossaryid = g.id JOIN {comments} com ON com.commentarea =:commentarea AND com.itemid = ge.id WHERE com.userid = :commentuserid"; $params = [ 'contextlevel' => CONTEXT_MODULE, 'modname' => 'glossary', 'commentarea' => 'glossary_entry', 'commentuserid' => $userid, ]; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. * */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!is_a($context, \context_module::class)) { return; } // Find users with glossary entries. $sql = "SELECT ge.userid FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {glossary} g ON g.id = cm.instance JOIN {glossary_entries} ge ON ge.glossaryid = g.id WHERE c.id = :contextid"; $params = [ 'contextid' => $context->id, 'contextlevel' => CONTEXT_MODULE, 'modname' => 'glossary', ]; $userlist->add_from_sql('userid', $sql, $params); // Find users with glossary comments. \core_comment\privacy\provider::get_users_in_context_from_sql($userlist, 'com', 'mod_glossary', 'glossary_entry', $context->id); // Find users with glossary ratings. $sql = "SELECT ge.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {glossary} g ON g.id = cm.instance JOIN {glossary_entries} ge ON ge.glossaryid = g.id WHERE c.id = :contextid"; $params = [ 'contextid' => $context->id, 'contextlevel' => CONTEXT_MODULE, 'modname' => 'glossary', ]; \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_glossary', 'entry', $sql, $params); } /** * Export personal data for the given approved_contextlist. * * User and context information is contained within the contextlist. * * @param approved_contextlist $contextlist a list of contexts approved for export. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $user = $contextlist->get_user(); list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); $ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_glossary', 'entry', 'ge.id', $user->id); $sql = "SELECT ge.id as entryid, cm.id AS cmid, ge.userid, ge.concept, ge.definition, ge.definitionformat, ge.attachment, ge.timecreated, ge.timemodified FROM {glossary_entries} ge JOIN {glossary} g ON ge.glossaryid = g.id JOIN {course_modules} cm ON g.id = cm.instance JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {context} c ON cm.id = c.instanceid AND c.contextlevel = :contextlevel LEFT JOIN {comments} com ON com.itemid = ge.id AND com.commentarea = :commentarea AND com.userid = :commentuserid {$ratingquery->join} WHERE c.id {$contextsql} AND (ge.userid = :userid OR com.id IS NOT NULL OR {$ratingquery->userwhere}) ORDER BY ge.id, cm.id"; $params = [ 'userid' => $user->id, 'modulename' => 'glossary', 'contextlevel' => CONTEXT_MODULE, 'commentarea' => 'glossary_entry', 'commentuserid' => $user->id ] + $contextparams; $params = array_merge($params, $ratingquery->params); $glossaryentries = $DB->get_recordset_sql($sql, $params); // Reference to the glossary activity seen in the last iteration of the loop. By comparing this with the // current record, and because we know the results are ordered, we know when we've moved to the entries // for a new glossary activity and therefore when we can export the complete data for the last activity. $lastcmid = null; $glossarydata = []; foreach ($glossaryentries as $record) { $concept = format_string($record->concept); $path = array_merge([get_string('entries', 'mod_glossary'), $concept . " ({$record->entryid})"]); // If we've moved to a new glossary, then write the last glossary data and reinit the glossary data array. if (!is_null($lastcmid)) { if ($lastcmid != $record->cmid) { if (!empty($glossarydata)) { $context = \context_module::instance($lastcmid); self::export_glossary_data_for_user($glossarydata, $context, [], $user); $glossarydata = []; } } } $lastcmid = $record->cmid; $context = \context_module::instance($lastcmid); // Export files added on the glossary entry definition field. $definition = format_text(writer::with_context($context)->rewrite_pluginfile_urls($path, 'mod_glossary', 'entry', $record->entryid, $record->definition), $record->definitionformat); // Export just the files attached to this user entry. if ($record->userid == $user->id) { // Get all files attached to the glossary attachment. writer::with_context($context)->export_area_files($path, 'mod_glossary', 'entry', $record->entryid); // Get all files attached to the glossary attachment. writer::with_context($context)->export_area_files($path, 'mod_glossary', 'attachment', $record->entryid); } // Export associated comments. \core_comment\privacy\provider::export_comments($context, 'mod_glossary', 'glossary_entry', $record->entryid, $path, $record->userid != $user->id); // Export associated tags. \core_tag\privacy\provider::export_item_tags($user->id, $context, $path, 'mod_glossary', 'glossary_entries', $record->entryid, $record->userid != $user->id); // Export associated ratings. \core_rating\privacy\provider::export_area_ratings($user->id, $context, $path, 'mod_glossary', 'entry', $record->entryid, $record->userid != $user->id); $glossarydata['entries'][] = [ 'concept' => $record->concept, 'definition' => $definition, 'timecreated' => \core_privacy\local\request\transform::datetime($record->timecreated), 'timemodified' => \core_privacy\local\request\transform::datetime($record->timemodified) ]; } $glossaryentries->close(); // The data for the last activity won't have been written yet, so make sure to write it now! if (!empty($glossarydata)) { $context = \context_module::instance($lastcmid); self::export_glossary_data_for_user($glossarydata, $context, [], $user); } } /** * Export the supplied personal data for a single glossary activity, along with any generic data or area files. * * @param array $glossarydata The personal data to export for the glossary. * @param \context_module $context The context of the glossary. * @param array $subcontext The location within the current context that this data belongs. * @param \stdClass $user the user record */ protected static function export_glossary_data_for_user(array $glossarydata, \context_module $context, array $subcontext, \stdClass $user) { // Fetch the generic module data for the glossary. $contextdata = helper::get_context_data($context, $user); // Merge with glossary data and write it. $contextdata = (object)array_merge((array)$contextdata, $glossarydata); writer::with_context($context)->export_data($subcontext, $contextdata); // Write generic module intro files. helper::export_context_files($context, $user); } /** * Delete all data for all users in the specified context. * * @param \context $context the context to delete in. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if ($context->contextlevel != CONTEXT_MODULE) { return; } if (!$cm = get_coursemodule_from_id('glossary', $context->instanceid)) { return; } $instanceid = $cm->instance; $entries = $DB->get_records('glossary_entries', ['glossaryid' => $instanceid], '', 'id'); // Delete related entry aliases. $DB->delete_records_list('glossary_alias', 'entryid', array_keys($entries)); // Delete related entry categories. $DB->delete_records_list('glossary_entries_categories', 'entryid', array_keys($entries)); // Delete entry and attachment files. get_file_storage()->delete_area_files($context->id, 'mod_glossary', 'entry'); get_file_storage()->delete_area_files($context->id, 'mod_glossary', 'attachment'); // Delete related ratings. \core_rating\privacy\provider::delete_ratings($context, 'mod_glossary', 'entry'); // Delete comments. \core_comment\privacy\provider::delete_comments_for_all_users($context, 'mod_glossary', 'glossary_entry'); // Delete tags. \core_tag\privacy\provider::delete_item_tags($context, 'mod_glossary', 'glossary_entries'); // Now delete all user related entries. $DB->delete_records('glossary_entries', ['glossaryid' => $instanceid]); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist a list of contexts approved for deletion. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel == CONTEXT_MODULE) { $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); $entries = $DB->get_records('glossary_entries', ['glossaryid' => $instanceid, 'userid' => $userid], '', 'id'); if (!$entries) { continue; } list($insql, $inparams) = $DB->get_in_or_equal(array_keys($entries), SQL_PARAMS_NAMED); // Delete related entry aliases. $DB->delete_records_list('glossary_alias', 'entryid', array_keys($entries)); // Delete related entry categories. $DB->delete_records_list('glossary_entries_categories', 'entryid', array_keys($entries)); // Delete related entry and attachment files. get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'entry', $insql, $inparams); get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'attachment', $insql, $inparams); // Delete user tags related to this glossary. \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_glossary', 'glossary_entries', $insql, $inparams); // Delete related ratings. \core_rating\privacy\provider::delete_ratings_select($context, 'mod_glossary', 'entry', $insql, $inparams); // Delete comments. \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_glossary', 'glossary_entry'); // Now delete all user related entries. $DB->delete_records('glossary_entries', ['glossaryid' => $instanceid, 'userid' => $userid]); } } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); $userids = $userlist->get_userids(); $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); list($userinsql, $userinparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $glossaryentrieswhere = "glossaryid = :instanceid AND userid {$userinsql}"; $userinstanceparams = $userinparams + ['instanceid' => $instanceid]; $entriesobject = $DB->get_recordset_select('glossary_entries', $glossaryentrieswhere, $userinstanceparams, 'id', 'id'); $entries = []; foreach ($entriesobject as $entry) { $entries[] = $entry->id; } $entriesobject->close(); if (!$entries) { return; } list($insql, $inparams) = $DB->get_in_or_equal($entries, SQL_PARAMS_NAMED); // Delete related entry aliases. $DB->delete_records_list('glossary_alias', 'entryid', $entries); // Delete related entry categories. $DB->delete_records_list('glossary_entries_categories', 'entryid', $entries); // Delete related entry and attachment files. get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'entry', $insql, $inparams); get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'attachment', $insql, $inparams); // Delete user tags related to this glossary. \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_glossary', 'glossary_entries', $insql, $inparams); // Delete related ratings. \core_rating\privacy\provider::delete_ratings_select($context, 'mod_glossary', 'entry', $insql, $inparams); // Delete comments. \core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_glossary', 'glossary_entry'); // Now delete all user related entries. $deletewhere = "glossaryid = :instanceid AND userid {$userinsql}"; $DB->delete_records_select('glossary_entries', $deletewhere, $userinstanceparams); } } classes/navigation/views/secondary.php000064400000002651152265517410014211 0ustar00. namespace mod_glossary\navigation\views; use core\navigation\views\secondary as core_secondary; /** * Class secondary_navigation_view. * * Custom implementation for a plugin. * * @package mod_glossary * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class secondary extends core_secondary { /** * Define a custom secondary nav order/view * * @return array */ protected function get_default_module_mapping(): array { return [ self::TYPE_SETTING => [ 'modedit' => 1, ], self::TYPE_CUSTOM => [ 'pendingapproval' => 2, ], ]; } } classes/local/concept_cache.php000064400000023623152265517410012600 0ustar00. /** * Entry caching for glossary filter. * * @package mod_glossary * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\local; defined('MOODLE_INTERNAL') || die(); /** * Concept caching for glossary filter. * * @package mod_glossary * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class concept_cache { /** * Event observer, do not call directly. * @param \core\event\course_module_updated $event */ public static function cm_updated(\core\event\course_module_updated $event) { if ($event->other['modulename'] !== 'glossary') { return; } // We do not know what changed exactly, so let's reset everything that might be affected. concept_cache::reset_course_muc($event->courseid); concept_cache::reset_global_muc(); } /** * Reset concept related caches. * @param bool $phpunitreset */ public static function reset_caches($phpunitreset = false) { if ($phpunitreset) { return; } $cache = \cache::make('mod_glossary', 'concepts'); $cache->purge(); } /** * Reset the cache for course concepts. * @param int $courseid */ public static function reset_course_muc($courseid) { if (empty($courseid)) { return; } $cache = \cache::make('mod_glossary', 'concepts'); $cache->delete((int)$courseid); } /** * Reset the cache for global concepts. */ public static function reset_global_muc() { $cache = \cache::make('mod_glossary', 'concepts'); $cache->delete(0); } /** * Utility method to purge caches related to given glossary. * @param \stdClass $glossary */ public static function reset_glossary($glossary) { if (!$glossary->usedynalink) { return; } self::reset_course_muc($glossary->course); if ($glossary->globalglossary) { self::reset_global_muc(); } } /** * Fetch concepts for given glossaries. * @param int[] $glossaries * @return array */ protected static function fetch_concepts(array $glossaries) { global $DB; $glossarylist = implode(',', $glossaries); $sql = "SELECT id, glossaryid, concept, casesensitive, 0 AS category, fullmatch FROM {glossary_entries} WHERE glossaryid IN ($glossarylist) AND usedynalink = 1 AND approved = 1 UNION SELECT id, glossaryid, name AS concept, 1 AS casesensitive, 1 AS category, 1 AS fullmatch FROM {glossary_categories} WHERE glossaryid IN ($glossarylist) AND usedynalink = 1 UNION SELECT ge.id, ge.glossaryid, ga.alias AS concept, ge.casesensitive, 0 AS category, ge.fullmatch FROM {glossary_alias} ga JOIN {glossary_entries} ge ON (ga.entryid = ge.id) WHERE ge.glossaryid IN ($glossarylist) AND ge.usedynalink = 1 AND ge.approved = 1"; $concepts = array(); $rs = $DB->get_recordset_sql($sql); foreach ($rs as $concept) { $currentconcept = trim($concept->concept); // Turn ampersands into & but keep HTML format for filters. $currentconcept = replace_ampersands_not_followed_by_entity($currentconcept); if (empty($currentconcept)) { continue; } // Rule out any small integers, see MDL-1446. if (is_number($currentconcept) and $currentconcept < 1000) { continue; } $concept->concept = $currentconcept; $concepts[$concept->glossaryid][] = $concept; } $rs->close(); return $concepts; } /** * Get all linked concepts from course. * @param int $courseid * @return array */ protected static function get_course_concepts($courseid) { global $DB; if (empty($courseid)) { return array(array(), array()); } $courseid = (int)$courseid; // Get info on any glossaries in this course. $modinfo = get_fast_modinfo($courseid); $cminfos = $modinfo->get_instances_of('glossary'); if (!$cminfos) { // No glossaries in this course, so don't do any work. return array(array(), array()); } $cache = \cache::make('mod_glossary', 'concepts'); $data = $cache->get($courseid); if (is_array($data)) { list($glossaries, $allconcepts) = $data; } else { // Find all course glossaries. $sql = "SELECT g.id, g.name FROM {glossary} g JOIN {course_modules} cm ON (cm.instance = g.id) JOIN {modules} m ON (m.name = 'glossary' AND m.id = cm.module) WHERE g.usedynalink = 1 AND g.course = :course AND cm.visible = 1 AND m.visible = 1 ORDER BY g.globalglossary, g.id"; $glossaries = $DB->get_records_sql_menu($sql, array('course' => $courseid)); if (!$glossaries) { $data = array(array(), array()); $cache->set($courseid, $data); return $data; } foreach ($glossaries as $id => $name) { $name = str_replace(':', '-', $name); $glossaries[$id] = $name; } $allconcepts = self::fetch_concepts(array_keys($glossaries)); foreach ($glossaries as $gid => $unused) { if (!isset($allconcepts[$gid])) { unset($glossaries[$gid]); } } if (!$glossaries) { // This means there are no interesting concepts in the existing glossaries. $data = array(array(), array()); $cache->set($courseid, $data); return $data; } $cache->set($courseid, array($glossaries, $allconcepts)); } $concepts = $allconcepts; // Verify access control to glossary instances. foreach ($concepts as $modid => $unused) { if (!isset($cminfos[$modid])) { // This should not happen. unset($concepts[$modid]); unset($glossaries[$modid]); continue; } if (!$cminfos[$modid]->uservisible) { unset($concepts[$modid]); unset($glossaries[$modid]); continue; } } return array($glossaries, $concepts); } /** * Get all linked global concepts. * @return array */ protected static function get_global_concepts() { global $DB; $cache = \cache::make('mod_glossary', 'concepts'); $data = $cache->get(0); if (is_array($data)) { list($glossaries, $allconcepts) = $data; } else { // Find all global glossaries - no access control here. $sql = "SELECT g.id, g.name FROM {glossary} g JOIN {course_modules} cm ON (cm.instance = g.id) JOIN {modules} m ON (m.name = 'glossary' AND m.id = cm.module) WHERE g.usedynalink = 1 AND g.globalglossary = 1 AND cm.visible = 1 AND m.visible = 1 ORDER BY g.globalglossary, g.id"; $glossaries = $DB->get_records_sql_menu($sql); if (!$glossaries) { $data = array(array(), array()); $cache->set(0, $data); return $data; } foreach ($glossaries as $id => $name) { $name = str_replace(':', '-', $name); $glossaries[$id] = replace_ampersands_not_followed_by_entity($name); } $allconcepts = self::fetch_concepts(array_keys($glossaries)); foreach ($glossaries as $gid => $unused) { if (!isset($allconcepts[$gid])) { unset($glossaries[$gid]); } } $cache->set(0, array($glossaries, $allconcepts)); } // NOTE: no access control is here because it would be way too expensive to check access // to all courses that contain the global glossaries. return array($glossaries, $allconcepts); } /** * Get all concepts that should be linked in the given course. * @param int $courseid * @return array with two elements - array of glossaries and concepts for each glossary */ public static function get_concepts($courseid) { list($glossaries, $concepts) = self::get_course_concepts($courseid); list($globalglossaries, $globalconcepts) = self::get_global_concepts(); foreach ($globalconcepts as $gid => $cs) { if (!isset($concepts[$gid])) { $concepts[$gid] = $cs; } } foreach ($globalglossaries as $gid => $name) { if (!isset($glossaries[$gid])) { $glossaries[$gid] = $name; } } return array($glossaries, $concepts); } } classes/entry_query_builder.php000064400000036566152265517410013036 0ustar00. /** * Entry query builder. * * @package mod_glossary * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Entry query builder class. * * The purpose of this class is to avoid duplicating SQL statements to fetch entries * which are very similar with each other. This builder is not meant to be smart, it * will not out rule any previously set condition, or join, etc... * * You should be using this builder just like you would be creating your SQL query. Only * some methods are shorthands to avoid logic duplication and common mistakes. * * @package mod_glossary * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.1 */ class mod_glossary_entry_query_builder { /** Alias for table glossary_alias. */ const ALIAS_ALIAS = 'ga'; /** Alias for table glossary_categories. */ const ALIAS_CATEGORIES = 'gc'; /** Alias for table glossary_entries_categories. */ const ALIAS_ENTRIES_CATEGORIES = 'gec'; /** Alias for table glossary_entries. */ const ALIAS_ENTRIES = 'ge'; /** Alias for table user. */ const ALIAS_USER = 'u'; /** Include none of the entries to approve. */ const NON_APPROVED_NONE = 'na_none'; /** Including all the entries. */ const NON_APPROVED_ALL = 'na_all'; /** Including only the entries to be approved. */ const NON_APPROVED_ONLY = 'na_only'; /** Including my entries to be approved. */ const NON_APPROVED_SELF = 'na_self'; /** @var array Raw SQL statements representing the fields to select. */ protected $fields = array(); /** @var array Raw SQL statements representing the JOINs to make. */ protected $joins = array(); /** @var string Raw SQL statement representing the FROM clause. */ protected $from; /** @var object The glossary we are fetching from. */ protected $glossary; /** @var int The number of records to fetch from. */ protected $limitfrom = 0; /** @var int The number of records to fetch. */ protected $limitnum = 0; /** @var array List of SQL parameters. */ protected $params = array(); /** @var array Raw SQL statements representing the ORDER clause. */ protected $order = array(); /** @var array Raw SQL statements representing the WHERE clause. */ protected $where = array(); /** * Constructor. * * @param object $glossary The glossary. */ public function __construct($glossary = null) { $this->from = sprintf('FROM {glossary_entries} %s', self::ALIAS_ENTRIES); if (!empty($glossary)) { $this->glossary = $glossary; $this->where[] = sprintf('(%s.glossaryid = :gid OR %s.sourceglossaryid = :gid2)', self::ALIAS_ENTRIES, self::ALIAS_ENTRIES); $this->params['gid'] = $glossary->id; $this->params['gid2'] = $glossary->id; } } /** * Add a field to select. * * @param string $field The field, or *. * @param string $table The table name, without the prefix 'glossary_'. * @param string $alias An alias for the field. */ public function add_field($field, $table, $alias = null) { $field = self::resolve_field($field, $table); if (!empty($alias)) { $field .= ' AS ' . $alias; } $this->fields[] = $field; } /** * Adds the user fields. * * @return void */ public function add_user_fields() { $userfieldsapi = \core_user\fields::for_userpic(); $fields = $userfieldsapi->get_sql('u', false, 'userdata', '', false)->selects; $this->fields[] = $fields; } /** * Internal method to build the query. * * @param bool $count Query to count? * @return string The SQL statement. */ protected function build_query($count = false) { $sql = 'SELECT '; if ($count) { $sql .= 'COUNT(\'x\') '; } else { $sql .= implode(', ', $this->fields) . ' '; } $sql .= $this->from . ' '; $sql .= implode(' ', $this->joins) . ' '; if (!empty($this->where)) { $sql .= 'WHERE (' . implode(') AND (', $this->where) . ') '; } if (!$count && !empty($this->order)) { $sql .= 'ORDER BY ' . implode(', ', $this->order); } return $sql; } /** * Count the records. * * @return int The number of records. */ public function count_records() { global $DB; return $DB->count_records_sql($this->build_query(true), $this->params); } /** * Filter a field using a letter. * * @param string $letter The letter. * @param string $finalfield The SQL statement representing the field. */ protected function filter_by_letter($letter, $finalfield) { global $DB; $letter = core_text::strtoupper($letter); $len = core_text::strlen($letter); $sql = $DB->sql_substr(sprintf('upper(%s)', $finalfield), 1, $len); $this->where[] = "$sql = :letter"; $this->params['letter'] = $letter; } /** * Filter a field by special characters. * * @param string $finalfield The SQL statement representing the field. */ protected function filter_by_non_letter($finalfield) { global $DB; $alphabet = explode(',', get_string('alphabet', 'langconfig')); list($nia, $aparams) = $DB->get_in_or_equal($alphabet, SQL_PARAMS_NAMED, 'nonletter', false); $sql = $DB->sql_substr(sprintf('upper(%s)', $finalfield), 1, 1); $this->where[] = "$sql $nia"; $this->params = array_merge($this->params, $aparams); } /** * Filter the author by letter. * * @param string $letter The letter. * @param bool $firstnamefirst Whether or not the firstname is first in the author's name. */ public function filter_by_author_letter($letter, $firstnamefirst = false) { $field = self::get_fullname_field($firstnamefirst); $this->filter_by_letter($letter, $field); } /** * Filter the author by special characters. * * @param bool $firstnamefirst Whether or not the firstname is first in the author's name. */ public function filter_by_author_non_letter($firstnamefirst = false) { $field = self::get_fullname_field($firstnamefirst); $this->filter_by_non_letter($field); } /** * Filter non approved entries. * * @param string $constant One of the NON_APPROVED_* constants. * @param int $userid The user ID when relevant, otherwise current user. */ public function filter_by_non_approved($constant, $userid = null) { global $USER; if (!$userid) { $userid = $USER->id; } if ($constant === self::NON_APPROVED_ALL) { // Nothing to do. } else if ($constant === self::NON_APPROVED_SELF) { $this->where[] = sprintf('%s != 0 OR %s = :toapproveuserid', self::resolve_field('approved', 'entries'), self::resolve_field('userid', 'entries')); $this->params['toapproveuserid'] = $USER->id; } else if ($constant === self::NON_APPROVED_NONE) { $this->where[] = sprintf('%s != 0', self::resolve_field('approved', 'entries')); } else if ($constant === self::NON_APPROVED_ONLY) { $this->where[] = sprintf('%s = 0', self::resolve_field('approved', 'entries')); } else { throw new coding_exception('Invalid constant'); } } /** * Filter by concept or alias. * * This requires the alias table to be joined in the query. See {@link self::join_alias()}. * * @param string $term What the concept or aliases should be. */ public function filter_by_term($term) { $this->where[] = sprintf("(%s LIKE :filterterma OR %s LIKE :filtertermb)", self::resolve_field('concept', 'entries'), self::resolve_field('alias', 'alias')); $this->params['filterterma'] = "%" . $term . "%"; $this->params['filtertermb'] = "%" . $term . "%"; } /** * Convenience method to get get the SQL statement for the full name. * * @param bool $firstnamefirst Whether or not the firstname is first in the author's name. * @return string The SQL statement. */ public static function get_fullname_field($firstnamefirst = false) { global $DB; if ($firstnamefirst) { return $DB->sql_fullname(self::resolve_field('firstname', 'user'), self::resolve_field('lastname', 'user')); } return $DB->sql_fullname(self::resolve_field('lastname', 'user'), self::resolve_field('firstname', 'user')); } /** * Get the records. * * @return array */ public function get_records() { global $DB; return $DB->get_records_sql($this->build_query(), $this->params, $this->limitfrom, $this->limitnum); } /** * Get the recordset. * * @return moodle_recordset */ public function get_recordset() { global $DB; return $DB->get_recordset_sql($this->build_query(), $this->params, $this->limitfrom, $this->limitnum); } /** * Retrieve a user object from a record. * * This comes handy when {@link self::add_user_fields} was used. * * @param stdClass $record The record. * @return stdClass A user object. */ public static function get_user_from_record($record) { return user_picture::unalias($record, null, 'userdataid', 'userdata'); } /** * Join the alias table. * * Note that this may cause the same entry to be returned more than once. You might want * to add a distinct on the entry id. * * @return void */ public function join_alias() { $this->joins[] = sprintf('LEFT JOIN {glossary_alias} %s ON %s = %s', self::ALIAS_ALIAS, self::resolve_field('id', 'entries'), self::resolve_field('entryid', 'alias')); } /** * Join on the category tables. * * Depending on the category passed the joins will be different. This is due to the display * logic that assumes that when displaying all categories the non categorised entries should * not be returned, etc... * * @param int $categoryid The category ID, or GLOSSARY_SHOW_* constant. */ public function join_category($categoryid) { if ($categoryid === GLOSSARY_SHOW_ALL_CATEGORIES) { $this->joins[] = sprintf('JOIN {glossary_entries_categories} %s ON %s = %s', self::ALIAS_ENTRIES_CATEGORIES, self::resolve_field('id', 'entries'), self::resolve_field('entryid', 'entries_categories')); $this->joins[] = sprintf('JOIN {glossary_categories} %s ON %s = %s', self::ALIAS_CATEGORIES, self::resolve_field('id', 'categories'), self::resolve_field('categoryid', 'entries_categories')); } else if ($categoryid === GLOSSARY_SHOW_NOT_CATEGORISED) { $this->joins[] = sprintf('LEFT JOIN {glossary_entries_categories} %s ON %s = %s', self::ALIAS_ENTRIES_CATEGORIES, self::resolve_field('id', 'entries'), self::resolve_field('entryid', 'entries_categories')); } else { $this->joins[] = sprintf('JOIN {glossary_entries_categories} %s ON %s = %s AND %s = :joincategoryid', self::ALIAS_ENTRIES_CATEGORIES, self::resolve_field('id', 'entries'), self::resolve_field('entryid', 'entries_categories'), self::resolve_field('categoryid', 'entries_categories')); $this->params['joincategoryid'] = $categoryid; } } /** * Join the user table. * * @param bool $strict When strict uses a JOIN rather than a LEFT JOIN. */ public function join_user($strict = false) { $join = $strict ? 'JOIN' : 'LEFT JOIN'; $this->joins[] = sprintf("$join {user} %s ON %s = %s", self::ALIAS_USER, self::resolve_field('id', 'user'), self::resolve_field('userid', 'entries')); } /** * Limit the number of records to fetch. * @param int $from Fetch from. * @param int $num Number to fetch. */ public function limit($from, $num) { $this->limitfrom = $from; $this->limitnum = $num; } /** * Normalise a direction. * * This ensures that the value is either ASC or DESC. * * @param string $direction The desired direction. * @return string ASC or DESC. */ protected function normalize_direction($direction) { $direction = core_text::strtoupper($direction); if ($direction == 'DESC') { return 'DESC'; } return 'ASC'; } /** * Order by a field. * * @param string $field The field, or *. * @param string $table The table name, without the prefix 'glossary_'. * @param string $direction ASC, or DESC. */ public function order_by($field, $table, $direction = '') { $direction = self::normalize_direction($direction); $this->order[] = self::resolve_field($field, $table) . ' ' . $direction; } /** * Order by author name. * * @param bool $firstnamefirst Whether or not the firstname is first in the author's name. * @param string $direction ASC, or DESC. */ public function order_by_author($firstnamefirst = false, $direction = '') { $field = self::get_fullname_field($firstnamefirst); $direction = self::normalize_direction($direction); $this->order[] = $field . ' ' . $direction; } /** * Convenience method to transform a field into SQL statement. * * @param string $field The field, or *. * @param string $table The table name, without the prefix 'glossary_'. * @return string SQL statement. */ protected static function resolve_field($field, $table) { $prefix = constant(__CLASS__ . '::ALIAS_' . core_text::strtoupper($table)); return sprintf('%s.%s', $prefix, $field); } /** * Simple where conditions. * * @param string $field The field, or *. * @param string $table The table name, without the prefix 'glossary_'. * @param mixed $value The value to be equal to. */ public function where($field, $table, $value) { static $i = 0; $sql = self::resolve_field($field, $table) . ' '; if ($value === null) { $sql .= 'IS NULL'; } else { $param = 'where' . $i++; $sql .= " = :$param"; $this->params[$param] = $value; } $this->where[] = $sql; } } classes/admin_setting_display_formats.php000064400000011064152265517410015031 0ustar00. defined('MOODLE_INTERNAL') || die(); /** * Class for glossary display formats management. * * @package mod_glossary * @copyright 2021 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_glossary_admin_setting_display_formats extends admin_setting { /** * Calls parent::__construct with specific arguments */ public function __construct() { $this->nosave = true; parent::__construct('glossarydisplayformats', get_string('displayformatssetup', 'glossary'), '', ''); } /** * Always returns true, does nothing * * @return true */ public function get_setting() { return true; } /** * Always returns true, does nothing * * @return true */ public function get_defaultsetting() { return true; } /** * Always returns '', does not write anything * * @param string $data Unused * @return string Always returns '' */ public function write_setting($data) { // Do not write any setting. return ''; } /** * Checks if $query is one of the available display formats * * @param string $query The string to search for * @return bool Returns true if found, false if not */ public function is_related($query) { global $DB; if (parent::is_related($query)) { return true; } $query = core_text::strtolower($query); $formats = $DB->get_records("glossary_formats"); foreach ($formats as $format) { if (strpos(core_text::strtolower($format->name), $query) !== false) { return true; } $localised = get_string("displayformat$format->name", "glossary"); if (strpos(core_text::strtolower($localised), $query) !== false) { return true; } } return false; } /** * Builds the XHTML to display the control * * @param string $data Unused * @param string $query * @return string */ public function output_html($data, $query='') { global $CFG, $OUTPUT, $DB; $stredit = get_string("edit"); $strhide = get_string("hide"); $strshow = get_string("show"); $str = $OUTPUT->heading(get_string('displayformatssetup', 'glossary'), 3, 'main', true); $recformats = $DB->get_records("glossary_formats"); $formats = array(); // Build alphabetized list of formats. foreach ($recformats as $format) { $formats[get_string("displayformat$format->name", "glossary")] = $format; } ksort($formats); $table = new html_table(); $table->align = array('left', 'center'); foreach ($formats as $formatname => $format) { $editicon = html_writer::link( new moodle_url( '/mod/glossary/formats.php', array('id' => $format->id, 'mode' => 'edit') ), $OUTPUT->pix_icon('t/edit', $stredit), array('title' => $stredit)); if ( $format->visible ) { $vtitle = $strhide; $vicon = "t/hide"; } else { $vtitle = $strshow; $vicon = "t/show"; } $visibleicon = html_writer::link( new moodle_url( '/mod/glossary/formats.php', array('id' => $format->id, 'mode' => 'visible', 'sesskey' => sesskey()) ), $OUTPUT->pix_icon($vicon, $vtitle), array('title' => $vtitle) ); $table->data[] = array( $formatname, $editicon . '  ' . $visibleicon ); } $str .= html_writer::table($table); return highlight($query, $str); } } classes/search/activity.php000064400000002630152265517410012024 0ustar00. /** * Search area for mod_glossary activities. * * @package mod_glossary * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\search; defined('MOODLE_INTERNAL') || die(); /** * Search area for mod_glossary activities. * * @package mod_glossary * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } } classes/search/entry.php000064400000017616152265517410011343 0ustar00. /** * Glossary entries search. * * @package mod_glossary * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\search; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/glossary/lib.php'); /** * Glossary entries search. * * @package mod_glossary * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry extends \core_search\base_mod { /** * @var array Internal quick static cache. */ protected $entriesdata = array(); /** * Returns recordset containing required data for indexing glossary entries. * * @param int $modifiedfrom timestamp * @param \context|null $context Optional context to restrict scope of returned results * @return moodle_recordset|null Recordset (or null if no results) */ public function get_document_recordset($modifiedfrom = 0, ?\context $context = null) { global $DB; list ($contextjoin, $contextparams) = $this->get_context_restriction_sql( $context, 'glossary', 'g'); if ($contextjoin === null) { return null; } $sql = "SELECT ge.*, g.course FROM {glossary_entries} ge JOIN {glossary} g ON g.id = ge.glossaryid $contextjoin WHERE ge.timemodified >= ? ORDER BY ge.timemodified ASC"; return $DB->get_recordset_sql($sql, array_merge($contextparams, [$modifiedfrom])); } /** * Returns the documents associated with this glossary entry id. * * @param stdClass $entry glossary entry. * @param array $options * @return \core_search\document */ public function get_document($entry, $options = array()) { global $DB; $keywords = array(); if ($aliases = $DB->get_records('glossary_alias', array('entryid' => $entry->id))) { foreach ($aliases as $alias) { $keywords[] = $alias->alias; } } try { $cm = $this->get_cm('glossary', $entry->glossaryid, $entry->course); $context = \context_module::instance($cm->id); } catch (\dml_missing_record_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving mod_glossary ' . $entry->id . ' document, not all required data is available: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } catch (\dml_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving mod_glossary' . $entry->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } // Prepare associative array with data from DB. $doc = \core_search\document_factory::instance($entry->id, $this->componentname, $this->areaname); $doc->set('title', content_to_text($entry->concept, false)); $doc->set('content', content_to_text($entry->definition, $entry->definitionformat)); $doc->set('contextid', $context->id); $doc->set('courseid', $entry->course); $doc->set('userid', $entry->userid); $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $entry->timemodified); // Check if this document should be considered new. if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $entry->timecreated)) { // If the document was created after the last index time, it must be new. $doc->set_is_new(true); } // Adding keywords as extra info. if ($keywords) { // No need to pass through content_to_text here as this is just a list of keywords. $doc->set('description1', implode(' ' , $keywords)); } return $doc; } /** * Whether the user can access the document or not. * * @throws \dml_missing_record_exception * @throws \dml_exception * @param int $id Glossary entry id * @return bool */ public function check_access($id) { global $USER; try { $entry = $this->get_entry($id); $cminfo = $this->get_cm('glossary', $entry->glossaryid, $entry->course); } catch (\dml_missing_record_exception $ex) { return \core_search\manager::ACCESS_DELETED; } catch (\dml_exception $ex) { return \core_search\manager::ACCESS_DENIED; } if (!glossary_can_view_entry($entry, $cminfo)) { return \core_search\manager::ACCESS_DENIED; } return \core_search\manager::ACCESS_GRANTED; } /** * Link to glossary entry. * * @param \core_search\document $doc * @return \moodle_url */ public function get_doc_url(\core_search\document $doc) { global $USER; // The post is already in static cache, we fetch it in self::search_access. $entry = $this->get_entry($doc->get('itemid')); $contextmodule = \context::instance_by_id($doc->get('contextid')); if ($entry->approved == false && $entry->userid != $USER->id) { // The URL should change when the entry is not approved and it was not created by the user. $docparams = array('id' => $contextmodule->instanceid, 'mode' => 'approval'); } else { $docparams = array('id' => $contextmodule->instanceid, 'mode' => 'entry', 'hook' => $doc->get('itemid')); } return new \moodle_url('/mod/glossary/view.php', $docparams); } /** * Link to the glossary. * * @param \core_search\document $doc * @return \moodle_url */ public function get_context_url(\core_search\document $doc) { $contextmodule = \context::instance_by_id($doc->get('contextid')); return new \moodle_url('/mod/glossary/view.php', array('id' => $contextmodule->instanceid)); } /** * Returns the specified glossary entry checking the internal cache. * * Store minimal information as this might grow. * * @throws \dml_exception * @param int $entryid * @return stdClass */ protected function get_entry($entryid) { global $DB; if (empty($this->entriesdata[$entryid])) { $this->entriesdata[$entryid] = $DB->get_record_sql("SELECT ge.*, g.course, g.defaultapproval FROM {glossary_entries} ge JOIN {glossary} g ON g.id = ge.glossaryid WHERE ge.id = ?", array('id' => $entryid), MUST_EXIST); } return $this->entriesdata[$entryid]; } /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } /** * Return the context info required to index files for * this search area. * * @return array */ public function get_search_fileareas() { $fileareas = array('attachment', 'entry'); // Fileareas. return $fileareas; } } classes/output/renderer.php000064400000002542152265517410012073 0ustar00. namespace mod_glossary\output; use plugin_renderer_base; /** * Class actionbar - Display the action bar * * @package mod_glossary * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * Render the glossary tertiary nav * * @param standard_action_bar $actionmenu * @return bool|string * @throws \moodle_exception */ public function main_action_bar(standard_action_bar $actionmenu) { $context = $actionmenu->export_for_template($this); return $this->render_from_template('mod_glossary/standard_action_menu', $context); } } classes/output/standard_action_bar.php000064400000024072152265517410014250 0ustar00. namespace mod_glossary\output; use moodle_url; use context_module; use renderable; use renderer_base; use single_button; use templatable; use url_select; /** * Class standard_action_bar - Display the action bar * * @package mod_glossary * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class standard_action_bar implements renderable, templatable { /** @var object $cm The course module. */ private $cm; /** @var string $mode The type of view. */ private $mode; /** @var string $hook The term, entry, cat, etc... to look for based on mode. */ private $hook; /** @var string $sortkey Sorted view: CREATION | UPDATE | FIRSTNAME | LASTNAME. */ private $sortkey; /** @var string $sortorder The sort order (ASC or DESC). */ private $sortorder; /** @var int $offset Entries to bypass (for paging purposes). */ private $offset; /** @var int $pagelimit The page to resume with. */ private $pagelimit; /** @var int $context The context of the glossary. */ private $context; /** @var object $module The glossary record . */ private $module; /** @var int $fullsearch Full search (concept and definition) when searching. */ private $fullsearch; /** @var object $displayformat Override of the glossary display format. */ private $displayformat; /** @var string $tab Browsing entries by categories. */ private $tab; /** * standard_action_bar constructor. * * @param object $cm * @param object $module * @param object $displayformat * @param string $mode * @param string $hook * @param string $sortkey * @param string $sortorder * @param int $offset * @param int $pagelimit * @param int $fullsearch * @param string $tab * @param string $defaulttab * @throws \coding_exception */ public function __construct(object $cm, object $module, object $displayformat, string $mode, string $hook, string $sortkey, string $sortorder, int $offset, int $pagelimit, int $fullsearch, string $tab, string $defaulttab) { $this->cm = $cm; $this->module = $module; $this->displayformat = $displayformat; $this->mode = $mode; $this->tab = $tab; $this->hook = $hook; $this->sortkey = $sortkey; $this->sortorder = $sortorder; $this->offset = $offset; $this->pagelimit = $pagelimit; $this->fullsearch = $fullsearch; $this->context = context_module::instance($this->cm->id); if (!has_capability('mod/glossary:approve', $this->context) && $this->tab == GLOSSARY_APPROVAL_VIEW) { // Non-teachers going to approval view go to defaulttab. $this->tab = $defaulttab; } } /** * Export the action bar * * @param renderer_base $output * @return array */ public function export_for_template(renderer_base $output) { return [ 'addnewbutton' => $this->create_add_button($output), 'searchbox' => $this->create_search_box(), 'tools' => $this->get_additional_tools($output), 'tabjumps' => $this->generate_tab_jumps($output) ]; } /** * Render the search box with the checkbox * * @return array */ private function create_search_box(): array { global $OUTPUT; $fullsearchchecked = false; if ($this->fullsearch || $this->mode != 'search') { $fullsearchchecked = true; } $check = [ 'name' => 'fullsearch', 'id' => 'fullsearch', 'value' => '1', 'checked' => $fullsearchchecked, 'label' => get_string("searchindefinition", "glossary"), ]; $checkbox = $OUTPUT->render_from_template('core/checkbox', $check); $hiddenfields = [ (object) ['name' => 'id', 'value' => $this->cm->id], (object) ['name' => 'mode', 'value' => 'search'], ]; $data = [ 'action' => new moodle_url('/mod/glossary/view.php'), 'hiddenfields' => $hiddenfields, 'otherfields' => $checkbox, 'inputname' => 'hook', 'query' => ($this->mode == 'search') ? s($this->hook) : '', 'searchstring' => get_string('search'), ]; return $data; } /** * Render the add entry button * * @param renderer_base $output * @return \stdClass|null */ private function create_add_button(renderer_base $output): ?\stdClass { if (!has_capability('mod/glossary:write', $this->context)) { return null; } $btn = new single_button(new moodle_url('/mod/glossary/edit.php', ['cmid' => $this->cm->id]), get_string('addsingleentry', 'glossary'), 'post', single_button::BUTTON_PRIMARY); return $btn->export_for_template($output); } /** * Render the additional tools required by the glossary * * @param renderer_base $output * @return array */ private function get_additional_tools(renderer_base $output): array { global $USER, $CFG; $items = []; $buttons = []; $openinnewwindow = []; if (has_capability('mod/glossary:import', $this->context)) { $items['button'] = new single_button( new moodle_url('/mod/glossary/import.php', ['id' => $this->cm->id]), get_string('importentries', 'glossary') ); } if (has_capability('mod/glossary:export', $this->context)) { $url = new moodle_url('/mod/glossary/export.php', [ 'id' => $this->cm->id, 'mode' => $this->mode, 'hook' => $this->hook ]); $buttons[get_string('export', 'glossary')] = $url->out(false); } if (has_capability('mod/glossary:manageentries', $this->context) or $this->module->allowprintview) { $params = array( 'id' => $this->cm->id, 'mode' => $this->mode, 'hook' => $this->hook, 'sortkey' => $this->sortkey, 'sortorder' => $this->sortorder, 'offset' => $this->offset, 'pagelimit' => $this->pagelimit ); $printurl = new moodle_url('/mod/glossary/print.php', $params); $buttons[get_string('printerfriendly', 'glossary')] = $printurl->out(false); $openinnewwindow[] = $printurl->out(false); } if (!empty($CFG->enablerssfeeds) && !empty($CFG->glossary_enablerssfeeds) && $this->module->rsstype && $this->module->rssarticles && has_capability('mod/glossary:view', $this->context)) { require_once("$CFG->libdir/rsslib.php"); $string = get_string('rssfeed', 'glossary'); $url = new moodle_url(rss_get_url($this->context->id, $USER->id, 'mod_glossary', $this->cm->instance)); $buttons[$string] = $url->out(false); $openinnewwindow[] = $url->out(false); } foreach ($items as $key => $value) { $items[$key] = $value->export_for_template($output); } if ($buttons) { foreach ($buttons as $index => $value) { $items['select']['options'][] = [ 'url' => $value, 'string' => $index, 'openinnewwindow' => ($openinnewwindow ? in_array($value, $openinnewwindow) : false) ]; } } return $items; } /** * Generate a url select to match any types of glossary views * * @param renderer_base $output * @return \stdClass|null */ private function generate_tab_jumps(renderer_base $output) { $tabs = glossary_get_visible_tabs($this->displayformat); $validtabs = [ GLOSSARY_STANDARD => [ 'mode' => 'letter', 'descriptor' => 'standardview' ], GLOSSARY_CATEGORY => [ 'mode' => 'cat', 'descriptor' => 'categoryview' ], GLOSSARY_DATE => [ 'mode' => 'date', 'descriptor' => 'dateview' ], GLOSSARY_AUTHOR => [ 'mode' => 'author', 'descriptor' => 'authorview' ], ]; $baseurl = new moodle_url('/mod/glossary/view.php', ['id' => $this->cm->id]); $active = null; $options = []; foreach ($validtabs as $key => $tabinfo) { if (in_array($key, $tabs)) { $baseurl->params(['mode' => $tabinfo['mode']]); $active = $active ?? $baseurl->out(false); $active = ($tabinfo['mode'] == $this->mode ? $baseurl->out(false) : $active); $options[get_string($tabinfo['descriptor'], 'glossary')] = $baseurl->out(false); } } if ($this->tab < GLOSSARY_STANDARD_VIEW || $this->tab > GLOSSARY_AUTHOR_VIEW) { $options[get_string('edit')] = '#'; } if (count($options) > 1) { $select = new url_select(array_flip($options), $active, null); $select->set_label(get_string('explainalphabet', 'glossary'), ['class' => 'visually-hidden']); return $select->export_for_template($output); } return null; } } classes/event/entry_updated.php000064400000006114152265517410012714 0ustar00. /** * The mod_glossary entry updated event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary entry updated event. * * @property-read array $other { * Extra information about event. * * - string concept: (optional) the concept of updated entry (after update). * } * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry_updated extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_entries'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('evententryupdated', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' has updated the glossary entry with id '$this->objectid' in " . "the glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/view.php", array('id' => $this->contextinstanceid, 'mode' => 'entry', 'hook' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_entries', 'restore' => 'glossary_entry'); } public static function get_other_mapping() { // Nothing to map. return false; } } classes/event/entry_disapproved.php000064400000005523152265517410013611 0ustar00. /** * The mod_glossary entry disapproved event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary entry disapproved event. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry_disapproved extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_entries'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('evententrydisapproved', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' has disapproved the glossary entry with id '$this->objectid' for " . "the glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/view.php", array('id' => $this->contextinstanceid, 'mode' => 'entry', 'hook' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_entries', 'restore' => 'glossary_entry'); } } classes/event/entry_deleted.php000064400000006744152265517410012705 0ustar00. /** * The mod_glossary entry deleted event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary entry deleted event class. * * @property-read array $other { * Extra information about event. * * - string concept: (optional) the concept of deleted entry. * - string mode: (optional) view mode user was in before deleting entry. * - int|string hook: (optional) hook parameter in the previous view mode. * } * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry_deleted extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_entries'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('evententrydeleted', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' has deleted the glossary entry with id '$this->objectid' in " . "the glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { // Entry does not exist any more, returning link to the module view page in the mode it was before deleting entry. $params = array('id' => $this->contextinstanceid); if (isset($this->other['hook'])) { $params['hook'] = $this->other['hook']; } if (isset($this->other['mode'])) { $params['mode'] = $this->other['mode']; } return new \moodle_url("/mod/glossary/view.php", $params); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_entries', 'restore' => 'glossary_entry'); } public static function get_other_mapping() { // Nothing to map. return false; } } classes/event/category_updated.php000064400000005425152265517410013374 0ustar00. /** * The mod_glossary glossary category updated event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary glossary category updated event class. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category_updated extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_categories'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventcategoryupdated', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' updated the category with id '$this->objectid' in the " . "glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/editcategories.php", array('id' => $this->contextinstanceid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_categories', 'restore' => 'glossary_category'); } } classes/event/category_created.php000064400000005426152265517410013356 0ustar00. /** * The mod_glossary glossary category created event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary glossary category created event class. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category_created extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_categories'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventcategorycreated', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' created the category with id '$this->objectid' for the " . "glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/editcategories.php", array('id' => $this->contextinstanceid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_categories', 'restore' => 'glossary_category'); } } classes/event/course_module_viewed.php000064400000004210152265517410014250 0ustar00. /** * The mod_glossary course module viewed event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary course module viewed event class. * * @property-read array $other { * Extra information about event. * * - string mode: (optional) * } * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary'; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { $params = array('id' => $this->contextinstanceid); if (!empty($this->other['mode'])) { $params['mode'] = $this->other['mode']; } return new \moodle_url("/mod/$this->objecttable/view.php", $params); } public static function get_objectid_mapping() { return array('db' => 'glossary', 'restore' => 'glossary'); } public static function get_other_mapping() { // Nothing to map. return false; } } classes/event/entry_viewed.php000064400000005500152265517410012547 0ustar00. /** * The mod_glossary entry viwed event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary entry viewed event class. * * Triggered when glossary entry is autolinked and viewed by user from another context. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry_viewed extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_entries'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('evententryviewed', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' has viewed the glossary entry with id '$this->objectid' in " . "the glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/showentry.php", array('eid' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_entries', 'restore' => 'glossary_entry'); } } classes/event/entry_approved.php000064400000005512152265517410013107 0ustar00. /** * The mod_glossary entry approved event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary entry approved event class. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry_approved extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_entries'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('evententryapproved', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' has approved the glossary entry with id '$this->objectid' for " . "the glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/view.php", array('id' => $this->contextinstanceid, 'mode' => 'entry', 'hook' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_entries', 'restore' => 'glossary_entry'); } } classes/event/comment_deleted.php000064400000003436152265517410013201 0ustar00. /** * The mod_glossary comment deleted event. * * @package mod_glossary * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary comment deleted event class. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_deleted extends \core\event\comment_deleted { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/glossary/view.php', array('id' => $this->contextinstanceid)); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the comment with id '$this->objectid' from the glossary activity " . "with course module id '$this->contextinstanceid'."; } } classes/event/category_deleted.php000064400000005425152265517410013354 0ustar00. /** * The mod_glossary glossary category deleted event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary glossary category deleted event class. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category_deleted extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_categories'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventcategorydeleted', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the category with id '$this->objectid' in the " . "glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/editcategories.php", array('id' => $this->contextinstanceid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_categories', 'restore' => 'glossary_category'); } } classes/event/entry_created.php000064400000006104152265517410012674 0ustar00. /** * The mod_glossary entry created event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary entry created event class. * * @property-read array $other { * Extra information about event. * * - string concept: (optional) the concept of created entry. * } * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry_created extends \core\event\base { /** * Init method */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'glossary_entries'; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('evententrycreated', 'mod_glossary'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' has created the glossary entry with id '$this->objectid' for " . "the glossary activity with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url("/mod/glossary/view.php", array('id' => $this->contextinstanceid, 'mode' => 'entry', 'hook' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); // Make sure this class is never used without proper object details. if (!$this->contextlevel === CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } public static function get_objectid_mapping() { return array('db' => 'glossary_entries', 'restore' => 'glossary_entry'); } public static function get_other_mapping() { // Nothing to map. return false; } } classes/event/comment_created.php000064400000003432152265517410013176 0ustar00. /** * The mod_glossary comment created event. * * @package mod_glossary * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary comment created event class. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_created extends \core\event\comment_created { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/glossary/view.php', array('id' => $this->contextinstanceid)); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' added the comment with id '$this->objectid' to the glossary activity " . "with course module id '$this->contextinstanceid'."; } } classes/event/course_module_instance_list_viewed.php000064400000002477152265517410017204 0ustar00. /** * The mod_glossary instance list viewed event. * * @package mod_glossary * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_glossary instance list viewed event class. * * @package mod_glossary * @since Moodle 2.7 * @copyright 2014 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { // No code required here as the parent class handles it all. } classes/completion/custom_completion.php000064400000005657152265517410014653 0ustar00. declare(strict_types=1); namespace mod_glossary\completion; use core_completion\activity_custom_completion; /** * Activity custom completion subclass for the glossary activity. * * Class for defining mod_glossary's custom completion rules and fetching the completion statuses * of the custom completion rules for a given glossary instance and a user. * * @package mod_glossary * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom_completion extends activity_custom_completion { /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ public function get_state(string $rule): int { global $DB; $this->validate_rule($rule); $glossaryid = $this->cm->instance; $userid = $this->userid; $userentries = $DB->count_records('glossary_entries', ['glossaryid' => $glossaryid, 'userid' => $userid, 'approved' => 1]); $completionentries = $this->cm->customdata['customcompletionrules']['completionentries']; return ($completionentries <= $userentries) ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; } /** * Fetch the list of custom completion rules that this module defines. * * @return array */ public static function get_defined_custom_rules(): array { return ['completionentries']; } /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ public function get_custom_rule_descriptions(): array { $completionentries = $this->cm->customdata['customcompletionrules']['completionentries'] ?? 0; return [ 'completionentries' => get_string('completiondetail:entries', 'glossary', $completionentries), ]; } /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ public function get_sort_order(): array { return [ 'completionview', 'completionentries', 'completionusegrade', 'completionpassgrade', ]; } } classes/analytics/indicator/cognitive_depth.php000064400000003340152265517410016040 0ustar00. /** * Cognitive depth indicator - glossary. * * @package mod_glossary * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Cognitive depth indicator - glossary. * * @package mod_glossary * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:cognitivedepth', 'mod_glossary'); } public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } public function get_cognitive_depth_level(\cm_info $cm) { return self::COGNITIVE_LEVEL_2; } } classes/analytics/indicator/activity_base.php000064400000002367152265517410015523 0ustar00. /** * Activity base class. * * @package mod_glossary * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Activity base class. * * @package mod_glossary * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { } classes/analytics/indicator/social_breadth.php000064400000003325152265517410015633 0ustar00. /** * Social breadth indicator - glossary. * * @package mod_glossary * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Social breadth indicator - glossary. * * @package mod_glossary * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:socialbreadth', 'mod_glossary'); } public function get_indicator_type() { return self::INDICATOR_SOCIAL; } public function get_social_breadth_level(\cm_info $cm) { return self::SOCIAL_LEVEL_1; } } templates/standard_action_menu.mustache000064400000013406152265517410014472 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_glossary/standard_action_menu Actions panel at the bottom of the assignment grading UI. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * see mod/glossary/classes/output/standard_action_bar.php Example context (json): { "addnewbutton": { "method" : "get", "url" : "#", "primary" : true, "tooltip" : "This is a tooltip", "label" : "This is a the button text", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] }, "tools": { "button": { "method" : "get", "url" : "#", "primary" : true, "tooltip" : "This is a tooltip", "label" : "This is a the button text", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] }, "select": { "options": [ { "url": "www.google.com", "string": "Google" }, { "url": "www.yahoo.com", "string": "Yahoo" } ] } }, "tabjumps": { "id": "url_select_test", "action": "https://example.com/post", "formid": "url_select_form", "sesskey": "sesskey", "label": "core/url_select", "helpicon": { "title": "Help with something", "text": "Help with something", "url": "http://example.org/help", "linktext": "", "icon":{ "extraclasses": "", "attributes": [ {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } }, "showbutton": "Go", "options": [{ "name": "Group 1", "isgroup": true, "options": [ {"name": "Item 1", "isgroup": false, "value": "1"}, {"name": "Item 2", "isgroup": false, "value": "2"} ]}, {"name": "Group 2", "isgroup": true, "options": [ {"name": "Item 3", "isgroup": false, "value": "3"}, {"name": "Item 4", "isgroup": false, "value": "4"} ]}], "disabled": false, "title": "Some cool title" }, "searchbox": { "action": "http://localhost/stable/mod/glossary/view.php", "hiddenfields": [ { "name": "id", "value": 23 }, { "name": "mode", "value": "search" } ], "otherfields": "Rendered checkbox", "inputname": "hook", "query": "hook", "searchstring": "Search" } } }}
{{#addnewbutton}} {{/addnewbutton}}
{{#tools}} {{#button}} {{/button}} {{#select}} {{/select}} {{/tools}}
{{#tabjumps}} {{/tabjumps}} {{#searchbox}} {{/searchbox}}
upgrade.txt000064400000002555152265517410006753 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /mod/glossary/*, information provided here is intended especially for developers. === 3.10 === * External function get_entries_by_id now returns and additional "permissions" field indicating the user permissions for managing the entry. === 3.8 === * The following functions have been finally deprecated and can not be used anymore: * glossary_scale_used() === 3.7 === * External functions get_entries_by_* and get_entry now return an additional field "tags" containing the entry tags. === 3.4 === * External functions returning entries now return an additional field "ratinginfo" containing the entry rating information. === 3.2 === * External functions that were returning file information now return the following file fields: filename, filepath, mimetype, filesize, timemodified and fileurl. Those fields are now marked as VALUE_OPTIONAL for backwards compatibility. === 2.8 === * The glossary_print_entry_attachment function no longer takes an `align` or `insidetable` property. Instead the attachments are printed within a div whose class is `attachments` and which can be targetted using appropriate CSS rules. * The glossary_print_entry_attachment function now displays the attachments on the left by default. locallib.php000064400000072452152265517410007060 0ustar00. /** * Library of functions and constants for module glossary * outside of what is required for the core moodle api * * @package mod_glossary * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->libdir . '/portfolio/caller.php'); require_once($CFG->libdir . '/filelib.php'); /** * class to handle exporting an entire glossary database */ class glossary_full_portfolio_caller extends portfolio_module_caller_base { private $glossary; private $exportdata; private $keyedfiles = array(); // keyed on entry /** * return array of expected call back arguments * and whether they are required or not * * @return array */ public static function expected_callbackargs() { return array( 'id' => true, ); } /** * load up all data required for this export. * * @return void */ public function load_data() { global $DB; if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) { throw new portfolio_caller_exception('invalidid', 'glossary'); } if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) { throw new portfolio_caller_exception('invalidid', 'glossary'); } $entries = $DB->get_records('glossary_entries', array('glossaryid' => $this->glossary->id)); list($where, $params) = $DB->get_in_or_equal(array_keys($entries)); $aliases = $DB->get_records_select('glossary_alias', 'entryid ' . $where, $params); $categoryentries = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec JOIN {glossary_categories} c ON c.id = ec.categoryid WHERE ec.entryid ' . $where, $params); $this->exportdata = array('entries' => $entries, 'aliases' => $aliases, 'categoryentries' => $categoryentries); $fs = get_file_storage(); $context = context_module::instance($this->cm->id); $this->multifiles = array(); foreach (array_keys($entries) as $entry) { $this->keyedfiles[$entry] = array_merge( $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry, "timemodified", false), $fs->get_area_files($context->id, 'mod_glossary', 'entry', $entry, "timemodified", false) ); $this->multifiles = array_merge($this->multifiles, $this->keyedfiles[$entry]); } } /** * how long might we expect this export to take * * @return constant one of PORTFOLIO_TIME_XX */ public function expected_time() { $filetime = portfolio_expected_time_file($this->multifiles); $dbtime = portfolio_expected_time_db(count($this->exportdata['entries'])); return ($filetime > $dbtime) ? $filetime : $dbtime; } /** * return the sha1 of this content * * @return string */ public function get_sha1() { $file = ''; if ($this->multifiles) { $file = $this->get_sha1_file(); } return sha1(serialize($this->exportdata) . $file); } /** * prepare the package ready to be passed off to the portfolio plugin * * @return void */ public function prepare_package() { $entries = $this->exportdata['entries']; $aliases = array(); $categories = array(); if (is_array($this->exportdata['aliases'])) { foreach ($this->exportdata['aliases'] as $alias) { if (!array_key_exists($alias->entryid, $aliases)) { $aliases[$alias->entryid] = array(); } $aliases[$alias->entryid][] = $alias->alias; } } if (is_array($this->exportdata['categoryentries'])) { foreach ($this->exportdata['categoryentries'] as $cat) { if (!array_key_exists($cat->entryid, $categories)) { $categories[$cat->entryid] = array(); } $categories[$cat->entryid][] = $cat->name; } } if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_SPREADSHEET) { $csv = glossary_generate_export_csv($entries, $aliases, $categories); $this->exporter->write_new_file($csv, clean_filename($this->cm->name) . '.csv', false); return; } else if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $ids = array(); // keep track of these to make into a selection later global $USER, $DB; $writer = $this->get('exporter')->get('format')->leap2a_writer($USER); $format = $this->exporter->get('format'); $filename = $this->get('exporter')->get('format')->manifest_name(); foreach ($entries as $e) { $content = glossary_entry_portfolio_caller::entry_content( $this->course, $this->cm, $this->glossary, $e, (array_key_exists($e->id, $aliases) ? $aliases[$e->id] : array()), $format ); $entry = new portfolio_format_leap2a_entry('glossaryentry' . $e->id, $e->concept, 'entry', $content); $entry->author = $DB->get_record('user', array('id' => $e->userid), 'id,firstname,lastname,email'); $entry->published = $e->timecreated; $entry->updated = $e->timemodified; if (!empty($this->keyedfiles[$e->id])) { $writer->link_files($entry, $this->keyedfiles[$e->id], 'glossaryentry' . $e->id . 'file'); foreach ($this->keyedfiles[$e->id] as $file) { $this->exporter->copy_existing_file($file); } } if (!empty($categories[$e->id])) { foreach ($categories[$e->id] as $cat) { // this essentially treats them as plain tags // leap has the idea of category schemes // but I think this is overkill here $entry->add_category($cat); } } $writer->add_entry($entry); $ids[] = $entry->id; } $selection = new portfolio_format_leap2a_entry('wholeglossary' . $this->glossary->id, get_string('modulename', 'glossary'), 'selection'); $writer->add_entry($selection); $writer->make_selection($selection, $ids, 'Grouping'); $content = $writer->to_xml(); } $this->exporter->write_new_file($content, $filename, true); } /** * make sure that the current user is allowed to do this * * @return boolean */ public function check_permissions() { return has_capability('mod/glossary:export', context_module::instance($this->cm->id)); } /** * return a nice name to be displayed about this export location * * @return string */ public static function display_name() { return get_string('modulename', 'glossary'); } /** * what formats this function *generally* supports * * @return array */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_SPREADSHEET, PORTFOLIO_FORMAT_LEAP2A); } } /** * class to export a single glossary entry * * @package mod_glossary * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class glossary_entry_portfolio_caller extends portfolio_module_caller_base { private $glossary; private $entry; protected $entryid; /** @var array Array that contains all aliases for the given glossary entry. */ private array $aliases = []; /** @var array categories. */ private array $categories = []; /* * @return array */ public static function expected_callbackargs() { return array( 'entryid' => true, 'id' => true, ); } /** * load up all data required for this export. * * @return void */ public function load_data() { global $DB; if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) { throw new portfolio_caller_exception('invalidid', 'glossary'); } if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) { throw new portfolio_caller_exception('invalidid', 'glossary'); } if ($this->entryid) { if (!$this->entry = $DB->get_record('glossary_entries', array('id' => $this->entryid))) { throw new portfolio_caller_exception('noentry', 'glossary'); } // in case we don't have USER this will make the entry be printed $this->entry->approved = true; } $this->categories = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec JOIN {glossary_categories} c ON c.id = ec.categoryid WHERE ec.entryid = ?', array($this->entryid)); $context = context_module::instance($this->cm->id); if ($this->entry->sourceglossaryid == $this->cm->instance) { if ($maincm = get_coursemodule_from_instance('glossary', $this->entry->glossaryid)) { $context = context_module::instance($maincm->id); } } $this->aliases = $DB->get_records('glossary_alias', ['entryid' => $this->entryid]); $fs = get_file_storage(); $this->multifiles = array_merge( $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $this->entry->id, "timemodified", false), $fs->get_area_files($context->id, 'mod_glossary', 'entry', $this->entry->id, "timemodified", false) ); if (!empty($this->multifiles)) { $this->add_format(PORTFOLIO_FORMAT_RICHHTML); } else { $this->add_format(PORTFOLIO_FORMAT_PLAINHTML); } } /** * how long might we expect this export to take * * @return constant one of PORTFOLIO_TIME_XX */ public function expected_time() { return PORTFOLIO_TIME_LOW; } /** * make sure that the current user is allowed to do this * * @return boolean */ public function check_permissions() { $context = context_module::instance($this->cm->id); return has_capability('mod/glossary:exportentry', $context) || ($this->entry->userid == $this->user->id && has_capability('mod/glossary:exportownentry', $context)); } /** * return a nice name to be displayed about this export location * * @return string */ public static function display_name() { return get_string('modulename', 'glossary'); } /** * prepare the package ready to be passed off to the portfolio plugin * * @return void */ public function prepare_package() { global $DB; $format = $this->exporter->get('format'); $content = self::entry_content($this->course, $this->cm, $this->glossary, $this->entry, $this->aliases, $format); if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_PLAINHTML) { $filename = clean_filename($this->entry->concept) . '.html'; $this->exporter->write_new_file($content, $filename); } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_RICHHTML) { if ($this->multifiles) { foreach ($this->multifiles as $file) { $this->exporter->copy_existing_file($file); } } $filename = clean_filename($this->entry->concept) . '.html'; $this->exporter->write_new_file($content, $filename); } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_LEAP2A) { $writer = $this->get('exporter')->get('format')->leap2a_writer(); $entry = new portfolio_format_leap2a_entry('glossaryentry' . $this->entry->id, $this->entry->concept, 'entry', $content); $entry->author = $DB->get_record('user', array('id' => $this->entry->userid), 'id,firstname,lastname,email'); $entry->published = $this->entry->timecreated; $entry->updated = $this->entry->timemodified; if ($this->multifiles) { $writer->link_files($entry, $this->multifiles); foreach ($this->multifiles as $file) { $this->exporter->copy_existing_file($file); } } if ($this->categories) { foreach ($this->categories as $cat) { // this essentially treats them as plain tags // leap has the idea of category schemes // but I think this is overkill here $entry->add_category($cat->name); } } $writer->add_entry($entry); $content = $writer->to_xml(); $filename = $this->get('exporter')->get('format')->manifest_name(); $this->exporter->write_new_file($content, $filename); } else { throw new portfolio_caller_exception('unexpected_format_class', 'glossary'); } } /** * return the sha1 of this content * * @return string */ public function get_sha1() { if ($this->multifiles) { return sha1(serialize($this->entry) . $this->get_sha1_file()); } return sha1(serialize($this->entry)); } /** * what formats this function *generally* supports * * @return array */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); } /** * helper function to get the html content of an entry * for both this class and the full glossary exporter * this is a very simplified version of the dictionary format output, * but with its 500 levels of indirection removed * and file rewriting handled by the portfolio export format. * * @param stdclass $course * @param stdclass $cm * @param stdclass $glossary * @param stdclass $entry * * @return string */ public static function entry_content($course, $cm, $glossary, $entry, $aliases, $format) { global $OUTPUT, $DB; $entry = clone $entry; $context = context_module::instance($cm->id); $options = portfolio_format_text_options(); $options->trusted = $entry->definitiontrust; $options->context = $context; $output = '' . "\n"; $output .= '' . "\n"; $output .= '' . "\n"; if (!empty($aliases)) { $output .= '' . "\n"; } if ($entry->sourceglossaryid == $cm->instance) { if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { return ''; } $filecontext = context_module::instance($maincm->id); } else { $filecontext = $context; } $fs = get_file_storage(); if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) { $output .= '
' . "\n"; $output .= '
'; $output .= format_text($OUTPUT->heading($entry->concept, 3), FORMAT_MOODLE, $options); $output .= '
' . "\n"; $entry->definition = format_text($entry->definition, $entry->definitionformat, $options); $output .= portfolio_rewrite_pluginfile_urls($entry->definition, $context->id, 'mod_glossary', 'entry', $entry->id, $format); if (isset($entry->footer)) { $output .= $entry->footer; } $output .= '
'; $key = (count($aliases) == 1) ? 'alias' : 'aliases'; $output .= get_string($key, 'glossary') . ': '; foreach ($aliases as $alias) { $output .= s($alias->alias) . ','; } $output = substr($output, 0, -1); $output .= '
' . "\n"; foreach ($files as $file) { $output .= $format->file_output($file); } $output .= '
' . "\n"; } $output .= '
' . "\n"; return $output; } } /** * Class representing the virtual node with all itemids in the file browser * * @category files * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class glossary_file_info_container extends file_info { /** @var file_browser */ protected $browser; /** @var stdClass */ protected $course; /** @var stdClass */ protected $cm; /** @var string */ protected $component; /** @var stdClass */ protected $context; /** @var array */ protected $areas; /** @var string */ protected $filearea; /** * Constructor (in case you did not realize it ;-) * * @param file_browser $browser * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @param array $areas * @param string $filearea */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->browser = $browser; $this->course = $course; $this->cm = $cm; $this->component = 'mod_glossary'; $this->context = $context; $this->areas = $areas; $this->filearea = $filearea; } /** * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array( 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null, ); } /** * Can new files or directories be added via the file browser * * @return bool */ public function is_writable() { return false; } /** * Should this node be considered as a folder in the file browser * * @return bool */ public function is_directory() { return true; } /** * Returns localised visible name of this node * * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Returns list of children nodes * * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $sql = 'SELECT DISTINCT f.itemid, ge.concept FROM {files} f JOIN {modules} m ON (m.name = :modulename AND m.visible = 1) JOIN {course_modules} cm ON (cm.module = m.id AND cm.id = :instanceid) JOIN {glossary} g ON g.id = cm.instance JOIN {glossary_entries} ge ON (ge.glossaryid = g.id AND ge.id = f.itemid) WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea'; $params = array( 'modulename' => 'glossary', 'instanceid' => $this->context->instanceid, 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea); if (!$returnemptyfolders) { $sql .= ' AND f.filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f'); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly !== false) { $sql .= ' ORDER BY ge.concept, f.itemid'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if ($child = $this->browser->get_file_info($this->context, 'mod_glossary', $this->filearea, $record->itemid)) { $children[] = $child; } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } /** * Returns glossary entries tagged with a specified tag. * * This is a callback used by the tag area mod_glossary/glossary_entries to search for glossary entries * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_glossary_get_tagged_entries($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT ge.id, ge.concept, ge.glossaryid, ge.approved, ge.userid, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {glossary_entries} ge JOIN {glossary} g ON g.id = ge.glossaryid JOIN {modules} m ON m.name='glossary' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = g.id JOIN {tag_instance} tt ON ge.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND ge.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array('itemtype' => 'glossary_entries', 'tagid' => $tag->id, 'component' => 'mod_glossary', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id, ge.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_glossary', 'glossary_entries', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { global $USER; if ($taggeditem->courseid == $courseid) { $accessible = false; if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) { if ($taggeditem->approved) { $accessible = true; } else if ($taggeditem->userid == $USER->id) { $accessible = true; } else { $accessible = has_capability('mod/glossary:approve', context_module::instance($cm->id)); } } $builder->set_accessible($taggeditem, $accessible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/glossary/showentry.php', array('eid' => $item->id, 'displayformat' => 'dictionary')); $pagename = format_string($item->concept, true, array('context' => context_module::instance($item->cmid))); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $approved = ""; if (!$item->approved) { $approved = '
'. html_writer::span(get_string('entrynotapproved', 'mod_glossary'), 'badge bg-warning text-dark'); } $tagfeed->add($icon, $pagename, $cmname.'
'.$coursename.$approved); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_glossary', 'glossary_entries', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } formats.php000064400000023026152265517410006743 0ustar00libdir.'/adminlib.php'); require_once("lib.php"); $id = required_param('id', PARAM_INT); $mode = optional_param('mode', '', PARAM_ALPHANUMEXT); $url = new moodle_url('/mod/glossary/formats.php', array('id'=>$id)); if ($mode !== '') { $url->param('mode', $mode); } $PAGE->set_url($url); admin_externalpage_setup('managemodules'); // this is hacky, tehre should be a special hidden page for it if ( !$displayformat = $DB->get_record("glossary_formats", array("id"=>$id))) { throw new \moodle_exception('invalidglossaryformat', 'glossary'); } $form = data_submitted(); if ( $mode == 'visible' and confirm_sesskey()) { if ( $displayformat ) { if ( $displayformat->visible ) { $displayformat->visible = 0; } else { $displayformat->visible = 1; } $DB->update_record("glossary_formats",$displayformat); } redirect("$CFG->wwwroot/$CFG->admin/settings.php?section=modsettingglossary#glossary_formats_header"); die; } elseif ( $mode == 'edit' and $form and confirm_sesskey()) { $displayformat->popupformatname = $form->popupformatname; $displayformat->showgroup = $form->showgroup; $displayformat->defaultmode = $form->defaultmode; $displayformat->defaulthook = $form->defaulthook; $displayformat->sortkey = $form->sortkey; $displayformat->sortorder = $form->sortorder; // Extract visible tabs from array into comma separated list. $visibletabs = implode(',', $form->visibletabs); // Include 'standard' tab by default along with other tabs. // This way we don't run into the risk of users not selecting any tab for displayformat. $displayformat->showtabs = GLOSSARY_STANDARD.','.$visibletabs; $DB->update_record("glossary_formats",$displayformat); redirect("$CFG->wwwroot/$CFG->admin/settings.php?section=modsettingglossary#glossary_formats_header"); die; } $strmodulename = get_string("modulename", "glossary"); $strdisplayformats = get_string("displayformats","glossary"); echo $OUTPUT->header(); echo $OUTPUT->heading($strmodulename . ': ' . get_string("displayformats","glossary")); echo $OUTPUT->box(get_string("configwarning", 'admin'), "generalbox boxaligncenter boxwidthnormal"); echo "
"; $yes = get_string("yes"); $no = get_string("no"); echo '
'; echo ''; ?> '; echo $OUTPUT->footer(); ?>edit.php000064400000006675152265517410006230 0ustar00get_record('course', array('id'=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } require_login($course, false, $cm); $context = context_module::instance($cm->id); if (!$glossary = $DB->get_record('glossary', array('id'=>$cm->instance))) { throw new \moodle_exception('invalidid', 'glossary'); } $url = new moodle_url('/mod/glossary/edit.php', array('cmid'=>$cm->id)); if (!empty($id)) { $url->param('id', $id); } $PAGE->set_url($url); if ($id) { // if entry is specified if (isguestuser()) { throw new \moodle_exception('guestnoedit', 'glossary', "$CFG->wwwroot/mod/glossary/view.php?id=$cmid"); } if (!$entry = $DB->get_record('glossary_entries', array('id'=>$id, 'glossaryid'=>$glossary->id))) { throw new \moodle_exception('invalidentry'); } // Check if the user can update the entry (trigger exception if he can't). mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false); // Prepare extra data. $entry = mod_glossary_prepare_entry_for_edition($entry); } else { // new entry require_capability('mod/glossary:write', $context); // note: guest user does not have any write capability $entry = new stdClass(); $entry->id = null; } list($definitionoptions, $attachmentoptions) = glossary_get_editor_and_attachment_options($course, $context, $entry); $entry = file_prepare_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_glossary', 'entry', $entry->id); $entry = file_prepare_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_glossary', 'attachment', $entry->id); $entry->cmid = $cm->id; // create form and set initial data $mform = new mod_glossary_entry_form(null, array('current'=>$entry, 'cm'=>$cm, 'glossary'=>$glossary, 'definitionoptions'=>$definitionoptions, 'attachmentoptions'=>$attachmentoptions)); if ($mform->is_cancelled()){ if ($id){ redirect("view.php?id=$cm->id&mode=entry&hook=$id"); } else { redirect("view.php?id=$cm->id"); } } else if ($data = $mform->get_data()) { $entry = glossary_edit_entry($data, $course, $cm, $glossary, $context); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries') && isset($data->tags)) { core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $data->id, $context, $data->tags); } redirect("view.php?id=$cm->id&mode=entry&hook=$entry->id"); } if (!empty($id)) { $PAGE->navbar->add(get_string('edit')); } $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); echo $OUTPUT->header(); if (!$id) { echo $OUTPUT->heading(get_string('addsingleentry', 'mod_glossary')); } else { echo $OUTPUT->heading(get_string('editentry', 'mod_glossary')); } $data = new StdClass(); $data->tags = core_tag_tag::get_item_tags_array('mod_glossary', 'glossary_entries', $id); $mform->set_data($data); $mform->display(); echo $OUTPUT->footer(); index.php000064400000010556152265517410006403 0ustar00libdir/rsslib.php"); require_once("$CFG->dirroot/course/lib.php"); $id = required_param('id', PARAM_INT); // course $PAGE->set_url('/mod/glossary/index.php', array('id'=>$id)); if (!$course = $DB->get_record('course', array('id'=>$id))) { throw new \moodle_exception('invalidcourseid'); } require_course_login($course); $PAGE->set_pagelayout('incourse'); $context = context_course::instance($course->id); $event = \mod_glossary\event\course_module_instance_list_viewed::create(array( 'context' => $context )); $event->add_record_snapshot('course', $course); $event->trigger(); /// Get all required strings $strglossarys = get_string("modulenameplural", "glossary"); $strglossary = get_string("modulename", "glossary"); $strrss = get_string("rss"); /// Print the header $PAGE->navbar->add($strglossarys, "index.php?id=$course->id"); $PAGE->set_title($strglossarys); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($strglossarys), 2); /// Get all the appropriate data if (! $glossarys = get_all_instances_in_course("glossary", $course)) { notice(get_string('thereareno', 'moodle', $strglossarys), "../../course/view.php?id=$course->id"); die; } $usesections = course_format_uses_sections($course->format); /// Print the list of instances (your module will probably extend this) $timenow = time(); $strname = get_string("name"); $strentries = get_string("entries", "glossary"); $table = new html_table(); if ($usesections) { $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strentries); $table->align = array ('center', 'left', 'center'); } else { $table->head = array ($strname, $strentries); $table->align = array ('left', 'center'); } if ($show_rss = (isset($CFG->enablerssfeeds) && isset($CFG->glossary_enablerssfeeds) && $CFG->enablerssfeeds && $CFG->glossary_enablerssfeeds)) { $table->head[] = $strrss; $table->align[] = 'center'; } $currentsection = ""; foreach ($glossarys as $glossary) { if (!$glossary->visible && has_capability('moodle/course:viewhiddenactivities', context_module::instance($glossary->coursemodule))) { // Show dimmed if the mod is hidden. $link = "coursemodule\">".format_string($glossary->name,true).""; } else if ($glossary->visible) { // Show normal if the mod is visible. $link = "coursemodule\">".format_string($glossary->name,true).""; } else { // Don't show the glossary. continue; } $printsection = ""; if ($usesections) { if ($glossary->section !== $currentsection) { if ($glossary->section) { $printsection = get_section_name($course, $glossary->section); } if ($currentsection !== "") { $table->data[] = 'hr'; } $currentsection = $glossary->section; } } // TODO: count only approved if not allowed to see them $count = $DB->count_records_sql("SELECT COUNT(*) FROM {glossary_entries} WHERE (glossaryid = ? OR sourceglossaryid = ?)", array($glossary->id, $glossary->id)); //If this glossary has RSS activated, calculate it if ($show_rss) { $rsslink = ''; if ($glossary->rsstype and $glossary->rssarticles) { //Calculate the tolltip text $tooltiptext = get_string("rsssubscriberss","glossary",format_string($glossary->name)); if (!isloggedin()) { $userid = 0; } else { $userid = $USER->id; } //Get html code for RSS link $rsslink = rss_get_link($context->id, $userid, 'mod_glossary', $glossary->id, $tooltiptext); } } if ($usesections) { $linedata = array ($printsection, $link, $count); } else { $linedata = array ($link, $count); } if ($show_rss) { $linedata[] = $rsslink; } $table->data[] = $linedata; } echo "
"; echo html_writer::table($table); /// Finish the page echo $OUTPUT->footer(); exportentry.php000064400000010741152265517410007673 0ustar00$id,'prevmode'=>$prevmode)); if ($confirm !== 0) { $url->param('confirm', $confirm); } if ($hook !== 'ALL') { $url->param('hook', $hook); } $PAGE->set_url($url); if (!$entry = $DB->get_record('glossary_entries', array('id'=>$id))) { throw new \moodle_exception('invalidentry'); } if ($entry->sourceglossaryid) { //already exported if (!$cm = get_coursemodule_from_id('glossary', $entry->sourceglossaryid)) { throw new \moodle_exception('invalidcoursemodule'); } redirect('view.php?id='.$cm->id.'&mode=entry&hook='.$entry->id); } if (!$cm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { throw new \moodle_exception('invalidcoursemodule'); } if (!$glossary = $DB->get_record('glossary', array('id'=>$cm->instance))) { throw new \moodle_exception('invalidid', 'glossary'); } if (!$course = $DB->get_record('course', array('id'=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } require_course_login($course->id, true, $cm); $context = context_module::instance($cm->id); require_capability('mod/glossary:export', $context); $returnurl = "view.php?id=$cm->id&mode=$prevmode&hook=".urlencode($hook); if (!$mainglossary = $DB->get_record('glossary', array('course'=>$cm->course, 'mainglossary'=>1))) { //main glossary not present redirect($returnurl); } if (!$maincm = get_coursemodule_from_instance('glossary', $mainglossary->id)) { throw new \moodle_exception('invalidcoursemodule'); } $context = context_module::instance($cm->id); $maincontext = context_module::instance($maincm->id); if (!$course = $DB->get_record('course', array('id'=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } $strglossaries = get_string('modulenameplural', 'glossary'); $entryalreadyexist = get_string('entryalreadyexist','glossary'); $entryexported = get_string('entryexported','glossary'); if (!$mainglossary->allowduplicatedentries) { if ($DB->record_exists_select('glossary_entries', 'glossaryid = :glossaryid AND LOWER(concept) = :concept', array( 'glossaryid' => $mainglossary->id, 'concept' => core_text::strtolower($entry->concept)))) { $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo $OUTPUT->notification(get_string('errconceptalreadyexists', 'glossary')); echo $OUTPUT->continue_button($returnurl); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); die; } } if (!data_submitted() or !$confirm or !confirm_sesskey()) { $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo '
'; $areyousure = '

'.format_string($entry->concept).'

'.get_string('areyousureexport','glossary').'
'.format_string($mainglossary->name).'?'; $linkyes = 'exportentry.php'; $linkno = 'view.php'; $optionsyes = array('id'=>$entry->id, 'confirm'=>1, 'sesskey'=>sesskey(), 'prevmode'=>$prevmode, 'hook'=>$hook); $optionsno = array('id'=>$cm->id, 'mode'=>$prevmode, 'hook'=>$hook); echo $OUTPUT->confirm($areyousure, new moodle_url($linkyes, $optionsyes), new moodle_url($linkno, $optionsno)); echo '

'; echo $OUTPUT->footer(); die; } else { $entry->glossaryid = $mainglossary->id; $entry->sourceglossaryid = $glossary->id; $DB->update_record('glossary_entries', $entry); // move attachments too $fs = get_file_storage(); if ($oldfiles = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id)) { foreach ($oldfiles as $oldfile) { $file_record = new stdClass(); $file_record->contextid = $maincontext->id; $fs->create_file_from_storedfile($file_record, $oldfile); } $fs->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id); $entry->attachment = '1'; } else { $entry->attachment = '0'; } $DB->update_record('glossary_entries', $entry); redirect ($returnurl); } import.php000064400000037461152265517410006612 0ustar00dirroot/course/lib.php"); require_once("$CFG->dirroot/course/modlib.php"); require_once('import_form.php'); $id = required_param('id', PARAM_INT); // Course Module ID $mode = optional_param('mode', 'letter', PARAM_ALPHA ); $hook = optional_param('hook', 'ALL', PARAM_ALPHANUM); $url = new moodle_url('/mod/glossary/import.php', array('id'=>$id)); if ($mode !== 'letter') { $url->param('mode', $mode); } if ($hook !== 'ALL') { $url->param('hook', $hook); } $PAGE->set_url($url); if (! $cm = get_coursemodule_from_id('glossary', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $glossary = $DB->get_record("glossary", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidid', 'glossary'); } require_login($course, false, $cm); $context = context_module::instance($cm->id); require_capability('mod/glossary:import', $context); $strglossaries = get_string("modulenameplural", "glossary"); $strglossary = get_string("modulename", "glossary"); $strallcategories = get_string("allcategories", "glossary"); $straddentry = get_string("addentry", "glossary"); $strnoentries = get_string("noentries", "glossary"); $strsearchindefinition = get_string("searchindefinition", "glossary"); $strsearch = get_string("search"); $strimportentries = get_string('importentriesfromxml', 'glossary'); $PAGE->navbar->add($strimportentries); $PAGE->set_title($glossary->name); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); $form = new mod_glossary_import_form(''); if ($form->is_cancelled()) { redirect(new moodle_url('view.php', ['id' => $id])); } echo $OUTPUT->header(); echo $OUTPUT->heading($strimportentries); if ( !$data = $form->get_data() ) { echo $OUTPUT->box_start('glossarydisplay generalbox'); // display upload form $data = new stdClass(); $data->id = $id; $form->set_data($data); $form->display(); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } $result = $form->get_file_content('file'); if (empty($result)) { echo $OUTPUT->box_start('glossarydisplay generalbox'); echo $OUTPUT->continue_button('import.php?id='.$id); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); die(); } // Large exports are likely to take their time and memory. core_php_time_limit::raise(); raise_memory_limit(MEMORY_EXTRA); if ($xml = glossary_read_imported_file($result)) { $importedentries = 0; $importedcats = 0; $entriesrejected = 0; $rejections = ''; $glossarycontext = $context; if ($data->dest == 'newglossary') { // If the user chose to create a new glossary $xmlglossary = $xml['GLOSSARY']['#']['INFO'][0]['#']; if ( $xmlglossary['NAME'][0]['#'] ) { $glossary = new stdClass(); $glossary->modulename = 'glossary'; $glossary->module = $cm->module; $glossary->name = ($xmlglossary['NAME'][0]['#']); $glossary->globalglossary = ($xmlglossary['GLOBALGLOSSARY'][0]['#']); $glossary->intro = ($xmlglossary['INTRO'][0]['#']); $glossary->introformat = isset($xmlglossary['INTROFORMAT'][0]['#']) ? $xmlglossary['INTROFORMAT'][0]['#'] : FORMAT_MOODLE; $glossary->showspecial = ($xmlglossary['SHOWSPECIAL'][0]['#']); $glossary->showalphabet = ($xmlglossary['SHOWALPHABET'][0]['#']); $glossary->showall = ($xmlglossary['SHOWALL'][0]['#']); $glossary->cmidnumber = null; // Setting the default values if no values were passed if ( isset($xmlglossary['ENTBYPAGE'][0]['#']) ) { $glossary->entbypage = ($xmlglossary['ENTBYPAGE'][0]['#']); } else { $glossary->entbypage = $CFG->glossary_entbypage; } if ( isset($xmlglossary['ALLOWDUPLICATEDENTRIES'][0]['#']) ) { $glossary->allowduplicatedentries = ($xmlglossary['ALLOWDUPLICATEDENTRIES'][0]['#']); } else { $glossary->allowduplicatedentries = $CFG->glossary_dupentries; } if ( isset($xmlglossary['DISPLAYFORMAT'][0]['#']) ) { $glossary->displayformat = ($xmlglossary['DISPLAYFORMAT'][0]['#']); } else { $glossary->displayformat = 2; } if ( isset($xmlglossary['ALLOWCOMMENTS'][0]['#']) ) { $glossary->allowcomments = ($xmlglossary['ALLOWCOMMENTS'][0]['#']); } else { $glossary->allowcomments = $CFG->glossary_allowcomments; } if ( isset($xmlglossary['USEDYNALINK'][0]['#']) ) { $glossary->usedynalink = ($xmlglossary['USEDYNALINK'][0]['#']); } else { $glossary->usedynalink = $CFG->glossary_linkentries; } if ( isset($xmlglossary['DEFAULTAPPROVAL'][0]['#']) ) { $glossary->defaultapproval = ($xmlglossary['DEFAULTAPPROVAL'][0]['#']); } else { $glossary->defaultapproval = $CFG->glossary_defaultapproval; } // These fields were not included in export, assume zero. $glossary->assessed = 0; $glossary->availability = null; // Check if we're creating the new glossary on the front page or inside a course. if ($cm->course == SITEID) { // On the front page, activities are in section 1. $glossary->section = 1; } else { // Inside a course, add to the general section (0). $glossary->section = 0; } // New glossary is always visible. $glossary->visible = 1; $glossary->visibleoncoursepage = 1; // Include new glossary and return the new ID if ( !($glossary = add_moduleinfo($glossary, $course)) ) { echo $OUTPUT->notification("Error while trying to create the new glossary."); glossary_print_tabbed_table_end(); echo $OUTPUT->footer(); exit; } else { $glossarycontext = context_module::instance($glossary->coursemodule); glossary_xml_import_files($xmlglossary, 'INTROFILES', $glossarycontext->id, 'intro', 0); echo $OUTPUT->box(get_string("newglossarycreated","glossary"),'generalbox boxaligncenter boxwidthnormal'); } } else { echo $OUTPUT->notification("Error while trying to create the new glossary."); echo $OUTPUT->footer(); exit; } } $xmlentries = $xml['GLOSSARY']['#']['INFO'][0]['#']['ENTRIES'][0]['#']['ENTRY']; $sizeofxmlentries = is_array($xmlentries) ? count($xmlentries) : 0; for($i = 0; $i < $sizeofxmlentries; $i++) { // Inserting the entries $xmlentry = $xmlentries[$i]; $newentry = new stdClass(); $newentry->concept = trim($xmlentry['#']['CONCEPT'][0]['#']); $definition = $xmlentry['#']['DEFINITION'][0]['#']; if (!is_string($definition)) { throw new \moodle_exception('errorparsingxml', 'glossary'); } $newentry->definition = trusttext_strip($definition); if (isset($xmlentry['#']['DEFINITIONTRUST'][0]['#'])) { $newentry->definitiontrust = !empty($xmlentry['#']['DEFINITIONTRUST'][0]['#']) && trusttext_trusted($context); } if ( isset($xmlentry['#']['CASESENSITIVE'][0]['#']) ) { $newentry->casesensitive = $xmlentry['#']['CASESENSITIVE'][0]['#']; } else { $newentry->casesensitive = $CFG->glossary_casesensitive; } $permissiongranted = 1; if ( $newentry->concept and $newentry->definition ) { if ( !$glossary->allowduplicatedentries ) { // checking if the entry is valid (checking if it is duplicated when should not be) if ( $newentry->casesensitive ) { $dupentry = $DB->record_exists_select('glossary_entries', 'glossaryid = :glossaryid AND concept = :concept', array( 'glossaryid' => $glossary->id, 'concept' => $newentry->concept)); } else { $dupentry = $DB->record_exists_select('glossary_entries', 'glossaryid = :glossaryid AND LOWER(concept) = :concept', array( 'glossaryid' => $glossary->id, 'concept' => core_text::strtolower($newentry->concept))); } if ($dupentry) { $permissiongranted = 0; } } } else { $permissiongranted = 0; } if ($permissiongranted) { $newentry->glossaryid = $glossary->id; $newentry->sourceglossaryid = 0; $newentry->approved = 1; $newentry->userid = $USER->id; $newentry->teacherentry = 1; $newentry->definitionformat = $xmlentry['#']['FORMAT'][0]['#']; $newentry->timecreated = time(); $newentry->timemodified = time(); // Setting the default values if no values were passed if ( isset($xmlentry['#']['USEDYNALINK'][0]['#']) ) { $newentry->usedynalink = $xmlentry['#']['USEDYNALINK'][0]['#']; } else { $newentry->usedynalink = $CFG->glossary_linkentries; } if ( isset($xmlentry['#']['FULLMATCH'][0]['#']) ) { $newentry->fullmatch = $xmlentry['#']['FULLMATCH'][0]['#']; } else { $newentry->fullmatch = $CFG->glossary_fullmatch; } $newentry->id = $DB->insert_record("glossary_entries",$newentry); $importedentries++; $xmlaliases = @$xmlentry['#']['ALIASES'][0]['#']['ALIAS']; // ignore missing ALIASES $sizeofxmlaliases = is_array($xmlaliases) ? count($xmlaliases) : 0; for($k = 0; $k < $sizeofxmlaliases; $k++) { /// Importing aliases $xmlalias = $xmlaliases[$k]; $aliasname = $xmlalias['#']['NAME'][0]['#']; if (!empty($aliasname)) { $newalias = new stdClass(); $newalias->entryid = $newentry->id; $newalias->alias = trim($aliasname); $newalias->id = $DB->insert_record("glossary_alias",$newalias); } } if (!empty($data->catsincl)) { // If the categories must be imported... $xmlcats = @$xmlentry['#']['CATEGORIES'][0]['#']['CATEGORY']; // ignore missing CATEGORIES $sizeofxmlcats = is_array($xmlcats) ? count($xmlcats) : 0; for($k = 0; $k < $sizeofxmlcats; $k++) { $xmlcat = $xmlcats[$k]; $newcat = new stdClass(); $newcat->name = $xmlcat['#']['NAME'][0]['#']; $newcat->usedynalink = $xmlcat['#']['USEDYNALINK'][0]['#']; if ( !$category = $DB->get_record("glossary_categories", array("glossaryid"=>$glossary->id,"name"=>$newcat->name))) { // Create the category if it does not exist $category = new stdClass(); $category->name = $newcat->name; $category->glossaryid = $glossary->id; $category->id = $DB->insert_record("glossary_categories",$category); $importedcats++; } if ( $category ) { // inserting the new relation $entrycat = new stdClass(); $entrycat->entryid = $newentry->id; $entrycat->categoryid = $category->id; $DB->insert_record("glossary_entries_categories",$entrycat); } } } // Import files embedded in the entry text. glossary_xml_import_files($xmlentry['#'], 'ENTRYFILES', $glossarycontext->id, 'entry', $newentry->id); // Import files attached to the entry. if (glossary_xml_import_files($xmlentry['#'], 'ATTACHMENTFILES', $glossarycontext->id, 'attachment', $newentry->id)) { $DB->update_record("glossary_entries", array('id' => $newentry->id, 'attachment' => '1')); } // Import tags associated with the entry. if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { $xmltags = @$xmlentry['#']['TAGS'][0]['#']['TAG']; // Ignore missing TAGS. $sizeofxmltags = is_array($xmltags) ? count($xmltags) : 0; for ($k = 0; $k < $sizeofxmltags; $k++) { // Importing tags. $tag = $xmltags[$k]['#']; if (!empty($tag)) { core_tag_tag::add_item_tag('mod_glossary', 'glossary_entries', $newentry->id, $glossarycontext, $tag); } } } } else { $entriesrejected++; if ( $newentry->concept and $newentry->definition ) { // add to exception report (duplicated entry)) $rejections .= "" . ""; } else { // add to exception report (no concept or definition found)) $rejections .= "" . ""; } } } // Reset caches. \mod_glossary\local\concept_cache::reset_glossary($glossary); // processed entries echo $OUTPUT->box_start('glossarydisplay generalbox'); echo '
name,'glossary'); ?>
get_records("glossary_formats"); $formats = array(); //Take names foreach ($recformats as $format) { $formats[$format->name] = get_string("displayformat$format->name", "glossary"); } //Sort it asort($formats); echo html_writer::select($formats, 'popupformatname', $displayformat->popupformatname, false); ?>













" />
$newentry->concept" . get_string("duplicateentry","glossary"). "
---" . get_string("noconceptfound","glossary"). "
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if (!empty($data->catsincl)) { echo ''; echo ''; echo ''; echo ''; } echo '
'; echo get_string("totalentries","glossary"); echo ':'; echo $importedentries + $entriesrejected; echo '
'; echo get_string("importedentries","glossary"); echo ':'; echo $importedentries; if ( $entriesrejected ) { echo ' (' . get_string("rejectedentries","glossary") . ": $entriesrejected)"; } echo '
'; echo get_string("importedcategories","glossary"); echo ':'; echo $importedcats; echo '

'; // rejected entries if ($rejections) { echo $OUTPUT->heading(get_string("rejectionrpt","glossary"), 4); echo ''; echo $rejections; echo '

'; } /// Print continue button, based on results if ($importedentries) { echo $OUTPUT->continue_button('view.php?id='.$id); } else { echo $OUTPUT->continue_button('import.php?id='.$id); } echo $OUTPUT->box_end(); } else { echo $OUTPUT->box_start('glossarydisplay generalbox'); echo get_string('errorparsingxml', 'glossary'); echo $OUTPUT->continue_button('import.php?id='.$id); echo $OUTPUT->box_end(); } /// Finish the page echo $OUTPUT->footer(); TODO.txt000064400000001137152265517410006064 0ustar00Things that are in the inkpot yet: ================================== - Allow grading of entries * Evaluation * Self-evaluation? * Co-evaluation? ********* IN BETA RIGHT NOW ********* - Adding a way to specify default values for display formats: * Default view * Default Order * Show (or do not) the group breaks. * Default view for automatic links, entry view, etc. * What else? Post in http://moodle.org/mod/forum/view.php?id=742 News about plugin-formats TODO (Updated: Jul 30, 2004 - Eloy) *Include support for a customised print view in each format. UPGRADING.md000064400000000371152265517410006417 0ustar00# mod_glossary Upgrade notes ## 5.0.4 ### Added - Function mod_glossary_rating_can_see_item_ratings is now implemented for checking permissions to view ratings. For more information see [MDL-86960](https://tracker.moodle.org/browse/MDL-86960) approve.php000064400000004344152265517410006746 0ustar00 $eid, 'mode' => $mode, 'hook' => $hook, 'newstate' => $newstate)); $PAGE->set_url($url); $entry = $DB->get_record('glossary_entries', array('id'=> $eid), '*', MUST_EXIST); $glossary = $DB->get_record('glossary', array('id'=> $entry->glossaryid), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('glossary', $glossary->id, 0, false, MUST_EXIST); $course = $DB->get_record('course', array('id'=> $cm->course), '*', MUST_EXIST); require_login($course, false, $cm); $context = context_module::instance($cm->id); require_capability('mod/glossary:approve', $context); if (($newstate != $entry->approved) && confirm_sesskey()) { $newentry = new stdClass(); $newentry->id = $entry->id; $newentry->approved = $newstate; $newentry->timemodified = time(); // wee need this date here to speed up recent activity, TODO: use timestamp in approved field instead in 2.0 $DB->update_record("glossary_entries", $newentry); // Trigger event about entry approval/disapproval. $params = array( 'context' => $context, 'objectid' => $entry->id ); if ($newstate) { $event = \mod_glossary\event\entry_approved::create($params); } else { $event = \mod_glossary\event\entry_disapproved::create($params); } $entry->approved = $newstate ? 1 : 0; $entry->timemodified = $newentry->timemodified; $event->add_record_snapshot('glossary_entries', $entry); $event->trigger(); // Update completion state $completion = new completion_info($course); if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $glossary->completionentries) { $completion->update_state($cm, COMPLETION_COMPLETE, $entry->userid); } // Reset caches. if ($entry->usedynalink) { \mod_glossary\local\concept_cache::reset_glossary($glossary); } } redirect("view.php?id=$cm->id&mode=$mode&hook=$hook"); formats/dictionary/dictionary_format.php000064400000003045152265517410014624 0ustar00'; echo ''; echo ''; glossary_print_entry_approval($cm, $entry, $mode); echo '
'; glossary_print_entry_concept($entry); echo '
'; glossary_print_entry_definition($entry, $glossary, $cm); glossary_print_entry_attachment($entry, $cm, 'html'); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { echo $OUTPUT->tag_list(core_tag_tag::get_item_tags('mod_glossary', 'glossary_entries', $entry->id), null, 'glossary-tags'); } echo ''; echo ''; glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, $printicons, $aliases); echo ''; echo ''; echo "\n"; } function glossary_print_entry_dictionary($course, $cm, $glossary, $entry, $mode='', $hook='', $printicons=1) { //The print view for this format is exactly the normal view, so we use it //Take out autolinking in definitions in print view $entry->definition = ''.$entry->definition.''; //Call to view function (without icons, ratings and aliases) and return its result return glossary_show_entry_dictionary($course, $cm, $glossary, $entry, $mode, $hook, false, false, false); } formats/TEMPLATE/TEMPLATE_format.php000064400000010206152265517410012735 0ustar00get_record('user', array('id'=>$entry->userid)); $strby = get_string('writtenby', 'glossary'); if ($entry) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo "
'; //Use this function to show author's image //Comments: Configuration not supported echo $OUTPUT->user_picture($user, array('courseid'=>$course->id)); //Line separator to show this template fine. :-) echo '
'; //Use this code to show author's name //Comments: Configuration not supported $fullname = fullname($user); $by = new stdClass(); $by->name = ''.$fullname.''; $by->date = userdate($entry->timemodified); echo ''.get_string('bynameondate', 'forum', $by).'' . '
'; //Use this code to show modification date //Comments: Configuration not supported echo get_string('lastedited').': '. userdate($entry->timemodified) . '
'; //Use this function to show the approval button. It'll be shown if necessary //Comments: You can configure this parameters: //----Define where to show the approval button $approvalalign = 'right'; //Values: left, center and right (default right) //----Define if the approval button must be showed into a 100% width table $approvalinsidetable = true; //Values: true, false (default true) //Call the function glossary_print_entry_approval($cm, $entry, $mode, $approvalalign, $approvalinsidetable); //Line separator to show this template fine. :-) echo '
'; echo '
'; //Line separator to show this template fine. :-) echo "
\n"; echo '
'; //Use this function to print the concept in a heading

//Comments: Configuration not supported glossary_print_entry_concept($entry); //Line separator not normally needed now. //echo "
\n"; //Use this function to show the definition //Comments: Configuration not supported glossary_print_entry_definition($entry, $glossary, $cm); // Use this function to show the attachment. It'll be shown if necessary. glossary_print_entry_attachment($entry, $cm, 'html'); //Line separator to show this template fine. :-) echo "
\n"; //Use this function to show aliases, editing icons and ratings (all know as the 'lower section') //Comments: You can configure this parameters: //----Define when to show the aliases popup // use it only if you are really sure! //$aliases = true; //Values: true, false (Default: true) //----Uncoment this line to avoid editing icons being showed // use it only if you are really sure! //$printicons = false; glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, $printicons, $aliases); echo '

\n"; } else { echo '
'; print_string('noentry', 'glossary'); echo '
'; } } function glossary_print_entry_TEMPLATE($course, $cm, $glossary, $entry, $mode='', $hook='', $printicons=1) { //The print view for this format is exactly the normal view, so we use it //Anyway, you can modify this to use your own print format!! //Take out autolinking in definitions in print view $entry->definition = ''.$entry->definition.''; //Call to view function (without icons, ratings and aliases) and return its result return glossary_show_entry_TEMPLATE($course, $cm, $glossary, $entry, $mode, $hook, false, false, false); } formats/fullwithoutauthor/fullwithoutauthor_format.php000064400000004204152265517410017732 0ustar00'; echo ''; echo ''; echo '
'; glossary_print_entry_concept($entry); echo '
'; echo '('.get_string('lastedited').': '. userdate($entry->timemodified).')'; echo ''; echo ''; glossary_print_entry_approval($cm, $entry, $mode); echo ''; echo ''; echo ''; echo ''; glossary_print_entry_definition($entry, $glossary, $cm); glossary_print_entry_attachment($entry, $cm, 'html'); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { echo $OUTPUT->tag_list( core_tag_tag::get_item_tags('mod_glossary', 'glossary_entries', $entry->id), null, 'glossary-tags'); } echo ''; echo ''; glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, $printicons, $aliases); echo ' '; echo ''; echo "\n"; } else { echo '
'; print_string('noentry', 'glossary'); echo '
'; } } function glossary_print_entry_fullwithoutauthor($course, $cm, $glossary, $entry, $mode="", $hook="", $printicons=1) { //The print view for this format is exactly the normal view, so we use it //Take out autolinking in definitions un print view $entry->definition = ''.$entry->definition.''; //Call to view function (without icons, ratings and aliases) and return its result return glossary_show_entry_fullwithoutauthor($course, $cm, $glossary, $entry, $mode, $hook, false, false, false); } formats/entrylist/entrylist_format.php000064400000005123152265517410014403 0ustar00'; echo ''; echo ''; if ($entry) { glossary_print_entry_approval($cm, $entry, $mode); $anchortagcontents = glossary_print_entry_concept($entry, true); $link = new moodle_url('/mod/glossary/showentry.php', array('courseid' => $course->id, 'eid' => $entry->id, 'displayformat' => 'dictionary')); $anchor = html_writer::link($link, $anchortagcontents); echo "
$anchor
"; echo ''; if ($printicons) { glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode, $hook,'print'); } if (!empty($entry->rating)) { echo '
'; echo ''; $return = glossary_print_entry_ratings($course, $entry); echo ''; } echo '
'; } else { echo '
'; print_string('noentry', 'glossary'); echo '
'; } echo ''; echo ""; echo "
\n"; return $return; } function glossary_print_entry_entrylist($course, $cm, $glossary, $entry, $mode='', $hook='', $printicons=1) { //Take out autolinking in definitions un print view // TODO use tags MDL-15555. $entry->definition = ''.$entry->definition.''; echo html_writer::start_tag('table', array('class' => 'glossarypost entrylist mod-glossary-entrylist')); echo html_writer::start_tag('tr'); echo html_writer::start_tag('td', array('class' => 'entry mod-glossary-entry')); echo html_writer::start_tag('div', array('class' => 'mod-glossary-concept')); glossary_print_entry_concept($entry); echo html_writer::end_tag('div'); echo html_writer::start_tag('div', array('class' => 'mod-glossary-definition')); glossary_print_entry_definition($entry, $glossary, $cm); echo html_writer::end_tag('div'); echo html_writer::start_tag('div', array('class' => 'mod-glossary-lower-section')); glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, false, false); echo html_writer::end_tag('div'); echo html_writer::end_tag('td'); echo html_writer::end_tag('tr'); echo html_writer::end_tag('table'); } formats/encyclopedia/encyclopedia_format.php000064400000005310152265517410015405 0ustar00get_record('user', array('id'=>$entry->userid)); $strby = get_string('writtenby', 'glossary'); if ($entry) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo "
'; echo $OUTPUT->user_picture($user, array('courseid'=>$course->id)); echo ''; echo '
'; glossary_print_entry_concept($entry); echo '
'; $fullname = fullname($user); $by = new stdClass(); $by->name = ''.$fullname.''; $by->date = userdate($entry->timemodified); echo ''.get_string('bynameondate', 'forum', $by).''; echo '
'; glossary_print_entry_approval($cm, $entry, $mode); echo '
 '; glossary_print_entry_definition($entry, $glossary, $cm); glossary_print_entry_attachment($entry, $cm, null); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { echo $OUTPUT->tag_list( core_tag_tag::get_item_tags('mod_glossary', 'glossary_entries', $entry->id), null, 'glossary-tags'); } if ($printicons or $aliases) { echo '
'; glossary_print_entry_lower_section($course, $cm, $glossary, $entry,$mode,$hook,$printicons,$aliases); echo ' '; } echo '
\n"; } else { echo '
'; print_string('noentry', 'glossary'); echo '
'; } } function glossary_print_entry_encyclopedia($course, $cm, $glossary, $entry, $mode='', $hook='', $printicons=1) { //The print view for this format is exactly the normal view, so we use it //Take out autolinking in definitions un print view $entry->definition = ''.$entry->definition.''; //Call to view function (without icons, ratings and aliases) and return its result return glossary_show_entry_encyclopedia($course, $cm, $glossary, $entry, $mode, $hook, false, false); } formats/continuous/continuous_format.php000064400000003110152265517410014717 0ustar00'; echo ''; echo ''; glossary_print_entry_approval($cm, $entry, $mode); echo '
'; glossary_print_entry_concept($entry); echo '
'; glossary_print_entry_definition($entry, $glossary, $cm); glossary_print_entry_attachment($entry, $cm, 'html'); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { echo $OUTPUT->tag_list(core_tag_tag::get_item_tags( 'mod_glossary', 'glossary_entries', $entry->id), null, 'glossary-tags'); } $entry->alias = ''; echo ''; echo ''; glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, $printicons, $aliases, false); echo ''; echo ''; echo "\n"; } function glossary_print_entry_continuous($course, $cm, $glossary, $entry, $mode='', $hook='', $printicons=1) { //The print view for this format is exactly the normal view, so we use it //Take out autolinking in definitions un print view $entry->definition = ''.$entry->definition.''; //Call to view function (without icons, ratings and aliases) and return its result glossary_show_entry_continuous($course, $cm, $glossary, $entry, $mode, $hook, false, false, false); } formats/faq/faq_format.php000064400000004254152265517410011633 0ustar00'; echo ''; echo ''; $entry->course = $course->id; echo '
' . get_string('question','glossary') . ': '; glossary_print_entry_concept($entry); echo '
'; echo '('.get_string('lastedited').': '. userdate($entry->timemodified).')'; echo ''; echo ''; glossary_print_entry_approval($cm, $entry, $mode); echo ''; echo ''; echo "\n"; echo ''; echo ''.get_string('answer','glossary').': '; glossary_print_entry_definition($entry, $glossary, $cm); glossary_print_entry_attachment($entry, $cm, 'html'); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { echo $OUTPUT->tag_list( core_tag_tag::get_item_tags('mod_glossary', 'glossary_entries', $entry->id), null, 'glossary-tags'); } echo ''; echo ''; glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, $printicons, $aliases); echo ''; } else { echo '
'; print_string('noentry', 'glossary'); echo '
'; } } function glossary_print_entry_faq($course, $cm, $glossary, $entry, $mode='', $hook='', $printicons=1) { //The print view for this format is exactly the normal view, so we use it //Take out autolinking in definitions un print view $entry->definition = ''.$entry->definition.''; //Call to view function (without icons, ratings and aliases) and return its result return glossary_show_entry_faq($course, $cm, $glossary, $entry, $mode, $hook, false, false, false); } formats/fullwithauthor/fullwithauthor_format.php000064400000005300152265517410016450 0ustar00get_record('user', array('id'=>$entry->userid)); $strby = get_string('writtenby', 'glossary'); if ($entry) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo "
'; echo $OUTPUT->user_picture($user, array('courseid'=>$course->id)); echo ''; echo '
'; glossary_print_entry_concept($entry); echo '
'; $fullname = fullname($user); $by = new stdClass(); $by->name = ''.$fullname.''; $by->date = userdate($entry->timemodified); echo ''.get_string('bynameondate', 'forum', $by).''; echo '
'; glossary_print_entry_approval($cm, $entry, $mode); echo '
 '; glossary_print_entry_definition($entry, $glossary, $cm); glossary_print_entry_attachment($entry, $cm, 'html'); if (core_tag_tag::is_enabled('mod_glossary', 'glossary_entries')) { echo $OUTPUT->tag_list( core_tag_tag::get_item_tags('mod_glossary', 'glossary_entries', $entry->id), null, 'glossary-tags'); } echo '
 '; glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, $printicons, $aliases); echo ' '; echo '
\n"; } else { echo '
'; print_string('noentry', 'glossary'); echo '
'; } } function glossary_print_entry_fullwithauthor($course, $cm, $glossary, $entry, $mode="", $hook="", $printicons=1) { //The print view for this format is exactly the normal view, so we use it //Take out autolinking in definitions un print view $entry->definition = ''.$entry->definition.''; //Call to view function (without icons, ratings and aliases) and return its result return glossary_show_entry_fullwithauthor($course, $cm, $glossary, $entry, $mode, $hook, false, false, false); } formats/README.txt000064400000003476152265517410007737 0ustar00GLOSSARY FORMAT PLUGINS ----------------------- Starting with Moodle 1.4, the glossary module supports a plugin architecture to create your own formats. This plugin system allows you to "invent" your own presentations for your glossary data. :-) To facilitate the creation process a TEMPLATE format has been created. You should use it as the base for your own modifications (this requires some basic PHP skills). The template includes all the available data. Please, follow these STEPS: 1.-Think of an English word (or short phrase) to define your format. For further reference in this document, we call it "THENAME". 2.-Duplicate the TEMPLATE directory (under mod/glossary/formats/TEMPLATE). 3.-Rename it to THENAME 4.-Go into the THENAME directory and rename the TEMPLATE_format.php file to THENAME_format.php 5.-Edit the THENAME_format.php file. Change every ocurrence of TEMPLATE to THENAME. 6.-Login into Moodle. Go to any glossary in your site. 7.-Edit (configure) your glossary. In the Format popup you'll see a new entry. It will be showed as "displayformatTHENAME". Select it and view your glossary. 8.-Edit the THENAME_format.php. Make your format modifications and reload your web page to see them in your glossary. This file has been commented to make things easier to understand (further suggestions welcome!) 9.-If you want to translate your THENAME format name to some nice name to be showed in the Format popup, simply, edit your lang/XX/glossary.php file (where XX is your language) and create a new displayformatTHENAME string. 10.-Enjoy!! (and don't forget to send your amazing glossary formats to the Glossary forum on http://moodle.org. They will be welcome!! ;-) To talk about Glossary formats, go to: http://moodle.org/mod/forum/view.php?id=742 Eloy (stronk7) 08/02/2004 (MM/DD/YYYY) :-D tests/fixtures/musicians.xml000064400000002030152265517410012277 0ustar00 Musicians 1 0 dictionary 1 1 1 0 1 1 0 10 Paul McCartney <p>Popular British composer, guitarist, and vocalist. <br></p> 1 1 0 0 1 Beatles The Quarrymen Wings tests/fixtures/texfilter_glossary_en.xml000064400000313431152265517410014731 0ustar00 <b>TeX Filter Reference English en_20040328_SR02</b> <br /><span style="color: rgb(128, 0, 0);">Find entries by searching for (partial) words or symbols. Enable <b>&quot;search full text&quot;</b> to search not only in the &quot;concept field&quot; but also in the description / definition field. Are you <b>new to the TeX Filter or to TeX?</b> Then you might want to read step by step through the different categories, starting with category <b>&quot;01 Getting started&quot;</b>.</span><br /> <p style="font-size: 8pt;"><u>Test your TeX-Syntax:</u><br /> <a href="http://moodle.org/filter/tex/texdebug.php" target="_new">Test your syntax interactively here</a>. This Link is also available on each moodle site itself: &quot;<i>../filter/tex/texdebug.php</i>&quot;<br /></p> <p style="font-size: 8pt;"><u>About the TeX filter:</u><br />The TeX filter for Moodle has been implemented by <a href="mailto:fiedorow@math.ohio-state.edu">Zbigniew Fiedorow</a>. It is based on <a href="http://www.forkosh.com" target="_new">John Forkosh's mimeTeX</a> 1.40.<br /><u>Contacts:</u><br />For suggestions, critics, help etc. about <i>this reference</i> visit the <a href="http://emathpool.net">eMathPool</a> website or <a href="mailto:andreas.leiser@emathpool.net">send me</a> an email.<br />For any other information about the TeX filter or about moodle, please visit the <a href="http://moodle.org">moodle</a> website.<br /> <u>Contributions by:</u> Andreas Leiser, Mark Burnet</p> 1 0 2 1 1 1 1 0 0 0 5 \small <ul><li>\small </li><li><u>Ex.:</u> $$$\small~3x$$$ gives $$\small~3x$$</li></ul> 1 0 0 0 1 font size, 03 Font Styles 1 \normalsize <ul><li>Everthing following the \normalsize command will be output in the smallest predefined font size until the system encounters another font size command. </li><li>\normalsize is the default font size, i.e. the size automatically chosen if there is no font size command</li><li><u>Ex.:</u> $$$\normalsize~3x$$$ gives $$\normalsize~3x$$</li></ul> 1 0 0 0 1 font size, 03 Font Styles 1 \hspace{n} <ul><li>inserts a space of n pixels</li><li><u>Ex.:</u> $$$f(x)\hspace{6}=\hspace{6}0$$$ gives $$f(x)\hspace{6}=\hspace{6}0$$ </li><li>can be combined with the preceding command \unitlength{m}(default: m=1px) , which defines the applied unit</li><li><u>Ex.:</u> $$$\unitlength{20}a\hspace{2}b$$$ gives $$\unitlength{20}a\hspace{2}b$$ , i.e. a space of 20x2=40px</li></ul> 1 0 0 0 1 math spaces 05 Spaces 1 \tiny <ul><li>Everthing following the \tiny command will be output in the smallest predefined font size until the system encounters another font size command.</li><li><u>Ex.:</u> $$$\tiny~3x$$$ gives $$\tiny~3x$$</li></ul> 1 0 0 0 1 font size, 03 Font Styles 1 \_ (where _ is blank) <ul><li>Ordinary whitespace to be used after a dot not denoting the end of a sentence</li><li>After commands without parameters use \~ (tilde) instead in order to avoid browser specific problems</li></ul> 1 0 0 0 1 math spaces, whitespace, blank space 05 Spaces 1 \/ (backslash slash) <ul><li>\/ (backslash slash) avoids ligatures </li><li><u>Ex.:</u> $$$V\/A$$$ gives $$V\/A$$ in contrast to $$$VA$$$ which gives $$VA$$</li></ul> 1 0 0 0 1 math spaces, ligature 05 Spaces 1 \quad <ul><li>inserts a space of current character set size</li><li><u>Ex.:</u> $$$a\quad~b$$$ gives $$a\quad~b$$</li></ul> 1 0 0 0 1 math spaces 05 Spaces 1 \; <ul><li>\; (backslash semicolon) inserts the third smallest predefined space in a formula </li><li>Equivalent: \hspace{6} </li><li><u>Ex.:</u> $$$a\;b$$$ gives $$a\;b$$ </li><li><u>Ex.:</u> $$$a~\hspace{6}~b$$$ gives also $$a~\hspace{6}~b$$</li></ul> 1 0 0 0 1 math spaces 05 Spaces 1 \: <ul><li>\: inserts the second smallest predefined space in a formula </li><li>Equivalent: \hspace{4}</li><li><u>Ex.:</u> $$$a\:b$$$ gives $$a\:b$$ </li><li><u>Ex.:</u> $$$a~\hspace{4}~b$$$ gives also $$a~\hspace{4}~b$$</li></ul> 1 0 0 0 1 math spaces 05 Spaces 1 \, <ul><li>\, inserts the smallest predefined space in a formula</li><li>Equivalent: \hspace{2}</li><li><u>Ex.:</u> $$$a\,b$$$ gives $$a\,b$$</li><li><u>Ex.:</u> $$$a~\hspace{2}~b$$$ gives also $$a~\hspace{2}~b$$</li></ul> 1 0 0 0 1 math spaces 05 Spaces 1 math spaces <p>List of predefined spaces:</p><p><table cellspacing="0" cellpadding="1" width="100%"><tbody><tr><td class="activitylabel"><table cellspacing="1" cellpadding="1" width="100%" align="left" border="1"><tbody><tr style="COLOR: #ffffff; BACKGROUND-COLOR: #aaaaaa" width="33%"><td valign="top" width="100%" colspan="3"><strong>Math Spaces</strong></td></tr><tr style="BACKGROUND-COLOR: #dddddd" width="33%"><td valign="top" width="33%"><strong>Command</strong></td><td valign="top" width="33%"><strong>Example</strong></td><td valign="top" width="33%"><strong>Result</strong></td></tr><tr><td valign="top" width="33%">\, (smallest predefined)</td><td valign="top" width="33%">$$$a\,b$$$</td><td valign="top" width="33%">$$a\,b$$</td></tr><tr><td valign="top" width="33%">\:  (second smallest predefined)</td><td valign="top" width="33%">$$$a\:b$$$</td><td valign="top" width="33%">$$a\:b$$</td></tr><tr><td valign="top" width="33%">\;  (third smallest predefined)</td><td valign="top" width="33%">$$$a\;b$$$</td><td valign="top" width="33%">$$a\;b$$ </td></tr><tr><td valign="top" width="33%">\/  (avoiding ligatures)</td><td valign="top" width="33%">$$$V\/A$$$ instead of $$$VA$$$</td><td valign="top" width="33%">$$V\/A$$ instead of $$VA$$</td></tr><tr><td valign="top" width="33%">\quad  (space of current character set size)</td><td valign="top" width="33%">$$$a\quad~b$$$</td><td valign="top" width="33%">$$a\quad~b$$</td></tr><tr><td valign="top" width="33%">\qquad  (double space of current character set size)</td><td valign="top" width="33%">$$$a\qquad~b$$$</td><td valign="top" width="33%">$$a\qquad~b$$</td></tr><tr><td valign="top" width="33%">\_ <em>(where _ is blank!)</em></td><td valign="top" width="33%"><p>$$$a\ b$$$</p><p><font color="#ff0000">(whereas $$$a\b$$$ is <em>not</em> a valid filter expression since the blank space is missing; it is recommended to use the tilde ~ instead of the simple whitespace)</font></p></td><td valign="top" width="33%"><p>$$a\ b$$</p><p /></td></tr><tr></td /><td valign="top" width="33%">\hspace{n} ,where n positive integer (= n Pixels)</td><td valign="top" width="33%"><p>$$$a~\hspace{30}~b$$$</p><p>$$$a~\hspace{15}~b$$$</p><p>$$$a~\hspace{2}~b$$$</p><p>$$$a~\hspace{1}~b$$$</p></td><td valign="top" width="33%"><p>$$a~\hspace{30}~b$$</p><p>$$a~\hspace{15}~b$$</p><p>$$a~\hspace{2}~b$$</p><p>$$a~\hspace{1}~b$$</p></td></tr><tr><td valign="top" width="33%">\unitlength{m}\hspace{n}, changes the default unit length (m=1px) to be applied</td><td valign="top" width="33%"><p>$$$a~\hspace{2}~b\unitlength{10}~\hspace{2}~c$$$</p><p><em>(second space is 10x2=20px)</em></p></td><td valign="top" width="33%">$$a~\hspace{2}~b\unitlength{10}~\hspace{2}~c$$</td></tr></tbody></table></td></tr></tbody></table></p><p><strong>Note:</strong> Simple blank spaces and tildes (~) are ignored by the TeX filter and don't produce any space. You must use one of the defined math spaces to get a visible (extra) space.</p> 1 0 0 0 1 spaces in formulas, predefined spaces 05 Spaces 1 \~ <ul><li>In order to prevent some browser specific problems with whitespaces, it is advisable to use ~ (tilde) as the whitespace instead of the normal blank key (in places where whitespaces are mandatory, e.g. after commands).</li><li><u>Ex.:</u> $$$\frac~xy$$$ to produce $$\frac~xy$$</li><li><u>Ex.:</u> $$$\sqrt~n$$$ to produce $$\sqrt~n$$</li></ul> 1 0 0 0 1 blank space, blank key, required whitespace 05 Spaces 1 contour integral <ul><li>General syntax for symbols with a kind of lower and upper limits:</li></ul><p align="center">\<em>symbolname</em>_{<em>lowerexpression</em>}^{<em>upperexpression</em>}</p><ul><li>In general, there are two ways how these lower and upper expressions can be placed: centered below and above the symbol or in a subscript / superscript manner. In the first case the symbol name is preceded by the word &quot;big&quot;, in the second there is no prefix. </li><li>Syntax for the contour integral symbol:</li></ul><p align="center">$$$\bigoint_{0}^{\infty}$$$   gives   </p><p align="center">$$\bigoint_{0}^{\infty}$$ </p><p align="center">and</p><p align="center">$$$\oint_{0}^{\infty}$$$   gives </p><p align="center">$$\oint_{0}^{\infty}$$</p><ul><li><div align="left">Use font size commands for a nicer picture:</div></li></ul><p align="left" /><p align="center">$$$\LARGE\bigoint_{\small0}^{\small\infty}$$$   gives   </p><p /><p align="center">$$\LARGE\bigoint_{\small0}^{\small\infty}$$ </p><p align="left" /><p align="center">and</p><p align="center">$$$\large\oint_{\small0}^{\small\infty}$$$   gives </p><p /><p align="center">$$\large\oint_{\small0}^{\small\infty}$$</p> 1 0 0 0 1 06 Symbols 1 sum (summation) <ul><li>General syntax for symbols with a kind of lower and upper limits:</li></ul><p align="center">\<em>symbolname</em>_{<em>lowerexpression</em>}^{<em>upperexpression</em>}</p><ul><li>In general, there are two ways how these lower and upper expressions can be placed: centered below and above the symbol or in a subscript / superscript manner. In the first case the symbol name is preceded by the word &quot;big&quot;, in the second there is no prefix.</li><li>Syntax for summation symbol:</li></ul><p align="center">$$$\bigsum_{i=k}^{n}$$$   gives   </p><p align="center">$$\bigsum_{i=k}^{n}$$ </p><p align="center">and</p><p align="center">$$$\sum_{i=k}^{n}$$$   gives </p><p align="center">$$\sum_{i=k}^{n}$$</p><ul><li><div align="left">Use font size commands for a nicer picture:</div></li></ul><p align="left" /><p align="center">$$$\LARGE\bigsum_{\small{i=1}}^{\small{n}}$$$   gives   </p><p /><p align="center">$$\LARGE\bigsum_{\small{i=1}}^{\small{n}}$$ </p><p align="left" /><p align="center">and</p><p align="center">$$$\large\sum_{\small{i=1}}^{\small{n}}$$$   gives </p><p /><p align="center">$$\large\sum_{\small{i=1}}^{\small{n}}$$</p> 1 0 0 0 1 big sum 06 Symbols 1 smiley $$$~\unitlength{.6}~\picture(100){~~(50,50){\circle(99)}~ ~(20,55;50,0;2){+1$\hat\bullet}~~(50,40){\bullet}~~(50,35){\circle(50,25;34)}~ ~(50,35){\circle(50,45;34)}}$$$  is $$~\unitlength{.6}~\picture(100){~~(50,50){\circle(99)}~ ~(20,55;50,0;2){+1$\hat\bullet}~~(50,40){\bullet}~~(50,35){\circle(50,25;34)}~ ~(50,35){\circle(50,45;34)}}$$ 1 0 0 0 1 smiley formula box <p>$$$\fbox{x=\frac{1}{2}}$$$  gives </p><p align="center">$$\fbox{x=\frac{1}{2}}$$ </p> 1 0 0 0 1 fbox infinity <p>$$$\infty$$$  gives $$\infty$$</p> 1 0 0 0 1 infinity 06 Symbols 1 mathematics expression <ul><li>A valid expression inside the $'s is rendered as mathematics in an inserted gif image.</li><li><u>Ex.:</u> $$$x=y^2$$$ creates  </li></ul><p align="center">$$x=y^2$$ </p> 1 0 0 0 1 mathematics expression 01 Getting started 1 subscript <ul><li>The command character &quot;_&quot; triggers subscription of the following expression(s).</li><li>For more than one subscripted character put them in braces {...}.</li><li>Use font sizing commands for appropriate sizing.</li><li><u>Ex.:</u>$$$x_1$$$ gives </li></ul><p style="TEXT-ALIGN: center">$$x_1$$</p><ul><li><u>Ex.:</u>$$$a_{m+2n}$$$ gives </li></ul><p style="TEXT-ALIGN: center">$$a_{m+2n}$$</p><ul><li><u>Ex. (with specific sizing):</u>  $$$x_{\small1}=a_{\small{m+2n}}$$$ gives</li></ul><p style="TEXT-ALIGN: center">$$x_{\small1}=a_{\small{m+2n}}$$</p><ul><li>Combine subscripting with superscripting (command character &quot;^&quot;). <br />Syntax: Expr_{subExpr}^{supExpr}.</li><li><u>Ex.:</u> $$$A_{\small{i,j,k}}^{\small{-n+2}}$$$ gives</li></ul><p style="TEXT-ALIGN: center">$$A_{\small{i,j,k}}^{\small{-n+2}}$$</p> 1 0 0 0 1 02 Arithmetic expressions, sub-/superscripts, roots 1 less than <p>$$$&lt;$$$   gives</p><p align="center">$$&lt;$$ </p> 1 0 0 0 1 less than < 07 Relations 1 not equal <p>$$$x\neq~y$$$ gives </p><p align="center">$$x\neq~y$$ </p><p align="left"><font color="#ff0000"><strong>note:</strong></font> \neg produces the logical negation, i.e. $$$\neg~A$$$ gives </p><p align="center">$$\neg~A$$</p> 1 0 0 0 1 <> 07 Relations 1 beta (lower case greek letter) $$$\beta$$$ gives $$\beta$$ 1 0 0 0 1 06 Symbols 1 superscript <ul><li>The command character &quot;^&quot; triggers superscription of the following expression(s).</li><li>For more than one superscripted character put them in braces {...}.</li><li>Use font sizing commands for appropriate sizing.</li><li><u>Ex.:</u> $$$x^2$$$ gives </li></ul><p style="TEXT-ALIGN: center">$$x^2$$</p><ul><li><u>Ex.:</u> $$$a^{m+2n}$$$ gives </li></ul><p style="TEXT-ALIGN: center">$$a^{m+2n}$$</p><ul><li><u>Ex. (with specific sizing):</u> $$$x^{\small2}=a^{\small{m+2n}}$$$ gives</li></ul><p style="TEXT-ALIGN: center">$$x^{\small2}=a^{\small{m+2n}}$$</p><ul><li>Combine superscripting with subscripting (command character &quot;_&quot;). <br />Syntax: Expr_{subExpr}^{supExpr}.</li><li><u>Ex.:</u> $$$A_{\small{i,j,k}}^{\small{-n+2}}$$$ gives</li></ul><p style="TEXT-ALIGN: center">$$A_{\small{i,j,k}}^{\small{-n+2}}$$</p> 1 0 0 0 1 02 Arithmetic expressions, sub-/superscripts, roots 1 greater than <p>$$$x&gt;y$$$  gives </p><p align="center">$$x&gt;y$$ </p> 1 0 0 0 1 > greater than 07 Relations 1 square root <ul><li>$$$\sqrt{a}$$$ or $$$\sqrt~a$$$ gives $$\sqrt~a$$</li><li>Use braces for terms with more than one character: $$$\sqrt{x+y}$$$ gives</li></ul><p align="center">$$\sqrt{x+y}$$</p> 1 0 0 0 1 02 Arithmetic expressions, sub-/superscripts, roots 1 triangle $$$\triangle~abc$$$ gives $$\triangle~abc$$ 1 0 0 0 1 triangle Learning Formula $$\frac{success}{problem}=~\unitlength{.6}~\picture(100){~~(50,50){\circle(99)}~ ~(20,55;50,0;2){+1$\hat\bullet}~~(50,40){\bullet}~~(50,35){\circle(50,25;34)}~ ~(50,35){\circle(50,45;34)}}$$ 1 0 0 0 1 learning formula delta (lower case greek letter) $$$\delta$$$ gives $$\delta $$ 1 0 0 0 1 delta 06 Symbols 1 TeX $$TeX$$  notation allows for the expression of ASCII characters to generate formatted graphics output 1 0 0 0 1 TeX array <ul><li>Syntax for an n-dimensional array: <br />\begin{array}a<sub>1</sub>&amp;...&amp;a<sub>n</sub>\end{array}</li><li><u>Ex.:</u> $$$\(\begin{array}a_{\fs{0}1}\fs{3},&amp;a_{\fs{0}2}\fs{3},&amp;a_{\fs{0}3}\end{array}\)$$$ gives</li></ul><p align="center">$$(\begin{array}a_{\fs{0}1}\fs{3},&amp;a_{\fs{0}2}\fs{3},&amp;a_{\fs{0}3}\end{array})$$</p> 1 0 0 0 1 array, vector, matrix 09 Structures 1 triggering the TeX filter <ul><li>Two double $'s embracing a valid math expression trigger the filter to generate and insert the formula gif. </li><li><u>Ex.:</u>  $$$a^2$$$ produces $$a^2$$<br /></li></ul> 1 1 0 0 1 trigger, TeX filter, start filter 01 Getting started 1 escaping the TeX filter <ul><li>With two triple $'s embracing an expression you can make the filter to escape and the code itself is shown (with two embracing double $'s). </li><li><u>Ex.:</u> $$$$a^2$$$$ produces $$$a^2$$$, i.e. prevents the filter to render it as a formula gif.</li></ul> 1 0 0 0 1 escape; suppress filter; prevent from filtering 01 Getting started 1 \large (all lower case letters) <ul><li>Everthing following the \large command will be output in the large font size until the system encounters another font size command. </li><li>Note: This command is case sensitive, since large, Large and LARGE are different sizes! </li><li><u>Ex.:</u> $$$\large~3x$$$ gives $$\large~3x$$</li></ul> 1 0 0 0 1 font size, fs{3} 03 Font Styles 1 \Large (L capital letter) <ul><li>Everthing following the \Large command will be output in the second largest font size until the system encounters another font size command. </li><li>Note: This command is case sensitive, since large, Large and LARGE are different sizes!  </li><li><u>Ex.:</u> $$$\Large~3x$$$ gives $$\Large~3x$$</li></ul> 1 0 0 0 1 font size, 03 Font Styles 1 \LARGE (all capital letters) <ul><li>Everthing following the \LARGE command will be output in the largest predefined font size until the system encounters another font size command. </li><li>Note: This command is case sensitive, since large, Large and LARGE are different sizes!  </li><li><u>Ex.:</u> $$$\LARGE~3x$$$ gives $$\LARGE~3x$$</li></ul> 1 0 0 0 1 font size, 03 Font Styles 1 absolute font sizes (overview) <p><table cellspacing="0" cellpadding="1" width="100%"><tbody><tr><td class="activitylabel"><table cellspacing="1" cellpadding="1" width="100%" align="left" border="1"><tbody><tr style="COLOR: #ffffff; BACKGROUND-COLOR: #aaaaaa" width="33%"><td valign="top" width="100%" colspan="3"><strong>Absolute Font Sizes</strong></td></tr><tr style="BACKGROUND-COLOR: #dddddd" width="33%"><td valign="top" width="33%"><strong>Command</strong></td><td valign="top" width="33%"><strong>Example</strong></td><td valign="top" width="33%"><strong>Result</strong></td></tr><tr><td valign="top" width="33%">\tiny</td><td valign="top" width="33%">$$$\tiny 3x$$$</td><td valign="top" width="33%">$$\tiny 3x$$</td></tr><tr><td valign="top" width="33%">\small</td><td valign="top" width="33%">$$$\small 3x$$$</td><td valign="top" width="33%">$$\small 3x$$</td></tr><tr><td valign="top" width="33%">\normalsize <em>(default)</em></td><td valign="top" width="33%">$$$\normalsize 3x$$$ or just $$$3x$$$</td><td valign="top" width="33%">$$\normalsize 3x$$ </td></tr><tr><td valign="top" width="33%">\large</td><td valign="top" width="33%">$$$\large 3x$$$</td><td valign="top" width="33%">$$\large 3x$$</td></tr><tr><td valign="top" width="33%">\Large</td><td valign="top" width="33%">$$$\Large 3x$$$</td><td valign="top" width="33%">$$\Large 3x$$</td></tr><tr><td valign="top" width="33%">\LARGE</td><td valign="top" width="33%">$$$\LARGE 3x$$$</td><td valign="top" width="33%">$$\LARGE 3x$$</td></tr><tr><td valign="top" width="33%"> </td><td valign="top" width="33%"> </td><td valign="top" width="33%"> </td></tr><tr></td /><td valign="top" width="33%"><p><font color="#000000">\huge and \Huge are <em>not</em> supported by the mimeTeX filter</font></p></td><td valign="top" width="33%"> </td><td valign="top" width="33%"> </td></tr><tr><td valign="top" width="33%"></td><td valign="top" width="33%"></td><td valign="top" width="33%"></td></tr></tbody></table></td></tr></tbody></table></p><p /> 1 0 0 0 1 03 Font Styles 1 \qquad <ul><li>inserts a double space of current character set size</li><li><u>Ex.:</u> $$$a\qquad~b$$$ gives $$a\qquad~b$$</li></ul> 1 0 0 0 1 math space 05 Spaces 1 parentheses <ul><li>Syntax: \left(...\right) or \(...\)</li><li><u>Ex.:</u> $$$2a\left(b+c\right)$$$ gives </li></ul><p align="center">$$2a\left(b+c\right)$$</p> 1 0 0 0 1 04 Delimiters (parentheses, braces,...) 1 braces <ul><li>Syntax: \left{...\right}</li><li><u>Ex.:</u> $$$M=\left{a, b, c\right}$$$ gives </li></ul><p align="center">$$M=\left{a, b, c\right}$$</p> 1 0 0 0 1 04 Delimiters (parentheses, braces,...) 1 square bracket <ul><li>Synatx: \left[...\right] </li><li><u>Ex.:</u> $$$\left[a,b\right]$$$ gives $$\left[a,b\right]$$</li></ul> 1 0 0 0 1 04 Delimiters (parentheses, braces,...) 1 angle bracket <ul><li>Syntax: \left&lt;...\right&gt;</li><li><u>Ex.:</u> $$$\left&lt;f,g\right&gt;$$$ gives </li></ul><p align="center">$$\left&lt;f,g\right&gt;$$</p> 1 0 0 0 1 04 Delimiters (parentheses, braces,...) 1 vertical line (absolute value, determinant, ...etc. symbol) <ul><li>Syntax: \left|...\right| </li><li><u>Ex.:</u> $$$\left|b-a\right|$$$ gives $$\left|b-a\right|$$ </li><li><u>Ex.:</u> $$${\rm~det}\left|\begin{array}{cc}a&amp;b\\c&amp;d \end{array}\right|$$$ gives  </li></ul><p align="center">$${\rm~det}\left|\begin{array}{cc}a&amp;b\\c&amp;d \end{array}\right|$$ </p><p align="left"> <br />(&quot;\rm~something&quot; renders &quot;something&quot; in roman style)</p> 1 0 0 0 1 absolute value symbol, determinant symbol 04 Delimiters (parentheses, braces,...) 1 double vertical line (norm symbol) <ul><li>Syntax: \left\|...\right\| </li><li><u>Exp.:</u> $$$\left\|af\right\| = \left|a\right|\left\|f\right\|$$$ gives </li></ul><p align="center">$$\left\|af\right\| = \left|a\right|\left\|f\right\|$$</p> 1 0 0 0 1 norm 04 Delimiters (parentheses, braces,...) 1 left only brace <ul><li>Syntax: \left{...\right.  <em><font color="#ff0000">(note the dot at the end!)</font></em></li><li><font color="#000000"><u>Ex.:</u> $$$f(x)=\left{{x^2, \rm~if x&gt;-1\atop~0, \rm~else}\right.$$$ gives </font></li></ul><p align="center"><font color="#000000">$$f(x)=\left{{x^2, \rm~if x&gt;-1\atop~0, \rm~else}\right.$$</font></p><p>(\rm~something switches to roman style)</p> 1 0 0 0 1 04 Delimiters (parentheses, braces,...) 1 right only brace <ul><li>Syntax: \left.{...\right}  <em><font color="#ff0000">(note the dot!)</font></em></li><li><u>Ex.:</u> $$$\left.{{\rm~term1\atop\rm~term2}\right}=y$$$ gives </li></ul><p align="center">$$\left.{{\rm~term1\atop\rm~term2}\right}=y$$</p><p>(\rm~something switches to roman style)</p> 1 0 0 0 1 04 Delimiters (parentheses, braces,...) 1 delimiters (overview) <p><table cellspacing="0" cellpadding="1" width="100%"><tbody><tr><td class="activitylabel"><table cellspacing="1" cellpadding="1" width="100%" align="left" border="1"><tbody><tr style="COLOR: #ffffff; BACKGROUND-COLOR: #aaaaaa" width="33%"><td valign="top" width="100%" colspan="3"><strong>Delimiters (parentheses, braces, brackets. ...)</strong></td></tr><tr style="BACKGROUND-COLOR: #dddddd" width="33%"><td valign="top" width="33%"><strong>Command</strong></td><td valign="top" width="33%"><strong>Example</strong></td><td valign="top" width="33%"><strong>Result</strong></td></tr><tr><td valign="top" width="33%"><p>\left(... \right)</p></td><td valign="top" width="33%">$$$2\left(a+b\right)$$$</td><td valign="top" width="33%">$$2~\left(a+b\right)$$</td></tr><tr><td valign="top" width="33%">\left[... \right]</td><td valign="top" width="33%">$$$\left[a^2+b^2~\right]$$$</td><td valign="top" width="33%">$$\left[a^2+b^2~\right]$$</td></tr><tr><td valign="top" width="33%">\left{... \right}</td><td valign="top" width="33%">$$$\left{x^2, x^3, x^4,... \right}$$$</td><td valign="top" width="33%">$$\left{x^2, x^3, x^4,... \right}$$</td></tr><tr><td valign="top" width="33%">\left\langle... \right\rangle</td><td valign="top" width="33%">$$$\left\langle a,b~\right\rangle$$$</td><td valign="top" width="33%">$$\left\langle a,b~\right\rangle$$</td></tr><tr><td valign="top" width="33%">\left| ... \right| </td><td valign="top" width="33%">$$$\det\left|\array{a&amp;b\\c&amp;d}\right| $$$</td><td valign="top" width="33%">$$\det\left|\array{a&amp;b\\c&amp;d}\right| $$</td></tr><tr><td valign="top" width="33%">\left\| ... \right\| </td><td valign="top" width="33%">$$$\left\|f~\right\|$$$</td><td valign="top" width="33%">$$\left\|f~\right\|$$</td></tr><tr><td valign="top" width="33%"><p>\left{ ... \right<strong>.</strong></p><p><font color="#ff0000"><em>(note the dot!)</em></font></p></td><td valign="top" width="33%"><p>$$$f(x)=\left{{x^2, \rm~if x&gt;-1\atop~0, \rm~else}\right.$$$</p><p>(\rm switches to roman style)</p></td><td valign="top" width="33%"><p>$$f(x)=\left{{x^2, \rm~if x&gt;-1\atop~0, \rm~else}\right.$$</p></td></tr><tr></td /><td valign="top" width="33%"><p>\left<strong>.</strong>{ ... \right\}</p><p><font color="#ff0000"><em>(note the dot!)</em></font></p></td><td valign="top" width="33%">$$$\left.{{\rm~term1\atop\rm~term2}\right}=y$$$</td><td valign="top" width="33%">$$\left.{{\rm~term1\atop \rm~term2}\right}=y$$</td></tr><tr><td valign="top" width="33%"></td><td valign="top" width="33%"></td><td valign="top" width="33%"></td></tr></tbody></table></td></tr></tbody></table></p><p><strong>Note:</strong> The delimiters are automatically sizes.</p> 1 0 0 0 1 04 Delimiters (parentheses, braces,...) 1 matrix <ul><li>An (m,n)-matrix is considered as an array of m*n elements, where the elements of a column are separated by &quot;&amp;&quot; and the rows by &quot;\\&quot;.</li></ul><ul><li>Syntax for an (m,n)-matrix: <br />\begin{array}{<em>colformat}</em>a<sub>11</sub>&amp;...&amp;a<sub>1n</sub>\\a<sub>21</sub>&amp;...&amp;a<sub>2n</sub>\\... \\a<sub>m1</sub>&amp;...&amp;a<sub>mn </sub>\end{array}<p dir="ltr" style="MARGIN-RIGHT: 0px">where<br /><em>colformat</em> defines the format of each of the n columns: l for left, r for right and c for center (hence {ccccc} defines for a (m,5)-matrix in which all columns are centered)</p></li><li><u>Ex.:</u> $$$\left(\begin{array}{lcr}a_{\tiny1}+d &amp; a_{\tiny2}+d &amp; a_{\tiny3}+d \\ b_{\tiny1}&amp; b_{\tiny2}&amp; b_{\tiny3} \\ c_{\tiny1} &amp; c_{\tiny2} &amp; c_{\tiny3} \end{array}\right)$$$ gives</li></ul><p align="center">$$\left(\begin{array}{lcr}a_{\tiny1}+d &amp; a_{\tiny2}+d &amp; a_{\tiny3}+d \\ b_{\tiny1}&amp; b_{\tiny2}&amp; b_{\tiny3} \\ c_{\tiny1} &amp; c_{\tiny2} &amp; c_{\tiny3} \end{array}\right)$$</p><p align="left">Note in the example above that &quot;lcr&quot; has the effect that column 1 is left aligned, column 2 centered and colums 3 right aligned.</p> 1 0 0 0 1 matrix, array 09 Structures 1 constants <ul><li>Numbers in formulas are interpreted as constants and they are rendered in non-italic roman font face, which is a widely used convention.</li><li>Following this convention, variables are shown in italic.</li><li><u>Exp.:</u> $$$f(x)=3a+x$$$ gives</li></ul><p style="TEXT-ALIGN: center">$$f(x)=3a+x$$</p> 1 0 0 0 1 02 Arithmetic expressions, sub-/superscripts, roots 1 variables <ul><li>Variables in formulas are rendered in italic roman font face, which is a widely used convention.</li><li>Following this convention, constants are shown as non-italic.</li><li><u>Exp.:</u> $$$f(x)=3a+x$$$ gives</li></ul><p style="TEXT-ALIGN: center">$$f(x)=3a+x$$</p> 1 0 0 0 1 02 Arithmetic expressions, sub-/superscripts, roots 1 arithmetic operations <ul><li>Type arithmetic operations and &quot;=&quot; as usual.</li><li><u>Exp.:</u> $$$f(x)=x-2b+(3a/c)$$$ gives</li></ul><p style="TEXT-ALIGN: center">$$f(x)=x-2b+(3a/c)$$</p><ul><li>See also keyword &quot;fraction&quot; for extended capabilities.</li></ul> 1 0 0 0 1 02 Arithmetic expressions, sub-/superscripts, roots 1 fraction <ul><li>Syntax: \frac{numerator}{denominator}</li><li>Use font sizing commands for specific sizing if you don't want the predefined one to be taken.</li><li><u>Ex. (with predefined sizing):</u> $$$f(x,y)=\frac{2a}{x+y}$$$ gives </li></ul><p style="TEXT-ALIGN: center">$$f(x,y)=\frac{2a}{x+y}$$</p><ul><li><u>Ex. (with specific sizing):</u> $$$f(x,y)=\frac{\fs{2}2a}{\fs{2}x+y}$$$ gives </li></ul><p style="TEXT-ALIGN: center">$$f(x,y)=\frac{\fs{2}2a}{\fs{2}x+y}$$</p><ul><li>You may nest fractions as much as you want.</li><li><u>Ex. (nested fractions):</u> $$$\frac{\frac{a}{x-y}+\frac{b}{x+y}}{1+\frac{a-b}{a+b}}$$$ gives </li></ul><p style="TEXT-ALIGN: center">$$\frac{\frac{a}{x-y}+\frac{b}{x+y}}{1+\frac{a-b}{a+b}}$$</p> 1 0 0 0 1 02 Arithmetic expressions, sub-/superscripts, roots 1 root <ul><li>Syntax: \sqrt[n]{arg} or simply  \sqrt{arg} for \sqrt[2]{arg}</li><li><u>Ex.:</u> $$$\sqrt[3]{8}$$$ gives</li></ul><p align="center">$$\sqrt[3]{8}$$</p><ul><li><u>Ex.:</u> $$$\sqrt{-1}$$$ gives</li></ul><p align="center">$$\sqrt{-1}$$</p><ul><li>Nesting of roots (and combining with fractions, ...etc.) are possible.</li><li><u>Ex.:</u> $$$\sqrt[n]{\frac{x^n-y^n}{1+u^{2n}}}$$$ gives</li></ul><p align="center">$$\sqrt[n]{\frac{x^n-y^n}{1+u^{2n}}}$$</p><ul><li><u>Ex.:</u> $$$\sqrt[3]{-q+\sqrt{q^2+p^3}}$$$ gives</li></ul><p align="center">$$\sqrt[3]{-q+\sqrt{q^2+p^3}}$$</p> 1 0 0 0 1 square root 02 Arithmetic expressions, sub-/superscripts, roots 1 greater than or equal <p>$$$x\ge~y$$$ or $$$x\geq~y$$$ gives </p><p align="center">$$x\ge~y$$ </p> 1 0 0 0 1 >= 07 Relations 1 less than or equal <p>$$$x\le~y$$$ or $$$x\leq~y$$$ gives </p><p align="center">$$x\le~y$$ </p> 1 0 0 0 1 <= 07 Relations 1 greek letters (overview) <p>Simply write \<em>greekletter</em> for lower case and \<em>Greekletter</em> for upper case<em>.</em></p><p>Here's a list of all known greek letters (Note: not all <em>upper case</em> greek letters are known):</p><p><strong>Lower Case Greek Letters:</strong></p><p><table cellspacing="1" cellpadding="5" width="100%" border="1"><tbody><tr style="FONT-WEIGHT: bold; BACKGROUND: #dddddd"><td valign="top" width="33%">Command</td><td valign="top" width="33%">Filter Expression</td><td valign="top" width="33%">Result</td></tr><tr><td valign="top" width="33%">\alpha</td><td valign="top" width="33%">$$$\alpha$$$</td><td valign="top" width="33%">$$\alpha$$</td></tr><tr><td valign="top" width="33%">\beta</td><td valign="top" width="33%">$$$\beta$$$</td><td valign="top" width="33%">$$\beta$$</td></tr><tr><td valign="top" width="33%">\gamma</td><td valign="top" width="33%">$$$\gamma$$$</td><td valign="top" width="33%">$$\gamma$$</td></tr><tr><td valign="top" width="33%">\delta</td><td valign="top" width="33%">$$$\delta$$$</td><td valign="top" width="33%">$$\delta$$</td></tr><tr><td valign="top" width="33%">\epsilon</td><td valign="top" width="33%">$$$\epsilon$$$</td><td valign="top" width="33%">$$\epsilon$$</td></tr><tr><td valign="top" width="33%">\varepsilon</td><td valign="top" width="33%">$$$\varepsilon$$$</td><td valign="top" width="33%">$$\varepsilon$$</td></tr><tr><td valign="top" width="33%">\zeta</td><td valign="top" width="33%">$$$\zeta$$$</td><td valign="top" width="33%">$$\zeta$$</td></tr><tr><td valign="top" width="33%">\eta</td><td valign="top" width="33%">$$$\eta$$$</td><td valign="top" width="33%">$$\eta$$</td></tr><tr><td valign="top" width="33%">\theta</td><td valign="top" width="33%">$$$\theta$$$</td><td valign="top" width="33%">$$\theta$$</td></tr><tr><td valign="top" width="33%">\vartheta</td><td valign="top" width="33%">$$$\vartheta$$$</td><td valign="top" width="33%">$$\vartheta$$</td></tr><tr><td valign="top" width="33%">\iota</td><td valign="top" width="33%">$$$\iota$$$</td><td valign="top" width="33%">$$\iota$$</td></tr><tr><td valign="top" width="33%">\kappa</td><td valign="top" width="33%">$$$\kappa$$$</td><td valign="top" width="33%">$$\kappa$$</td></tr><tr><td valign="top" width="33%">\lambda</td><td valign="top" width="33%">$$$\lambda$$$</td><td valign="top" width="33%">$$\lambda$$</td></tr><tr><td valign="top" width="33%">\mu</td><td valign="top" width="33%">$$$\mu$$$</td><td valign="top" width="33%">$$\mu$$</td></tr><tr><td valign="top" width="33%">\nu</td><td valign="top" width="33%">$$$\nu$$$</td><td valign="top" width="33%">$$\nu$$</td></tr><tr><td valign="top" width="33%">\xi</td><td valign="top" width="33%">$$$\xi$$$</td><td valign="top" width="33%">$$\xi$$</td></tr><tr><td valign="top" width="33%">o  <font color="#ff0000">(!)</font></td><td valign="top" width="33%">$$$o$$$</td><td valign="top" width="33%">$$o$$</td></tr><tr><td valign="top" width="33%">\pi</td><td valign="top" width="33%">$$$\pi$$$</td><td valign="top" width="33%">$$\pi$$</td></tr><tr><td valign="top" width="33%">\varpi</td><td valign="top" width="33%">$$$\varpi$$$</td><td valign="top" width="33%">$$\varpi$$</td></tr><tr><td valign="top" width="33%">\rho</td><td valign="top" width="33%">$$$\rho$$$</td><td valign="top" width="33%">$$\rho$$</td></tr><tr><td valign="top" width="33%">\varrho</td><td valign="top" width="33%">$$$\varrho$$$</td><td valign="top" width="33%">$$\varrho$$</td></tr><tr><td valign="top" width="33%">\sigma</td><td valign="top" width="33%">$$$\sigma$$$</td><td valign="top" width="33%">$$\sigma$$</td></tr><tr><td valign="top" width="33%">\varsigma</td><td valign="top" width="33%">$$$\varsima$$$</td><td valign="top" width="33%">$$\varsigma$$</td></tr><tr><td valign="top" width="33%">\tau</td><td valign="top" width="33%">$$$\tau$$$</td><td valign="top" width="33%">$$\tau$$</td></tr><tr><td valign="top" width="33%">\upsilon</td><td valign="top" width="33%">$$$\upsilon$$$</td><td valign="top" width="33%">$$\upsilon$$</td></tr><tr><td valign="top" width="33%">\phi</td><td valign="top" width="33%">$$$\phi$$$</td><td valign="top" width="33%">$$\phi$$</td></tr><tr><td valign="top" width="33%">\varphi</td><td valign="top" width="33%">$$$\varphi$$$</td><td valign="top" width="33%">$$\varphi$$</td></tr><tr><td valign="top" width="33%">\chi</td><td valign="top" width="33%">$$$\chi$$$</td><td valign="top" width="33%">$$\chi$$</td></tr><tr><td valign="top" width="33%">\psi</td><td valign="top" width="33%">$$$\psi$$$</td><td valign="top" width="33%">$$\psi$$</td></tr><tr><td valign="top" width="33%">\omega</td><td valign="top" width="33%">$$$\omega$$$</td><td valign="top" width="33%">$$\omega$$</td></tr><tr><td valign="top" width="33%"></td><td valign="top" width="33%"></td><td valign="top" width="33%"></td></tr></tbody></table></p><p /><p><b>Upper Case Greek Letters:</b></p><p><table cellspacing="1" cellpadding="5" width="100%" border="1"><tbody><tr style="FONT-WEIGHT: bold; BACKGROUND: #dddddd"><td valign="top" width="33%">Command</td><td valign="top" width="33%">Filter Expression</td><td valign="top" width="33%">Result</td></tr><tr><td valign="top" width="33%">\Gamma</td><td valign="top" width="33%">$$$\Gamma$$$</td><td valign="top" width="33%">$$\Gamma$$</td></tr><tr><td valign="top" width="33%">\Delta</td><td valign="top" width="33%">$$$\Delta$$$</td><td valign="top" width="33%">$$\Delta$$</td></tr><tr><td valign="top" width="33%">\Theta</td><td valign="top" width="33%">$$$\Theta$$$</td><td valign="top" width="33%">$$\Theta$$</td></tr><tr><td valign="top" width="33%">\Lambda</td><td valign="top" width="33%">$$$\Lambda$$$</td><td valign="top" width="33%">$$\Lambda$$</td></tr><tr><td valign="top" width="33%">\Xi</td><td valign="top" width="33%">$$$\Xi$$$</td><td valign="top" width="33%">$$\Xi$$</td></tr><tr><td valign="top" width="33%">\Pi</td><td valign="top" width="33%">$$$\Pi$$$</td><td valign="top" width="33%">$$\Pi$$</td></tr><tr><td valign="top" width="33%">\Sigma</td><td valign="top" width="33%">$$$\Sigma$$$</td><td valign="top" width="33%">$$\Sigma$$</td></tr><tr><td valign="top" width="33%">\Upsilon</td><td valign="top" width="33%">$$$\Upsilon$$$</td><td valign="top" width="33%">$$\Upsilon$$</td></tr><tr><td valign="top" width="33%">\Phi</td><td valign="top" width="33%">$$$\Phi$$$</td><td valign="top" width="33%">$$\Phi$$</td></tr><tr><td valign="top" width="33%">\Psi</td><td valign="top" width="33%">$$$\Psi$$$</td><td valign="top" width="33%">$$\Psi$$</td></tr><tr><td valign="top" width="33%">\Omega</td><td valign="top" width="33%">$$$\Omega$$$</td><td valign="top" width="33%">$$\Omega$$</td></tr></tbody></table></p><p /> 1 0 0 0 1 06 Symbols 1 integral <ul><li>General syntax for symbols with a kind of lower and upper limits:</li></ul><p align="center">\<em>symbolname</em>_{<em>lowerexpression</em>}^{<em>upperexpression</em>}</p><ul><li>In general, there are two ways how these lower and upper expressions can be placed: centered below and above the symbol or in a subscript / superscript manner. In the first case the symbol name is preceded by the word &quot;big&quot;, in the second there is no prefix. </li><li>Syntax for integral symbol:</li></ul><p align="center">$$$\bigint_{0}^{\infty}$$$   gives   </p><p align="center">$$\bigint_{0}^{\infty}$$ </p><p align="center">and</p><p align="center">$$$\int_{0}^{\infty}$$$   gives </p><p align="center">$$\int_{0}^{\infty}$$</p><ul><li><div align="left">Use font size commands for a nicer picture:</div></li></ul><p align="left" /><p align="center">$$$\LARGE\bigint_{\small0}^{\small\infty}$$$   gives   </p><p /><p align="center">$$\LARGE\bigint_{\small0}^{\small\infty}$$ </p><p align="left" /><p align="center">and</p><p align="center">$$$\large\int_{\small0}^{\small\infty}$$$   gives </p><p /><p align="center">$$\large\int_{\small0}^{\small\infty}$$</p> 1 0 0 0 1 int 06 Symbols 1 product <ul><li>General syntax for symbols with a kind of lower and upper limits:</li></ul><p align="center">\<em>symbolname</em>_{<em>lowerexpression</em>}^{<em>upperexpression</em>}</p><ul><li>In general, there are two ways how these lower and upper expressions can be placed: centered below and above the symbol or in a subscript / superscript manner. In the first case the symbol name is preceded by the word &quot;big&quot;, in the second there is no prefix. </li><li>Syntax for product symbol:</li></ul><p align="center">$$$\bigprod_{i=k}^{n}$$$   gives   </p><p align="center">$$\bigprod_{i=k}^{n}$$ </p><p align="center">and</p><p align="center">$$$\prod_{i=k}^{n}$$$   gives </p><p align="center">$$\prod_{i=k}^{n}$$</p><ul><li><div align="left">Use font size commands for a nicer picture:</div></li></ul><p align="left" /><p align="center">$$$\LARGE\bigprod_{\tiny{i=k}}^{\tiny{n}}$$$   gives   </p><p /><p align="center">$$\LARGE\bigprod_{\tiny{i=k}}^{\tiny{n}}$$  </p><p align="left" /><p align="center">and</p><p align="center">$$$\large\prod_{\small{i=k}}^{\small{n}}$$$   gives </p><p /><p align="center">$$\large\prod_{\small{i=k}}^{\small{n}}$$</p> 1 0 0 0 1 06 Symbols 1 coproduct <ul><li>General syntax for symbols with a kind of lower and upper limits:</li></ul><p align="center">\<em>symbolname</em>_{<em>lowerexpression</em>}^{<em>upperexpression</em>}</p><ul><li>In general, there are two ways how these lower and upper expressions can be placed: centered below and above the symbol or in a subscript / superscript manner. In the first case the symbol name is preceded by the word &quot;big&quot;, in the second there is no prefix. </li><li><font color="#ff0000"><strong>Note:</strong> mimeTeX seems currently only to support the \bigcoprod command.</font></li><li>Syntax for coproduct symbol: </li></ul><p align="center">$$$\bigcoprod_{i=k}^{n}$$$   gives   </p><p align="center">$$\bigcoprod_{i=k}^{n}$$ </p><ul><li><div align="left">Use font size commands for a nicer picture:</div></li></ul><p align="left" /><p align="center">$$$\LARGE\bigcoprod_{\small{i=k}}^{\small~n}$$$   gives   </p><p /><p align="center">$$\LARGE\bigcoprod_{\small{i=k}}^{\small~n}$$</p> 1 0 0 0 1 coprod 06 Symbols 1 alpha (lower case greek letter) $$$\alpha$$$ gives $$\alpha$$ 1 0 0 0 1 06 Symbols 1 gamma (lower case greek letter) $$$\gamma$$$ gives $$\gamma$$ 1 0 0 0 1 06 Symbols 1 epsilon (lower case greek letter) $$$\epsilon$$$ gives $$\epsilon$$ 1 0 0 0 1 06 Symbols 1 varepsilon (special lower case greek letter) $$$\varepsilon$$$ gives $$\varepsilon$$ 1 0 0 0 1 06 Symbols 1 zeta (lower case greek letter) $$$\zeta$$$ gives $$\zeta$$ 1 0 0 0 1 06 Symbols 1 eta (lower case greek letter) $$$\eta$$$ gives $$\eta$$ 1 0 0 0 1 06 Symbols 1 theta (lower case greek letter) $$$\theta$$$ gives $$\theta$$ 1 0 0 0 1 06 Symbols 1 vartheta (special lower case greek letter) $$$\vartheta$$$ gives $$\vartheta$$ 1 0 0 0 1 06 Symbols 1 iota (lower case greek letter) $$$\iota$$$ gives $$\iota$$ 1 0 0 0 1 06 Symbols 1 kappa $$$\kappa$$$ gives $$\kappa$$ 1 0 0 0 1 06 Symbols 1 lambda (lower case greek letter) $$$\lambda$$$ gives $$\lambda$$ 1 0 0 0 1 06 Symbols 1 mu (lower case greek letter) $$$\mu$$$ gives $$\mu$$ 1 0 0 0 1 06 Symbols 1 nu (lower case greek letter) $$$\nu$$$ gives $$\nu$$ 1 0 0 0 1 06 Symbols 1 xi (lower case greek letter) $$$\xi$$$ gives $$\xi$$ 1 0 0 0 1 06 Symbols 1 omikron (lower case greek letter) <p><font color="#ff0000">$$$o$$$</font> gives $$o$$ </p><p><em>(note this exceptional syntax!)</em></p> 1 0 0 0 1 06 Symbols 1 pi (lower case greek letter) $$$\pi$$$ gives $$\pi$$ 1 0 0 0 1 06 Symbols 1 varpi (special lower case greek letter) $$$\varpi$$$ gives $$\varpi$$ 1 0 0 0 1 06 Symbols 1 rho (lower case greek letter) $$$\rho$$$ gives $$\rho$$ 1 0 0 0 1 06 Symbols 1 varrho (special lower case greek letter) $$$\varrho$$$ gives $$\varrho$$ 1 0 0 0 1 06 Symbols 1 sigma (lower case greek letter) $$$\sigma$$$ gives $$\sigma$$ 1 0 0 0 1 06 Symbols 1 varsigma (special lower greek letter) $$$\varsigma$$$ gives $$\varsigma$$ 1 0 0 0 1 06 Symbols 1 tau (lower case greek letter) $$$\tau$$$ gives $$\tau$$ 1 0 0 0 1 06 Symbols 1 upsilon (lower case greek letter) $$$\upsilon$$$ gives $$\upsilon$$ 1 0 0 0 1 06 Symbols 1 phi (lower case greek letter) $$$\phi$$$ gives $$\phi$$ 1 0 0 0 1 06 Symbols 1 varphi (special lower case greek letter) $$$\varphi$$$ gives $$\varphi$$ 1 0 0 0 1 06 Symbols 1 chi (lower case greek letter) $$$\chi$$$ gives $$\chi$$ 1 0 0 0 1 06 Symbols 1 psi (lower case greek letter) $$$\psi$$$ gives $$\psi$$ 1 0 0 0 1 06 Symbols 1 omega (lower case greek letter) $$$\omega$$$ gives $$\omega$$ 1 0 0 0 1 06 Symbols 1 Gamma (upper case greek letter) $$$\Gamma$$$ gives $$\Gamma$$ 1 0 0 0 1 06 Symbols 1 Delta (upper case greek letter) $$$\Delta$$$ gives $$\Delta$$ 1 0 0 0 1 06 Symbols 1 Theta (upper case greek letter) $$$\Theta$$$ gives $$\Theta$$ 1 0 0 0 1 06 Symbols 1 Lambda (upper case greek letter) $$$\Lambda$$$ gives $$\Lambda$$ 1 0 0 0 1 06 Symbols 1 Xi (upper case greek letter) $$$\Xi$$$ gives $$\Xi$$ 1 0 0 0 1 06 Symbols 1 Pi (upper case greek letter) $$$\Pi$$$ gives $$\Pi$$ 1 0 0 0 1 06 Symbols 1 Sigma (upper case greek letter) $$$\Sigma$$$ gives $$\Sigma$$ 1 0 0 0 1 06 Symbols 1 Upsilon (upper case greek letter) $$$\Upsilon$$$ gives $$\Upsilon$$ 1 0 0 0 1 06 Symbols 1 Phi (upper case greek letter) $$$\Phi$$$ gives $$\Phi$$ 1 0 0 0 1 06 Symbols 1 Psi (upper case greek letter) $$$\Psi$$$ gives $$\Psi$$ 1 0 0 0 1 06 Symbols 1 Omega (upper case greek letter) $$$\Omega$$$ gives $$\Omega$$ 1 0 0 0 1 06 Symbols 1 plus minus $$$a\pm~b$$$ gives $$a\pm~b$$ 1 0 0 0 1 06 Symbols 1 minus plus $$$\mp~a$$$ gives $$\mp~a$$ 1 0 0 0 1 06 Symbols 1 times $$$a\times~b$$$ gives $$a\times~b$$ 1 0 0 0 1 06 Symbols 1 div (division) $$$x\div~y$$$ gives $$x\div~y$$ 1 0 0 0 1 06 Symbols 1 cdot (multiplication) $$$a\cdot~b$$$ gives $$a\cdot~b$$ 1 0 0 0 1 06 Symbols 1 multiplication (with cdot) $$$a\cdot~b$$$ gives $$a\cdot~b$$ 1 0 0 0 1 cdot 02 Arithmetic expressions, sub-/superscripts, roots 1 tests/backup/restore_date_test.php000064400000007167152265517410013426 0ustar00. namespace mod_glossary\backup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); require_once($CFG->dirroot . '/rating/lib.php'); /** * Restore date tests. * * @package mod_glossary * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_date_test extends \restore_date_testcase { /** * Test restore dates. */ public function test_restore_dates(): void { global $DB, $USER; $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $record = ['assesstimefinish' => 100, 'assesstimestart' => 100, 'ratingtime' => 1, 'assessed' => 2, 'scale' => 1]; list($course, $glossary) = $this->create_course_and_module('glossary', $record); // Glossary entries. $entry1 = $gg->create_content($glossary, array('approved' => 1)); $gg->create_content($glossary, array('approved' => 0, 'userid' => $USER->id)); $gg->create_content($glossary, array('approved' => 0, 'userid' => -1)); $gg->create_content($glossary, array('approved' => 1)); $timestamp = 10000; $DB->set_field('glossary_entries', 'timecreated', $timestamp); $DB->set_field('glossary_entries', 'timemodified', $timestamp); $ratingoptions = new \stdClass; $ratingoptions->context = \context_module::instance($glossary->cmid); $ratingoptions->ratingarea = 'entry'; $ratingoptions->component = 'mod_glossary'; $ratingoptions->itemid = $entry1->id; $ratingoptions->scaleid = 2; $ratingoptions->userid = $USER->id; $rating = new \rating($ratingoptions); $rating->update_rating(2); $rating = $DB->get_record('rating', ['itemid' => $entry1->id]); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); $newglossary = $DB->get_record('glossary', ['course' => $newcourseid]); $this->assertFieldsNotRolledForward($glossary, $newglossary, ['timecreated', 'timemodified']); $props = ['assesstimefinish', 'assesstimestart']; $this->assertFieldsRolledForward($glossary, $newglossary, $props); $newentries = $DB->get_records('glossary_entries', ['glossaryid' => $newglossary->id]); $newcm = $DB->get_record('course_modules', ['course' => $newcourseid, 'instance' => $newglossary->id]); // Entries test. foreach ($newentries as $entry) { $this->assertEquals($timestamp, $entry->timecreated); $this->assertEquals($timestamp, $entry->timemodified); } // Rating test. $newrating = $DB->get_record('rating', ['contextid' => \context_module::instance($newcm->id)->id]); $this->assertEquals($rating->timecreated, $newrating->timecreated); $this->assertEquals($rating->timemodified, $newrating->timemodified); } } tests/generator/behat_mod_glossary_generator.php000064400000006573152265517410016343 0ustar00. /** * Behat data generator for mod_glossary. * * @package mod_glossary * @category test * @copyright 2021 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_glossary_generator extends behat_generator_base { /** * Get a list of the entities that Behat can create using the generator step. * * @return array */ protected function get_creatable_entities(): array { return [ 'categories' => [ 'singular' => 'category', 'datagenerator' => 'category', 'required' => ['glossary', 'name'], 'switchids' => ['glossary' => 'glossaryid'], ], 'entries' => [ 'singular' => 'entry', 'datagenerator' => 'entry', 'required' => ['glossary', 'concept', 'definition'], 'switchids' => ['glossary' => 'glossaryid', 'user' => 'userid'], ], ]; } /** * Get the glossary id using an activity idnumber. * * @param string $idnumber * @return int The glossary id */ protected function get_glossary_id(string $idnumber): int { $cm = $this->get_cm_by_activity_name('glossary', $idnumber); return $cm->instance; } /** * Add a category. * * @param array $data Category data. */ public function process_category(array $data) { global $DB; $glossary = $DB->get_record('glossary', ['id' => $data['glossaryid']], '*', MUST_EXIST); unset($data['glossaryid']); $this->get_data_generator()->create_category($glossary, $data); } /** * Preprocess entry data. * * @param array $data Raw data. * @return array Processed data. */ protected function preprocess_entry(array $data): array { if (isset($data['categories'])) { $categorynames = array_map('trim', explode(',', $data['categories'])); $categoryids = array_map(function ($categoryname) { global $DB; if (!$id = $DB->get_field('glossary_categories', 'id', ['name' => $categoryname])) { throw new Exception('The specified category with name "' . $categoryname . '" could not be found.'); } return $id; }, $categorynames); $data['categoryids'] = $categoryids; unset($data['categories']); } return $data; } /** * Get the module data generator. * * @return mod_glossary_generator Glossary data generator. */ protected function get_data_generator() { return $this->componentdatagenerator; } } tests/generator/lib.php000064400000016056152265517410011173 0ustar00. /** * mod_glossary data generator. * * @package mod_glossary * @category test * @copyright 2013 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * mod_glossary data generator class. * * @package mod_glossary * @category test * @copyright 2013 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_glossary_generator extends testing_module_generator { /** * @var int keep track of how many entries have been created. */ protected $entrycount = 0; /** * @var int keep track of how many entries have been created. */ protected $categorycount = 0; /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { $this->entrycount = 0; $this->categorycount = 0; parent::reset(); } public function create_instance($record = null, ?array $options = null) { global $CFG; // Add default values for glossary. $record = (array)$record + array( 'globalglossary' => 0, 'mainglossary' => 0, 'defaultapproval' => $CFG->glossary_defaultapproval, 'allowduplicatedentries' => $CFG->glossary_dupentries, 'allowcomments' => $CFG->glossary_allowcomments, 'usedynalink' => $CFG->glossary_linkbydefault, 'displayformat' => 'dictionary', 'approvaldisplayformat' => 'default', 'entbypage' => !empty($CFG->glossary_entbypage) ? $CFG->glossary_entbypage : 10, 'showalphabet' => 1, 'showall' => 1, 'showspecial' => 1, 'allowprintview' => 1, 'rsstype' => 0, 'rssarticles' => 0, 'grade' => 100, 'assessed' => 0, ); return parent::create_instance($record, (array)$options); } public function create_category($glossary, $record = array(), $entries = array()) { global $CFG, $DB; $this->categorycount++; $record = (array)$record + array( 'name' => 'Glossary category '.$this->categorycount, 'usedynalink' => $CFG->glossary_linkbydefault, ); $record['glossaryid'] = $glossary->id; $id = $DB->insert_record('glossary_categories', $record); if ($entries) { foreach ($entries as $entry) { $ce = new stdClass(); $ce->categoryid = $id; $ce->entryid = $entry->id; $DB->insert_record('glossary_entries_categories', $ce); } } return $DB->get_record('glossary_categories', array('id' => $id), '*', MUST_EXIST); } public function create_content($glossary, $record = array(), $aliases = array()) { global $DB; $entry = $this->create_entry((array)$record + ['glossaryid' => $glossary->id]); if ($aliases) { foreach ($aliases as $alias) { $ar = new stdClass(); $ar->entryid = $entry->id; $ar->alias = $alias; $DB->insert_record('glossary_alias', $ar); } } if (array_key_exists('tags', $record)) { $tags = is_array($record['tags']) ? $record['tags'] : preg_split('/,/', $record['tags']); core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $entry->id, context_module::instance($glossary->cmid), $tags); } return $entry; } /** * Create an entry. * * @param array $data Data to create the entry record. * In addition to columns in the entry table, the following attributes are supported: * - glossaryid (required): Id of the glossary where the entry will be created. * - categoryids: Array of ids for the categories this entry belongs to. * * @return stdClass Entry record. */ public function create_entry(array $data): stdClass { global $DB, $USER, $CFG; // Prepare glossary. $coursemodule = get_coursemodule_from_instance('glossary', $data['glossaryid']); $glossary = $DB->get_record('glossary', ['id' => $data['glossaryid']], '*', MUST_EXIST); $glossary->cmid = $coursemodule->id; unset($data['glossaryid']); // Prepare category ids. $categoryids = $data['categoryids'] ?? []; unset($data['categoryids']); // Create entry. $this->entrycount++; $now = time(); $record = $data + [ 'glossaryid' => $glossary->id, 'timecreated' => $now, 'timemodified' => $now, 'userid' => $USER->id, 'concept' => 'Glossary entry '.$this->entrycount, 'definition' => 'Definition of glossary entry '.$this->entrycount, 'definitionformat' => FORMAT_MOODLE, 'definitiontrust' => 0, 'usedynalink' => $CFG->glossary_linkentries, 'casesensitive' => $CFG->glossary_casesensitive, 'fullmatch' => $CFG->glossary_fullmatch ]; if (!isset($record['teacherentry']) || !isset($record['approved'])) { $context = context_module::instance($glossary->cmid); if (!isset($record['teacherentry'])) { $record['teacherentry'] = has_capability('mod/glossary:manageentries', $context, $record['userid']); } if (!isset($record['approved'])) { $defaultapproval = $glossary->defaultapproval; $record['approved'] = ($defaultapproval || has_capability('mod/glossary:approve', $context)); } } $id = $DB->insert_record('glossary_entries', $record); foreach ($categoryids as $categoryid) { $DB->insert_record('glossary_entries_categories', ['entryid' => $id, 'categoryid' => $categoryid]); } $entries = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST); if (isset($record['tags'])) { $cm = get_coursemodule_from_instance('glossary', $glossary->id); $tags = is_array($record['tags']) ? $record['tags'] : explode(',', $record['tags']); core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $id, context_module::instance($cm->id), $tags); } return $entries; } } tests/external/prepare_entry_test.php000064400000005026152265517410014172 0ustar00. namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; use externallib_advanced_testcase; /** * External function test for prepare_entry. * * @package mod_glossary * @category external * @covers \mod_glossary\external\prepare_entry * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class prepare_entry_test extends externallib_advanced_testcase { /** * test_prepare_entry */ public function test_prepare_entry(): void { global $USER; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setAdminUser(); $aliases = ['alias1', 'alias2']; $entry = $gg->create_content( $glossary, ['approved' => 1, 'userid' => $USER->id], $aliases ); $cat1 = $gg->create_category($glossary, [], [$entry]); $gg->create_category($glossary); $return = prepare_entry::execute($entry->id); $return = external_api::clean_returnvalue(prepare_entry::execute_returns(), $return); $this->assertNotEmpty($return['inlineattachmentsid']); $this->assertNotEmpty($return['attachmentsid']); $this->assertEquals($aliases, $return['aliases']); $this->assertEquals([$cat1->id], $return['categories']); $this->assertCount(2, $return['areas']); $this->assertNotEmpty($return['areas'][0]['options']); $this->assertNotEmpty($return['areas'][1]['options']); } } tests/external/update_entry_test.php000064400000027661152265517410014027 0ustar00. namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; use externallib_advanced_testcase; use mod_glossary_external; use context_module; use context_user; use core_external\util as external_util; /** * External function test for update_entry. * * @package mod_glossary * @category external * @covers \mod_glossary\external\update_entry * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class update_entry_test extends externallib_advanced_testcase { /** * test_update_entry_without_optional_settings */ public function test_update_entry_without_optional_settings(): void { global $CFG, $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $this->setAdminUser(); $concept = 'A concept'; $definition = '

A definition

'; $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $entryid = $return['entryid']; // Updates the entry. $concept .= ' Updated!'; $definition .= '

Updated!

'; $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML); $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); // Get entry from DB. $entry = $DB->get_record('glossary_entries', ['id' => $entryid]); $this->assertEquals($concept, $entry->concept); $this->assertEquals($definition, $entry->definition); $this->assertEquals($CFG->glossary_linkentries, $entry->usedynalink); $this->assertEquals($CFG->glossary_casesensitive, $entry->casesensitive); $this->assertEquals($CFG->glossary_fullmatch, $entry->fullmatch); $this->assertEmpty($DB->get_records('glossary_alias', ['entryid' => $entryid])); $this->assertEmpty($DB->get_records('glossary_entries_categories', ['entryid' => $entryid])); } /** * test_update_entry_duplicated */ public function test_update_entry_duplicated(): void { global $CFG, $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id, 'allowduplicatedentries' => 1]); // Create three entries. $this->setAdminUser(); $concept = 'A concept'; $definition = '

A definition

'; mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); $concept = 'B concept'; $definition = '

B definition

'; mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); $concept = 'Another concept'; $definition = '

Another definition

'; $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $entryid = $return['entryid']; // Updates the entry using an existing entry name when duplicateds are allowed. $concept = 'A concept'; update_entry::execute($entryid, $concept, $definition, FORMAT_HTML); // Updates the entry using an existing entry name when duplicateds are NOT allowed. $DB->set_field('glossary', 'allowduplicatedentries', 0, ['id' => $glossary->id]); $concept = 'B concept'; $this->expectExceptionMessage(get_string('errconceptalreadyexists', 'glossary')); update_entry::execute($entryid, $concept, $definition, FORMAT_HTML); } /** * test_update_entry_with_aliases */ public function test_update_entry_with_aliases(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; $paramaliases = 'abc, def, gez'; $options = [ [ 'name' => 'aliases', 'value' => $paramaliases, ] ]; $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $entryid = $return['entryid']; // Updates the entry. $newaliases = 'abz, xyz'; $options[0]['value'] = $newaliases; $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); $aliases = $DB->get_records('glossary_alias', ['entryid' => $entryid]); $this->assertCount(2, $aliases); foreach ($aliases as $alias) { $this->assertStringContainsString($alias->alias, $newaliases); } } /** * test_update_entry_in_categories */ public function test_update_entry_in_categories(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $cat1 = $gg->create_category($glossary); $cat2 = $gg->create_category($glossary); $cat3 = $gg->create_category($glossary); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; $paramcategories = "$cat1->id, $cat2->id"; $options = [ [ 'name' => 'categories', 'value' => $paramcategories, ] ]; $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $entryid = $return['entryid']; // Updates the entry. $newcategories = "$cat1->id, $cat3->id"; $options[0]['value'] = $newcategories; $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); $categories = $DB->get_records('glossary_entries_categories', ['entryid' => $entryid]); $this->assertCount(2, $categories); foreach ($categories as $category) { $this->assertStringContainsString($category->categoryid, $newcategories); } } /** * test_update_entry_with_attachments */ public function test_update_entry_with_attachments(): void { global $DB, $USER; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $context = context_module::instance($glossary->cmid); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; // Draft files. $draftidinlineattach = file_get_unused_draft_itemid(); $draftidattach = file_get_unused_draft_itemid(); $usercontext = context_user::instance($USER->id); $filerecordinline = [ 'contextid' => $usercontext->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $draftidinlineattach, 'filepath' => '/', 'filename' => 'shouldbeanimage.png', ]; $fs = get_file_storage(); // Create a file in a draft area for regular attachments. $filerecordattach = $filerecordinline; $attachfilename = 'attachment.txt'; $filerecordattach['filename'] = $attachfilename; $filerecordattach['itemid'] = $draftidattach; $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $fs->create_file_from_string($filerecordattach, 'simple text attachment'); $options = [ [ 'name' => 'inlineattachmentsid', 'value' => $draftidinlineattach, ], [ 'name' => 'attachmentsid', 'value' => $draftidattach, ] ]; $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $entryid = $return['entryid']; $entry = $DB->get_record('glossary_entries', ['id' => $entryid]); list($definitionoptions, $attachmentoptions) = glossary_get_editor_and_attachment_options($course, $context, $entry); $entry = file_prepare_standard_editor($entry, 'definition', $definitionoptions, $context, 'mod_glossary', 'entry', $entry->id); $entry = file_prepare_standard_filemanager($entry, 'attachment', $attachmentoptions, $context, 'mod_glossary', 'attachment', $entry->id); $inlineattachmentsid = $entry->definition_editor['itemid']; $attachmentsid = $entry->attachment_filemanager; // Change the file areas. // Delete one inline editor file. $selectedfile = (object)[ 'filename' => $filerecordinline['filename'], 'filepath' => $filerecordinline['filepath'], ]; $return = repository_delete_selected_files($usercontext, 'user', 'draft', $inlineattachmentsid, [$selectedfile]); // Add more files. $filerecordinline['filename'] = 'newvideo.mp4'; $filerecordinline['itemid'] = $inlineattachmentsid; $filerecordattach['filename'] = 'newattach.txt'; $filerecordattach['itemid'] = $attachmentsid; $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $fs->create_file_from_string($filerecordattach, 'simple text attachment'); // Updates the entry. $options[0]['value'] = $inlineattachmentsid; $options[1]['value'] = $attachmentsid; $return = update_entry::execute($entryid, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(update_entry::execute_returns(), $return); $editorfiles = external_util::get_area_files($context->id, 'mod_glossary', 'entry', $entryid); $attachmentfiles = external_util::get_area_files($context->id, 'mod_glossary', 'attachment', $entryid); $this->assertCount(1, $editorfiles); $this->assertCount(2, $attachmentfiles); $this->assertEquals('newvideo.mp4', $editorfiles[0]['filename']); $this->assertEquals('attachment.txt', $attachmentfiles[0]['filename']); $this->assertEquals('newattach.txt', $attachmentfiles[1]['filename']); } } tests/external/external_test.php000064400000242513152265517410013141 0ustar00. /** * External glossary functions unit tests * * @package mod_glossary * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\external; use core_external\external_api; use core_external\util as external_util; use externallib_advanced_testcase; use mod_glossary_external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External glossary functions unit tests * * @package mod_glossary * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class external_test extends externallib_advanced_testcase { /** * Test get_glossaries_by_courses */ public function test_get_glossaries_by_courses(): void { $this->resetAfterTest(true); // As admin. $this->setAdminUser(); $c1 = self::getDataGenerator()->create_course(); $c2 = self::getDataGenerator()->create_course(); $g1 = self::getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'name' => 'First Glossary')); $g2 = self::getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'name' => 'Second Glossary')); $g3 = self::getDataGenerator()->create_module('glossary', array('course' => $c2->id, 'name' => 'Third Glossary')); $s1 = $this->getDataGenerator()->create_user(); self::getDataGenerator()->enrol_user($s1->id, $c1->id); // Check results where student is enrolled. $this->setUser($s1); $glossaries = mod_glossary_external::get_glossaries_by_courses(array()); $glossaries = external_api::clean_returnvalue(mod_glossary_external::get_glossaries_by_courses_returns(), $glossaries); $this->assertCount(2, $glossaries['glossaries']); $this->assertEquals('First Glossary', $glossaries['glossaries'][0]['name']); $this->assertEquals('Second Glossary', $glossaries['glossaries'][1]['name']); $this->assertEquals(1, $glossaries['glossaries'][0]['canaddentry']); $this->assertEquals(1, $glossaries['glossaries'][1]['canaddentry']); // Check results with specific course IDs. $glossaries = mod_glossary_external::get_glossaries_by_courses(array($c1->id, $c2->id)); $glossaries = external_api::clean_returnvalue(mod_glossary_external::get_glossaries_by_courses_returns(), $glossaries); $this->assertCount(2, $glossaries['glossaries']); $this->assertEquals('First Glossary', $glossaries['glossaries'][0]['name']); $this->assertEquals('Second Glossary', $glossaries['glossaries'][1]['name']); $this->assertEquals('course', $glossaries['warnings'][0]['item']); $this->assertEquals($c2->id, $glossaries['warnings'][0]['itemid']); $this->assertEquals('1', $glossaries['warnings'][0]['warningcode']); $this->assertEquals(1, $glossaries['glossaries'][0]['canaddentry']); // Now as admin. $this->setAdminUser(); $glossaries = mod_glossary_external::get_glossaries_by_courses(array($c2->id)); $glossaries = external_api::clean_returnvalue(mod_glossary_external::get_glossaries_by_courses_returns(), $glossaries); $this->assertCount(1, $glossaries['glossaries']); $this->assertEquals('Third Glossary', $glossaries['glossaries'][0]['name']); $this->assertEquals(1, $glossaries['glossaries'][0]['canaddentry']); } public function test_view_glossary(): void { $this->resetAfterTest(true); // Generate all the things. $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $sink = $this->redirectEvents(); $this->setUser($u1); $return = mod_glossary_external::view_glossary($g1->id, 'letter'); $return = external_api::clean_returnvalue(mod_glossary_external::view_glossary_returns(), $return); $events = $sink->get_events(); // Assertion. $this->assertTrue($return['status']); $this->assertEmpty($return['warnings']); $this->assertCount(1, $events); $this->assertEquals('\mod_glossary\event\course_module_viewed', $events[0]->eventname); $sink->close(); } public function test_view_glossary_without_permission(): void { $this->resetAfterTest(true); // Generate all the things. $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $ctx = \context_module::instance($g1->cmid); // Revoke permission. $roles = get_archetype_roles('user'); $role = array_shift($roles); assign_capability('mod/glossary:view', CAP_PROHIBIT, $role->id, $ctx, true); accesslib_clear_all_caches_for_unit_testing(); // Assertion. $this->setUser($u1); $this->expectException(\require_login_exception::class); $this->expectExceptionMessage('Activity is hidden'); mod_glossary_external::view_glossary($g1->id, 'letter'); } public function test_view_entry(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'visible' => false)); $u1 = $this->getDataGenerator()->create_user(); $e1 = $gg->create_content($g1, array('approved' => 1)); $e2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e3 = $gg->create_content($g1, array('approved' => 0, 'userid' => -1)); $e4 = $gg->create_content($g2, array('approved' => 1)); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setUser($u1); // Test readable entry. $sink = $this->redirectEvents(); $return = mod_glossary_external::view_entry($e1->id); $return = external_api::clean_returnvalue(mod_glossary_external::view_entry_returns(), $return); $events = $sink->get_events(); $this->assertTrue($return['status']); $this->assertEmpty($return['warnings']); $this->assertCount(1, $events); $this->assertEquals('\mod_glossary\event\entry_viewed', $events[0]->eventname); $sink->close(); // Test non-approved of self. $return = mod_glossary_external::view_entry($e2->id); $return = external_api::clean_returnvalue(mod_glossary_external::view_entry_returns(), $return); $events = $sink->get_events(); $this->assertTrue($return['status']); $this->assertEmpty($return['warnings']); $this->assertCount(1, $events); $this->assertEquals('\mod_glossary\event\entry_viewed', $events[0]->eventname); $sink->close(); // Test non-approved of other. try { mod_glossary_external::view_entry($e3->id); $this->fail('Cannot view non-approved entries of others.'); } catch (\invalid_parameter_exception $e) { // All good. } // Test non-readable entry. $this->expectException(\require_login_exception::class); $this->expectExceptionMessage('Activity is hidden'); mod_glossary_external::view_entry($e4->id); } public function test_get_entries_by_letter(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Bob', 'userid' => 2, 'tags' => array('Cats', 'Dogs'))); $e1b = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Jane', 'userid' => 2)); $e1c = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Alice', 'userid' => $u1->id)); $e1d = $gg->create_content($g1, array('approved' => 0, 'concept' => '0-day', 'userid' => $u1->id)); $e2a = $gg->create_content($g2); $this->setAdminUser(); // Just a normal request from admin user. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); $this->assertEquals($e1a->id, $return['entries'][1]['id']); $this->assertEquals('Cats', $return['entries'][1]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][1]['tags'][1]['rawname']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // An admin user requesting all the entries. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array('includenotapproved' => 1)); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); $this->assertEquals($e1b->id, $return['entries'][3]['id']); // A normal user. $this->setUser($u1); $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // A normal user requesting to view all non approved entries. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array('includenotapproved' => 1)); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); } public function test_get_entries_by_letter_with_parameters(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a = $gg->create_content($g1, array('approved' => 1, 'concept' => '0-day', 'userid' => $u1->id)); $e1b = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Bob', 'userid' => 2)); $e1c = $gg->create_content($g1, array('approved' => 1, 'concept' => '1-dayb', 'userid' => $u1->id)); $this->setUser($u1); // Requesting a single letter. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'b', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); // Requesting special letters. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'SPECIAL', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); // Requesting with limit. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 1, 2, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); $this->assertEquals($e1b->id, $return['entries'][1]['id']); } public function test_get_entries_by_date(): void { global $DB; $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $now = time(); $e1a = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Bob', 'userid' => $u1->id, 'timecreated' => 1, 'timemodified' => $now + 3600, 'tags' => array('Cats', 'Dogs'))); $e1b = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Jane', 'userid' => $u1->id, 'timecreated' => $now + 3600, 'timemodified' => 1)); $e1c = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Alice', 'userid' => $u1->id, 'timecreated' => $now + 1, 'timemodified' => $now + 1)); $e1d = $gg->create_content($g1, array('approved' => 0, 'concept' => '0-day', 'userid' => $u1->id, 'timecreated' => $now + 2, 'timemodified' => $now + 2)); $e2a = $gg->create_content($g2); $this->setAdminUser($u1); // Ordering by time modified descending. $return = mod_glossary_external::get_entries_by_date($g1->id, 'UPDATE', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // Ordering by time modified ascending. $return = mod_glossary_external::get_entries_by_date($g1->id, 'UPDATE', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); // Ordering by time created asc. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // Ordering by time created descending. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); // Ordering including to approve. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1d->id, $return['entries'][2]['id']); $this->assertEquals($e1b->id, $return['entries'][3]['id']); // Ordering including to approve and pagination. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 2, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1b->id, $return['entries'][1]['id']); } public function test_get_categories(): void { $this->resetAfterTest(true); $this->setAdminUser(); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $cat1a = $gg->create_category($g1); $cat1b = $gg->create_category($g1); $cat1c = $gg->create_category($g1); $cat2a = $gg->create_category($g2); $return = mod_glossary_external::get_categories($g1->id, 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_categories_returns(), $return); $this->assertCount(3, $return['categories']); $this->assertEquals(3, $return['count']); $this->assertEquals($cat1a->id, $return['categories'][0]['id']); $this->assertEquals($cat1b->id, $return['categories'][1]['id']); $this->assertEquals($cat1c->id, $return['categories'][2]['id']); $return = mod_glossary_external::get_categories($g1->id, 1, 2); $return = external_api::clean_returnvalue(mod_glossary_external::get_categories_returns(), $return); $this->assertCount(2, $return['categories']); $this->assertEquals(3, $return['count']); $this->assertEquals($cat1b->id, $return['categories'][0]['id']); $this->assertEquals($cat1c->id, $return['categories'][1]['id']); } public function test_get_entries_by_category(): void { $this->resetAfterTest(true); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $e1a1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'tags' => array('Cats', 'Dogs'))); $e1a2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1a3 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1b1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1b2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e1x1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1x2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e2a1 = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $e2a2 = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $cat1a = $gg->create_category($g1, array('name' => 'Fish'), array($e1a1, $e1a2, $e1a3)); $cat1b = $gg->create_category($g1, array('name' => 'Cat'), array($e1b1, $e1b2)); $cat1c = $gg->create_category($g1, array('name' => 'Zebra'), array($e1b1)); // Entry $e1b1 is in two categories. $cat2a = $gg->create_category($g2, array(), array($e2a1, $e2a2)); $this->setAdminUser(); // Browse one category. $return = mod_glossary_external::get_entries_by_category($g1->id, $cat1a->id, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); $this->assertEquals($e1a2->id, $return['entries'][1]['id']); $this->assertEquals($e1a3->id, $return['entries'][2]['id']); // Browse all categories. $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_ALL_CATEGORIES, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(5, $return['entries']); $this->assertEquals(5, $return['count']); $this->assertEquals($e1b1->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); $this->assertEquals($e1b1->id, $return['entries'][4]['id']); // Browse uncategorised. $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_NOT_CATEGORISED, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1x1->id, $return['entries'][0]['id']); // Including to approve. $return = mod_glossary_external::get_entries_by_category($g1->id, $cat1b->id, 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e1b1->id, $return['entries'][0]['id']); $this->assertEquals($e1b2->id, $return['entries'][1]['id']); // Using limit. $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_ALL_CATEGORIES, 0, 3, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1b1->id, $return['entries'][0]['id']); $this->assertEquals($e1b2->id, $return['entries'][1]['id']); $this->assertEquals($e1a1->id, $return['entries'][2]['id']); $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_ALL_CATEGORIES, 3, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); } public function test_get_authors(): void { $this->resetAfterTest(true); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(array('lastname' => 'Upsilon')); $u2 = $this->getDataGenerator()->create_user(array('lastname' => 'Alpha')); $u3 = $this->getDataGenerator()->create_user(array('lastname' => 'Omega')); $ctx = \context_module::instance($g1->cmid); $e1a = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1)); $e1b = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1)); $e1c = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1)); $e2a = $gg->create_content($g1, array('userid' => $u2->id, 'approved' => 1)); $e3a = $gg->create_content($g1, array('userid' => $u3->id, 'approved' => 0)); $this->setAdminUser(); // Simple request. $return = mod_glossary_external::get_authors($g1->id, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_authors_returns(), $return); $this->assertCount(2, $return['authors']); $this->assertEquals(2, $return['count']); $this->assertEquals($u2->id, $return['authors'][0]['id']); $this->assertEquals($u1->id, $return['authors'][1]['id']); // Include users with entries pending approval. $return = mod_glossary_external::get_authors($g1->id, 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_authors_returns(), $return); $this->assertCount(3, $return['authors']); $this->assertEquals(3, $return['count']); $this->assertEquals($u2->id, $return['authors'][0]['id']); $this->assertEquals($u3->id, $return['authors'][1]['id']); $this->assertEquals($u1->id, $return['authors'][2]['id']); // Pagination. $return = mod_glossary_external::get_authors($g1->id, 1, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_authors_returns(), $return); $this->assertCount(1, $return['authors']); $this->assertEquals(3, $return['count']); $this->assertEquals($u3->id, $return['authors'][0]['id']); } public function test_get_entries_by_author(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $u1 = $this->getDataGenerator()->create_user(array('lastname' => 'Upsilon', 'firstname' => 'Zac')); $u2 = $this->getDataGenerator()->create_user(array('lastname' => 'Ultra', 'firstname' => '1337')); $u3 = $this->getDataGenerator()->create_user(array('lastname' => 'Alpha', 'firstname' => 'Omega')); $u4 = $this->getDataGenerator()->create_user(array('lastname' => '0-day', 'firstname' => 'Zoe')); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1a2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1a3 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1b1 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u2->id)); $e1b2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u2->id, 'tags' => array('Cats', 'Dogs'))); $e1c1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u3->id)); $e1d1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u4->id)); $e2a = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $this->setUser($u1); // Requesting a single letter. $return = mod_glossary_external::get_entries_by_author($g1->id, 'u', 'LASTNAME', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b2->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); // Requesting special letters. $return = mod_glossary_external::get_entries_by_author($g1->id, 'SPECIAL', 'LASTNAME', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); // Requesting with limit. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'ASC', 1, 2, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1c1->id, $return['entries'][0]['id']); $this->assertEquals($e1b2->id, $return['entries'][1]['id']); // Including non-approved. $this->setAdminUser(); $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(7, $return['entries']); $this->assertEquals(7, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); $this->assertEquals($e1c1->id, $return['entries'][1]['id']); $this->assertEquals($e1b1->id, $return['entries'][2]['id']); $this->assertEquals($e1b2->id, $return['entries'][3]['id']); $this->assertEquals($e1a1->id, $return['entries'][4]['id']); $this->assertEquals($e1a2->id, $return['entries'][5]['id']); $this->assertEquals($e1a3->id, $return['entries'][6]['id']); // Changing order. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); // Sorting by firstname. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'FIRSTNAME', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1b2->id, $return['entries'][0]['id']); // Sorting by firstname descending. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'FIRSTNAME', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); // Filtering by firstname descending. $return = mod_glossary_external::get_entries_by_author($g1->id, 'z', 'FIRSTNAME', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); // Test with a deleted user. delete_user($u2); $return = mod_glossary_external::get_entries_by_author($g1->id, 'u', 'LASTNAME', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b2->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); } public function test_get_entries_by_author_id(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $u1 = $this->getDataGenerator()->create_user(array('lastname' => 'Upsilon', 'firstname' => 'Zac')); $u2 = $this->getDataGenerator()->create_user(array('lastname' => 'Ultra', 'firstname' => '1337')); $u3 = $this->getDataGenerator()->create_user(array('lastname' => 'Alpha', 'firstname' => 'Omega')); $u4 = $this->getDataGenerator()->create_user(array('lastname' => '0-day', 'firstname' => 'Zoe')); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'concept' => 'Zoom', 'timecreated' => 3600, 'timemodified' => time() - 3600)); $e1a2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'concept' => 'Alpha')); $e1a3 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'concept' => 'Dog', 'timecreated' => 1, 'timemodified' => time() - 1800)); $e1a4 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id, 'concept' => 'Bird')); $e1b1 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u2->id)); $e2a = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $this->setAdminUser(); // Standard request. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a1->id, $return['entries'][2]['id']); // Standard request descending. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); // Requesting ordering by time created. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CREATION', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a3->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); // Requesting ordering by time created descending. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CREATION', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a3->id, $return['entries'][2]['id']); // Requesting ordering by time modified. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'UPDATE', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); // Requesting ordering by time modified descending. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'UPDATE', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a1->id, $return['entries'][2]['id']); // Including non approved. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a4->id, $return['entries'][1]['id']); $this->assertEquals($e1a3->id, $return['entries'][2]['id']); $this->assertEquals($e1a1->id, $return['entries'][3]['id']); // Pagination. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a4->id, $return['entries'][1]['id']); $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 1, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a4->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); } public function test_get_entries_by_search(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setUser($u1); $e1 = $gg->create_content($g1, array('approved' => 1, 'concept' => 'House', 'timecreated' => time() + 3600)); $e2 = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Mouse', 'timemodified' => 1)); $e3 = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Hero', 'tags' => array('Cats', 'Dogs'))); $e4 = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Toulouse')); $e5 = $gg->create_content($g1, array('approved' => 1, 'definition' => 'Heroes', 'concept' => 'Abcd')); $e6 = $gg->create_content($g1, array('approved' => 0, 'definition' => 'When used for Heroes')); $e7 = $gg->create_content($g1, array('approved' => 1, 'timecreated' => 1, 'timemodified' => time() + 3600, 'concept' => 'Z'), array('Couscous')); $e8 = $gg->create_content($g1, array('approved' => 0), array('Heroes')); $e9 = $gg->create_content($g2, array('approved' => 0)); $this->setAdminUser(); // Test simple query. $query = 'hero'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e3->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); // Enabling full search. $query = 'hero'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e5->id, $return['entries'][0]['id']); $this->assertEquals($e3->id, $return['entries'][1]['id']); // Concept descending. $query = 'hero'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e3->id, $return['entries'][0]['id']); $this->assertEquals($e5->id, $return['entries'][1]['id']); // Search on alias. $query = 'couscous'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); // Pagination and ordering on created date. $query = 'ou'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CREATION', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CREATION', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1->id, $return['entries'][0]['id']); // Ordering on updated date. $query = 'ou'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'UPDATE', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e2->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'UPDATE', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); // Including not approved. $query = 'ou'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CONCEPT', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1->id, $return['entries'][0]['id']); $this->assertEquals($e2->id, $return['entries'][1]['id']); $this->assertEquals($e4->id, $return['entries'][2]['id']); $this->assertEquals($e7->id, $return['entries'][3]['id']); // Advanced query string. $query = '+Heroes -Abcd'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e6->id, $return['entries'][0]['id']); $this->assertEquals($e8->id, $return['entries'][1]['id']); } public function test_get_entries_by_term(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setAdminUser(); $e1 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1, 'concept' => 'cat', 'tags' => array('Cats', 'Dogs'))); $e2 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1), array('cat', 'dog')); $e3 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1), array('dog')); $e4 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 0, 'concept' => 'dog')); $e5 = $gg->create_content($g2, array('userid' => $u1->id, 'approved' => 1, 'concept' => 'dog'), array('cat')); // Search concept + alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'cat', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array($e1->id, $e2->id); $actual = array($return['entries'][0]['id'], $return['entries'][1]['id']); $this->assertEqualsCanonicalizing($expected, $actual); // Compare rawnames of all expected tags, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array('Cats', 'Dogs'); // Only $e1 has 2 tags. $actual = array(); // Accumulate all tags returned. foreach ($return['entries'] as $entry) { foreach ($entry['tags'] as $tag) { $actual[] = $tag['rawname']; } } $this->assertEqualsCanonicalizing($expected, $actual); // Search alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array($e2->id, $e3->id); $actual = array($return['entries'][0]['id'], $return['entries'][1]['id']); $this->assertEqualsCanonicalizing($expected, $actual); // Search including not approved. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array($e4->id, $e2->id, $e3->id); $actual = array($return['entries'][0]['id'], $return['entries'][1]['id'], $return['entries'][2]['id']); $this->assertEqualsCanonicalizing($expected, $actual); // Pagination. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); // We don't compare the returned entry id because it may be different depending on the DBMS, // for example, Postgres does a random sorting in this case. $this->assertEquals(3, $return['count']); $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 1, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); } /** * Test get_entries_by_multilingual_term. * * @covers \mod_glossary_external::get_entries_by_term * @return void * @throws \coding_exception * @throws \invalid_response_exception * @throws \moodle_exception */ public function test_get_entries_by_multilingual_term(): void { $this->resetAfterTest(true); // Enable multilang filter to on content and heading. filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_applies_to_strings('multilang', 1); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', ['course' => $c1->id]); $g2 = $this->getDataGenerator()->create_module('glossary', ['course' => $c1->id]); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setAdminUser(); $e1 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1, 'concept' => 'catchat', 'tags' => ['Cats', 'Dogs']]); $e2 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1], ['catchat', '' . 'dogchien']); $e3 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1], ['dogchien']); $e4 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 0, 'concept' => 'dogchien']); $e5 = $gg->create_content($g2, ['userid' => $u1->id, 'approved' => 1, 'concept' => 'cogchien'], ['cat']); // Search concept + alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'cat', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = [$e1->id, $e2->id]; $actual = [$return['entries'][0]['id'], $return['entries'][1]['id']]; $this->assertEqualsCanonicalizing($expected, $actual); // Compare rawnames of all expected tags, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = ['Cats', 'Dogs']; // Only $e1 has 2 tags. $actual = []; // Accumulate all tags returned. foreach ($return['entries'] as $entry) { foreach ($entry['tags'] as $tag) { $actual[] = $tag['rawname']; } } $this->assertEqualsCanonicalizing($expected, $actual); // Search alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = [$e2->id, $e3->id]; $actual = [$return['entries'][0]['id'], $return['entries'][1]['id']]; $this->assertEqualsCanonicalizing($expected, $actual); // Search including not approved. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = [$e4->id, $e2->id, $e3->id]; $actual = [$return['entries'][0]['id'], $return['entries'][1]['id'], $return['entries'][2]['id']]; $this->assertEqualsCanonicalizing($expected, $actual); // Pagination. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); // We don't compare the returned entry id because it may be different depending on the DBMS, // for example, Postgres does a random sorting in this case. $this->assertEquals(3, $return['count']); $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 1, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); } public function test_get_entries_to_approve(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Bob', 'userid' => $u1->id, 'timecreated' => time() + 3600)); $e1b = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Jane', 'userid' => $u1->id, 'timecreated' => 1)); $e1c = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Alice', 'userid' => $u1->id, 'timemodified' => 1)); $e1d = $gg->create_content($g1, array('approved' => 0, 'concept' => '0-day', 'userid' => $u1->id, 'timemodified' => time() + 3600)); $e1e = $gg->create_content($g1, array('approved' => 1, 'concept' => '1-day', 'userid' => $u1->id)); $e2a = $gg->create_content($g2); $this->setAdminUser(true); // Simple listing. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); $this->assertEquals($e1b->id, $return['entries'][3]['id']); // Revert ordering of concept. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'DESC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); $this->assertEquals($e1a->id, $return['entries'][1]['id']); $this->assertEquals($e1c->id, $return['entries'][2]['id']); $this->assertEquals($e1d->id, $return['entries'][3]['id']); // Filtering by letter. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'a', 'CONCEPT', 'ASC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); // Filtering by special. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'SPECIAL', 'CONCEPT', 'ASC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); // Pagination. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 0, 2); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 1, 2); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); $this->assertEquals($e1a->id, $return['entries'][1]['id']); // Ordering by creation date. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CREATION', 'ASC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); // Ordering by creation date desc. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CREATION', 'DESC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); // Ordering by update date. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'UPDATE', 'ASC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); // Ordering by update date desc. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'UPDATE', 'DESC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); // Permissions are checked. $this->setUser($u1); $this->expectException('required_capability_exception'); mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 0, 1); $this->fail('Do not test anything else after this.'); } public function test_get_entry_by_id(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $c2 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c2->id, 'visible' => 0)); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->getDataGenerator()->enrol_user($u2->id, $c1->id); $this->getDataGenerator()->enrol_user($u3->id, $c1->id); $e1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'tags' => array('Cats', 'Dogs'))); // Add a fake inline image to the entry. $filename = 'shouldbeanimage.jpg'; $filerecordinline = array( 'contextid' => $ctx->id, 'component' => 'mod_glossary', 'filearea' => 'entry', 'itemid' => $e1->id, 'filepath' => '/', 'filename' => $filename, ); $fs = get_file_storage(); $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $e2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e3 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u2->id)); $e4 = $gg->create_content($g2, array('approved' => 1)); $this->setUser($u1); $return = mod_glossary_external::get_entry_by_id($e1->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e1->id, $return['entry']['id']); $this->assertEquals('Cats', $return['entry']['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entry']['tags'][1]['rawname']); $this->assertEquals($filename, $return['entry']['definitioninlinefiles'][0]['filename']); $this->assertTrue($return['permissions']['candelete']); $return = mod_glossary_external::get_entry_by_id($e2->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e2->id, $return['entry']['id']); $this->assertTrue($return['permissions']['candelete']); try { $return = mod_glossary_external::get_entry_by_id($e3->id); $this->fail('Cannot view unapproved entries of others.'); } catch (\invalid_parameter_exception $e) { // All good. } try { $return = mod_glossary_external::get_entry_by_id($e4->id); $this->fail('Cannot view entries from another course.'); } catch (\require_login_exception $e) { // All good. } // An admin can see other's entries to be approved. $this->setAdminUser(); $return = mod_glossary_external::get_entry_by_id($e3->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e3->id, $return['entry']['id']); $this->assertTrue($return['permissions']['candelete']); // Students can see other students approved entries but they will not be able to delete them. $this->setUser($u3); $return = mod_glossary_external::get_entry_by_id($e1->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e1->id, $return['entry']['id']); $this->assertFalse($return['permissions']['candelete']); } public function test_add_entry_without_optional_settings(): void { global $CFG, $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $this->setAdminUser(); $concept = 'A concept'; $definition = '

A definition

'; $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); // Get entry from DB. $entry = $DB->get_record('glossary_entries', array('id' => $return['entryid'])); $this->assertEquals($concept, $entry->concept); $this->assertEquals($definition, $entry->definition); $this->assertEquals($CFG->glossary_linkentries, $entry->usedynalink); $this->assertEquals($CFG->glossary_casesensitive, $entry->casesensitive); $this->assertEquals($CFG->glossary_fullmatch, $entry->fullmatch); $this->assertEmpty($DB->get_records('glossary_alias', array('entryid' => $return['entryid']))); $this->assertEmpty($DB->get_records('glossary_entries_categories', array('entryid' => $return['entryid']))); } public function test_add_entry_with_aliases(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; $paramaliases = 'abc, def, gez'; $options = array( array( 'name' => 'aliases', 'value' => $paramaliases, ) ); $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $aliases = $DB->get_records('glossary_alias', array('entryid' => $return['entryid'])); $this->assertCount(3, $aliases); foreach ($aliases as $alias) { $this->assertStringContainsString($alias->alias, $paramaliases); } } public function test_add_entry_in_categories(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $cat1 = $gg->create_category($glossary); $cat2 = $gg->create_category($glossary); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; $paramcategories = "$cat1->id, $cat2->id"; $options = array( array( 'name' => 'categories', 'value' => $paramcategories, ) ); $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $categories = $DB->get_records('glossary_entries_categories', array('entryid' => $return['entryid'])); $this->assertCount(2, $categories); foreach ($categories as $category) { $this->assertStringContainsString($category->categoryid, $paramcategories); } } public function test_add_entry_with_attachments(): void { global $DB, $USER; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $context = \context_module::instance($glossary->cmid); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; // Draft files. $draftidinlineattach = file_get_unused_draft_itemid(); $draftidattach = file_get_unused_draft_itemid(); $usercontext = \context_user::instance($USER->id); $filerecordinline = array( 'contextid' => $usercontext->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $draftidinlineattach, 'filepath' => '/', 'filename' => 'shouldbeanimage.txt', ); $fs = get_file_storage(); // Create a file in a draft area for regular attachments. $filerecordattach = $filerecordinline; $attachfilename = 'attachment.txt'; $filerecordattach['filename'] = $attachfilename; $filerecordattach['itemid'] = $draftidattach; $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $fs->create_file_from_string($filerecordattach, 'simple text attachment'); $options = array( array( 'name' => 'inlineattachmentsid', 'value' => $draftidinlineattach, ), array( 'name' => 'attachmentsid', 'value' => $draftidattach, ) ); $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $editorfiles = external_util::get_area_files($context->id, 'mod_glossary', 'entry', $return['entryid']); $attachmentfiles = external_util::get_area_files($context->id, 'mod_glossary', 'attachment', $return['entryid']); $this->assertCount(1, $editorfiles); $this->assertCount(1, $attachmentfiles); $this->assertEquals('shouldbeanimage.txt', $editorfiles[0]['filename']); $this->assertEquals('attachment.txt', $attachmentfiles[0]['filename']); } /** * Test get entry including rating information. */ public function test_get_entry_rating_information(): void { $this->resetAfterTest(true); global $DB, $CFG; require_once($CFG->dirroot . '/rating/lib.php'); $this->resetAfterTest(true); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $teacher = self::getDataGenerator()->create_user(); // Create course to add the module. $course = self::getDataGenerator()->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual'); // Create the glossary and contents. $record = new \stdClass(); $record->course = $course->id; $record->assessed = RATING_AGGREGATE_AVERAGE; $scale = $this->getDataGenerator()->create_scale(array('scale' => 'A,B,C,D')); $record->scale = "-$scale->id"; $glossary = $this->getDataGenerator()->create_module('glossary', $record); $context = \context_module::instance($glossary->cmid); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $gg->create_content($glossary, array('approved' => 1, 'userid' => $user1->id)); // Rate the entry as user2. $rating1 = new \stdClass(); $rating1->contextid = $context->id; $rating1->component = 'mod_glossary'; $rating1->ratingarea = 'entry'; $rating1->itemid = $entry->id; $rating1->rating = 1; // 1 is A. $rating1->scaleid = "-$scale->id"; $rating1->userid = $user2->id; $rating1->timecreated = time(); $rating1->timemodified = time(); $rating1->id = $DB->insert_record('rating', $rating1); // Rate the entry as user3. $rating2 = new \stdClass(); $rating2->contextid = $context->id; $rating2->component = 'mod_glossary'; $rating2->ratingarea = 'entry'; $rating2->itemid = $entry->id; $rating2->rating = 3; // 3 is C. $rating2->scaleid = "-$scale->id"; $rating2->userid = $user3->id; $rating2->timecreated = time() + 1; $rating2->timemodified = time() + 1; $rating2->id = $DB->insert_record('rating', $rating2); // As student, retrieve ratings information. $this->setUser($user1); $result = mod_glossary_external::get_entry_by_id($entry->id); $result = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $result); $this->assertCount(1, $result['ratinginfo']['ratings']); $this->assertFalse($result['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertFalse($result['ratinginfo']['canviewall']); $this->assertFalse($result['ratinginfo']['ratings'][0]['canrate']); $this->assertTrue(!isset($result['ratinginfo']['ratings'][0]['count'])); // Now, as teacher, I should see the info correctly. $this->setUser($teacher); $result = mod_glossary_external::get_entry_by_id($entry->id); $result = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $result); $this->assertCount(1, $result['ratinginfo']['ratings']); $this->assertTrue($result['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertTrue($result['ratinginfo']['canviewall']); $this->assertTrue($result['ratinginfo']['ratings'][0]['canrate']); $this->assertEquals(2, $result['ratinginfo']['ratings'][0]['count']); $this->assertEquals(2, $result['ratinginfo']['ratings'][0]['aggregate']); // 2 is B, that is the average of A + C. } } tests/external/delete_entry_test.php000064400000005666152265517410014010 0ustar00. namespace mod_glossary\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; use externallib_advanced_testcase; /** * External function test for delete_entry. * * @package mod_glossary * @category external * @covers \mod_glossary\external\delete_entry * @since Moodle 3.10 * @copyright 2020 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class delete_entry_test extends externallib_advanced_testcase { /** * Test the behaviour of delete_entry(). */ public function test_delete_entry(): void { global $DB; $this->resetAfterTest(); // Create required data. $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student'); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setUser($student); $entry = $gg->create_content($glossary); // Test entry creator can delete. $result = delete_entry::execute($entry->id); $result = external_api::clean_returnvalue(delete_entry::execute_returns(), $result); $this->assertTrue($result['result']); $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id])); // Test admin can delete. $this->setAdminUser(); $entry = $gg->create_content($glossary); $result = delete_entry::execute($entry->id); $result = external_api::clean_returnvalue(delete_entry::execute_returns(), $result); $this->assertTrue($result['result']); $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id])); $entry = $gg->create_content($glossary); // Test a different student is not able to delete. $this->setUser($anotherstudent); $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error')); delete_entry::execute($entry->id); } } tests/privacy/provider_test.php000064400000047375152265517410013015 0ustar00. /** * Privacy provider tests. * * @package mod_glossary * @copyright 2018 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\deletion_criteria; use mod_glossary\privacy\provider; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); require_once($CFG->dirroot . '/rating/lib.php'); /** * Privacy provider tests class. * * @package mod_glossary * @copyright 2018 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** @var stdClass The student object. */ protected $student; /** @var stdClass The teacher object. */ protected $teacher; /** @var stdClass The glossary object. */ protected $glossary; /** @var stdClass The course object. */ protected $course; /** @var stdClass The plugin generator object. */ protected $plugingenerator; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); global $DB; $generator = $this->getDataGenerator(); $course = $generator->create_course(); $this->course = $course; $this->plugingenerator = $generator->get_plugin_generator('mod_glossary'); // The glossary activity the user will answer. $glossary = $this->plugingenerator->create_instance(['course' => $course->id]); $this->glossary = $glossary; $cm = get_coursemodule_from_instance('glossary', $glossary->id); $context = \context_module::instance($cm->id); // Create a student which will add an entry to a glossary. $student = $generator->create_user(); $generator->enrol_user($student->id, $course->id, 'student'); $this->student = $student; $teacher = $generator->create_user(); $generator->enrol_user($teacher->id, $course->id, 'editingteacher'); $this->teacher = $teacher; $this->setUser($student->id); $ge1 = $this->plugingenerator->create_content($glossary, ['concept' => 'first', 'approved' => 1], ['one']); // Student create a comment on a glossary entry. $this->setUser($student); $comment = $this->get_comment_object($context, $ge1->id); $comment->add('Hello, it\'s me!'); // Attach tags. \core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge1->id, $context, ['Beer', 'Golf']); } /** * Test for provider::get_metadata(). */ public function test_get_metadata(): void { $collection = new collection('mod_glossary'); $newcollection = provider::get_metadata($collection); $itemcollection = $newcollection->get_collection(); $this->assertCount(5, $itemcollection); $table = reset($itemcollection); $this->assertEquals('glossary_entries', $table->get_name()); $privacyfields = $table->get_privacy_fields(); $this->assertArrayHasKey('glossaryid', $privacyfields); $this->assertArrayHasKey('concept', $privacyfields); $this->assertArrayHasKey('definition', $privacyfields); $this->assertArrayHasKey('attachment', $privacyfields); $this->assertArrayHasKey('userid', $privacyfields); $this->assertArrayHasKey('timemodified', $privacyfields); $this->assertEquals('privacy:metadata:glossary_entries', $table->get_summary()); } /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); $contextlist = provider::get_contexts_for_userid($this->student->id); $this->assertCount(1, $contextlist); $contextforuser = $contextlist->current(); $cmcontext = \context_module::instance($cm->id); $this->assertEquals($cmcontext->id, $contextforuser->id); } /** * Test for provider::get_users_in_context(). */ public function test_get_users_in_context(): void { $component = 'mod_glossary'; $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); $cmcontext = \context_module::instance($cm->id); $userlist = new \core_privacy\local\request\userlist($cmcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$this->student->id]; $actual = $userlist->get_userids(); sort($expected); sort($actual); $this->assertEquals($expected, $actual); } /** * Test for provider::export_user_data(). */ public function test_export_for_context(): void { $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); $cmcontext = \context_module::instance($cm->id); // Export all of the data for the context. $writer = \core_privacy\local\request\writer::with_context($cmcontext); $contextlist = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_glossary' , [$cmcontext->id]); \mod_glossary\privacy\provider::export_user_data($contextlist); $this->assertTrue($writer->has_any_data()); $data = $writer->get_data([]); $this->assertEquals('Glossary 1', $data->name); $this->assertEquals('first', $data->entries[0]['concept']); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $generator = $this->getDataGenerator(); $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); $context = \context_module::instance($cm->id); // Create another student who will add an entry the glossary activity. $student2 = $generator->create_user(); $generator->enrol_user($student2->id, $this->course->id, 'student'); $this->setUser($student2); $ge3 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'first', 'approved' => 1], ['three']); $comment = $this->get_comment_object($context, $ge3->id); $comment->add('User 2 comment'); $this->plugingenerator->create_category($this->glossary, ['cat1'], [$ge3]); $count = $DB->count_records('glossary_entries_categories', ['entryid' => $ge3->id]); $this->assertEquals(1, $count); \core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context, ['Pizza', 'Noodles']); // As a teacher, rate student 2 entry. $this->setUser($this->teacher); $rating = $this->get_rating_object($context, $ge3->id); $rating->update_rating(2); // Before deletion, we should have 2 entries. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); $this->assertEquals(2, $count); $aliascount = $DB->count_records('glossary_alias'); $this->assertEquals(2, $aliascount); // Delete data based on context. provider::delete_data_for_all_users_in_context($context); // After deletion, the glossary entries and aliases for that glossary activity should have been deleted. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); $this->assertEquals(0, $count); $this->assertEquals(0, $DB->count_records('glossary_alias')); $count = $DB->count_records('glossary_entries_categories', ['entryid' => $ge3->id]); $this->assertEquals(0, $count); $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', 'itemid' => $ge3->id]); $this->assertEquals(0, $tagcount); $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', 'itemid' => $ge3->id, 'userid' => $student2->id]); $this->assertEquals(0, $commentcount); $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', 'itemid' => $ge3->id]); $this->assertEquals(0, $ratingcount); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $generator = $this->getDataGenerator(); // Create another student who will add an entry to the first glossary. $student2 = $generator->create_user(); $generator->enrol_user($student2->id, $this->course->id, 'student'); $cm1 = get_coursemodule_from_instance('glossary', $this->glossary->id); $glossary2 = $this->plugingenerator->create_instance(['course' => $this->course->id]); $cm2 = get_coursemodule_from_instance('glossary', $glossary2->id); $ge1 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'first user glossary entry', 'approved' => 1]); $this->plugingenerator->create_content($glossary2, ['concept' => 'first user second glossary entry', 'approved' => 1]); $context1 = \context_module::instance($cm1->id); $context2 = \context_module::instance($cm2->id); \core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge1->id, $context1, ['Parmi', 'Sushi']); $this->setUser($student2); $ge3 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'second user glossary entry', 'approved' => 1], ['three']); $comment = $this->get_comment_object($context1, $ge3->id); $comment->add('User 2 comment'); \core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context1, ['Pizza', 'Noodles']); // As a teacher, rate student 2's entry. $this->setUser($this->teacher); $rating = $this->get_rating_object($context1, $ge3->id); $rating->update_rating(2); // Before deletion, we should have 3 entries, one rating and 2 tag instances. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); $this->assertEquals(3, $count); $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', 'itemid' => $ge3->id]); $this->assertEquals(2, $tagcount); $aliascount = $DB->count_records('glossary_alias', ['entryid' => $ge3->id]); $this->assertEquals(1, $aliascount); $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', 'itemid' => $ge3->id]); $this->assertEquals(1, $ratingcount); $contextlist = new \core_privacy\local\request\approved_contextlist($student2, 'glossary', [$context1->id, $context2->id]); provider::delete_data_for_user($contextlist); // After deletion, the glossary entry and tags for the second student should have been deleted. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id, 'userid' => $student2->id]); $this->assertEquals(0, $count); $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', 'itemid' => $ge3->id]); $this->assertEquals(0, $tagcount); $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', 'itemid' => $ge3->id, 'userid' => $student2->id]); $this->assertEquals(0, $commentcount); $aliascount = $DB->count_records('glossary_alias', ['entryid' => $ge3->id]); $this->assertEquals(0, $aliascount); // Student's 1 entries, comments and tags should not be removed. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id, 'userid' => $this->student->id]); $this->assertEquals(2, $count); $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', 'itemid' => $ge1->id]); $this->assertEquals(2, $tagcount); $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', 'userid' => $this->student->id]); $this->assertEquals(1, $commentcount); $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', 'itemid' => $ge3->id]); $this->assertEquals(0, $ratingcount); } /** * Test for provider::delete_data_for_users(). */ public function test_delete_data_for_users(): void { global $DB; $generator = $this->getDataGenerator(); $student2 = $generator->create_user(); $generator->enrol_user($student2->id, $this->course->id, 'student'); $cm1 = get_coursemodule_from_instance('glossary', $this->glossary->id); $glossary2 = $this->plugingenerator->create_instance(['course' => $this->course->id]); $cm2 = get_coursemodule_from_instance('glossary', $glossary2->id); $ge1 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'first user glossary entry', 'approved' => 1]); $ge2 = $this->plugingenerator->create_content($glossary2, ['concept' => 'first user second glossary entry', 'approved' => 1], ['two']); $context1 = \context_module::instance($cm1->id); $context2 = \context_module::instance($cm2->id); \core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge1->id, $context1, ['Parmi', 'Sushi']); $this->setUser($student2); $ge3 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'second user glossary entry', 'approved' => 1], ['three']); $comment = $this->get_comment_object($context1, $ge3->id); $comment->add('User 2 comment 1'); $comment = $this->get_comment_object($context2, $ge2->id); $comment->add('User 2 comment 2'); \core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context1, ['Pizza', 'Noodles']); \core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge2->id, $context2, ['Potato', 'Kumara']); // As a teacher, rate student 2's entry. $this->setUser($this->teacher); $rating = $this->get_rating_object($context1, $ge3->id); $rating->update_rating(2); // Check correct glossary 1 record counts before deletion. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); // Note: There is an additional student entry from setUp(). $this->assertEquals(3, $count); list($context1itemsql, $context1itemparams) = $DB->get_in_or_equal([$ge1->id, $ge3->id], SQL_PARAMS_NAMED); $geparams = [ 'component' => 'mod_glossary', 'itemtype' => 'glossary_entries', ]; $geparams += $context1itemparams; $wheresql = "component = :component AND itemtype = :itemtype AND itemid {$context1itemsql}"; $tagcount = $DB->count_records_select('tag_instance', $wheresql, $geparams); $this->assertEquals(4, $tagcount); $aliascount = $DB->count_records_select('glossary_alias', "entryid {$context1itemsql}", $context1itemparams); $this->assertEquals(1, $aliascount); $commentparams = [ 'component' => 'mod_glossary', 'commentarea' => 'glossary_entry', ]; $commentparams += $context1itemparams; $commentwhere = "component = :component AND commentarea = :commentarea AND itemid {$context1itemsql}"; $commentcount = $DB->count_records_select('comments', $commentwhere, $commentparams); $this->assertEquals(1, $commentcount); $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', 'itemid' => $ge3->id]); $this->assertEquals(1, $ratingcount); // Perform deletion within context 1 for both students. $approveduserlist = new approved_userlist($context1, 'mod_glossary', [$this->student->id, $student2->id]); provider::delete_data_for_users($approveduserlist); // After deletion, all context 1 entries, tags and comment should be deleted. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); $this->assertEquals(0, $count); $tagcount = $DB->count_records_select('tag_instance', $wheresql, $geparams); $this->assertEquals(0, $tagcount); $aliascount = $DB->count_records_select('glossary_alias', "entryid {$context1itemsql}", $context1itemparams); $this->assertEquals(0, $aliascount); $commentcount = $DB->count_records_select('comments', $commentwhere, $commentparams); $this->assertEquals(0, $commentcount); // Context 2 entries should remain intact. $count = $DB->count_records('glossary_entries', ['glossaryid' => $glossary2->id]); $this->assertEquals(1, $count); $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', 'itemid' => $ge2->id]); $this->assertEquals(2, $tagcount); $aliascount = $DB->count_records('glossary_alias', ['entryid' => $ge2->id]); $this->assertEquals(1, $aliascount); $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', 'itemid' => $ge2->id]); $this->assertEquals(1, $commentcount); $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', 'itemid' => $ge3->id]); $this->assertEquals(0, $ratingcount); } /** * Get the comment area for glossary module. * * @param context $context The context. * @param int $itemid The item ID. * @return comment */ protected function get_comment_object(\context $context, $itemid) { $args = new \stdClass(); $args->context = $context; $args->course = get_course(SITEID); $args->area = 'glossary_entry'; $args->itemid = $itemid; $args->component = 'mod_glossary'; $comment = new \comment($args); $comment->set_post_permission(true); return $comment; } /** * Get the rating area for glossary module. * * @param context $context The context. * @param int $itemid The item ID. * @return rating object */ protected function get_rating_object(\context $context, $itemid) { global $USER; $ratingoptions = new \stdClass; $ratingoptions->context = $context; $ratingoptions->ratingarea = 'entry'; $ratingoptions->component = 'mod_glossary'; $ratingoptions->itemid = $itemid; $ratingoptions->scaleid = 2; $ratingoptions->userid = $USER->id; return new \rating($ratingoptions); } } tests/lib_test.php000064400000107652152265517410010247 0ustar00. /** * Glossary lib tests. * * @package mod_glossary * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/glossary/lib.php'); require_once($CFG->dirroot . '/mod/glossary/locallib.php'); /** * Glossary lib testcase. * * @package mod_glossary * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { public function test_glossary_view(): void { global $CFG; $origcompletion = $CFG->enablecompletion; $CFG->enablecompletion = true; $this->resetAfterTest(true); // Generate all the things. $c1 = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $g1 = $this->getDataGenerator()->create_module('glossary', array( 'course' => $c1->id, 'completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1 )); $g2 = $this->getDataGenerator()->create_module('glossary', array( 'course' => $c1->id, 'completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1 )); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $modinfo = \course_modinfo::instance($c1->id); $cm1 = $modinfo->get_cm($g1->cmid); $cm2 = $modinfo->get_cm($g2->cmid); $ctx1 = $cm1->context; $completion = new \completion_info($c1); $this->setUser($u1); // Confirm what we've set up. $this->assertEquals(COMPLETION_NOT_VIEWED, $completion->get_data($cm1, false, $u1->id)->viewed); $this->assertEquals(COMPLETION_INCOMPLETE, $completion->get_data($cm1, false, $u1->id)->completionstate); $this->assertEquals(COMPLETION_NOT_VIEWED, $completion->get_data($cm2, false, $u1->id)->viewed); $this->assertEquals(COMPLETION_INCOMPLETE, $completion->get_data($cm2, false, $u1->id)->completionstate); // Simulate the view call. $sink = $this->redirectEvents(); glossary_view($g1, $c1, $cm1, $ctx1, 'letter'); $events = $sink->get_events(); // Assertions. $this->assertCount(3, $events); $this->assertEquals('\core\event\course_module_completion_updated', $events[0]->eventname); $this->assertEquals('\core\event\course_module_completion_updated', $events[1]->eventname); $this->assertEquals('\mod_glossary\event\course_module_viewed', $events[2]->eventname); $this->assertEquals($g1->id, $events[2]->objectid); $this->assertEquals('letter', $events[2]->other['mode']); $this->assertEquals(COMPLETION_VIEWED, $completion->get_data($cm1, false, $u1->id)->viewed); $this->assertEquals(COMPLETION_COMPLETE, $completion->get_data($cm1, false, $u1->id)->completionstate); $this->assertEquals(COMPLETION_NOT_VIEWED, $completion->get_data($cm2, false, $u1->id)->viewed); $this->assertEquals(COMPLETION_INCOMPLETE, $completion->get_data($cm2, false, $u1->id)->completionstate); // Tear down. $sink->close(); $CFG->enablecompletion = $origcompletion; } public function test_glossary_entry_view(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $e1 = $gg->create_content($g1); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); // Assertions. $sink = $this->redirectEvents(); glossary_entry_view($e1, $ctx); $events = $sink->get_events(); $this->assertCount(1, $events); $this->assertEquals('\mod_glossary\event\entry_viewed', $events[0]->eventname); $this->assertEquals($e1->id, $events[0]->objectid); $sink->close(); } public function test_glossary_core_calendar_provide_event_action(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $glossary->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_glossary_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('view'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_glossary_core_calendar_provide_event_action_as_non_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $glossary->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Now log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_glossary_core_calendar_provide_event_action($event, $factory); // Confirm the event is not shown at all. $this->assertNull($actionevent); } public function test_glossary_core_calendar_provide_event_action_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create the activity. $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $glossary->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Now log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_glossary_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('view'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_glossary_core_calendar_provide_event_action_in_hidden_section(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create the activity. $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $glossary->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Set sections 0 as hidden. set_section_visible($course->id, 0, 0); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_glossary_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event is not shown at all. $this->assertNull($actionevent); } public function test_glossary_core_calendar_provide_event_action_already_completed(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Get some additional data. $cm = get_coursemodule_from_instance('glossary', $glossary->id); // Create a calendar event. $event = $this->create_action_event($course->id, $glossary->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed. $completion = new \completion_info($course); $completion->set_module_viewed($cm); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_glossary_core_calendar_provide_event_action($event, $factory); // Ensure result was null. $this->assertNull($actionevent); } public function test_glossary_core_calendar_provide_event_action_already_completed_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create the activity. $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Get some additional data. $cm = get_coursemodule_from_instance('glossary', $glossary->id); // Create a calendar event. $event = $this->create_action_event($course->id, $glossary->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed for the user. $completion = new \completion_info($course); $completion->set_module_viewed($cm, $student->id); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_glossary_core_calendar_provide_event_action($event, $factory, $student->id); // Ensure result was null. $this->assertNull($actionevent); } /** * Creates an action event. * * @param int $courseid The course id. * @param int $instanceid The instance id. * @param string $eventtype The event type. * @return bool|calendar_event */ private function create_action_event($courseid, $instanceid, $eventtype) { $event = new \stdClass(); $event->name = 'Calendar event'; $event->modulename = 'glossary'; $event->courseid = $courseid; $event->instance = $instanceid; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = $eventtype; $event->timestart = time(); return \calendar_event::create($event); } /** * Test the callback responsible for returning the completion rule descriptions. * This function should work given either an instance of the module (cm_info), such as when checking the active rules, * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type. */ public function test_mod_glossary_completion_get_active_rule_descriptions(): void { $this->resetAfterTest(); $this->setAdminUser(); // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]); $glossary1 = $this->getDataGenerator()->create_module('glossary', [ 'course' => $course->id, 'completion' => 2, 'completionentries' => 3 ]); $glossary2 = $this->getDataGenerator()->create_module('glossary', [ 'course' => $course->id, 'completion' => 2, 'completionentries' => 0 ]); $cm1 = \cm_info::create(get_coursemodule_from_instance('glossary', $glossary1->id)); $cm2 = \cm_info::create(get_coursemodule_from_instance('glossary', $glossary2->id)); // Data for the stdClass input type. // This type of input would occur when checking the default completion rules for an activity type, where we don't have // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info. $moddefaults = new \stdClass(); $moddefaults->customdata = ['customcompletionrules' => ['completionentries' => 3]]; $moddefaults->completion = 2; $activeruledescriptions = [get_string('completionentriesdesc', 'glossary', $glossary1->completionentries)]; $this->assertEquals(mod_glossary_get_completion_active_rule_descriptions($cm1), $activeruledescriptions); $this->assertEquals(mod_glossary_get_completion_active_rule_descriptions($cm2), []); $this->assertEquals(mod_glossary_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions); $this->assertEquals(mod_glossary_get_completion_active_rule_descriptions(new \stdClass()), []); } public function test_mod_glossary_get_tagged_entries(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $course3 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course(); // Create and enrol a student. $student = self::getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual'); // Create glossaries and entries. $glossary1 = $this->getDataGenerator()->create_module('glossary', array('course' => $course1->id)); $glossary2 = $this->getDataGenerator()->create_module('glossary', array('course' => $course2->id)); $glossary3 = $this->getDataGenerator()->create_module('glossary', array('course' => $course3->id)); $entry11 = $glossarygenerator->create_content($glossary1, array('tags' => array('Cats', 'Dogs'))); $entry12 = $glossarygenerator->create_content($glossary1, array('tags' => array('Cats', 'mice'))); $entry13 = $glossarygenerator->create_content($glossary1, array('tags' => array('Cats'))); $entry14 = $glossarygenerator->create_content($glossary1); $entry15 = $glossarygenerator->create_content($glossary1, array('tags' => array('Cats'))); $entry16 = $glossarygenerator->create_content($glossary1, array('tags' => array('Cats'), 'approved' => false)); $entry17 = $glossarygenerator->create_content($glossary1, array('tags' => array('Cats'), 'approved' => false, 'userid' => $student->id)); $entry21 = $glossarygenerator->create_content($glossary2, array('tags' => array('Cats'))); $entry22 = $glossarygenerator->create_content($glossary2, array('tags' => array('Cats', 'Dogs'))); $entry23 = $glossarygenerator->create_content($glossary2, array('tags' => array('mice', 'Cats'))); $entry31 = $glossarygenerator->create_content($glossary3, array('tags' => array('mice', 'Cats'))); $tag = \core_tag_tag::get_by_name(0, 'Cats'); // Admin can see everything. // Get first page of tagged entries (first 5 entries). $res = mod_glossary_get_tagged_entries($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$entry = */0); $this->assertMatchesRegularExpression('/'.$entry11->concept.'content); $this->assertMatchesRegularExpression('/'.$entry12->concept.'content); $this->assertMatchesRegularExpression('/'.$entry13->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry14->concept.'content); $this->assertMatchesRegularExpression('/'.$entry15->concept.'content); $this->assertMatchesRegularExpression('/'.$entry16->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry17->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry21->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry22->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry23->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry31->concept.'content); $this->assertEmpty($res->prevpageurl); $this->assertNotEmpty($res->nextpageurl); // Get second page of tagged entries (second 5 entries). $res = mod_glossary_get_tagged_entries($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$entry = */1); $this->assertDoesNotMatchRegularExpression('/'.$entry11->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry12->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry13->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry14->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry15->concept.'content); $this->assertDoesNotMatchRegularExpression('/'.$entry16->concept.'content); $this->assertMatchesRegularExpression('/'.$entry17->concept.'content); $this->assertMatchesRegularExpression('/'.$entry21->concept.'content); $this->assertMatchesRegularExpression('/'.$entry22->concept.'content); $this->assertMatchesRegularExpression('/'.$entry23->concept.'content); $this->assertMatchesRegularExpression('/'.$entry31->concept.'content); $this->assertNotEmpty($res->prevpageurl); $this->assertEmpty($res->nextpageurl); $this->setUser($student); \core_tag_index_builder::reset_caches(); // User can not see entries in course 3 because he is not enrolled. $res = mod_glossary_get_tagged_entries($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$entry = */1); $this->assertMatchesRegularExpression('/'.$entry22->concept.'/', $res->content); $this->assertMatchesRegularExpression('/'.$entry23->concept.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$entry31->concept.'/', $res->content); // User can search glossary entries inside a course. $coursecontext = \context_course::instance($course1->id); $res = mod_glossary_get_tagged_entries($tag, /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$coursecontext->id, /*$rec = */1, /*$entry = */0); $this->assertMatchesRegularExpression('/'.$entry11->concept.'/', $res->content); $this->assertMatchesRegularExpression('/'.$entry12->concept.'/', $res->content); $this->assertMatchesRegularExpression('/'.$entry13->concept.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$entry14->concept.'/', $res->content); $this->assertMatchesRegularExpression('/'.$entry15->concept.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$entry21->concept.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$entry22->concept.'/', $res->content); $this->assertDoesNotMatchRegularExpression('/'.$entry23->concept.'/', $res->content); $this->assertEmpty($res->nextpageurl); // User cannot see unapproved entries unless he is an author. $this->assertDoesNotMatchRegularExpression('/'.$entry16->concept.'/', $res->content); $this->assertMatchesRegularExpression('/'.$entry17->concept.'/', $res->content); } public function test_glossary_get_entries_search(): void { $this->resetAfterTest(); $this->setAdminUser(); // Turn on glossary autolinking (usedynalink). set_config('glossary_linkentries', 1); $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); // Note this entry is not case sensitive by default (casesensitive = 0). $entry = $glossarygenerator->create_content($glossary); // Check that a search for the concept return the entry. $concept = $entry->concept; $search = glossary_get_entries_search($concept, $course->id); $this->assertCount(1, $search); $foundentry = array_shift($search); $this->assertEquals($foundentry->concept, $entry->concept); // Now try the same search but with a lowercase term. $concept = strtolower($entry->concept); $search = glossary_get_entries_search($concept, $course->id); $this->assertCount(1, $search); $foundentry = array_shift($search); $this->assertEquals($foundentry->concept, $entry->concept); // Make an entry that is case sensitive (casesensitive = 1). set_config('glossary_casesensitive', 1); $entry = $glossarygenerator->create_content($glossary); $concept = $entry->concept; $search = glossary_get_entries_search($concept, $course->id); $this->assertCount(1, $search); $foundentry = array_shift($search); $this->assertEquals($foundentry->concept, $entry->concept); // Now try the same search but with a lowercase term. $concept = strtolower($entry->concept); $search = glossary_get_entries_search($concept, $course->id); $this->assertCount(0, $search); } public function test_mod_glossary_can_delete_entry_users(): void { $this->resetAfterTest(); // Create required data. $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student'); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setUser($student); $entry = $gg->create_content($glossary); $context = \context_module::instance($glossary->cmid); // Test student can delete. $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context)); // Test teacher can delete. $this->setUser($teacher); $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context)); // Test admin can delete. $this->setAdminUser(); $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context)); // Test a different student is not able to delete. $this->setUser($anotherstudent); $this->assertFalse(mod_glossary_can_delete_entry($entry, $glossary, $context)); // Test exception. $this->expectExceptionMessage(get_string('nopermissiontodelentry', 'error')); mod_glossary_can_delete_entry($entry, $glossary, $context, false); } public function test_mod_glossary_can_delete_entry_edit_period(): void { global $CFG; $this->resetAfterTest(); // Create required data. $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id, 'editalways' => 1]); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setUser($student); $entry = $gg->create_content($glossary); $context = \context_module::instance($glossary->cmid); // Test student can always delete when edit always is set to 1. $entry->timecreated = time() - 2 * $CFG->maxeditingtime; $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context)); // Test student cannot delete old entries when edit always is set to 0. $glossary->editalways = 0; $this->assertFalse(mod_glossary_can_delete_entry($entry, $glossary, $context)); // Test student can delete recent entries when edit always is set to 0. $entry->timecreated = time(); $this->assertTrue(mod_glossary_can_delete_entry($entry, $glossary, $context)); // Check exception. $entry->timecreated = time() - 2 * $CFG->maxeditingtime; $this->expectExceptionMessage(get_string('errdeltimeexpired', 'glossary')); mod_glossary_can_delete_entry($entry, $glossary, $context, false); } public function test_mod_glossary_delete_entry(): void { global $DB, $CFG; $this->resetAfterTest(); require_once($CFG->dirroot . '/rating/lib.php'); // Create required data. $course = $this->getDataGenerator()->create_course(); $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $record = new \stdClass(); $record->course = $course->id; $record->assessed = RATING_AGGREGATE_AVERAGE; $scale = $this->getDataGenerator()->create_scale(['scale' => 'A,B,C,D']); $record->scale = "-$scale->id"; $glossary = $this->getDataGenerator()->create_module('glossary', $record); $context = \context_module::instance($glossary->cmid); $cm = get_coursemodule_from_instance('glossary', $glossary->id); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setUser($student1); // Create entry with tags and rating. $entry = $gg->create_content( $glossary, ['approved' => 1, 'userid' => $student1->id, 'tags' => ['Cats', 'Dogs']], ['alias1', 'alias2'] ); // Rate the entry as user2. $rating1 = new \stdClass(); $rating1->contextid = $context->id; $rating1->component = 'mod_glossary'; $rating1->ratingarea = 'entry'; $rating1->itemid = $entry->id; $rating1->rating = 1; // 1 is A. $rating1->scaleid = "-$scale->id"; $rating1->userid = $student2->id; $rating1->timecreated = time(); $rating1->timemodified = time(); $rating1->id = $DB->insert_record('rating', $rating1); $sink = $this->redirectEvents(); mod_glossary_delete_entry(fullclone($entry), $glossary, $cm, $context, $course); $events = $sink->get_events(); $event = array_pop($events); // Check events. $this->assertEquals('\mod_glossary\event\entry_deleted', $event->eventname); $this->assertEquals($entry->id, $event->objectid); $sink->close(); // No entry, no alias, no ratings, no tags. $this->assertEquals(0, $DB->count_records('glossary_entries', ['id' => $entry->id])); $this->assertEquals(0, $DB->count_records('glossary_alias', ['entryid' => $entry->id])); $this->assertEquals(0, $DB->count_records('rating', ['component' => 'mod_glossary', 'itemid' => $entry->id])); $this->assertEmpty(\core_tag_tag::get_by_name(0, 'Cats')); } public function test_mod_glossary_delete_entry_imported(): void { global $DB; $this->resetAfterTest(); // Create required data. $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $glossary1 = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $glossary2 = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $context = \context_module::instance($glossary2->cmid); $cm = get_coursemodule_from_instance('glossary', $glossary2->id); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setUser($student); $entry1 = $gg->create_content($glossary1); $entry2 = $gg->create_content( $glossary2, ['approved' => 1, 'userid' => $student->id, 'sourceglossaryid' => $glossary1->id, 'tags' => ['Cats', 'Dogs']] ); $sink = $this->redirectEvents(); mod_glossary_delete_entry(fullclone($entry2), $glossary2, $cm, $context, $course); $events = $sink->get_events(); $event = array_pop($events); // Check events. $this->assertEquals('\mod_glossary\event\entry_deleted', $event->eventname); $this->assertEquals($entry2->id, $event->objectid); $sink->close(); // Check source. $this->assertEquals(0, $DB->get_field('glossary_entries', 'sourceglossaryid', ['id' => $entry2->id])); $this->assertEquals($glossary1->id, $DB->get_field('glossary_entries', 'glossaryid', ['id' => $entry2->id])); // Tags. $this->assertEmpty(\core_tag_tag::get_by_name(0, 'Cats')); } public function test_mod_glossary_can_update_entry_users(): void { $this->resetAfterTest(); // Create required data. $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $anotherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student'); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setUser($student); $entry = $gg->create_content($glossary); $context = \context_module::instance($glossary->cmid); $cm = get_coursemodule_from_instance('glossary', $glossary->id); // Test student can update. $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm)); // Test teacher can update. $this->setUser($teacher); $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm)); // Test admin can update. $this->setAdminUser(); $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm)); // Test a different student is not able to update. $this->setUser($anotherstudent); $this->assertFalse(mod_glossary_can_update_entry($entry, $glossary, $context, $cm)); // Test exception. $this->expectExceptionMessage(get_string('errcannoteditothers', 'glossary')); mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false); } public function test_mod_glossary_can_update_entry_edit_period(): void { global $CFG; $this->resetAfterTest(); // Create required data. $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id, 'editalways' => 1)); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setUser($student); $entry = $gg->create_content($glossary); $context = \context_module::instance($glossary->cmid); $cm = get_coursemodule_from_instance('glossary', $glossary->id); // Test student can always update when edit always is set to 1. $entry->timecreated = time() - 2 * $CFG->maxeditingtime; $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm)); // Test student cannot update old entries when edit always is set to 0. $glossary->editalways = 0; $this->assertFalse(mod_glossary_can_update_entry($entry, $glossary, $context, $cm)); // Test student can update recent entries when edit always is set to 0. $entry->timecreated = time(); $this->assertTrue(mod_glossary_can_update_entry($entry, $glossary, $context, $cm)); // Check exception. $entry->timecreated = time() - 2 * $CFG->maxeditingtime; $this->expectExceptionMessage(get_string('erredittimeexpired', 'glossary')); mod_glossary_can_update_entry($entry, $glossary, $context, $cm, false); } public function test_prepare_entry_for_edition(): void { global $USER; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', ['course' => $course->id]); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $this->setAdminUser(); $aliases = ['alias1', 'alias2']; $entry = $gg->create_content( $glossary, ['approved' => 1, 'userid' => $USER->id], $aliases ); $cat1 = $gg->create_category($glossary, [], [$entry]); $gg->create_category($glossary); $entry = mod_glossary_prepare_entry_for_edition($entry); $this->assertCount(1, $entry->categories); $this->assertEquals($cat1->id, $entry->categories[0]); $returnedaliases = array_values(explode("\n", trim($entry->aliases))); sort($returnedaliases); $this->assertEquals($aliases, $returnedaliases); } } tests/custom_completion_test.php000064400000016066152265517410013242 0ustar00. /** * Contains unit tests for core_completion/activity_custom_completion. * * @package mod_glossary * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_glossary; use advanced_testcase; use cm_info; use coding_exception; use mod_glossary\completion\custom_completion; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); /** * Class for unit testing mod_glossary/activity_custom_completion. * * @package mod_glossary * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class custom_completion_test extends advanced_testcase { /** * Data provider for get_state(). * * @return array[] */ public static function get_state_provider(): array { return [ 'Undefined rule' => [ 'somenonexistentrule', COMPLETION_DISABLED, 0, null, coding_exception::class ], 'Rule not available' => [ 'completionentries', COMPLETION_DISABLED, 0, null, moodle_exception::class ], 'Rule available, user has not submitted' => [ 'completionentries', COMPLETION_ENABLED, 0, COMPLETION_INCOMPLETE, null ], 'Rule available, user has submitted' => [ 'completionentries', COMPLETION_ENABLED, 2, COMPLETION_COMPLETE, null ], ]; } /** * Test for get_state(). * * @dataProvider get_state_provider * @param string $rule The custom completion rule. * @param int $available Whether this rule is available. * @param int $entries The number of entries. * @param int|null $status Expected status. * @param string|null $exception Expected exception. */ public function test_get_state(string $rule, int $available, int $entries, ?int $status, ?string $exception): void { global $DB; if (!is_null($exception)) { $this->expectException($exception); } // Custom completion rule data for cm_info::customdata. $customdataval = [ 'customcompletionrules' => [ $rule => $available ] ]; // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of the magic getter method when fetching the cm_info object's customdata and instance values. $mockcminfo->expects($this->any()) ->method('__get') ->will($this->returnValueMap([ ['customdata', $customdataval], ['instance', 1], ])); // Mock the DB calls. $DB = $this->createMock(get_class($DB)); $DB->expects($this->atMost(1)) ->method('count_records') ->willReturn($entries); $customcompletion = new custom_completion($mockcminfo, 2); $this->assertEquals($status, $customcompletion->get_state($rule)); } /** * Test for get_defined_custom_rules(). */ public function test_get_defined_custom_rules(): void { $rules = custom_completion::get_defined_custom_rules(); $this->assertCount(1, $rules); $this->assertEquals('completionentries', $rules[0]); } /** * Test for get_defined_custom_rule_descriptions(). */ public function test_get_custom_rule_descriptions(): void { // Get defined custom rules. $rules = custom_completion::get_defined_custom_rules(); // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Instantiate a custom_completion object using the mocked cm_info. $customcompletion = new custom_completion($mockcminfo, 1); // Get custom rule descriptions. $ruledescriptions = $customcompletion->get_custom_rule_descriptions(); // Confirm that defined rules and rule descriptions are consistent with each other. $this->assertEquals(count($rules), count($ruledescriptions)); foreach ($rules as $rule) { $this->assertArrayHasKey($rule, $ruledescriptions); } } /** * Test for is_defined(). */ public function test_is_defined(): void { // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->getMock(); $customcompletion = new custom_completion($mockcminfo, 1); // Rule is defined. $this->assertTrue($customcompletion->is_defined('completionentries')); // Undefined rule. $this->assertFalse($customcompletion->is_defined('somerandomrule')); } /** * Data provider for test_get_available_custom_rules(). * * @return array[] */ public static function get_available_custom_rules_provider(): array { return [ 'Completion submit available' => [ COMPLETION_ENABLED, ['completionentries'] ], 'Completion submit not available' => [ COMPLETION_DISABLED, [] ], ]; } /** * Test for get_available_custom_rules(). * * @dataProvider get_available_custom_rules_provider * @param int $status * @param array $expected */ public function test_get_available_custom_rules(int $status, array $expected): void { $customdataval = [ 'customcompletionrules' => [ 'completionentries' => $status ] ]; // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of magic getter for the customdata attribute. $mockcminfo->expects($this->any()) ->method('__get') ->with('customdata') ->willReturn($customdataval); $customcompletion = new custom_completion($mockcminfo, 1); $this->assertEquals($expected, $customcompletion->get_available_custom_rules()); } } tests/search/search_test.php000064400000022742152265517410012207 0ustar00. /** * Glossary search unit tests. * * @package mod_glossary * @category test * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\search; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); require_once($CFG->dirroot . '/mod/glossary/tests/generator/lib.php'); /** * Provides the unit tests for glossary search. * * @package mod_glossary * @category test * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class search_test extends \advanced_testcase { /** * @var string Area id */ protected $entryareaid = null; public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); set_config('enableglobalsearch', true); // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this. $search = \testable_core_search::instance(); $this->entryareaid = \core_search\manager::generate_areaid('mod_glossary', 'entry'); } /** * Availability. * * @return void */ public function test_search_enabled(): void { $searcharea = \core_search\manager::get_search_area($this->entryareaid); list($componentname, $varname) = $searcharea->get_config_var_name(); // Enabled by default once global search is enabled. $this->assertTrue($searcharea->is_enabled()); set_config($varname . '_enabled', 0, $componentname); $this->assertFalse($searcharea->is_enabled()); set_config($varname . '_enabled', 1, $componentname); $this->assertTrue($searcharea->is_enabled()); } /** * Indexing contents. * * @return void */ public function test_entries_indexing(): void { global $DB; $searcharea = \core_search\manager::get_search_area($this->entryareaid); $this->assertInstanceOf('\mod_glossary\search\entry', $searcharea); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student'); $record = new \stdClass(); $record->course = $course1->id; $this->setUser($user1); // Approved entries by default glossary. $glossary1 = self::getDataGenerator()->create_module('glossary', $record); $entry1 = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary1); $entry2 = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary1); // All records. $recordset = $searcharea->get_recordset_by_timestamp(0); $this->assertTrue($recordset->valid()); $nrecords = 0; foreach ($recordset as $record) { $this->assertInstanceOf('stdClass', $record); $doc = $searcharea->get_document($record); $this->assertInstanceOf('\core_search\document', $doc); // Static caches are working. $dbreads = $DB->perf_get_reads(); $doc = $searcharea->get_document($record); // The +1 is because we are not caching glossary alias (keywords) as they depend on a single entry. $this->assertEquals($dbreads + 1, $DB->perf_get_reads()); $this->assertInstanceOf('\core_search\document', $doc); $nrecords++; } // If there would be an error/failure in the foreach above the recordset would be closed on shutdown. $recordset->close(); $this->assertEquals(2, $nrecords); // The +2 is to prevent race conditions. $recordset = $searcharea->get_recordset_by_timestamp(time() + 2); // No new records. $this->assertFalse($recordset->valid()); $recordset->close(); // Create a second glossary with one entry. $glossary2 = self::getDataGenerator()->create_module('glossary', ['course' => $course1->id]); self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary2); // Test indexing with each activity then combined course context. $rs = $searcharea->get_document_recordset(0, \context_module::instance($glossary1->cmid)); $this->assertEquals(2, iterator_count($rs)); $rs->close(); $rs = $searcharea->get_document_recordset(0, \context_module::instance($glossary2->cmid)); $this->assertEquals(1, iterator_count($rs)); $rs->close(); $rs = $searcharea->get_document_recordset(0, \context_course::instance($course1->id)); $this->assertEquals(3, iterator_count($rs)); $rs->close(); } /** * Document contents. * * @return void */ public function test_entries_document(): void { global $DB; $searcharea = \core_search\manager::get_search_area($this->entryareaid); $user = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course1->id, 'teacher'); $record = new \stdClass(); $record->course = $course1->id; $this->setUser($user); $glossary = self::getDataGenerator()->create_module('glossary', $record); $entry = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary); $entry->course = $glossary->course; $doc = $searcharea->get_document($entry); $this->assertInstanceOf('\core_search\document', $doc); $this->assertEquals($entry->id, $doc->get('itemid')); $this->assertEquals($course1->id, $doc->get('courseid')); $this->assertEquals($user->id, $doc->get('userid')); $this->assertEquals($entry->concept, $doc->get('title')); $this->assertEquals($entry->definition, $doc->get('content')); } /** * Document accesses. * * @return void */ public function test_entries_access(): void { global $DB; // Returns the instance as long as the component is supported. $searcharea = \core_search\manager::get_search_area($this->entryareaid); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student'); $record = new \stdClass(); $record->course = $course1->id; // Approved entries by default glossary, created by teacher. $this->setUser($user1); $glossary1 = self::getDataGenerator()->create_module('glossary', $record); $teacherapproved = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary1); $teachernotapproved = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary1, array('approved' => false)); // Entries need to be approved and created by student. $glossary2 = self::getDataGenerator()->create_module('glossary', $record); $this->setUser($user2); $studentapproved = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary2); $studentnotapproved = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary2, array('approved' => false)); // Activity hidden to students. $this->setUser($user1); $glossary3 = self::getDataGenerator()->create_module('glossary', $record); $hidden = self::getDataGenerator()->get_plugin_generator('mod_glossary')->create_content($glossary3); set_coursemodule_visible($glossary3->cmid, 0); $this->setUser($user2); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($teacherapproved->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($teachernotapproved->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($studentapproved->id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($studentnotapproved->id)); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($hidden->id)); } } tests/event/events_test.php000064400000047164152265517410012127 0ustar00. /** * Unit tests for lib.php * * @package mod_glossary * @category test * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\event; /** * Unit tests for glossary events. * * @package mod_glossary * @category test * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test comment_created event. */ public function test_comment_created(): void { global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); // Create a record for adding comment. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $glossarygenerator->create_content($glossary); $context = \context_module::instance($glossary->cmid); $cm = get_coursemodule_from_instance('glossary', $glossary->id, $course->id); $cmt = new \stdClass(); $cmt->component = 'mod_glossary'; $cmt->context = $context; $cmt->course = $course; $cmt->cm = $cm; $cmt->area = 'glossary_entry'; $cmt->itemid = $entry->id; $cmt->showcount = true; $comment = new \comment($cmt); // Triggering and capturing the event. $sink = $this->redirectEvents(); $comment->add('New comment'); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\comment_created', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/glossary/view.php', array('id' => $glossary->cmid)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); } /** * Test comment_deleted event. */ public function test_comment_deleted(): void { global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); // Create a record for deleting comment. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $glossarygenerator->create_content($glossary); $context = \context_module::instance($glossary->cmid); $cm = get_coursemodule_from_instance('glossary', $glossary->id, $course->id); $cmt = new \stdClass(); $cmt->component = 'mod_glossary'; $cmt->context = $context; $cmt->course = $course; $cmt->cm = $cm; $cmt->area = 'glossary_entry'; $cmt->itemid = $entry->id; $cmt->showcount = true; $comment = new \comment($cmt); $newcomment = $comment->add('New comment 1'); // Triggering and capturing the event. $sink = $this->redirectEvents(); $comment->delete($newcomment->id); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\comment_deleted', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/glossary/view.php', array('id' => $glossary->cmid)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); } public function test_course_module_viewed(): void { global $DB; // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $dbcourse = $DB->get_record('course', array('id' => $course->id)); $dbglossary = $DB->get_record('glossary', array('id' => $glossary->id)); $context = \context_module::instance($glossary->cmid); $mode = 'letter'; $event = \mod_glossary\event\course_module_viewed::create(array( 'objectid' => $dbglossary->id, 'context' => $context, 'other' => array('mode' => $mode) )); $event->add_record_snapshot('course', $dbcourse); $event->add_record_snapshot('glossary', $dbglossary); // Triggering and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\course_module_viewed', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEquals($glossary->id, $event->objectid); $this->assertEquals(new \moodle_url('/mod/glossary/view.php', array('id' => $glossary->cmid, 'mode' => $mode)), $event->get_url()); $this->assertEventContextNotUsed($event); } public function test_course_module_instance_list_viewed(): void { // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $course = $this->getDataGenerator()->create_course(); $event = \mod_glossary\event\course_module_instance_list_viewed::create(array( 'context' => \context_course::instance($course->id) )); // Triggering and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\course_module_instance_list_viewed', $event); $this->assertEquals(CONTEXT_COURSE, $event->contextlevel); $this->assertEquals($course->id, $event->contextinstanceid); $this->assertEventContextNotUsed($event); } public function test_entry_created(): void { // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $context = \context_module::instance($glossary->cmid); $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $glossarygenerator->create_content($glossary); $eventparams = array( 'context' => $context, 'objectid' => $entry->id, 'other' => array('concept' => $entry->concept) ); $event = \mod_glossary\event\entry_created::create($eventparams); $event->add_record_snapshot('glossary_entries', $entry); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\entry_created', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); } public function test_entry_updated(): void { // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $context = \context_module::instance($glossary->cmid); $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $glossarygenerator->create_content($glossary); $eventparams = array( 'context' => $context, 'objectid' => $entry->id, 'other' => array('concept' => $entry->concept) ); $event = \mod_glossary\event\entry_updated::create($eventparams); $event->add_record_snapshot('glossary_entries', $entry); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\entry_updated', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); } public function test_entry_deleted(): void { global $DB; // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $context = \context_module::instance($glossary->cmid); $prevmode = 'view'; $hook = 'ALL'; $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $glossarygenerator->create_content($glossary); $DB->delete_records('glossary_entries', array('id' => $entry->id)); $eventparams = array( 'context' => $context, 'objectid' => $entry->id, 'other' => array( 'mode' => $prevmode, 'hook' => $hook, 'concept' => $entry->concept ) ); $event = \mod_glossary\event\entry_deleted::create($eventparams); $event->add_record_snapshot('glossary_entries', $entry); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\entry_deleted', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); } public function test_category_created(): void { global $DB; // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $context = \context_module::instance($glossary->cmid); // Create category and trigger event. $category = new \stdClass(); $category->name = 'New category'; $category->usedynalink = 0; $category->id = $DB->insert_record('glossary_categories', $category); $event = \mod_glossary\event\category_created::create(array( 'context' => $context, 'objectid' => $category->id )); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\category_created', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); // Update category and trigger event. $category->name = 'Updated category'; $DB->update_record('glossary_categories', $category); $event = \mod_glossary\event\category_updated::create(array( 'context' => $context, 'objectid' => $category->id )); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\category_updated', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); // Delete category and trigger event. $category = $DB->get_record('glossary_categories', array('id' => $category->id)); $DB->delete_records('glossary_categories', array('id' => $category->id)); $event = \mod_glossary\event\category_deleted::create(array( 'context' => $context, 'objectid' => $category->id )); $event->add_record_snapshot('glossary_categories', $category); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\category_deleted', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); } public function test_entry_approved(): void { global $DB; // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_user(); $rolestudent = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $rolestudent->id); $teacher = $this->getDataGenerator()->create_user(); $roleteacher = $DB->get_record('role', array('shortname' => 'teacher')); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $roleteacher->id); $this->setUser($teacher); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course, 'defaultapproval' => 0)); $context = \context_module::instance($glossary->cmid); $this->setUser($student); $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $glossarygenerator->create_content($glossary); $this->assertEquals(0, $entry->approved); // Approve entry, trigger and validate event. $this->setUser($teacher); $newentry = new \stdClass(); $newentry->id = $entry->id; $newentry->approved = true; $newentry->timemodified = time(); $DB->update_record("glossary_entries", $newentry); $params = array( 'context' => $context, 'objectid' => $entry->id ); $event = \mod_glossary\event\entry_approved::create($params); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\entry_approved', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); // Disapprove entry, trigger and validate event. $this->setUser($teacher); $newentry = new \stdClass(); $newentry->id = $entry->id; $newentry->approved = false; $newentry->timemodified = time(); $DB->update_record("glossary_entries", $newentry); $params = array( 'context' => $context, 'objectid' => $entry->id ); $event = \mod_glossary\event\entry_disapproved::create($params); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\entry_disapproved', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); } public function test_entry_viewed(): void { // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $context = \context_module::instance($glossary->cmid); $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $glossarygenerator->create_content($glossary); $event = \mod_glossary\event\entry_viewed::create(array( 'objectid' => $entry->id, 'context' => $context )); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_glossary\event\entry_viewed', $event); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($glossary->cmid, $event->contextinstanceid); $this->assertEventContextNotUsed($event); } } tests/generator_test.php000064400000010432152265517410011454 0ustar00. namespace mod_glossary; /** * Genarator tests class for mod_glossary. * * @package mod_glossary * @category test * @copyright 2013 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends \advanced_testcase { public function test_create_instance(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $this->assertFalse($DB->record_exists('glossary', array('course' => $course->id))); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); $records = $DB->get_records('glossary', array('course' => $course->id), 'id'); $this->assertCount(1, $records); $this->assertTrue(array_key_exists($glossary->id, $records)); $params = array('course' => $course->id, 'name' => 'Another glossary'); $glossary = $this->getDataGenerator()->create_module('glossary', $params); $records = $DB->get_records('glossary', array('course' => $course->id), 'id'); $this->assertCount(2, $records); $this->assertEquals('Another glossary', $records[$glossary->id]->name); } public function test_create_content(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course)); /** @var mod_glossary_generator $glossarygenerator */ $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry1 = $glossarygenerator->create_content($glossary); $entry2 = $glossarygenerator->create_content($glossary, array('concept' => 'Custom concept', 'tags' => array('Cats', 'mice')), array('alias1', 'alias2')); $records = $DB->get_records('glossary_entries', array('glossaryid' => $glossary->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($entry1->id, $records[$entry1->id]->id); $this->assertEquals($entry2->id, $records[$entry2->id]->id); $this->assertEquals('Custom concept', $records[$entry2->id]->concept); $this->assertEquals(array('Cats', 'mice'), array_values(\core_tag_tag::get_item_tags_array('mod_glossary', 'glossary_entries', $entry2->id))); $aliases = $DB->get_records_menu('glossary_alias', array('entryid' => $entry2->id), 'id ASC', 'id, alias'); $this->assertSame(array('alias1', 'alias2'), array_values($aliases)); // Test adding of category to entry. $categories = $DB->get_records('glossary_categories', array('glossaryid' => $glossary->id)); $this->assertCount(0, $categories); $entry3 = $glossarygenerator->create_content($glossary, array('concept' => 'In category')); $category1 = $glossarygenerator->create_category($glossary, array()); $categories = $DB->get_records('glossary_categories', array('glossaryid' => $glossary->id)); $this->assertCount(1, $categories); $category2 = $glossarygenerator->create_category($glossary, array('name' => 'Some category'), array($entry2, $entry3)); $categories = $DB->get_records('glossary_categories', array('glossaryid' => $glossary->id)); $this->assertCount(2, $categories); $members = $DB->get_records_menu('glossary_entries_categories', array('categoryid' => $category2->id), 'id ASC', 'id, entryid'); $this->assertSame(array($entry2->id, $entry3->id), array_values($members)); } } tests/behat/prevent_duplicate_entries.feature000064400000003001152265517410015616 0ustar00@mod @mod_glossary Feature: A teacher can choose whether to allow duplicate entries in a glossary In order to avoid confusion As a teacher I need to avoid having duplicate concept definitions @javascript Scenario: Prevent duplicate entries Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activity" exists: | course | C1 | | activity | glossary | | name | Test glossary name | | intro | Test glossary description | | allowduplicatedentries | 0 | And I am on the "Test glossary name" "glossary activity" page logged in as teacher1 And I add a glossary entry with the following data: | Concept | Unique concept | | Definition | I'm the definition of an unique concept | When I press "Add entry" And I set the following fields to these values: | Concept | Unique concept | | Definition | There is no definition restriction | And I press "Save changes" Then I should see "This concept already exists. No duplicates allowed in this glossary." And I press "Cancel" tests/behat/glossary_activity_completion_pass_grade.feature000064400000014461152265517410020564 0ustar00@mod @mod_glossary @core_completion Feature: Pass grade completion in the glossary activity In order to have visibility of glossary completion requirements As a student I need to be able to view my glossary completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | glossary | | course | C1 | | idnumber | mh1 | | name | Music history | | section | 1 | When I am on the "Music history" "glossary activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | Aggregate type | Average of ratings | | scale[modgrade_type] | Point | | scale[modgrade_point] | 100 | | Ratings > Grade to pass | 50 | | Add requirements | 1 | | View the activity | 1 | | Receive a grade | 1 | | Passing grade | 1 | | completionentriesenabled | 1 | | completionentries | 1 | And I press "Save and display" And I log out Scenario: A teacher can view a glossary activity completion conditions Given I log in as "teacher1" And I am on "Course 1" course homepage When I follow "Music history" Then "Music history" should have the "View" completion condition And "Music history" should have the "Make entries: 1" completion condition And "Music history" should have the "Receive a grade" completion condition And "Music history" should have the "Receive a passing grade" completion condition Scenario: A student will not complete a glossary activity if the conditions are not met Given I am on the "Music history" "glossary activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" When I am on "Course 1" course homepage And I follow "Music history" And I press "Add entry" And I set the following fields to these values: | Concept | Blast beats | | Definition | Repeated fast tempo hits combining bass, snare and cymbal | And I press "Save changes" And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I log out And I am on the "Music history" "glossary activity" page logged in as teacher1 And I set the field "rating" to "3" And I press "Rate" And I log out When I am on the "Music history" "glossary activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "failed" Scenario: A student can complete a glossary activity by meeting the completion conditions Given I am on the "Music history" "glossary activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" When I am on "Course 1" course homepage And I follow "Music history" And I press "Add entry" And I set the following fields to these values: | Concept | Blast beats | | Definition | Repeated fast tempo hits combining bass, snare and cymbal | And I press "Save changes" And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Music history" And I set the field "rating" to "60" And I press "Rate" And I log out When I log in as "student1" And I am on "Course 1" course homepage And I follow "Music history" Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "done" tests/behat/glossary_display_formats.feature000064400000010654152265517410015507 0ustar00@mod @mod_glossary Feature: Glossary can be set to various display formats In order to display different glossary formats As a teacher I can set the glossary activity display format Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Given the following "activities" exist: | activity | course | name | | glossary | C1 | Glossary 1 | And the following "mod_glossary > entries" exist: | glossary | concept | definition | | Glossary 1 | Entry 1 | Entry 1 definition | | Glossary 1 | Entry 2 | Entry 2 definition | Scenario: Glossary display format is entry list style Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1 And I set the following fields to these values: | displayformat | entrylist | When I press "Save and display" # Confirm that glossary display format is entry list. # In this format, the concept definitions are not displayed. Then I should not see "by Admin User" And I should not see "Entry 1 definition" And I should not see "Entry 2 definition" And ".entrylist" "css_element" should exist Scenario: Glossary display format is FAQ-style Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1 And I set the following fields to these values: | displayformat | faq | When I press "Save and display" # Confirm that glossary format is FAQ. # In this format, the words Question and Answer are displayed. Then I should see "Question:" And I should see "Answer:" And ".faq" "css_element" should exist @_file_upload @javascript Scenario: Glossary display format is full without author style Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1 And I set the following fields to these values: | displayformat | fullwithoutauthor | And I press "Save and display" And I press "Add entry" # Add an entry with an attachment. And I set the following fields to these values: | Concept | Entry 3 | | Definition | Entry 3 definition | | Attachment | lib/tests/fixtures/gd-logo.png | When I press "Save changes" # Confirm that glossary format is full without author style. # In this format, the image link should exist and author's name should not be visible. Then "gd-logo.png" "link" should exist And I should not see "by Admin User" And ".fullwithoutauthor" "css_element" should exist @_file_upload @javascript Scenario: Glossary display format is encyclopedia style Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1 And I set the following fields to these values: | displayformat | encyclopedia | And I press "Save and display" And I press "Add entry" # Add an entry with an attachment. And I set the following fields to these values: | Concept | Entry 3 | | Definition | Entry 3 definition | | Attachment | lib/tests/fixtures/gd-logo.png | When I press "Save changes" # Confirm that glossary format is encyclopedia. # In this format, the image element should be displayed. Then "//img[contains(@src, 'gd-logo.png')]" "xpath_element" should exist And ".encyclopedia" "css_element" should exist Scenario Outline: Glossary display format can be set to dictionary, continuous and full with author Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1 # Assign the corresponding display format to glossary activity. And I set the following fields to these values: | displayformat | | When I press "Save and display" # Confirm that glossary format is the display format set in the previous step. Then I should "by Admin User" And "." "css_element" should exist Examples: | display_format | visibility | | dictionary | not see | | continuous | not see | | fullwithauthor | see | tests/behat/showentry.feature000064400000002701152265517410012420 0ustar00@mod @mod_glossary Feature: Show glossary entry In order to see the details of a glossary entry As a user I should be able to view the entry details on one page Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | displayformat | course | idnumber | | glossary | Underused words | Say these more often. | fullwithauthor | C1 | g1 | And the following "mod_glossary > categories" exist: | glossary | name | | g1 | nouns | And the following "mod_glossary > entries" exist: | glossary | concept | definition | user | categories | | g1 | clamor | A loud noise. | teacher1 | nouns | Scenario: View glossary entry details Given I am on the "Underused words" "glossary activity" page logged in as teacher1 When I click on "Entry link: clamor" "link" Then the page title should contain "clamor | Underused words" And "clamor" "heading" should exist And I should see "A loud noise." tests/behat/entries_require_approval.feature000064400000007211152265517410015470 0ustar00@mod @mod_glossary Feature: A teacher can choose whether glossary entries require approval In order to check entries before they are displayed As a user I need to enable entries requiring approval Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activity" exists: | activity | glossary | | course | C1 | | idnumber | 0001 | | name | Test glossary name | | intro | Test glossary entries require approval | | section | 1 | | defaultapproval | 0 | Scenario: Approve and undo approve glossary entries Given I am on the "Test glossary name" "glossary activity" page logged in as student1 When I add a glossary entry with the following data: | Concept | Just a test concept | | Definition | Concept definition | | Keyword(s) | Black | And I log out # Test that students can not see the unapproved entry. And I am on the "Test glossary name" "glossary activity" page logged in as student2 Then I should see "No entries found in this section" And I log out # Approve the entry. And I am on the "Test glossary name" "glossary activity" page logged in as teacher1 And I follow "Pending approval (1)" Then I should see "(this entry is currently hidden)" And I follow "Approve" And I am on the "Test glossary name" "glossary activity" page Then I should see "Concept definition" And I log out # Check that the entry can now be viewed by students. And I am on the "Test glossary name" "glossary activity" page logged in as student2 Then I should see "Concept definition" And I log out # Undo the approval of the previous entry. And I am on the "Test glossary name" "glossary activity" page logged in as teacher1 And I follow "Undo approval" And I log out # Check that the entry is no longer visible by students. And I am on the "Test glossary name" "glossary activity" page logged in as student2 Then I should see "No entries found in this section" @javascript Scenario: View pending approval glossary items Given I am on the "Test glossary name" "glossary activity" page logged in as student1 When I add a glossary entry with the following data: | Concept | Just a test concept | | Definition | Concept definition | | Keyword(s) | Black | | Tags | Test | And I log out And I log in as "teacher1" And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I expand "Site pages" node And I click on "Tags" "link" in the "Navigation" "block" And I follow "Test" Then I should see "Glossary entries" And I should see "Just a test concept" And I should see "Entry not approved" tests/behat/create_entry.feature000064400000004337152265517410013051 0ustar00@mod @mod_glossary Feature: Create a glossary entry. In order to create glossary entries As a user I should be able to enter an entry without using reserved keywords Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | glossary | | course | C1 | | name | Test glossary | Scenario: Glossary entry edition of custom tags works as expected Given I am on the "Test glossary" "glossary activity" page logged in as "teacher1" When I press "Add entry" And I set the following fields to these values: | Concept | Dummy first entry | | Definition | Dream is the start of a journey | | Keyword(s) | " | And I press "Save changes" Then I should see "One or more keywords contain a special character which cannot be used." @javascript @_file_upload Scenario: Create glossary entry with attached file Given I am on the "Test glossary" "glossary activity" page logged in as student1 # As a student, add a glossary entry with attachment And I press "Add entry" And I set the following fields to these values: | Concept | Entry 1 | | Definition | Definition of Entry 1 | | Attachment | mod/glossary/tests/fixtures/musicians.xml | And I press "Save changes" # Confirm you can download attachment from student's entry as teacher When I am on the "Test glossary" "glossary activity" page logged in as teacher1 Then I should see "Entry 1" And following "musicians.xml" should download a file that: | Has mimetype | text/xml | | Contains text in xml element | Paul McCartney | tests/behat/search_entries.feature000064400000014276152265517410013366 0ustar00@mod @mod_glossary Feature: Glossary entries can be searched or browsed by alphabet, category, date or author In order to find entries in a glossary As a user I need to search the entries list by keyword, alphabet, category, date and author Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | displayformat | course | idnumber | | glossary | Test glossary name | Test glossary description | fullwithauthor | C1 | g1 | And the following "mod_glossary > categories" exist: | glossary | name | | g1 | The ones I likeCeux qui me plaisent | | g1 | All for youTout pour toi | And the following "mod_glossary > entries" exist: | glossary | concept | definition | user | categories | | g1 | EggplantAubergine | Sour eggplantsAubergines aigres | teacher1 | All for youTout pour toi | | g1 | 7up | 7up is a softdrink7up est une boisson | teacher1 | The ones I likeCeux qui me plaisent | | g1 | CucumberConcombre | Sweet cucumberDoux concombre | student1 | The ones I likeCeux qui me plaisent | And the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" And I log out And I am on the "Test glossary name" "glossary activity" page logged in as teacher1 @javascript Scenario: Search by keyword and browse by alphabet When I set the field "hook" to "cucumber" And I press "Search" Then I should see "Sweet cucumber" And I should see "Search: cucumber" And I set the field "hook" to "aubergine" And I press "Search" And I should see "Sour eggplants" And I should see "Search: aubergine" And I should see "E" in the ".glossarycategoryheader" "css_element" And I click on "E" "link" in the ".entrybox" "css_element" And I should see "Sour eggplants" And I should not see "Sweet cucumber" And I should not see "No entries found in this section" And I click on "Special" "link" in the ".entrybox" "css_element" And I should see "7up" And I should not see "Sweet cucumber" And I should not see "Sour eggplants" And I should not see "No entries found in this section" And I click on "X" "link" in the ".entrybox" "css_element" And I should not see "Sweet cucumber" And I should see "No entries found in this section" @javascript Scenario: Search by keyword and browse by alphabet when several multilang entries can be found When I add a glossary entry with the following data: | Concept | ConcombreCucumber | | Definition | Doux concombreSweet cucumber alternate entry | And I set the field "hook" to "cucumber" And I press "Search" Then I should see "Sweet cucumber" And I should see "Sweet cucumber alternate entry" And I should see "Search: cucumber" @javascript Scenario: Browse by category When I select "Browse by category" from the "Browse the glossary using this index" singleselect And I set the field "Categories" to "The ones I like" Then I should see "Sweet cucumber" And I should not see "Sour eggplants" And I set the field "Categories" to "All for you" And I should see "Sour eggplants" And I should not see "Sweet cucumber" @javascript Scenario: Browse by date When I select "Browse by date" from the "Browse the glossary using this index" singleselect And I follow "By creation date" Then "Delete entry: Eggplant" "link" should appear before "Delete entry: Cucumber" "link" And I follow "By last update" And I follow "By last update change to descending" And "Delete entry: Cucumber" "link" should appear before "Delete entry: Eggplant" "link" @javascript Scenario: Browse by author When I select "Browse by Author" from the "Browse the glossary using this index" singleselect And I click on "T" "link" in the ".entrybox" "css_element" Then I should see "Teacher 1" And I should see "Sour eggplants" And I should not see "Sweet cucumber" And I click on "S" "link" in the ".entrybox" "css_element" And I should see "Student 1" And I should see "Sweet cucumber" And I should not see "Sour eggplants" tests/behat/entries_always_editable.feature000064400000003172152265517410015243 0ustar00@mod @mod_glossary Feature: A teacher can set whether glossary entries are always editable or not In order to ensure students think before adding new entries As a teacher I need to prevent entries to be always editable Scenario: Glossary entries are not always editable Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | editalways | | glossary | Test glossary name | Test glossary description | C1 | glossary1 | 0 | And the following config values are set as admin: | maxeditingtime | 5 | And I log in as "student1" And I am on "Course 1" course homepage And I follow "Test glossary name" When I add a glossary entry with the following data: | Concept | Test concept name | | Definition | Test concept description | Then "Delete entry: Test concept name" "link" should exist And "Edit entry: Test concept name" "link" should exist And I wait "6" seconds And I reload the page And "Delete entry: Test concept name" "link" should not exist And "Edit entry: Test concept name" "link" should not exist tests/behat/print_friendly_version.feature000064400000004157152265517410015162 0ustar00@mod @mod_glossary Feature: A teacher can choose whether to provide a printer-friendly glossary entries list In order to print glossaries easily As a user I need to provide users a different view to print the glossary contents Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Printer-friendly glossary view enabled Given the following "activity" exists: | course | C1 | | activity | glossary | | name | Test glossary name | | intro | Test glossary description | | allowprintview | 1 | And I am on the "Test glossary name" "glossary activity" page logged in as student1 When I add a glossary entry with the following data: | Concept | Just a test concept | | Definition | Concept definition | And I click on "Export entries" "button" And I click on "Print" "link" Then I should see "Just a test concept" @javascript Scenario: Printer-friendly glossary view disabled Given the following "activity" exists: | course | C1 | | activity | glossary | | name | Test glossary name | | intro | Test glossary description | | allowprintview | 0 | And I am on the "Test glossary name" "glossary activity" page logged in as student1 When I add a glossary entry with the following data: | Concept | Just a test concept | | Definition | Concept definition | And "//select[contains(concat(' ', normalize-space(@class), ' '), ' urlselect ')]" "xpath_element" should not exist tests/behat/edit_tags.feature000064400000013307152265517410012325 0ustar00@mod @mod_glossary @core_tag Feature: Edited glossary entries handle tags correctly In order to get glossary entries properly labelled As a user I need to introduce the tags while editing Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activity" exists: | course | C1 | | activity | glossary | | name | Test glossary | | intro | A glossary about dreams! | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | tags | System | 1 | my-index | side-pre | @javascript Scenario: Glossary entry edition of custom tags works as expected Given I am on the "Test glossary" "glossary activity" page logged in as teacher1 And I press "Add entry" And I set the following fields to these values: | Concept | Dummy first entry | | Definition | Dream is the start of a journey | | Tags | Example, Entry, Cool | And I press "Save changes" Then I should see "Example" in the ".glossary-tags" "css_element" And I should see "Entry" in the ".glossary-tags" "css_element" And I should see "Cool" in the ".glossary-tags" "css_element" And I click on "Edit" "link" in the ".entrylowersection" "css_element" And I expand all fieldsets Then I should see "Example" in the ".form-autocomplete-selection" "css_element" Then I should see "Entry" in the ".form-autocomplete-selection" "css_element" Then I should see "Cool" in the ".form-autocomplete-selection" "css_element" @javascript Scenario: Glossary entry edition of standard tags works as expected Given the following "tags" exist: | name | isstandard | | OT1 | 1 | | OT2 | 1 | | OT3 | 1 | And I am on the "Test glossary" "glossary activity" page logged in as teacher1 And I press "Add entry" And I expand all fieldsets And I open the autocomplete suggestions list And I should see "OT1" in the ".form-autocomplete-suggestions" "css_element" And I should see "OT2" in the ".form-autocomplete-suggestions" "css_element" And I should see "OT3" in the ".form-autocomplete-suggestions" "css_element" When I set the following fields to these values: | Concept | Dummy first entry | | Definition | Dream is the start of a journey | | Tags | OT1, OT3 | And I press "Save changes" Then I should see "OT1" in the ".glossary-tags" "css_element" And I should see "OT3" in the ".glossary-tags" "css_element" And I should not see "OT2" in the ".glossary-tags" "css_element" And I click on "Edit" "link" in the ".entrylowersection" "css_element" And I expand all fieldsets And I should see "OT1" in the ".form-autocomplete-selection" "css_element" And I should see "OT3" in the ".form-autocomplete-selection" "css_element" And I should not see "OT2" in the ".form-autocomplete-selection" "css_element" @javascript Scenario: Student delete glossary entry tags # Add glossary entry with tags as an enrolled student. Given the following "mod_glossary > entries" exist: | glossary | concept | definition | tags | user | | Test glossary | Entry 1 | Entry 1 definition | OT1, OT2, OT3 | student1 | And I am on the "Test glossary" "glossary activity" page logged in as student1 When I click on "Edit entry: Entry 1" "link" And I expand all fieldsets # Delete a glossary entry tag. And I click on "[data-value='OT1']" "css_element" And I press "Save changes" # Confirm that only the selected glossary entry tag is deleted. Then I should not see "OT1" in the ".glossary-tags" "css_element" And I should see "OT2" in the ".glossary-tags" "css_element" And I should see "OT3" in the ".glossary-tags" "css_element" Scenario Outline: Student glossary entry tags can be viewed depending on approval # Add glossary entry with tags as an enrolled student. Given the following "mod_glossary > entries" exist: | glossary | concept | definition | tags | user | approved | | Test glossary | Entry 1 | Entry 1 definition | OT1, OT2, OT3 | student1 | | When I log in as "student2" And I click on "OT1" "link" in the "Tags" "block" Then I see "Entry 1" Examples: | approved | entryvisibility | | 0 | should not | | 1 | should | @javascript Scenario: Hidden glossary activity is not visible in tag index Given the following "mod_glossary > entries" exist: | glossary | concept | definition | tags | user | | Test glossary | Entry 1 | Entry 1 definition | OT1, OT2, OT3 | student1 | And I am on the "Course 1" course page logged in as teacher1 And I turn editing mode on And I open "Test glossary" actions menu And I click on "Hide" "link" in the "Test glossary" activity When I log in as "student2" And I click on "OT1" "link" in the "Tags" "block" Then I should not see "Entry 1" tests/behat/glossary_autolink.feature000064400000036047152265517410014141 0ustar00@mod @mod_glossary Feature: Glossary entries are displayed properly when autolinked In order to display linked glossary entries for concepts As an admin I can set the glossary activity to autolink the entries Background: Given remote langimport tests are enabled And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | displayformat | course | idnumber | globalglossary | | glossary | Test glossaryglossaireglossario | Test glossary description | encyclopedia | C1 | glossary1 | 0 | | glossary | Autolinked Glossary | Autolinked glossary description | encyclopedia | Acceptance test site | glossary2 | 1 | And the following "mod_glossary > entries" exist: | glossary | concept | definition | usedynalink | | glossary1 | EnglishAnglaisinglés | Relating to England, its people, or the language spoken there.Relatif à l'Angleterre, à son peuple ou à la langue parlée là-bas.Relacionado con Inglaterra, su gente o el idioma hablado allí. | 1 | | glossary1 | SpanishEspagnolCastellano | Relating to Spain, its people, or the language spoken there.Relatif à l'Espagne, à son peuple ou à la langue parlée là-bas.Relacionado con España, su gente o el idioma hablado allí. | 1 | | glossary2 | Linked entry | This is the linked entry definition | 1 | | glossary2 | Normal entry | This is the normal entry definition | 0 | And the following "activities" exist: | activity | name | intro | course | idnumber | content | showdescription | | label | Text and media area |

This is a text with the multilang syntax on the EnglishAnglaisInglés word that should be auto-linked.

This are plain text words that should also be auto-linked: English, Anglais, Inglés.

| C1 | label1 | | 0 | And the "glossary" filter is "on" And the following "language pack" exists: | language | fr | es | And the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" Scenario: Autolinked glossary entries are correctly linked in front page context Given I log in as "admin" When I am on site homepage And I turn editing mode on And I follow "Edit" And I set the following fields to these values: | Description | This is a Linked entry. This is a Normal entry | And I press "Save changes" # Confirm that the text "linked entry" is automatically linked when referenced. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Scenario: Autolinked glossary entries are correctly linked in course category Given I log in as "admin" And I navigate to "Courses > Manage courses and categories" in site administration And I click on "edit" action for "Category 1" in management category listing And I set the following fields to these values: | Description | This is a Linked entry. This is a Normal entry | And I press "Save changes" When I click on "view" action for "Category 1" in management category listing # Confirm that the text "linked entry" is automatically linked when referenced. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Scenario: Autolinked glossary entries are correctly linked in course summary # Course summary is automatically generated so editing it is necessary. Given I am on the "Course 1" "course editing" page logged in as "teacher1" And I set the following fields to these values: | Course summary | This is a Linked entry. This is the Normal entry | And I press "Save and display" When I am on site homepage # Confirm that the text "linked entry" is automatically linked when referenced. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Scenario Outline: Autolinked glossary entries are correctly linked in text and media areas and url Given the following "activities" exist: | activity | name | intro | course | showdescription | | | | This is a Linked entry. This is a Normal entry | C1 | 1 | When I am on the "Course 1" course page logged in as teacher1 # Confirm that the text "linked entry" is automatically linked when referenced. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Examples: | activityname | activitytype | | Text and media area | label | | Url 1 | url | @javascript Scenario: Autolinked glossary entries are correctly linked in text blocks Given I am on the "Course 1" course page logged in as teacher1 And I turn editing mode on When I add the "Text" block to the default region with: | Text block title | Text block | | Content | This is a Linked entry. This is the Normal entry | # Confirm that the text "linked entry" is automatically linked when referenced. And "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Scenario Outline: Autolinked glossary entries are correctly linked in activities and resources Given the following "activities" exist: | activity | name | intro | course | content | | | | This is a Linked entry. This is a Normal entry | C1 | | When I am on the "" " activity" page logged in as teacher1 # Confirm that the text "linked entry" is automatically linked when referenced. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Examples: | activityname | activitytype | pagecontent | | Page 1 | page | This is a Linked entry. This is a Normal entry | | Assign 1 | assign | | | Folder 1 | folder | | | Quiz 1 | quiz | | | Workshop 1 | workshop | | | Choice 1 | choice | | Scenario: Autolinked glossary entries are correctly linked in database Given the following "activities" exist: | activity | name | intro | course | idnumber | | data | Database 1 | This is a Linked entry. This is a Normal entry | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | And the following "mod_data > entries" exist: | database | user | Test field name | | data1 | student1 | Student entry | When I am on the "Database 1" "data activity" page logged in as teacher1 # Confirm that the text "linked entry" is automatically linked when referenced in list view. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced in list view. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist And I select "Single view" from the "jump" singleselect # Confirm that the text "linked entry" is automatically linked when referenced in single view. And "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced in single view. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Scenario: Autolinked glossary entries are correctly linked in wiki page Given the following "activities" exist: | activity | course | name | wikimode | | wiki | C1 | Wiki 1 | collaborative | When I am on the "Wiki 1" "wiki activity" page logged in as teacher1 And I press "Create page" And I set the following fields to these values: | HTML format | This is a Linked entry. This is a Normal entry. | And I press "Save" # Confirm that the text "linked entry" is automatically linked when referenced. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist Scenario: Autolinked glossary entries are correctly linked in forum posts Given the following "activities" exist: | activity | course | name | idnumber | | forum | C1 | Forum 1 | forum1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum1 | Discussion 1 | This is a Linked entry. This is a Normal entry. | And I am on the "Course 1" course page logged in as teacher1 When I navigate to post "Discussion 1" in "Forum 1" forum # Confirm that the text "linked entry" is automatically linked when referenced. Then "Linked entry" "link" should exist # Confirm that the text "normal entry" is not linked when referenced. And "Normal entry" "link" should not exist And "Normal entry" "text" should exist @javascript Scenario: Glossary entries show up in text and media areas in the correct user interface/language combination When I am on the "Course 1" course page logged in as teacher1 Then "English" "link" should exist in the ".modtype_label" "css_element" And "Anglais" "link" should not exist in the ".modtype_label" "css_element" And "Inglés" "link" should not exist in the ".modtype_label" "css_element" And the "title" attribute of ".glossary.autolink" "css_element" should contain "Test glossary: English" And I follow "Preferences" in the user menu And I follow "Preferred language" # Change preferred language to Spanish. And I set the field "Preferred language" to "es" And I press "Save changes" And I am on "Course 1" course homepage Then "English" "link" should not exist in the ".modtype_label" "css_element" And "Anglais" "link" should not exist in the ".modtype_label" "css_element" And "Inglés" "link" should exist in the ".modtype_label" "css_element" And the "title" attribute of ".glossary.autolink" "css_element" should contain "Test glossario: inglés" And I follow "Preferencias" in the user menu And I follow "Idioma preferido" # Change preferred language to French. And I set the field "Idioma preferido" to "fr" And I press "Guardar cambios" And I am on "Course 1" course homepage Then "English" "link" should not exist in the ".modtype_label" "css_element" And "Anglais" "link" should exist in the ".modtype_label" "css_element" And "Inglés" "link" should not exist in the ".modtype_label" "css_element" And the "title" attribute of ".glossary.autolink" "css_element" should contain "Test glossaire : Anglais" tests/behat/import_entries.feature000064400000004151152265517410013422 0ustar00@mod @mod_glossary @_file_upload Feature: Importing glossary entries In order to add glossary entries by bulk As a teacher I need to be able to import glossary entries from a file Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Terry1 | Teacher1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | course | idnumber | name | | glossary | C1 | glossary1 | Glossary 1 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | recent_activity | Course | C1 | course-view-* | side-pre | | tags | Course | C1 | course-view-* | side-pre | And I am on the "Glossary 1" "glossary activity" page logged in as teacher1 @javascript @block_recent_activity Scenario: Importing glossary entries and checking the Recent activity block Given I press "Import entries" And I upload "mod/glossary/tests/fixtures/texfilter_glossary_en.xml" file to "File to import" filemanager When I press "Submit" Then I should see "103" in the "Total entries:" "table_row" And I should see "103" in the "Imported entries:" "table_row" And I am on "Course 1" course homepage And I should see "Added Glossary" in the "Recent activity" "block" And I should see "New glossary entries:" in the "Recent activity" "block" @javascript @block_tags Scenario: Importing glossary entries and checking Tags block Given I press "Import entries" And I upload "mod/glossary/tests/fixtures/musicians.xml" file to "File to import" filemanager When I press "Submit" And I am on "Course 1" course homepage And I click on "Beatles" "link" in the "Tags" "block" Then I should see "Paul McCartney" tests/behat/categories.feature000064400000014003152265517410012501 0ustar00@mod @mod_glossary Feature: Glossary entries can be organised in categories In order to organise glossary entries As a teacher I need to be able to create, edit and delete categories @javascript Scenario: Glossary entries can be organised in categories and categories can be autolinked Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | displayformat | course | idnumber | | glossary | MyGlossary | Test glossary description | encyclopedia | C1 | glossary1 | And the following "activities" exist: | activity | name | intro | course | idnumber | | label | name | check autolinking of CategoryAutoLinks and CategoryNoLinks text | C1 | label1 | And the "glossary" filter is "on" # Log in as a teacher and make sure nothing is yet autolinked When I am on the "Course 1" course page logged in as teacher1 Then I should see "CategoryAutoLinks" And I should see "CategoryNoLinks" And "a.glossary.autolink" "css_element" should not exist # Create, edit and delete categories And I am on the MyGlossary "glossary activity" page And I select "Browse by category" from the "Browse the glossary using this index" singleselect And I press "Edit categories" And I press "Add category" And I set the field "name" to "CategoryNoLinks" And I press "Save changes" And I should see "0 Entries" in the "CategoryNoLinks" "table_row" And I press "Add category" And I set the field "name" to "CategoryAutoLinks" And I set the field "usedynalink" to "Yes" And I press "Save changes" And I should see "0 Entries" in the "CategoryAutoLinks" "table_row" And I press "Add category" And I set the field "name" to "Category2" And I press "Save changes" And I click on "Edit" "link" in the "Category2" "table_row" And I set the field "name" to "Category3" And I press "Save changes" And I should see "Category3" And I should not see "Category2" And I click on "Delete" "link" in the "Category3" "table_row" And I press "No" And I should see "Category3" And I click on "Delete" "link" in the "Category3" "table_row" And I press "Yes" And I should not see "Category3" And I press "Back" # Add glossary entries in categories and outside And I add a glossary entry with the following data: | Concept | EntryNoCategory | | Definition | Definition | And I add a glossary entry with the following data: | Concept | EntryCategoryNL | | Definition | Definition | | Categories | CategoryNoLinks | And I add a glossary entry with the following data: | Concept | EntryCategoryAL | | Definition | Definition | | Categories | CategoryAutoLinks | And I press "Add entry" And I set the following fields to these values: | Concept | EntryCategoryBoth | | Definition | Definition | | Categories | CategoryAutoLinks,CategoryNoLinks | And I press "Save changes" # Make sure entries appear in their categories And I select "Browse by category" from the "Browse the glossary using this index" singleselect And "//h3[contains(.,'CATEGORYAUTOLINKS')]" "xpath_element" should appear before "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element" And "//h4[contains(.,'EntryCategoryAL')]" "xpath_element" should appear before "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element" And "(//h4[contains(.,'EntryCategoryBoth')])[1]" "xpath_element" should appear before "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element" And "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element" should appear before "(//h4[contains(.,'EntryCategoryBoth')])[2]" "xpath_element" And "//h4[contains(.,'EntryCategoryNL')]" "xpath_element" should appear after "//h3[contains(.,'CATEGORYNOLINKS')]" "xpath_element" And I should not see "EntryNoCategory" And I set the field "hook" to "Not categorised" And I set the field "Categories" to "Not categorised" And I should see "EntryNoCategory" And I should not see "EntryCategoryNL" And I should not see "EntryCategoryAL" And I should not see "EntryCategoryBoth" # Check that category is autolinked from the text in the course And I am on "Course 1" course homepage And I should see "CategoryAutoLinks" And I should see "CategoryAutoLinks" in the "a.glossary.autolink" "css_element" And I should see "CategoryNoLinks" And "//a[contains(.,'CategoryNoLinks')]" "xpath_element" should not exist # Delete a category with entries And I am on the MyGlossary "glossary activity" page And I select "Browse by category" from the "Browse the glossary using this index" singleselect And I press "Edit categories" And I should see "2 Entries" in the "CategoryNoLinks" "table_row" And I should see "2 Entries" in the "CategoryAutoLinks" "table_row" And I click on "Delete" "link" in the "CategoryAutoLinks" "table_row" And I press "Yes" And I wait to be redirected And I am on the MyGlossary "glossary activity" page And I select "Browse by category" from the "Browse the glossary using this index" singleselect And I should see "EntryCategoryNL" And I should not see "EntryNoCategory" And I should not see "EntryCategoryAL" And I should see "EntryCategoryBoth" And I set the field "Categories" to "Not categorised" And I should see "EntryNoCategory" And I should see "EntryCategoryAL" And I should not see "EntryCategoryBoth" tests/behat/behat_mod_glossary.php000064400000005677152265517410013376 0ustar00. /** * Steps definitions related with the glossary activity. * * @package mod_glossary * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode as TableNode; /** * Glossary-related steps definitions. * * @package mod_glossary * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_glossary extends behat_base { /** * Adds an entry to the current glossary with the provided data. You should be in the glossary page. * * @Given /^I add a glossary entry with the following data:$/ * @param TableNode $data */ public function i_add_a_glossary_entry_with_the_following_data(TableNode $data) { $this->execute("behat_forms::press_button", get_string('addsingleentry', 'mod_glossary')); $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data); $this->execute("behat_forms::press_button", get_string('savechanges')); } /** * Adds a category with the specified name to the current glossary. You need to be in the glossary page. * * @Given /^I add a glossary entries category named "(?P(?:[^"]|\\")*)"$/ * @param string $categoryname Category name */ public function i_add_a_glossary_entries_category_named($categoryname) { $params = [ get_string('categoryview', 'mod_glossary'), get_string('explainalphabet', 'glossary') ]; $this->execute("behat_forms::i_select_from_the_singleselect", $params); $this->execute("behat_forms::press_button", get_string('editcategories', 'mod_glossary')); $this->execute("behat_forms::press_button", get_string('addcategory', 'glossary')); $this->execute('behat_forms::i_set_the_field_to', ['name', $categoryname]); $this->execute("behat_forms::press_button", get_string('savechanges')); $this->execute("behat_forms::press_button", get_string('back', 'mod_glossary')); } } tests/behat/glossary_activity_completion.feature000064400000011164152265517410016371 0ustar00@mod @mod_glossary @core_completion Feature: View activity completion in the glossary activity In order to have visibility of glossary completion requirements As a student I need to be able to view my glossary completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | enablecompletion | showcompletionconditions | | Course 1 | C1 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | glossary | | course | C1 | | name | Music history | | section | 1 | And I am on the "Music history" "glossary activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | Aggregate type | Average of ratings | | scale[modgrade_type] | Point | | scale[modgrade_point] | 100 | | Add requirements | 1 | | View the activity | 1 | | Receive a grade | 1 | | Any grade | 1 | | completionentriesenabled | 1 | | completionentries | 1 | And I press "Save and display" And I log out Scenario: View glossary automatic completion conditions as a teacher Given I am on the "Music history" "glossary activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Make entries: 1" completion condition And "Music history" should have the "Receive a grade" completion condition Scenario: A student completes a glossary Given I am on the "Music history" "glossary activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And I am on the "Music history" "glossary activity" page And I press "Add entry" And I set the following fields to these values: | Concept | Blast beats | | Definition | Repeated fast tempo hits combining bass, snare and cymbal | And I press "Save changes" And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And I log out And I am on the "Music history" "glossary activity" page logged in as teacher1 And I set the field "rating" to "3" And I press "Rate" And I log out When I am on the "Music history" "glossary activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 1" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" @javascript Scenario: A student can manually mark the glossary activity as done but a teacher cannot Given I am on the "Music history" "glossary activity editing" page logged in as teacher1 And I expand all fieldsets And I set the field "Students must manually mark the activity as done" to "1" And I press "Save and display" # Teacher view. And the manual completion button for "Music history" should be disabled And I log out # Student view. When I am on the "Music history" "glossary activity" page logged in as student1 Then the manual completion button of "Music history" is displayed as "Mark as done" And I toggle the manual completion state of "Music history" And the manual completion button of "Music history" is displayed as "Done" tests/concept_cache_test.php000064400000022707152265517410012254 0ustar00. namespace mod_glossary; /** * Concept fetching and caching tests. * * @package mod_glossary * @category test * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class concept_cache_test extends \advanced_testcase { /** * Test convect fetching. */ public function test_concept_fetching(): void { global $CFG, $DB; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->glossary_linkbydefault = 1; $CFG->glossary_linkentries = 0; // Create a test courses. $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $site = $DB->get_record('course', array('id' => SITEID)); // Create a glossary. $glossary1a = $this->getDataGenerator()->create_module('glossary', array('course' => $course1->id, 'mainglossary' => 1, 'usedynalink' => 1)); $glossary1b = $this->getDataGenerator()->create_module('glossary', array('course' => $course1->id, 'mainglossary' => 1, 'usedynalink' => 1)); $glossary1c = $this->getDataGenerator()->create_module('glossary', array('course' => $course1->id, 'mainglossary' => 1, 'usedynalink' => 0)); $glossary2 = $this->getDataGenerator()->create_module('glossary', array('course' => $course2->id, 'mainglossary' => 1, 'usedynalink' => 1)); $glossary3 = $this->getDataGenerator()->create_module('glossary', array('course' => $site->id, 'mainglossary' => 1, 'usedynalink' => 1, 'globalglossary' => 1)); /** @var mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry1a1 = $generator->create_content($glossary1a, array('concept' => 'first', 'usedynalink' => 1), array('prvni', 'erste')); $entry1a2 = $generator->create_content($glossary1a, array('concept' => 'A&B', 'usedynalink' => 1)); $entry1a3 = $generator->create_content($glossary1a, array('concept' => 'neee', 'usedynalink' => 0)); $entry1b1 = $generator->create_content($glossary1b, array('concept' => 'second', 'usedynalink' => 1)); $entry1c1 = $generator->create_content($glossary1c, array('concept' => 'third', 'usedynalink' => 1)); $entry31 = $generator->create_content($glossary3, array('concept' => 'global', 'usedynalink' => 1), array('globalni')); $cat1 = $generator->create_category($glossary1a, array('name' => 'special'), array($entry1a1, $entry1a2)); \mod_glossary\local\concept_cache::reset_caches(); $concepts1 = \mod_glossary\local\concept_cache::get_concepts($course1->id); $this->assertCount(3, $concepts1[0]); $this->assertArrayHasKey($glossary1a->id, $concepts1[0]); $this->assertArrayHasKey($glossary1b->id, $concepts1[0]); $this->assertArrayHasKey($glossary3->id, $concepts1[0]); $this->assertCount(3, $concepts1[1]); $this->assertArrayHasKey($glossary1a->id, $concepts1[1]); $this->assertArrayHasKey($glossary1b->id, $concepts1[1]); $this->assertArrayHasKey($glossary3->id, $concepts1[1]); $this->assertCount(5, $concepts1[1][$glossary1a->id]); foreach($concepts1[1][$glossary1a->id] as $concept) { $this->assertSame(array('id', 'glossaryid', 'concept', 'casesensitive', 'category', 'fullmatch'), array_keys((array)$concept)); if ($concept->concept === 'first') { $this->assertEquals($entry1a1->id, $concept->id); $this->assertEquals($glossary1a->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else if ($concept->concept === 'prvni') { $this->assertEquals($entry1a1->id, $concept->id); $this->assertEquals($glossary1a->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else if ($concept->concept === 'erste') { $this->assertEquals($entry1a1->id, $concept->id); $this->assertEquals($glossary1a->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else if ($concept->concept === 'A&B') { $this->assertEquals($entry1a2->id, $concept->id); $this->assertEquals($glossary1a->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else if ($concept->concept === 'special') { $this->assertEquals($cat1->id, $concept->id); $this->assertEquals($glossary1a->id, $concept->glossaryid); $this->assertEquals(1, $concept->category); } else { $this->fail('Unexpected concept: ' . $concept->concept); } } $this->assertCount(1, $concepts1[1][$glossary1b->id]); foreach($concepts1[1][$glossary1b->id] as $concept) { $this->assertSame(array('id', 'glossaryid', 'concept', 'casesensitive', 'category', 'fullmatch'), array_keys((array)$concept)); if ($concept->concept === 'second') { $this->assertEquals($entry1b1->id, $concept->id); $this->assertEquals($glossary1b->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else { $this->fail('Unexpected concept: ' . $concept->concept); } } $this->assertCount(2, $concepts1[1][$glossary3->id]); foreach($concepts1[1][$glossary3->id] as $concept) { $this->assertSame(array('id', 'glossaryid', 'concept', 'casesensitive', 'category', 'fullmatch'), array_keys((array)$concept)); if ($concept->concept === 'global') { $this->assertEquals($entry31->id, $concept->id); $this->assertEquals($glossary3->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else if ($concept->concept === 'globalni') { $this->assertEquals($entry31->id, $concept->id); $this->assertEquals($glossary3->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else { $this->fail('Unexpected concept: ' . $concept->concept); } } $concepts3 = \mod_glossary\local\concept_cache::get_concepts($site->id); $this->assertCount(1, $concepts3[0]); $this->assertArrayHasKey($glossary3->id, $concepts3[0]); $this->assertCount(1, $concepts3[1]); $this->assertArrayHasKey($glossary3->id, $concepts3[1]); foreach($concepts3[1][$glossary3->id] as $concept) { $this->assertSame(array('id', 'glossaryid', 'concept', 'casesensitive', 'category', 'fullmatch'), array_keys((array)$concept)); if ($concept->concept === 'global') { $this->assertEquals($entry31->id, $concept->id); $this->assertEquals($glossary3->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else if ($concept->concept === 'globalni') { $this->assertEquals($entry31->id, $concept->id); $this->assertEquals($glossary3->id, $concept->glossaryid); $this->assertEquals(0, $concept->category); } else { $this->fail('Unexpected concept: ' . $concept->concept); } } $concepts2 = \mod_glossary\local\concept_cache::get_concepts($course2->id); $this->assertEquals($concepts3, $concepts2); // Test uservisible flag. set_config('enableavailability', 1); $glossary1d = $this->getDataGenerator()->create_module('glossary', array('course' => $course1->id, 'mainglossary' => 1, 'usedynalink' => 1, 'availability' => json_encode(\core_availability\tree::get_root_json( array(\availability_group\condition::get_json()))))); $entry1d1 = $generator->create_content($glossary1d, array('concept' => 'membersonly', 'usedynalink' => 1)); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course1->id); $this->getDataGenerator()->enrol_user($user->id, $course2->id); \mod_glossary\local\concept_cache::reset_caches(); $concepts1 = \mod_glossary\local\concept_cache::get_concepts($course1->id); $this->assertCount(4, $concepts1[0]); $this->assertCount(4, $concepts1[1]); $this->setUser($user); \course_modinfo::clear_instance_cache(); \mod_glossary\local\concept_cache::reset_caches(); $concepts1 = \mod_glossary\local\concept_cache::get_concepts($course1->id); $this->assertCount(3, $concepts1[0]); $this->assertCount(3, $concepts1[1]); } } sql.php000064400000007516152265517410006075 0ustar00glossaryid == $glossary->id || $entry->sourceglossaryid != $glossary->id) && (!empty($entry->approved) || $entry->userid == $USER->id || $canapprove)) { $count = 1; $allentries = array($entry); } else { $count = 0; $allentries = array(); } unset($entry, $canapprove); break; case 'letter': default: list($allentries, $count) = glossary_get_entries_by_letter($glossary, $context, $hook, $offset, $pagelimit); break; } break; } import_form.php000064400000002212152265517410007617 0ustar00libdir.'/formslib.php'); class mod_glossary_import_form extends moodleform { function definition() { global $CFG; $mform =& $this->_form; $cmid = $this->_customdata['id'] ?? null; $mform->addElement('filepicker', 'file', get_string('filetoimport', 'glossary')); $mform->addHelpButton('file', 'filetoimport', 'glossary'); $options = array(); $options['current'] = get_string('currentglossary', 'glossary'); $options['newglossary'] = get_string('newglossary', 'glossary'); $mform->addElement('select', 'dest', get_string('destination', 'glossary'), $options); $mform->addHelpButton('dest', 'destination', 'glossary'); $mform->addElement('checkbox', 'catsincl', get_string('importcategories', 'glossary')); $submit_string = get_string('submit'); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $this->add_action_buttons(true, $submit_string); } } showentry.php000064400000007277152265517410007344 0ustar00param('concept', $concept); $url->param('courseid', $courseid); $url->param('eid', $eid); $url->param('displayformat', $displayformat); $PAGE->set_url($url); if ($CFG->forcelogin) { require_login(); } if ($eid) { $entry = $DB->get_record('glossary_entries', array('id'=>$eid), '*', MUST_EXIST); $glossary = $DB->get_record('glossary', array('id'=>$entry->glossaryid), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('glossary', $glossary->id, 0, false, MUST_EXIST); $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); require_course_login($course, true, $cm); $entry->glossaryname = $glossary->name; $entry->cmid = $cm->id; $entry->courseid = $cm->course; $entry->concept = format_string($entry->concept, true, ["escape" => false]); $entries = array($entry); $PAGE->set_title(implode(\moodle_page::TITLE_SEPARATOR, [ $entry->concept, $entry->glossaryname, ])); } else if ($concept) { $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); require_course_login($course); $entries = glossary_get_entries_search($concept, $courseid); } else { throw new \moodle_exception('invalidelementid'); } $PAGE->set_pagelayout('incourse'); $PAGE->activityheader->disable(); if ($entries) { foreach ($entries as $key => $entry) { // Need to get the course where the entry is, // in order to check for visibility/approve permissions there $entrycourse = $DB->get_record('course', array('id' => $entry->courseid), '*', MUST_EXIST); $modinfo = get_fast_modinfo($entrycourse); // make sure the entry is visible if (empty($modinfo->cms[$entry->cmid]->uservisible)) { unset($entries[$key]); continue; } // make sure the entry is approved (or approvable by current user) if (!$entry->approved and ($USER->id != $entry->userid)) { $context = context_module::instance($entry->cmid); if (!has_capability('mod/glossary:approve', $context)) { unset($entries[$key]); continue; } } $entries[$key]->footer = "

» wwwroot/mod/glossary/view.php?g=$entry->glossaryid\">".format_string($entry->glossaryname,true)."

"; glossary_entry_view($entry, $modinfo->cms[$entry->cmid]->context); } } if (!empty($courseid)) { $strglossaries = get_string('modulenameplural', 'glossary'); $strsearch = get_string('search'); $PAGE->navbar->add($strglossaries); $PAGE->navbar->add($strsearch); $PAGE->set_title(implode(\moodle_page::TITLE_SEPARATOR, [ $strsearch, $strglossaries, $course->shortname, ])); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); } else { echo $OUTPUT->header(); // Needs to be something here to allow linking back to the whole glossary } if (isset($glossary)) { $url = new moodle_url('view.php', ['id' => $cm->id]); $backlink = html_writer::link($url, get_string('back'), ['class' => 'btn btn-secondary']); echo html_writer::tag('div', $backlink, ['class' => 'tertiary-navigation']); } if ($entries) { glossary_print_dynaentry($courseid, $entries, $displayformat); } /// Show one reduced footer echo $OUTPUT->footer(); settings.php000064400000005321152265517410007126 0ustar00fulltree) { require_once($CFG->dirroot.'/mod/glossary/lib.php'); $settings->add(new admin_setting_heading('glossary_normal_header', get_string('glossaryleveldefaultsettings', 'glossary'), '')); $settings->add(new admin_setting_configtext('glossary_entbypage', get_string('entbypage', 'glossary'), get_string('entbypage', 'glossary'), 10, PARAM_INT)); $settings->add(new admin_setting_configcheckbox('glossary_dupentries', get_string('allowduplicatedentries', 'glossary'), get_string('cnfallowdupentries', 'glossary'), 0)); $settings->add(new admin_setting_configcheckbox('glossary_allowcomments', get_string('allowcomments', 'glossary'), get_string('cnfallowcomments', 'glossary'), 0)); $settings->add(new admin_setting_configcheckbox('glossary_linkbydefault', get_string('usedynalink', 'glossary'), get_string('cnflinkglossaries', 'glossary'), 1)); $settings->add(new admin_setting_configcheckbox('glossary_defaultapproval', get_string('defaultapproval', 'glossary'), get_string('cnfapprovalstatus', 'glossary'), 1)); if (empty($CFG->enablerssfeeds)) { $options = array(0 => get_string('rssglobaldisabled', 'admin')); $str = get_string('configenablerssfeeds', 'glossary').'
'.get_string('configenablerssfeedsdisabled2', 'admin'); } else { $options = array(0=>get_string('no'), 1=>get_string('yes')); $str = get_string('configenablerssfeeds', 'glossary'); } $settings->add(new admin_setting_configselect('glossary_enablerssfeeds', get_string('enablerssfeeds', 'admin'), $str, 0, $options)); $settings->add(new admin_setting_heading('glossary_levdev_header', get_string('entryleveldefaultsettings', 'glossary'), '')); $settings->add(new admin_setting_configcheckbox('glossary_linkentries', get_string('usedynalink', 'glossary'), get_string('cnflinkentry', 'glossary'), 0)); $settings->add(new admin_setting_configcheckbox('glossary_casesensitive', get_string('casesensitive', 'glossary'), get_string('cnfcasesensitive', 'glossary'), 0)); $settings->add(new admin_setting_configcheckbox('glossary_fullmatch', get_string('fullmatch', 'glossary'), get_string('cnffullmatch', 'glossary'), 0)); // This is unfortunately necessary to ensure that the glossary_formats table is populated and up to date. // Ensure the table is in sync with what display formats are available in code. glossary_get_available_formats(); $settings->add(new mod_glossary_admin_setting_display_formats()); } README.txt000064400000003310152265517410006247 0ustar00Glossary version 0.5 dev ------------------------ By Williams Castillo (castillow@tutopia.com) This is the Glossary module. Created and maintained by Williams Castillo. This are my first lines in PHP... It means that, although I've tried to do my best, it might (in fact, it does) shows my inexperience... in PHP... and moodle as well. Basically, this module allows you to maintain a repository of data in the form of concepts, Entry->Definitions, etc. Thus: - Entries could be automatically linked from within moodle resources, labels, forum posts, etc. - Entries can have aliases which allows to automatically link the concept to other words than itself. - Glossaries can be viewed in a printer-friendly version format - The display format of the entries is modular so you can create your own formats (and share them with the Moodle community!) - Entries are posted by teachers and can be posted by students as well - Entries can be approved before make them public. - Entries can have an optional attached file. - Entries can be commented by any registered user. - Entries can be categorized (one entry can belongs to zero or more categories) - Entries can be browsed by letters (initials), categories or by date. - Entries can be exported and imported Quick install instructions 1) Be sure you have at least Moodle 1.2 dev installed 2) Be sure to have the latest version of the module 3) Be sure to have the latest language for english (en) and your own language. 3) Create a directory inside you /mod directory and name it: glossary 4) Copy all the files in this directory inside the one you've created in step 3. 5) Pay a visit to your admin area 6) Have fun. Comments and suggestions are always welcome. All the best, Willmod_form.php000064400000026270152265517410007076 0ustar00dirroot.'/course/moodleform_mod.php'); class mod_glossary_mod_form extends moodleform_mod { function definition() { global $CFG, $COURSE, $DB; $mform =& $this->_form; //------------------------------------------------------------------------------- $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text', 'name', get_string('name'), array('size'=>'64')); if (!empty($CFG->formatstringstriptags)) { $mform->setType('name', PARAM_TEXT); } else { $mform->setType('name', PARAM_CLEANHTML); } $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $this->standard_intro_elements(); if (has_capability('mod/glossary:manageentries', context_system::instance())) { $mform->addElement('checkbox', 'globalglossary', get_string('isglobal', 'glossary')); $mform->addHelpButton('globalglossary', 'isglobal', 'glossary'); }else{ $mform->addElement('hidden', 'globalglossary'); $mform->setType('globalglossary', PARAM_INT); } $options = array(1=>get_string('mainglossary', 'glossary'), 0=>get_string('secondaryglossary', 'glossary')); $mform->addElement('select', 'mainglossary', get_string('glossarytype', 'glossary'), $options); $mform->addHelpButton('mainglossary', 'glossarytype', 'glossary'); $mform->setDefault('mainglossary', 0); // ---------------------------------------------------------------------- $mform->addElement('header', 'entrieshdr', get_string('entries', 'glossary')); $mform->addElement('selectyesno', 'defaultapproval', get_string('defaultapproval', 'glossary')); $mform->setDefault('defaultapproval', $CFG->glossary_defaultapproval); $mform->addHelpButton('defaultapproval', 'defaultapproval', 'glossary'); $mform->addElement('selectyesno', 'editalways', get_string('editalways', 'glossary')); $mform->setDefault('editalways', 0); $mform->addHelpButton('editalways', 'editalways', 'glossary'); $mform->addElement('selectyesno', 'allowduplicatedentries', get_string('allowduplicatedentries', 'glossary')); $mform->setDefault('allowduplicatedentries', $CFG->glossary_dupentries); $mform->addHelpButton('allowduplicatedentries', 'allowduplicatedentries', 'glossary'); $mform->addElement('selectyesno', 'allowcomments', get_string('allowcomments', 'glossary')); $mform->setDefault('allowcomments', $CFG->glossary_allowcomments); $mform->addHelpButton('allowcomments', 'allowcomments', 'glossary'); $mform->addElement('selectyesno', 'usedynalink', get_string('usedynalink', 'glossary')); $mform->setDefault('usedynalink', $CFG->glossary_linkbydefault); $mform->addHelpButton('usedynalink', 'usedynalink', 'glossary'); // ---------------------------------------------------------------------- $mform->addElement('header', 'appearancehdr', get_string('appearance')); // Get and update available formats. $recformats = glossary_get_available_formats(); $formats = array(); foreach ($recformats as $format) { $formats[$format->name] = get_string('displayformat'.$format->name, 'glossary'); } asort($formats); $mform->addElement('select', 'displayformat', get_string('displayformat', 'glossary'), $formats); $mform->setDefault('displayformat', 'dictionary'); $mform->addHelpButton('displayformat', 'displayformat', 'glossary'); $displayformats['default'] = get_string('displayformatdefault', 'glossary'); $displayformats = array_merge($displayformats, $formats); $mform->addElement('select', 'approvaldisplayformat', get_string('approvaldisplayformat', 'glossary'), $displayformats); $mform->setDefault('approvaldisplayformat', 'default'); $mform->addHelpButton('approvaldisplayformat', 'approvaldisplayformat', 'glossary'); $mform->addElement('text', 'entbypage', get_string('entbypage', 'glossary')); $mform->setDefault('entbypage', $this->get_default_entbypage()); $mform->addRule('entbypage', null, 'numeric', null, 'client'); $mform->setType('entbypage', PARAM_INT); $mform->addElement('selectyesno', 'showalphabet', get_string('showalphabet', 'glossary')); $mform->setDefault('showalphabet', 1); $mform->addHelpButton('showalphabet', 'showalphabet', 'glossary'); $mform->addElement('selectyesno', 'showall', get_string('showall', 'glossary')); $mform->setDefault('showall', 1); $mform->addHelpButton('showall', 'showall', 'glossary'); $mform->addElement('selectyesno', 'showspecial', get_string('showspecial', 'glossary')); $mform->setDefault('showspecial', 1); $mform->addHelpButton('showspecial', 'showspecial', 'glossary'); $mform->addElement('selectyesno', 'allowprintview', get_string('allowprintview', 'glossary')); $mform->setDefault('allowprintview', 1); $mform->addHelpButton('allowprintview', 'allowprintview', 'glossary'); if ($CFG->enablerssfeeds && isset($CFG->glossary_enablerssfeeds) && $CFG->glossary_enablerssfeeds) { //------------------------------------------------------------------------------- $mform->addElement('header', 'rssheader', get_string('rss')); $choices = array(); $choices[0] = get_string('none'); $choices[1] = get_string('withauthor', 'glossary'); $choices[2] = get_string('withoutauthor', 'glossary'); $mform->addElement('select', 'rsstype', get_string('rsstype', 'glossary'), $choices); $mform->addHelpButton('rsstype', 'rsstype', 'glossary'); $choices = array(); $choices[0] = '0'; $choices[1] = '1'; $choices[2] = '2'; $choices[3] = '3'; $choices[4] = '4'; $choices[5] = '5'; $choices[10] = '10'; $choices[15] = '15'; $choices[20] = '20'; $choices[25] = '25'; $choices[30] = '30'; $choices[40] = '40'; $choices[50] = '50'; $mform->addElement('select', 'rssarticles', get_string('rssarticles'), $choices); $mform->addHelpButton('rssarticles', 'rssarticles', 'glossary'); $mform->hideIf('rssarticles', 'rsstype', 'eq', 0); } //------------------------------------------------------------------------------- $this->standard_grading_coursemodule_elements(); $this->standard_coursemodule_elements(); //------------------------------------------------------------------------------- // buttons $this->add_action_buttons(); } function definition_after_data() { global $COURSE, $DB; parent::definition_after_data(); $mform =& $this->_form; $mainglossaryel =& $mform->getElement('mainglossary'); $mainglossary = $DB->get_record('glossary', array('mainglossary'=>1, 'course'=>$COURSE->id)); if ($mainglossary && ($mainglossary->id != $mform->getElementValue('instance'))){ //secondary glossary, a main one already exists in this course. $mainglossaryel->setValue(0); $mainglossaryel->freeze(); $mainglossaryel->setPersistantFreeze(true); } else { $mainglossaryel->unfreeze(); $mainglossaryel->setPersistantFreeze(false); } } public function data_preprocessing(&$defaultvalues) { parent::data_preprocessing($defaultvalues); // Fallsback on the default setting if 'Entries shown per page' has been left blank. // This prevents the field from being required and expand its section which should not // be the case if there is a default value defined. if (empty($defaultvalues['entbypage']) || $defaultvalues['entbypage'] < 0) { $defaultvalues['entbypage'] = $this->get_default_entbypage(); } $suffix = $this->get_suffix(); $completionentriesel = 'completionentries' . $suffix; $completionentriesenabledel = 'completionentriesenabled' . $suffix; // Set up the completion checkboxes which aren't part of standard data. // Tick by default if Add mode or if completion entries settings is set to 1 or more. if (empty($this->_instance) || !empty($defaultvalues[$completionentriesel])) { $defaultvalues[$completionentriesenabledel] = 1; } else { $defaultvalues[$completionentriesenabledel] = 0; } if (empty($defaultvalues[$completionentriesel])) { $defaultvalues[$completionentriesel] = 1; } } public function add_completion_rules() { $mform = $this->_form; $suffix = $this->get_suffix(); $group = []; $completionentriesenabledel = 'completionentriesenabled' . $suffix; $group[] =& $mform->createElement( 'checkbox', $completionentriesenabledel, '', get_string('completionentries', 'glossary') ); $completionentriesel = 'completionentries' . $suffix; $group[] =& $mform->createElement('text', $completionentriesel, '', ['size' => 3]); $mform->setType($completionentriesel, PARAM_INT); $completionentriesgroupel = 'completionentriesgroup' . $suffix; $mform->addGroup($group, $completionentriesgroupel, '', ' ', false); $mform->hideIf($completionentriesel, $completionentriesenabledel, 'notchecked'); return [$completionentriesgroupel]; } public function completion_rule_enabled($data) { $suffix = $this->get_suffix(); return (!empty($data['completionentriesenabled' . $suffix]) && $data['completionentries' . $suffix] != 0); } /** * Allows module to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data the form data to be modified. */ public function data_postprocessing($data) { parent::data_postprocessing($data); if (!empty($data->completionunlocked)) { // Turn off completion settings if the checkboxes aren't ticked. $suffix = $this->get_suffix(); $completion = $data->{'completion' . $suffix}; $autocompletion = !empty($completion) && $completion == COMPLETION_TRACKING_AUTOMATIC; if (empty($data->{'completionentriesenabled' . $suffix}) || !$autocompletion) { $data->{'completionentries' . $suffix} = 0; } } } /** * Returns the default value for 'Entries shown per page'. * * @return int default for number of entries per page. */ protected function get_default_entbypage() { global $CFG; return !empty($CFG->glossary_entbypage) ? $CFG->glossary_entbypage : 10; } } db/install.php000064400000002014152266573170007323 0ustar00. /** * Glossary filter post install hook * * @package filter * @subpackage glossary * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); function xmldb_filter_glossary_install() { global $DB; } db/renamedclasses.php000064400000001773152266573170010661 0ustar00. /** * Renamed classes for the filter_glossary plugin. * * @package filter_glossary * @copyright Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $renamedclasses = [ 'filter_glossary' => \filter_glossary\text_filter::class, ]; lang/en/filter_glossary.php000064400000002414152266573170012027 0ustar00. /** * Strings for filter_glossary * * @package filter * @subpackage glossary * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['glossarycategory'] = '{$a->glossary}: Category {$a->category}'; $string['glossaryconcept'] = '{$a->glossary}: {$a->concept}'; $string['filtername'] = 'Glossary auto-linking'; $string['privacy:metadata'] = 'The Glossary auto-linking plugin does not store any personal data.'; $string['attachments'] = 'Attachments'; filter.php000064400000002326152266573170006563 0ustar00. /** * File only retained to prevent fatal errors in code that tries to require/include this. * * @todo MDL-82708 delete this file as part of Moodle 6.0 development. * @deprecated This file is no longer required in Moodle 4.5+. * @package filter_glossary * @copyright Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); debugging('This file is no longer required in Moodle 4.5+. Please do not include/require it.', DEBUG_DEVELOPER); classes/text_filter.php000064400000021633152266573170011266 0ustar00. namespace filter_glossary; use cache; use cache_store; use core\output\html_writer; use core\url; use core_filters\filter_object; use stdClass; // phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase -- GLOSSARY_EXCLUDEENTRY // phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameUnderscore -- GLOSSARY_EXCLUDEENTRY /** * This filter provides automatic linking to glossary entries, aliases and categories when found inside every Moodle text. * * @package filter_glossary * @copyright 2004 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class text_filter extends \core_filters\text_filter { /** @var null|cache_store cache used to store the terms for this course. */ protected $cache = null; #[\Override] public function setup($page, $context) { if ($page->requires->should_create_one_time_item_now('filter_glossary_autolinker')) { $page->requires->js_call_amd('filter_glossary/autolinker', 'init', []); } } /** * Get all the concepts for this context. * @return filter_object[] the concepts, and filterobjects. */ protected function get_all_concepts() { global $USER; if ($this->cache === null) { $this->cache = cache::make_from_params(cache_store::MODE_REQUEST, 'filter', 'glossary'); } // Try to get current course. $coursectx = $this->context->get_course_context(false); if (!$coursectx) { // Only global glossaries will be linked. $courseid = 0; } else { $courseid = $coursectx->instanceid; } $cached = $this->cache->get('concepts'); if ($cached !== false && ($cached->cachecourseid != $courseid || $cached->cacheuserid != $USER->id)) { // Invalidate the page cache. $cached = false; } if ($cached !== false && is_array($cached->cacheconceptlist)) { return $cached->cacheconceptlist; } [$glossaries, $allconcepts] = \mod_glossary\local\concept_cache::get_concepts($courseid); if (!$allconcepts) { $tocache = new stdClass(); $tocache->cacheuserid = $USER->id; $tocache->cachecourseid = $courseid; $tocache->cacheconceptlist = []; $this->cache->set('concepts', $tocache); return []; } $conceptlist = []; foreach ($allconcepts as $concepts) { foreach ($concepts as $concept) { $conceptlist[] = new filter_object( $this->glossary_format_string($concept->concept), null, null, $concept->casesensitive, $concept->fullmatch, null, [$this, 'filterobject_prepare_replacement_callback'], [$concept, $glossaries] ); } } // We sort longest first, so that when we replace the terms, // the longest ones are replaced first. This does the right thing // when you have two terms like 'Moodle' and 'Moodle 3.5'. You want the longest match. usort($conceptlist, [$this, 'sort_entries_by_length']); $conceptlist = filter_prepare_phrases_for_filtering($conceptlist); $tocache = new stdClass(); $tocache->cacheuserid = $USER->id; $tocache->cachecourseid = $courseid; $tocache->cacheconceptlist = $conceptlist; $this->cache->set('concepts', $tocache); return $conceptlist; } /** * Callback used by filterobject / filter_phrases. * * @param object $concept the concept that is being replaced (from get_all_concepts). * @param array $glossaries the list of glossary titles (from get_all_concepts). * @return array [$hreftagbegin, $hreftagend, $replacementphrase] for filterobject. */ public function filterobject_prepare_replacement_callback($concept, $glossaries) { global $CFG; if ($concept->category) { // Link to a category. $title = get_string( 'glossarycategory', 'filter_glossary', ['glossary' => $glossaries[$concept->glossaryid], 'category' => $concept->concept] ); $link = new url( '/mod/glossary/view.php', ['g' => $concept->glossaryid, 'mode' => 'cat', 'hook' => $concept->id] ); $attributes = [ 'href' => $link, 'title' => $title, 'class' => 'glossary autolink category glossaryid' . $concept->glossaryid, ]; } else { // Link to entry or alias. $title = get_string( 'glossaryconcept', 'filter_glossary', [ 'glossary' => replace_ampersands_not_followed_by_entity( strip_tags($this->glossary_format_string($glossaries[$concept->glossaryid])) ), 'concept' => $this->glossary_format_string($concept->concept), ] ); // Hardcoding dictionary format in the URL rather than defaulting // to the current glossary format which may not work in a popup. // for example "entry list" means the popup would only contain // a link that opens another popup. $link = new url( '/mod/glossary/showentry.php', ['eid' => $concept->id, 'displayformat' => 'dictionary'] ); $attributes = [ 'href' => $link, 'title' => str_replace('&', '&', $title), // Undo the s() mangling. 'class' => 'glossary autolink concept glossaryid' . $concept->glossaryid, 'data-entryid' => $concept->id, ]; } // This flag is optionally set by resource_pluginfile() // if processing an embedded file use target to prevent getting nested Moodles. if (!empty($CFG->embeddedsoforcelinktarget)) { $attributes['target'] = '_top'; } return [html_writer::start_tag('a', $attributes), '', null]; } #[\Override] public function filter($text, array $options = []) { global $GLOSSARY_EXCLUDEENTRY; $conceptlist = $this->get_all_concepts(); if (empty($conceptlist)) { return $text; } if (!empty($GLOSSARY_EXCLUDEENTRY)) { foreach ($conceptlist as $key => $filterobj) { // The original concept object was stored here in when $filterobj was constructed in // get_all_concepts(). Get it back out now so we can check to see if it is excluded. $concept = $filterobj->replacementcallbackdata[0]; if (!$concept->category && $concept->id == $GLOSSARY_EXCLUDEENTRY) { unset($conceptlist[$key]); } } } if (empty($conceptlist)) { return $text; } return filter_phrases($text, $conceptlist, null, null, false, true); } /** * usort helper used in get_all_concepts above. * @param filter_object $filterobject0 first item to compare. * @param filter_object $filterobject1 second item to compare. * @return int -1, 0 or 1. */ private function sort_entries_by_length($filterobject0, $filterobject1) { return strlen($filterobject1->phrase) <=> strlen($filterobject0->phrase); } /** * Format text while temporarily disabling the glossary filter to prevent recursion. * * @param string $text The text to format. * @return string The formatted string. */ private function glossary_format_string(string $text): string { $filtermanager = \filter_manager::instance(); try { // Basically runs format_text, but without the glossary filter to prevent recursion. $filtered = $filtermanager->filter_text($text, $this->context, [], ['glossary']); } catch (\Exception $e) { // Fallback in case of error. $filtered = $text; } return $filtered; } } templates/linked_glossary_entry.mustache000064400000004402152266573170014725 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template filter_glossary/linked_glossary_entry Renders the linked glossary entry. Context variables required for this template: * definition - The definition of the linked glossary entry. * taglistdata - The data for the tag list related to the linked glossary entry. * hasattachments - Whether the linked glossary entry has attachments. * attachments - The data for the attachments related to the linked glossary entry. Example context (json): { "definition": 1, "taglistdata": { "tags": [ { "id": 1, "name": "Cats", "viewurl": "http://moodle.org/tag/index.php?tag=Cats", "isstandard": true, "flag": 0 } ], "label": "Tags", "tagscount": 1, "overflow": false }, "hasattachments": true, "attachments": [ { "filename": "example.jpg", "fileurl": "www.example.com/example.jpg", "icon": "f/image" } ] } }}

{{{definition}}}

{{#taglistdata}}
{{> core_tag/taglist }}
{{/taglistdata}} {{#hasattachments}} {{#str}} attachments, filter_glossary {{/str}}:
    {{#attachments}}
  • {{#pix}} {{icon}}, core {{/pix}} {{filename}}
  • {{/attachments}}
{{/hasattachments}} amd/src/autolinker.js000064400000011116152266573170010625 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Module for auto-linking glossary entries. * * @module filter_glossary/autolinker * @copyright 2023 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; import ModalCancel from "core/modal_cancel"; import Templates from 'core/templates'; import {getString} from 'core/str'; /** @constant {Object} The object containing the relevant selectors. */ const Selectors = { glossaryEntryAutoLink: 'a.glossary.autolink.concept', }; /** * Register the event listeners for the glossary entry auto-linker. * * @return {void} */ const registerEventListeners = () => { document.addEventListener('click', async(e) => { const glossaryEntryAutoLink = e.target.closest(Selectors.glossaryEntryAutoLink); if (glossaryEntryAutoLink) { e.preventDefault(); const entryId = glossaryEntryAutoLink.dataset.entryid; await showGlossaryEntry(entryId); await glossaryEntryViewed(entryId); } }); }; /** * Show the linked glossary entry in a modal. * * @method showGlossaryEntry * @param {int} entryId The id of the linked glossary entry. * @returns {Promise} The modal promise. */ const showGlossaryEntry = async(entryId) => { const entryData = await fetchGlossaryEntry(entryId); // Obtain the HTML and JS used for rendering the auto-linked glossary entry. const {html, js} = await Templates.renderForPromise('filter_glossary/linked_glossary_entry', { definition: entryData.entry.definition, taglistdata: await generateTagListData(entryData.entry.tags), hasattachments: Boolean(entryData.entry.attachment), attachments: entryData.entry.attachments }); // Create the modal. const modal = await ModalCancel.create({ title: entryData.entry.concept, body: html, isVerticallyCentered: true, buttons: { cancel: await getString('ok') } }); // Execute the JS code returned from the template once the modal is created. Templates.runTemplateJS(js); // Display the modal. modal.show(); return modal; }; /** * Fetch the linked glossary entry. * * @method fetchGlossaryEntry * @param {int} entryId The id of the linked glossary entry. * @returns {Promise} The glossary entry promise. */ const fetchGlossaryEntry = (entryId) => { const request = { methodname: 'mod_glossary_get_entry_by_id', args: { id: entryId, }, }; return Ajax.call([request])[0]; }; /** * Notify that the linked glossary entry was viewed. * * @method glossaryEntryViewed * @param {int} entryId The id of the linked glossary entry. * @returns {Promise} The promise object. */ const glossaryEntryViewed = (entryId) => { const request = { methodname: 'mod_glossary_view_entry', args: { id: entryId, }, }; return Ajax.call([request])[0]; }; /** * Generates an object that contains the data required to render a tag list. * * @method generateTagListData * @param {array} tags The array containing the tags related to the linked glossary entry. * @returns {Object} The data required to render a tag list. */ const generateTagListData = async(tags) => { // Define the number of initially displayed tags. const limit = 10; const hasOverflow = tags.length > limit; // If the total number of tags exceeds the defined limit, then we need to mark all the excess tags as over the limit. // By specifying this, these tags will be initially hidden. if (hasOverflow) { for (let i = limit; i < tags.length; i++) { tags[i].overlimit = true; } } return { tags: tags, tagscount: tags.length, overflow: hasOverflow, label: await getString('tags') }; }; /** * Initialize the module. */ export const init = () => { registerEventListeners(); }; amd/build/autolinker.min.js000064400000004276152266573170011730 0ustar00define("filter_glossary/autolinker",["exports","core/ajax","core/modal_cancel","core/templates","core/str"],(function(_exports,_ajax,_modal_cancel,_templates,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Module for auto-linking glossary entries. * * @module filter_glossary/autolinker * @copyright 2023 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_modal_cancel=_interopRequireDefault(_modal_cancel),_templates=_interopRequireDefault(_templates);const Selectors_glossaryEntryAutoLink="a.glossary.autolink.concept",showGlossaryEntry=async entryId=>{const entryData=await fetchGlossaryEntry(entryId),{html:html,js:js}=await _templates.default.renderForPromise("filter_glossary/linked_glossary_entry",{definition:entryData.entry.definition,taglistdata:await generateTagListData(entryData.entry.tags),hasattachments:Boolean(entryData.entry.attachment),attachments:entryData.entry.attachments}),modal=await _modal_cancel.default.create({title:entryData.entry.concept,body:html,isVerticallyCentered:!0,buttons:{cancel:await(0,_str.getString)("ok")}});return _templates.default.runTemplateJS(js),modal.show(),modal},fetchGlossaryEntry=entryId=>{const request={methodname:"mod_glossary_get_entry_by_id",args:{id:entryId}};return _ajax.default.call([request])[0]},glossaryEntryViewed=entryId=>{const request={methodname:"mod_glossary_view_entry",args:{id:entryId}};return _ajax.default.call([request])[0]},generateTagListData=async tags=>{const hasOverflow=tags.length>10;if(hasOverflow)for(let i=10;i{document.addEventListener("click",(async e=>{const glossaryEntryAutoLink=e.target.closest(Selectors_glossaryEntryAutoLink);if(glossaryEntryAutoLink){e.preventDefault();const entryId=glossaryEntryAutoLink.dataset.entryid;await showGlossaryEntry(entryId),await glossaryEntryViewed(entryId)}}))}})); //# sourceMappingURL=autolinker.min.js.mapamd/build/autolinker.min.js.map000064400000014554152266573170012504 0ustar00{"version":3,"file":"autolinker.min.js","sources":["../src/autolinker.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module for auto-linking glossary entries.\n *\n * @module filter_glossary/autolinker\n * @copyright 2023 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport ModalCancel from \"core/modal_cancel\";\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\n\n/** @constant {Object} The object containing the relevant selectors. */\nconst Selectors = {\n glossaryEntryAutoLink: 'a.glossary.autolink.concept',\n};\n\n/**\n * Register the event listeners for the glossary entry auto-linker.\n *\n * @return {void}\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', async(e) => {\n const glossaryEntryAutoLink = e.target.closest(Selectors.glossaryEntryAutoLink);\n if (glossaryEntryAutoLink) {\n e.preventDefault();\n const entryId = glossaryEntryAutoLink.dataset.entryid;\n await showGlossaryEntry(entryId);\n await glossaryEntryViewed(entryId);\n }\n });\n};\n\n/**\n * Show the linked glossary entry in a modal.\n *\n * @method showGlossaryEntry\n * @param {int} entryId The id of the linked glossary entry.\n * @returns {Promise} The modal promise.\n */\nconst showGlossaryEntry = async(entryId) => {\n const entryData = await fetchGlossaryEntry(entryId);\n // Obtain the HTML and JS used for rendering the auto-linked glossary entry.\n const {html, js} = await Templates.renderForPromise('filter_glossary/linked_glossary_entry', {\n definition: entryData.entry.definition,\n taglistdata: await generateTagListData(entryData.entry.tags),\n hasattachments: Boolean(entryData.entry.attachment),\n attachments: entryData.entry.attachments\n });\n // Create the modal.\n const modal = await ModalCancel.create({\n title: entryData.entry.concept,\n body: html,\n isVerticallyCentered: true,\n buttons: {\n cancel: await getString('ok')\n }\n });\n // Execute the JS code returned from the template once the modal is created.\n Templates.runTemplateJS(js);\n // Display the modal.\n modal.show();\n\n return modal;\n};\n\n/**\n * Fetch the linked glossary entry.\n *\n * @method fetchGlossaryEntry\n * @param {int} entryId The id of the linked glossary entry.\n * @returns {Promise} The glossary entry promise.\n */\nconst fetchGlossaryEntry = (entryId) => {\n const request = {\n methodname: 'mod_glossary_get_entry_by_id',\n args: {\n id: entryId,\n },\n };\n return Ajax.call([request])[0];\n};\n\n/**\n * Notify that the linked glossary entry was viewed.\n *\n * @method glossaryEntryViewed\n * @param {int} entryId The id of the linked glossary entry.\n * @returns {Promise} The promise object.\n */\nconst glossaryEntryViewed = (entryId) => {\n const request = {\n methodname: 'mod_glossary_view_entry',\n args: {\n id: entryId,\n },\n };\n return Ajax.call([request])[0];\n};\n\n/**\n * Generates an object that contains the data required to render a tag list.\n *\n * @method generateTagListData\n * @param {array} tags The array containing the tags related to the linked glossary entry.\n * @returns {Object} The data required to render a tag list.\n */\nconst generateTagListData = async(tags) => {\n // Define the number of initially displayed tags.\n const limit = 10;\n const hasOverflow = tags.length > limit;\n // If the total number of tags exceeds the defined limit, then we need to mark all the excess tags as over the limit.\n // By specifying this, these tags will be initially hidden.\n if (hasOverflow) {\n for (let i = limit; i < tags.length; i++) {\n tags[i].overlimit = true;\n }\n }\n\n return {\n tags: tags,\n tagscount: tags.length,\n overflow: hasOverflow,\n label: await getString('tags')\n };\n};\n\n/**\n * Initialize the module.\n */\nexport const init = () => {\n registerEventListeners();\n};\n"],"names":["Selectors","showGlossaryEntry","async","entryData","fetchGlossaryEntry","entryId","html","js","Templates","renderForPromise","definition","entry","taglistdata","generateTagListData","tags","hasattachments","Boolean","attachment","attachments","modal","ModalCancel","create","title","concept","body","isVerticallyCentered","buttons","cancel","runTemplateJS","show","request","methodname","args","id","Ajax","call","glossaryEntryViewed","hasOverflow","length","i","overlimit","tagscount","overflow","label","document","addEventListener","glossaryEntryAutoLink","e","target","closest","preventDefault","dataset","entryid"],"mappings":";;;;;;;8NA6BMA,gCACqB,8BA2BrBC,kBAAoBC,MAAAA,gBAChBC,gBAAkBC,mBAAmBC,UAErCC,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,wCAAyC,CACzFC,WAAYP,UAAUQ,MAAMD,WAC5BE,kBAAmBC,oBAAoBV,UAAUQ,MAAMG,MACvDC,eAAgBC,QAAQb,UAAUQ,MAAMM,YACxCC,YAAaf,UAAUQ,MAAMO,cAG3BC,YAAcC,sBAAYC,OAAO,CACnCC,MAAOnB,UAAUQ,MAAMY,QACvBC,KAAMlB,KACNmB,sBAAsB,EACtBC,QAAS,CACLC,aAAc,kBAAU,mCAItBC,cAAcrB,IAExBY,MAAMU,OAECV,OAULf,mBAAsBC,gBAClByB,QAAU,CACZC,WAAY,+BACZC,KAAM,CACFC,GAAI5B,iBAGL6B,cAAKC,KAAK,CAACL,UAAU,IAU1BM,oBAAuB/B,gBACnByB,QAAU,CACZC,WAAY,0BACZC,KAAM,CACFC,GAAI5B,iBAGL6B,cAAKC,KAAK,CAACL,UAAU,IAU1BjB,oBAAsBX,MAAAA,aAGlBmC,YAAcvB,KAAKwB,OADX,MAIVD,gBACK,IAAIE,EALC,GAKUA,EAAIzB,KAAKwB,OAAQC,IACjCzB,KAAKyB,GAAGC,WAAY,QAIrB,CACH1B,KAAMA,KACN2B,UAAW3B,KAAKwB,OAChBI,SAAUL,YACVM,YAAa,kBAAU,wBAOX,KA5GhBC,SAASC,iBAAiB,SAAS3C,MAAAA,UACzB4C,sBAAwBC,EAAEC,OAAOC,QAAQjD,oCAC3C8C,sBAAuB,CACvBC,EAAEG,uBACI7C,QAAUyC,sBAAsBK,QAAQC,cACxCnD,kBAAkBI,eAClB+B,oBAAoB/B"}tests/text_filter_test.php000064400000032365152266573170012036 0ustar00. namespace filter_glossary; /** * Unit tests. * * @package filter_glossary * @category test * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \filter_glossary\text_filter */ final class text_filter_test extends \advanced_testcase { public function test_link_to_entry_with_alias(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $normal = $generator->create_content( $glossary, ['concept' => 'entry name'], ['first alias', 'second alias'] ); // Format text with all three entries in HTML. $html = '

First we have entry name, then we have it twp aliases first alias and second alias.

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. $this->assertEquals(3, count($matches[1])); $this->assertEquals($normal->id, $matches[1][0]); $this->assertEquals($normal->id, $matches[1][1]); $this->assertEquals($normal->id, $matches[1][2]); // Check text of title attribute. $this->assertEquals($glossary->name . ': entry name', $matches[2][0]); $this->assertEquals($glossary->name . ': first alias', $matches[2][1]); $this->assertEquals($glossary->name . ': second alias', $matches[2][2]); } public function test_longest_link_used(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $shorter = $generator->create_content($glossary, ['concept' => 'Tim']); $longer = $generator->create_content($glossary, ['concept' => 'Time']); // Format text with all three entries in HTML. $html = '

Time will tell

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link to Time, not Tim. $this->assertEquals(1, count($matches[1])); $this->assertEquals($longer->id, $matches[1][0]); // Check text of title attribute. $this->assertEquals($glossary->name . ': Time', $matches[2][0]); } public function test_link_to_category(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $category = $generator->create_category($glossary, ['name' => 'My category', 'usedynalink' => 1]); // Format text with all three entries in HTML. $html = '

This is My category you know.

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~hook=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link. $this->assertEquals(1, count($matches[1])); $this->assertEquals($category->id, $matches[1][0]); $this->assertEquals($glossary->name . ': Category My category', $matches[2][0]); } /** * Test ampersands. */ public function test_ampersands(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $normal = $generator->create_content($glossary, ['concept' => 'normal']); $amp1 = $generator->create_content($glossary, ['concept' => 'A&B']); $amp2 = $generator->create_content($glossary, ['concept' => 'C&D']); // Format text with all three entries in HTML. $html = '

A&B C&D normal

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. $this->assertEquals(3, count($matches[1])); $this->assertEquals($amp1->id, $matches[1][0]); $this->assertEquals($amp2->id, $matches[1][1]); $this->assertEquals($normal->id, $matches[1][2]); // Check text and escaping of title attribute. $this->assertEquals($glossary->name . ': A&B', $matches[2][0]); $this->assertEquals($glossary->name . ': C&D', $matches[2][1]); $this->assertEquals($glossary->name . ': normal', $matches[2][2]); } /** * Test brackets. */ public function test_brackets(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $simple = $generator->create_content($glossary, ['concept' => 'simple']); $withbrackets = $generator->create_content($glossary, ['concept' => 'more complex (perhaps)']); $test2 = $generator->create_content($glossary, ['concept' => 'Test (2)']); // Format text with all three entries in HTML. $html = '

Some thigns are simple. Others are more complex (perhaps). Test (2).

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. $this->assertEquals(3, count($matches[1])); $this->assertEquals($simple->id, $matches[1][0]); $this->assertEquals($withbrackets->id, $matches[1][1]); $this->assertEquals($test2->id, $matches[1][2]); // Check text and escaping of title attribute. $this->assertEquals($glossary->name . ': simple', $matches[2][0]); $this->assertEquals($glossary->name . ': more complex (perhaps)', $matches[2][1]); $this->assertEquals($glossary->name . ': Test (2)', $matches[2][2]); } public function test_exclude_excludes_link_to_entry_with_alias(): void { global $CFG, $GLOSSARY_EXCLUDEENTRY; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $tobeexcluded = $generator->create_content( $glossary, ['concept' => 'entry name'], ['first alias', 'second alias'] ); $normal = $generator->create_content($glossary, ['concept' => 'other entry']); // Format text with all three entries in HTML. $html = '

First we have entry name, then we have it twp aliases first alias and second alias. ' . 'In this case, those should not be linked, but this other entry should be.

'; $GLOSSARY_EXCLUDEENTRY = $tobeexcluded->id; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); $GLOSSARY_EXCLUDEENTRY = null; // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary links. $this->assertEquals(1, count($matches[1])); $this->assertEquals($normal->id, $matches[1][0]); $this->assertEquals($glossary->name . ': other entry', $matches[2][0]); } public function test_exclude_does_not_exclude_categories(): void { global $CFG, $GLOSSARY_EXCLUDEENTRY; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $category = $generator->create_category($glossary, ['name' => 'My category', 'usedynalink' => 1]); // Format text with all three entries in HTML. $html = '

This is My category you know.

'; $GLOSSARY_EXCLUDEENTRY = $category->id; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); $GLOSSARY_EXCLUDEENTRY = null; // Find all the glossary links in the result. $matches = []; preg_match_all('~hook=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link. $this->assertEquals(1, count($matches[1])); $this->assertEquals($category->id, $matches[1][0]); $this->assertEquals($glossary->name . ': Category My category', $matches[2][0]); } }