MDL-44366 rework glossary filter caching

This commit is contained in:
Petr Škoda
2014-04-14 09:42:39 +08:00
parent 069fe267b6
commit 37625e2abb
14 changed files with 690 additions and 168 deletions
+72 -150
View File
@@ -17,7 +17,7 @@
/**
* This filter provides automatic linking to
* glossary entries, aliases and categories when
* found inside every Moodle text
* found inside every Moodle text.
*
* @package filter
* @subpackage glossary
@@ -28,12 +28,17 @@
defined('MOODLE_INTERNAL') || die();
/**
* Glossary filtering
* Glossary linking filter class.
*
* TODO: erase the $GLOSSARY_EXCLUDECONCEPTS global => require format_text()
* to be able to pass arbitrary $options['filteroptions']['glossary'] to filter_text()
* NOTE: multilang glossary entries are not compatible with this filter.
*/
class filter_glossary extends moodle_text_filter {
/** @var int $cachecourseid cache invalidation flag in case content from multiple courses displayed. */
protected $cachecourseid = null;
/** @var int $cacheuserid cache invalidation flag in case user is switched. */
protected $cacheuserid = null;
/** @var array $cacheconceptlist page level filter cache, this should be always faster than MUC */
protected $cacheconceptlist = null;
public function setup($page, $context) {
// This only requires execution once per request.
@@ -49,185 +54,102 @@ class filter_glossary extends moodle_text_filter {
}
public function filter($text, array $options = array()) {
global $CFG, $DB, $GLOSSARY_EXCLUDECONCEPTS;
// Trivial-cache - keyed on $cachedcontextid
static $cachedcontextid;
static $conceptlist;
static $nothingtodo; // To avoid processing if no glossaries / concepts are found
global $CFG, $USER, $GLOSSARY_EXCLUDEENTRY;
// Try to get current course.
$coursectx = $this->context->get_course_context(false);
if (!$coursectx) {
// Only global glossaries will be linked.
$course = null;
$courseid = 0;
} else {
$courseid = $coursectx->instanceid;
$course = get_course($coursectx->instanceid, false);
$courseid = (int)$course->id;
}
// Initialise/invalidate our trivial cache if dealing with a different context
if (!isset($cachedcontextid) || $cachedcontextid !== $this->context->id) {
$cachedcontextid = $this->context->id;
$conceptlist = array();
$nothingtodo = false;
if ($this->cachecourseid !== $courseid or $this->cacheuserid !== $USER->id) {
// Invalidate the page cache.
$this->cacheconceptlist = null;
}
if (($nothingtodo === true) || (!has_capability('mod/glossary:view', $this->context))) {
if (is_array($this->cacheconceptlist) and empty($GLOSSARY_EXCLUDEENTRY)) {
if (empty($this->cacheconceptlist)) {
return $text;
}
return filter_phrases($text, $this->cacheconceptlist);
}
list($glossaries, $allconcepts) = \mod_glossary\local\concept_cache::get_concepts($course);
if (!$allconcepts) {
$this->cacheuserid = $USER->id;
$this->cachecourseid = $courseid;
$this->cacheconcepts = array();
return $text;
}
// Create a list of all the concepts to search for. It may be cached already.
if (empty($conceptlist)) {
$strcategory = get_string('category', 'glossary');
// Find all the glossaries we need to examine
if (!$glossaries = $DB->get_records_sql_menu('
SELECT g.id, g.name
FROM {glossary} g, {course_modules} cm, {modules} m
WHERE m.name = \'glossary\'
AND cm.module = m.id
AND cm.visible = 1
AND g.id = cm.instance
AND g.usedynalink != 0
AND (g.course = ? OR g.globalglossary = 1)
ORDER BY g.globalglossary, g.id', array($courseid))) {
$nothingtodo = true;
return $text;
}
// Make a list of glossary IDs for searching
$glossarylist = implode(',', array_keys($glossaries));
// Pull out all the raw data from the database for entries, categories and aliases
$entries = $DB->get_records_select('glossary_entries',
'glossaryid IN ('.$glossarylist.') AND usedynalink != 0 AND approved != 0 ', null, '',
'id,glossaryid, concept, casesensitive, 0 AS category, fullmatch');
$categories = $DB->get_records_select('glossary_categories',
'glossaryid IN ('.$glossarylist.') AND usedynalink != 0', null, '',
'id,glossaryid,name AS concept, 1 AS casesensitive, 1 AS category, 1 AS fullmatch');
$aliases = $DB->get_records_sql('
SELECT ga.id, ge.id AS entryid, ge.glossaryid,
ga.alias AS concept, ge.concept AS originalconcept,
casesensitive, 0 AS category, fullmatch
FROM {glossary_alias} ga,
{glossary_entries} ge
WHERE ga.entryid = ge.id
AND ge.glossaryid IN ('.$glossarylist.')
AND ge.usedynalink != 0
AND ge.approved != 0', null);
// Combine them into one big list
$concepts = array();
if ($entries and $categories) {
$concepts = array_merge($entries, $categories);
} else if ($categories) {
$concepts = $categories;
} else if ($entries) {
$concepts = $entries;
}
if ($aliases) {
$concepts = array_merge($concepts, $aliases);
}
if (!empty($concepts)) {
foreach ($concepts as $key => $concept) {
// Trim empty or unlinkable concepts
$currentconcept = trim(strip_tags($concept->concept));
// Concept must be HTML-escaped, so do the same as format_string
// to turn ampersands into &.
$currentconcept = replace_ampersands_not_followed_by_entity($currentconcept);
if (empty($currentconcept)) {
unset($concepts[$key]);
continue;
} else {
$concepts[$key]->concept = $currentconcept;
}
// Rule out any small integers. See bug 1446
$currentint = intval($currentconcept);
if ($currentint && (strval($currentint) == $currentconcept) && $currentint < 1000) {
unset($concepts[$key]);
}
}
}
if (empty($concepts)) {
$nothingtodo = true;
return $text;
}
usort($concepts, 'filter_glossary::sort_entries_by_length');
$strcategory = get_string('category', 'glossary');
// Loop through all the concepts, setting up our data structure for the filter
$conceptlist = array(); // We will store all the concepts here
$conceptlist = array();
$excluded = false;
foreach ($allconcepts as $concepts) {
foreach ($concepts as $concept) {
$glossaryname = str_replace(':', '-', $glossaries[$concept->glossaryid]);
if ($concept->category) { // Link to a category
// TODO: Fix this string usage
$title = strip_tags($glossaryname.': '.$strcategory.' '.$concept->concept);
$href_tag_begin = '<a class="glossary autolink category glossaryid'.$concept->glossaryid.'" title="'.$title.'" '.
'href="'.$CFG->wwwroot.'/mod/glossary/view.php?g='.$concept->glossaryid.
'&amp;mode=cat&amp;hook='.$concept->id.'">';
if (!empty($GLOSSARY_EXCLUDEENTRY) and $concept->id == $GLOSSARY_EXCLUDEENTRY) {
$excluded = true;
continue;
}
if ($concept->category) { // Link to a category.
// TODO: Fix this string usage.
$title = $glossaries[$concept->glossaryid] . ': ' . $strcategory . ' ' . $concept->concept;
$link = new moodle_url('/mod/glossary/view.php', array('g' => $concept->glossaryid, 'mode' => 'cat', 'hook' => $concept->id));
$attributes = array(
'href' => $link,
'title' => $title,
'class' => 'glossary autolink category glossaryid' . $concept->glossaryid);
} else { // Link to entry or alias
if (!empty($concept->originalconcept)) { // We are dealing with an alias (so show and point to original)
$title = str_replace('"', "'", html_entity_decode(
strip_tags($glossaryname.': '.$concept->originalconcept)));
$concept->id = $concept->entryid;
} else { // This is an entry
// We need to remove entities from the content here because it
// will be escaped by html_writer below.
$title = str_replace('"', "'", html_entity_decode(
strip_tags($glossaryname.': '.$concept->concept)));
}
// hardcoding dictionary format in the URL rather than defaulting
$title = $glossaries[$concept->glossaryid] . ': ' . $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 moodle_url('/mod/glossary/showentry.php', array('courseid'=>$courseid, 'eid'=>$concept->id, 'displayformat'=>'dictionary'));
$link = new moodle_url('/mod/glossary/showentry.php', array('eid' => $concept->id, 'displayformat' => 'dictionary'));
$attributes = array(
'href' => $link,
'title'=> $title,
'class'=> 'glossary autolink concept glossaryid'.$concept->glossaryid);
// this flag is optionally set by resource_pluginfile()
// if processing an embedded file use target to prevent getting nested Moodles
if (isset($CFG->embeddedsoforcelinktarget) && $CFG->embeddedsoforcelinktarget) {
$attributes['target'] = '_top';
}
$href_tag_begin = html_writer::start_tag('a', $attributes);
'href' => $link,
'title' => str_replace('&amp;', '&', $title), // Undo the s() mangling.
'class' => 'glossary autolink concept glossaryid' . $concept->glossaryid);
}
// 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';
}
$href_tag_begin = html_writer::start_tag('a', $attributes);
$conceptlist[] = new filterobject($concept->concept, $href_tag_begin, '</a>',
$concept->casesensitive, $concept->fullmatch);
}
$conceptlist = filter_remove_duplicates($conceptlist);
}
if (!empty($GLOSSARY_EXCLUDECONCEPTS)) {
$reducedconceptlist=array();
foreach($conceptlist as $concept) {
if(!in_array($concept->phrase,$GLOSSARY_EXCLUDECONCEPTS)) {
$reducedconceptlist[]=$concept;
}
}
return filter_phrases($text, $reducedconceptlist);
usort($conceptlist, 'filter_glossary::sort_entries_by_length');
if (!$excluded) {
// Do not cache the excluded list here, it is used once per page only.
$this->cacheuserid = $USER->id;
$this->cachecourseid = $courseid;
$this->cacheconceptlist = $conceptlist;
}
if (empty($conceptlist)) {
return $text;
}
return filter_phrases($text, $conceptlist); // Actually search for concepts!
}
private static function sort_entries_by_length($entry0, $entry1) {
$len0 = strlen($entry0->concept);
$len1 = strlen($entry1->concept);
$len0 = strlen($entry0->phrase);
$len1 = strlen($entry1->phrase);
if ($len0 < $len1) {
return 1;
+5 -1
View File
@@ -53,18 +53,22 @@ class filter_glossary_filter_testcase extends advanced_testcase {
array('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, array('concept' => 'normal'));
$amp1 = $generator->create_content($glossary, array('concept' => 'A&B'));
$amp2 = $generator->create_content($glossary, array('concept' => 'C&amp;D'));
filter_manager::reset_caches();
\mod_glossary\local\concept_cache::reset_caches();
// Format text with all three entries in HTML.
$html = '<p>A&amp;B C&amp;D normal</p>';
$filtered = format_text($html, FORMAT_HTML, array('context' => $context));
// Find all the glossary links in the result.
$matches = array();
preg_match_all('~courseid=' . $course->id . '&amp;eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches);
preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches);
// There should be 3 glossary links.
$this->assertEquals(3, count($matches[1]));
+5
View File
@@ -49,6 +49,11 @@ if (($newstate != $entry->approved) && confirm_sesskey()) {
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&amp;mode=$mode&amp;hook=$hook");
@@ -0,0 +1,279 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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(strip_tags($concept->concept));
// Concept must be HTML-escaped, so do the same as format_string to turn ampersands into &amp;.
$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 \stdClass $course
* @return array
*/
protected static function get_course_concepts($course) {
global $DB;
if (empty($course->id)) {
return array(array(), array());
}
$cache = \cache::make('mod_glossary', 'concepts');
$data = $cache->get((int)$course->id);
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' => $course->id));
if (!$glossaries) {
$data = array(array(), array());
$cache->set((int)$course->id, $data);
return $data;
}
foreach ($glossaries as $id => $name) {
$name = str_replace(':', '-', $name);
$glossaries[$id] = replace_ampersands_not_followed_by_entity(strip_tags($name));
}
$allconcepts = self::fetch_concepts(array_keys($glossaries));
foreach ($glossaries as $gid => $unused) {
if (!isset($allconcepts[$gid])) {
unset($glossaries[$gid]);
}
}
$cache->set((int)$course->id, array($glossaries, $allconcepts));
}
$concepts = $allconcepts;
// Verify access control to glossary instances.
$modinfo = get_fast_modinfo($course);
$cminfos = $modinfo->get_instances_of('glossary');
foreach ($concepts as $modid => $unused) {
if (!isset($cminfos[$modid])) {
// This should not happen.
unset($concepts[$modinfo]);
continue;
}
if (!$cminfos[$modid]->uservisible) {
unset($concepts[$modinfo]);
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(strip_tags($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 \stdClass $course
* @return array with two elements - array of glossaries and concepts for each glossary
*/
public static function get_concepts($course) {
list($glossaries, $concepts) = self::get_course_concepts($course);
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);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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,
),
);
+32
View File
@@ -0,0 +1,32 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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',
),
);
+6
View File
@@ -125,6 +125,12 @@ if ($confirm and confirm_sesskey()) { // the operation was confirmed.
));
$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);
}
redirect("view.php?id=$cm->id&amp;mode=$prevmode&amp;hook=$hook");
} else { // the operation has not been confirmed yet so ask the user to do so
+11
View File
@@ -186,6 +186,17 @@ if ($mform->is_cancelled()){
}
}
// 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);
}
redirect("view.php?id=$cm->id&mode=entry&hook=$entry->id");
}
+9
View File
@@ -111,6 +111,9 @@ if ( $hook >0 ) {
$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);
@@ -136,6 +139,9 @@ if ( $hook >0 ) {
$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();
@@ -201,6 +207,9 @@ if ( $hook >0 ) {
$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();
+4
View File
@@ -272,6 +272,10 @@ if ($xml = glossary_read_imported_file($result)) {
}
}
}
// Reset caches.
\mod_glossary\local\concept_cache::reset_glossary($glossary);
// processed entries
echo $OUTPUT->box_start('glossarydisplay generalbox');
echo '<table class="glossaryimportexport">';
+10 -12
View File
@@ -201,7 +201,12 @@ function glossary_delete_instance($id) {
glossary_grade_item_delete($glossary);
return $DB->delete_records('glossary', array('id'=>$id));
$DB->delete_records('glossary', array('id'=>$id));
// Reset caches.
\mod_glossary\local\concept_cache::reset_glossary($glossary);
return true;
}
/**
@@ -1145,19 +1150,12 @@ function glossary_print_entry_concept($entry, $return=false) {
* @param object $cm
*/
function glossary_print_entry_definition($entry, $glossary, $cm) {
global $DB, $GLOSSARY_EXCLUDECONCEPTS;
global $GLOSSARY_EXCLUDEENTRY;
$definition = $entry->definition;
//Calculate all the strings to be no-linked
//First, the concept
$GLOSSARY_EXCLUDECONCEPTS = array($entry->concept);
//Now the aliases
if ( $aliases = $DB->get_records('glossary_alias', array('entryid'=>$entry->id))) {
foreach ($aliases as $alias) {
$GLOSSARY_EXCLUDECONCEPTS[]=trim($alias->alias);
}
}
// 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);
@@ -1171,7 +1169,7 @@ function glossary_print_entry_definition($entry, $glossary, $cm) {
$text = format_text($definition, $entry->definitionformat, $options);
// Stop excluding concepts from autolinking
unset($GLOSSARY_EXCLUDECONCEPTS);
unset($GLOSSARY_EXCLUDEENTRY);
if (!empty($entry->highlight)) {
$text = highlight($entry->highlight, $text);
+162
View File
@@ -0,0 +1,162 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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
*/
/**
* 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
*/
class mod_glossary_concept_cache_testcase extends advanced_testcase {
/**
* Test convect fetching.
*/
public function test_concept_fetching() {
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);
$this->assertCount(3, $concepts1[0]);
$this->arrayHasKey($concepts1[0], $glossary1a->id);
$this->arrayHasKey($concepts1[0], $glossary1b->id);
$this->arrayHasKey($concepts1[0], $glossary3->id);
$this->assertCount(3, $concepts1[1]);
$this->arrayHasKey($concepts1[1], $glossary1a->id);
$this->arrayHasKey($concepts1[1], $glossary1b->id);
$this->arrayHasKey($concepts1[0], $glossary3->id);
$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&amp;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);
$this->assertCount(1, $concepts3[0]);
$this->arrayHasKey($concepts3[0], $glossary3->id);
$this->assertCount(1, $concepts3[1]);
$this->arrayHasKey($concepts3[0], $glossary3->id);
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);
$this->assertEquals($concepts3, $concepts2);
}
}
+40 -1
View File
@@ -40,6 +40,11 @@ class mod_glossary_generator extends testing_module_generator {
*/
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.
@@ -47,6 +52,7 @@ class mod_glossary_generator extends testing_module_generator {
*/
public function reset() {
$this->entrycount = 0;
$this->categorycount = 0;
parent::reset();
}
@@ -77,7 +83,30 @@ class mod_glossary_generator extends testing_module_generator {
return parent::create_instance($record, (array)$options);
}
public function create_content($glossary, $record = array()) {
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, $USER, $CFG;
$this->entrycount++;
$now = time();
@@ -106,6 +135,16 @@ class mod_glossary_generator extends testing_module_generator {
}
$id = $DB->insert_record('glossary_entries', $record);
if ($aliases) {
foreach ($aliases as $alias) {
$ar = new stdClass();
$ar->entryid = $id;
$ar->alias = $alias;
$DB->insert_record('glossary_alias', $ar);
}
}
return $DB->get_record('glossary_entries', array('id' => $id), '*', MUST_EXIST);
}
}
+20 -4
View File
@@ -43,13 +43,13 @@ class mod_glossary_generator_testcase extends advanced_testcase {
$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->assertEquals(1, count($records));
$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->assertEquals(2, count($records));
$this->assertCount(2, $records);
$this->assertEquals('Another glossary', $records[$glossary->id]->name);
}
@@ -60,14 +60,30 @@ class mod_glossary_generator_testcase extends advanced_testcase {
$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'));
$entry2 = $glossarygenerator->create_content($glossary, array('concept' => 'Custom concept'), array('alias1', 'alias2'));
$records = $DB->get_records('glossary_entries', array('glossaryid' => $glossary->id), 'id');
$this->assertEquals(2, count($records));
$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);
$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));
}
}