MDL-51119 glossary: Allow multilang concepts.

This commit is contained in:
Luca Bösch 2024-09-15 08:35:48 +02:00
parent ab5692acdf
commit 9e791db25a
14 changed files with 500 additions and 152 deletions

View File

@ -81,75 +81,92 @@ class block_glossary_random extends block_base {
$glossaryctx = context_module::instance($cm->id);
$limitfrom = 0;
$limitnum = 1;
$entries = $DB->get_records_sql('SELECT id, concept, definition, definitionformat, definitiontrust
FROM {glossary_entries}
WHERE glossaryid = ? AND approved = 1
ORDER BY timemodified ASC', [$this->config->glossary]);
$orderby = 'timemodified ASC';
if (empty($entries)) {
$text = get_string('noentriesyet', 'block_glossary_random');
} else {
// Now picking out the correct entry from the array.
switch ($this->config->type) {
case BGR_RANDOMLY:
$i = ($numberofentries > 1) ? rand(1, $numberofentries) : 1;
if (count($entries) == 1) {
$entry = reset($entries);
} else {
$entry = $entries[$i - 1];
}
break;
switch ($this->config->type) {
case BGR_NEXTONE:
// The array is already sorted by last modified.
if (isset($this->config->previous)) {
$i = $this->config->previous + 1;
} else {
$i = 1;
}
if ($i > $numberofentries) { // Loop back to beginning.
$i = 1;
}
if (count($entries) == 1) {
$entry = reset($entries);
} else {
$entry = $entries[$i - 1];
}
break;
case BGR_RANDOMLY:
$i = ($numberofentries > 1) ? rand(1, $numberofentries) : 1;
$limitfrom = $i-1;
break;
case BGR_NEXTALPHA:
// Now sort the array in regard to the current language.
usort($entries, function($a, $b) {
return format_string($a->concept) <=> format_string($b->concept);
});
if (isset($this->config->previous)) {
$i = $this->config->previous + 1;
} else {
$i = 1;
}
if ($i > $numberofentries) { // Loop back to beginning.
$i = 1;
}
if (count($entries) == 1) {
$entry = $entries;
} else {
$entry = $entries[$i - 1];
}
break;
case BGR_NEXTONE:
if (isset($this->config->previous)) {
$i = $this->config->previous + 1;
} else {
$i = 1;
}
if ($i > $numberofentries) { // Loop back to beginning
$i = 1;
}
$limitfrom = $i-1;
break;
case BGR_NEXTALPHA:
$orderby = 'concept ASC';
if (isset($this->config->previous)) {
$i = $this->config->previous + 1;
} else {
$i = 1;
}
if ($i > $numberofentries) { // Loop back to beginning
$i = 1;
}
$limitfrom = $i-1;
break;
default: // BGR_LASTMODIFIED
$i = $numberofentries;
$limitfrom = 0;
$orderby = 'timemodified DESC, id DESC';
break;
}
if ($entry = $DB->get_records_sql("SELECT id, concept, definition, definitionformat, definitiontrust
FROM {glossary_entries}
WHERE glossaryid = ? AND approved = 1
ORDER BY $orderby", array($this->config->glossary), $limitfrom, $limitnum)) {
$entry = reset($entry);
if (empty($this->config->showconcept)) {
default: // BGR_LASTMODIFIED
// The array is already sorted by last modified.
$i = $numberofentries;
if (count($entries) == 1) {
$entry = reset($entries);
} else {
$entry = array_pop($entries);
}
break;
}
if (empty($this->config->showconcept) || (!isset($entry->concept))) {
$text = '';
} else {
$text = "<h3>".format_string($entry->concept,true)."</h3>";
$text = "<h3>" . format_string($entry->concept, true, ["context" => $glossaryctx]) . "</h3>";
}
$options = new stdClass();
$options->trusted = $entry->definitiontrust;
if (isset($entry->definitiontrust)) {
$options->trusted = $entry->definitiontrust;
}
$options->overflowdiv = true;
$entry->definition = file_rewrite_pluginfile_urls($entry->definition, 'pluginfile.php', $glossaryctx->id, 'mod_glossary', 'entry', $entry->id);
$text .= format_text($entry->definition, $entry->definitionformat, $options);
if (isset($entry->definitiontrust) && isset($entry->id) && isset($entry->definition)) {
$entry->definition = file_rewrite_pluginfile_urls($entry->definition, 'pluginfile.php', $glossaryctx->id,
'mod_glossary', 'entry', $entry->id);
$text .= format_text($entry->definition, $entry->definitionformat, $options);
}
$this->config->nexttime = usergetmidnight(time()) + DAYSECS * $this->config->refresh;
$this->config->previous = $i;
} else {
$text = get_string('noentriesyet','block_glossary_random');
}
// store the text
$this->config->cache = $text;
$this->instance_config_commit();

View File

@ -8,6 +8,14 @@ Feature: Add the glossary random block when main feature is enabled
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
And I am on the "C1" "course" page logged in as "admin"
Scenario: The glossary random block can be added when glossary module is enabled
@ -21,3 +29,32 @@ Feature: Add the glossary random block when main feature is enabled
And I am on "Course 1" course homepage with editing mode on
When I click on "Add a block" "link"
Then I should not see "Random glossary entry"
Scenario: View alphabetical order multilang entries in the glossary block
Given the following "activities" exist:
| activity | name | intro | course | idnumber | defaultapproval |
| glossary | Animals | An animal glossary | C1 | glossary3 | 1 |
And the following "mod_glossary > entries" exist:
| glossary | user | concept | definition |
| Animals | student1 | Aardvark | <span lang="en" class="multilang">Aardvark</span><span lang="de" class="multilang">Erdferkel</span> |
| Animals | student1 | Kangaroo | <span lang="en" class="multilang">Kangaroo</span><span lang="de" class="multilang">Känguru</span> |
| Animals | student1 | Zebra | <span lang="en" class="multilang">Zebra</span><span lang="de" class="multilang">Zebra</span> |
And the "multilang" filter is "on"
And the "multilang" filter applies to "content and headings"
And I log out
And I log in as "teacher1"
And I am on "C1" course homepage with editing mode on
And I add the "Random glossary entry..." block
And I set the following fields to these values:
| Title | ManualGlossaryblock |
| Take entries from this glossary | Animals |
| Days before a new entry is chosen | 0 |
| How a new entry is chosen | Alphabetical order |
And I press "Save changes"
And I should see "Aardvark" in the "ManualGlossaryblock" "block"
And I should not see "AardvarkErdferkel" in the "ManualGlossaryblock" "block"
And I reload the page
And I should see "Kangaroo" in the "ManualGlossaryblock" "block"
And I reload the page
Then I should see "Zebra" in the "ManualGlossaryblock" "block"
And I log out

View File

@ -29,8 +29,6 @@ use stdClass;
/**
* This filter provides automatic linking to glossary entries, aliases and categories when found inside every Moodle text.
*
* NOTE: multilang glossary entries are not compatible with this filter.
*
* @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
@ -92,7 +90,7 @@ class text_filter extends \core_filters\text_filter {
foreach ($allconcepts as $concepts) {
foreach ($concepts as $concept) {
$conceptlist[] = new filter_object(
$concept->concept,
format_string($concept->concept, true, ['context' => $this->context]),
null,
null,
$concept->casesensitive,
@ -148,7 +146,9 @@ class text_filter extends \core_filters\text_filter {
$title = get_string(
'glossaryconcept',
'filter_glossary',
['glossary' => $glossaries[$concept->glossaryid], 'concept' => $concept->concept]
['glossary' => replace_ampersands_not_followed_by_entity(strip_tags(format_string($glossaries[$concept->glossaryid],
true, ['context' => $this->context]))),
'concept' => format_string($concept->concept, true, ['context' => $this->context])]
);
// Hardcoding dictionary format in the URL rather than defaulting
// to the current glossary format which may not work in a popup.

View File

@ -216,24 +216,6 @@ class mod_glossary_entry_query_builder {
$this->filter_by_non_letter($field);
}
/**
* Filter the concept by letter.
*
* @param string $letter The letter.
*/
public function filter_by_concept_letter($letter) {
$this->filter_by_letter($letter, self::resolve_field('concept', 'entries'));
}
/**
* Filter the concept by special characters.
*
* @return void
*/
public function filter_by_concept_non_letter() {
$this->filter_by_non_letter(self::resolve_field('concept', 'entries'));
}
/**
* Filter non approved entries.
*
@ -273,11 +255,11 @@ class mod_glossary_entry_query_builder {
* @param string $term What the concept or aliases should be.
*/
public function filter_by_term($term) {
$this->where[] = sprintf("(%s = :filterterma OR %s = :filtertermb)",
$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;
$this->params['filterterma'] = "%" . $term . "%";
$this->params['filtertermb'] = "%" . $term . "%";
}
/**

View File

@ -148,7 +148,8 @@ class mod_glossary_external extends external_api {
$context,
'mod_glossary',
'entry',
$entry->id
$entry->id,
['trusted' => true],
);
// Author details.

View File

@ -122,9 +122,9 @@ class concept_cache {
$concepts = array();
$rs = $DB->get_recordset_sql($sql);
foreach ($rs as $concept) {
$currentconcept = trim(strip_tags($concept->concept));
$currentconcept = trim($concept->concept);
// Concept must be HTML-escaped, so do the same as format_string to turn ampersands into &amp;.
// Turn ampersands into &amp; but keep HTML format for filters.
$currentconcept = replace_ampersands_not_followed_by_entity($currentconcept);
if (empty($currentconcept)) {
@ -188,7 +188,7 @@ class concept_cache {
}
foreach ($glossaries as $id => $name) {
$name = str_replace(':', '-', $name);
$glossaries[$id] = replace_ampersands_not_followed_by_entity(strip_tags($name));
$glossaries[$id] = $name;
}
$allconcepts = self::fetch_concepts(array_keys($glossaries));
@ -254,7 +254,7 @@ class concept_cache {
}
foreach ($glossaries as $id => $name) {
$name = str_replace(':', '-', $name);
$glossaries[$id] = replace_ampersands_not_followed_by_entity(strip_tags($name));
$glossaries[$id] = replace_ampersands_not_followed_by_entity($name);
}
$allconcepts = self::fetch_concepts(array_keys($glossaries));
foreach ($glossaries as $gid => $unused) {

View File

@ -120,7 +120,7 @@ if ( $hook >0 ) {
echo $OUTPUT->heading(format_string($glossary->name), 2);
echo $OUTPUT->heading(format_string(get_string("editcategory", "glossary")), 3);
$name = $category->name;
$name = format_string($category->name, true, ['context' => $context]);
$usedynalink = $category->usedynalink;
require "editcategories.html";
echo $OUTPUT->footer();

View File

@ -3452,18 +3452,13 @@ function glossary_entry_view($entry, $context) {
* @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.
* @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 ($letter != 'ALL' && $letter != 'SPECIAL' && core_text::strlen($letter)) {
$qb->filter_by_concept_letter($letter);
}
if ($letter == 'SPECIAL') {
$qb->filter_by_concept_non_letter();
}
if (!empty($options['includenotapproved']) && has_capability('mod/glossary:approve', $context)) {
$qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ALL);
@ -3476,12 +3471,59 @@ function glossary_get_entries_by_letter($glossary, $context, $letter, $from, $li
$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.
$qb->limit($from, $limit);
// Fetching the entries.
$count = $qb->count_records();
// 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);
}
@ -3497,7 +3539,8 @@ function glossary_get_entries_by_letter($glossary, $context, $letter, $from, $li
* @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.
* @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()) {
@ -3539,7 +3582,8 @@ function glossary_get_entries_by_date($glossary, $context, $order, $sort, $from,
* @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.
* @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()) {
@ -3596,7 +3640,8 @@ function glossary_get_entries_by_category($glossary, $context, $categoryid, $fro
* @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.
* @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()) {
@ -3644,7 +3689,8 @@ function glossary_get_entries_by_author($glossary, $context, $letter, $field, $s
* @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.
* @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()) {
@ -3689,7 +3735,8 @@ function glossary_get_entries_by_author_id($glossary, $context, $authorid, $orde
* @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, the second the number of entries.
* @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()) {
@ -3729,7 +3776,8 @@ function glossary_get_authors($glossary, $context, $limit, $from, $options = arr
* @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, the second the number of entries.
* @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) {
@ -3833,7 +3881,8 @@ function glossary_get_search_terms_sql(array $terms, $fullsearch = true, $glossa
* @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 array of results, the second the number of entries.
* @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,
@ -3915,6 +3964,7 @@ function glossary_get_entries_by_term($glossary, $context, $term, $from, $limit,
}
$qb->add_field('*', 'entries');
$qb->add_field('alias', 'alias');
$qb->join_alias();
$qb->join_user();
$qb->add_user_fields();
@ -3922,12 +3972,46 @@ function glossary_get_entries_by_term($glossary, $context, $term, $from, $limit,
$qb->order_by('concept', 'entries');
$qb->order_by('id', 'entries'); // Sort on ID to avoid random ordering when entries share an ordering value.
$qb->limit($from, $limit);
// Fetching the entries.
$count = $qb->count_records();
// 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);
}
@ -3947,32 +4031,87 @@ function glossary_get_entries_by_term($glossary, $context, $term, $from, $limit,
function glossary_get_entries_to_approve($glossary, $context, $letter, $order, $sort, $from, $limit) {
$qb = new mod_glossary_entry_query_builder($glossary);
if ($letter != 'ALL' && $letter != 'SPECIAL' && core_text::strlen($letter)) {
$qb->filter_by_concept_letter($letter);
}
if ($letter == 'SPECIAL') {
$qb->filter_by_concept_non_letter();
}
$qb->add_field('*', 'entries');
$qb->join_user();
$qb->add_user_fields();
$qb->filter_by_non_approved(mod_glossary_entry_query_builder::NON_APPROVED_ONLY);
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();
// Fetching the entries. Those are all non approved entries.
$entries = $qb->get_records();
return array($entries, $count);
// 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 ($sort == "DESC") {
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 ($sort == "DESC") {
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 ($sort == "DESC") {
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];
}
/**

View File

@ -28,6 +28,7 @@ if ($eid) {
$entry->glossaryname = $glossary->name;
$entry->cmid = $cm->id;
$entry->courseid = $cm->course;
$entry->concept = format_string($entry->concept, true, ["escape" => false]);
$entries = array($entry);
} else if ($concept) {

View File

@ -69,7 +69,7 @@ class behat_mod_glossary extends behat_base {
$this->execute("behat_forms::press_button", get_string('addcategory', 'glossary'));
$this->execute('behat_forms::i_set_the_field_to', array('name', $this->escape($categoryname)));
$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'));

View File

@ -0,0 +1,53 @@
@mod @mod_glossary
Feature: Glossary can set autolinked entries in text and media areas
In order to display the glossary entries for concepts in texts
As a teacher
I can set the glossary activity to autolink the entries
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 <span class="multilang" lang="en">glossary</span><span class="multilang" lang="fr">glossaire</span><span class="multilang" lang="es">glossario</span> | Test glossary description | encyclopedia | C1 | glossary1 |
And the following "mod_glossary > entries" exist:
| glossary | concept | definition | usedynalink |
| glossary1 | <span class="multilang" lang="en">English</span><span class="multilang" lang="fr">Anglais</span><span class="multilang" lang="es">inglés</span> | <span class="multilang" lang="en">Relating to England, its people, or the language spoken there.</span><span class="multilang" lang="fr">Relatif à l'Angleterre, à son peuple ou à la langue parlée là-bas.</span><span class="multilang" lang="es">Relacionado con Inglaterra, su gente o el idioma hablado allí.</span> | 1 |
| glossary1 | <span class="multilang" lang="en">Spanish</span><span class="multilang" lang="fr">Espagnol</span><span class="multilang" lang="es">Castellano</span> | <span class="multilang" lang="en">Relating to Spain, its people, or the language spoken there.</span><span class="multilang" lang="fr">Relatif à l'Espagne, à son peuple ou à la langue parlée là-bas.</span><span class="multilang" lang="es">Relacionado con España, su gente o el idioma hablado allí.</span> | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| label | Text and media area | <p>This is a text with the multilang syntax on the <span class="multilang" lang="en">English</span><span class="multilang" lang="fr">Anglais</span><span class="multilang" lang="es">Inglés</span> word that should be auto-linked.</p><p>This are plain text words that should also be auto-linked: English, Anglais, Inglés.</p> | C1 | label1 |
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"
@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 "Language" in the user menu
And I click on "//a[contains(@href, 'lang=es')]" "xpath"
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 "Idioma" in the user menu
And I click on "//a[contains(@href, 'lang=fr')]" "xpath"
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"

View File

@ -20,13 +20,18 @@ Feature: Glossary entries can be searched or browsed by alphabet, category, date
| 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 like |
| g1 | All for you |
| glossary | name |
| g1 | <span lang=\"en\" class=\"multilang\">The ones I like</span><span lang=\"fr\" class=\"multilang\">Ceux qui me plaisent</span> |
| g1 | <span lang=\"en\" class=\"multilang\">All for you</span><span lang=\"fr\" class=\"multilang\">Tout pour toi</span> |
And the following "mod_glossary > entries" exist:
| glossary | concept | definition | user | categories |
| g1 | Eggplant | Sour eggplants | teacher1 | All for you |
| g1 | Cucumber | Sweet cucumber | student1 | The ones I like |
| glossary | concept | definition | user | categories |
| g1 | <span lang="en" class="multilang">Eggplant</span><span lang="fr" class="multilang">Aubergine</span> | <span lang="en" class="multilang">Sour eggplants</span><span lang="fr" class="multilang">Aubergines aigres</span> | teacher1 | <span lang=\"en\" class=\"multilang\">All for you</span><span lang=\"fr\" class=\"multilang\">Tout pour toi</span> |
| g1 | 7up | <span lang="en" class="multilang">7up is a softdrink</span><span lang="fr" class="multilang">7up est une boisson</span> | teacher1 | <span lang=\"en\" class=\"multilang\">The ones I like</span><span lang=\"fr\" class=\"multilang\">Ceux qui me plaisent</span> |
| g1 | <span lang="en" class="multilang">Cucumber</span><span lang="fr" class="multilang">Concombre</span> | <span lang="en" class="multilang">Sweet cucumber</span><span lang="fr" class="multilang">Doux concombre</span> | student1 | <span lang=\"en\" class=\"multilang\">The ones I like</span><span lang=\"fr\" class=\"multilang\">Ceux qui me plaisent</span> |
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
@ -35,13 +40,35 @@ Feature: Glossary entries can be searched or browsed by alphabet, category, date
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 | <span lang="de" class="multilang">Concombre</span><span lang="en" class="multilang">Cucumber</span> |
| Definition | <span lang="fr" class="multilang">Doux concombre</span><span lang="en" class="multilang">Sweet cucumber alternate entry</span> |
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

View File

@ -367,7 +367,7 @@ final class external_test extends externallib_advanced_testcase {
// Ordering including to approve.
$return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 20,
array('includenotapproved' => true));
['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']);
@ -378,14 +378,14 @@ final class external_test extends externallib_advanced_testcase {
// Ordering including to approve and pagination.
$return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 2,
array('includenotapproved' => true));
['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,
array('includenotapproved' => true));
['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']);
@ -480,7 +480,7 @@ final class external_test extends externallib_advanced_testcase {
// Including to approve.
$return = mod_glossary_external::get_entries_by_category($g1->id, $cat1b->id, 0, 20,
array('includenotapproved' => true));
['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']);
@ -489,7 +489,7 @@ final class external_test extends externallib_advanced_testcase {
// Using limit.
$return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_ALL_CATEGORIES, 0, 3,
array('includenotapproved' => true));
['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']);
@ -497,7 +497,7 @@ final class external_test extends externallib_advanced_testcase {
$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,
array('includenotapproved' => true));
['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']);
@ -536,7 +536,7 @@ final class external_test extends externallib_advanced_testcase {
$this->assertEquals($u1->id, $return['authors'][1]['id']);
// Include users with entries pending approval.
$return = mod_glossary_external::get_authors($g1->id, 0, 20, array('includenotapproved' => true));
$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']);
@ -545,7 +545,7 @@ final class external_test extends externallib_advanced_testcase {
$this->assertEquals($u1->id, $return['authors'][2]['id']);
// Pagination.
$return = mod_glossary_external::get_authors($g1->id, 1, 1, array('includenotapproved' => true));
$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']);
@ -613,7 +613,7 @@ final class external_test extends externallib_advanced_testcase {
// Including non-approved.
$this->setAdminUser();
$return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'ASC', 0, 20,
array('includenotapproved' => true));
['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']);
@ -750,7 +750,7 @@ final class external_test extends externallib_advanced_testcase {
// Including non approved.
$return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 20,
array('includenotapproved' => true));
['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']);
@ -761,14 +761,14 @@ final class external_test extends externallib_advanced_testcase {
// Pagination.
$return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 2,
array('includenotapproved' => true));
['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,
array('includenotapproved' => true));
['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']);
@ -872,7 +872,7 @@ final class external_test extends externallib_advanced_testcase {
// Including not approved.
$query = 'ou';
$return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CONCEPT', 'ASC', 0, 20,
array('includenotapproved' => true));
['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']);
@ -884,7 +884,7 @@ final class external_test extends externallib_advanced_testcase {
// Advanced query string.
$query = '+Heroes -Abcd';
$return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'ASC', 0, 20,
array('includenotapproved' => true));
['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']);
@ -914,7 +914,7 @@ final class external_test extends externallib_advanced_testcase {
$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, array('includenotapproved' => false));
$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']);
@ -933,7 +933,7 @@ final class external_test extends externallib_advanced_testcase {
$this->assertEqualsCanonicalizing($expected, $actual);
// Search alias.
$return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, array('includenotapproved' => false));
$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']);
@ -944,7 +944,7 @@ final class external_test extends externallib_advanced_testcase {
$this->assertEqualsCanonicalizing($expected, $actual);
// Search including not approved.
$return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, array('includenotapproved' => true));
$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']);
@ -954,13 +954,104 @@ final class external_test extends externallib_advanced_testcase {
$this->assertEqualsCanonicalizing($expected, $actual);
// Pagination.
$return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 1, array('includenotapproved' => true));
$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, array('includenotapproved' => true));
$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' => '<span lang="en" ' .
'class="multilang">cat</span><span lang="fr" class="multilang">chat</span>', 'tags' => ['Cats', 'Dogs']]);
$e2 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1], ['<span lang="en" class="' .
'multilang">cat</span><span lang="fr" class="multilang">chat</span>', '<span lang="en" class="multilang">' .
'dog</span><span lang="fr" class="multilang">chien</span>']);
$e3 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1], ['<span lang="en" class="' .
'multilang">dog</span><span lang="fr" class="multilang">chien</span>']);
$e4 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 0, 'concept' => '<span lang="en" class="' .
'multilang">dog</span><span lang="fr" class="multilang">chien</span>']);
$e5 = $gg->create_content($g2, ['userid' => $u1->id, 'approved' => 1, 'concept' => '<span lang="en" class="' .
'multilang">cog</span><span lang="fr" class="multilang">chien</span>'], ['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']);

View File

@ -418,7 +418,7 @@ if ($allentries) {
// Setting the pivot for the current entry
if ($printpivot) {
$pivot = $entry->{$pivotkey};
$pivot = format_string($entry->{$pivotkey}, false, ["context" => $context]);
$upperpivot = core_text::strtoupper($pivot);
$pivottoshow = core_text::strtoupper(format_string($pivot, true, $fmtoptions));