diff --git a/lib/db/services.php b/lib/db/services.php index 3642c0742ae..d9867da6dce 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1277,6 +1277,19 @@ $services = array( 'mod_wiki_get_wikis_by_courses', 'mod_wiki_view_wiki', 'mod_wiki_view_page', + 'mod_glossary_view_glossary', + 'mod_glossary_view_entry', + 'mod_glossary_get_entries_by_letter', + 'mod_glossary_get_entries_by_date', + 'mod_glossary_get_categories', + 'mod_glossary_get_entries_by_category', + 'mod_glossary_get_authors', + 'mod_glossary_get_entries_by_author', + 'mod_glossary_get_entries_by_author_id', + 'mod_glossary_get_entries_by_search', + 'mod_glossary_get_entries_by_term', + 'mod_glossary_get_entries_to_approve', + 'mod_glossary_get_entry_by_id', ), 'enabled' => 0, 'restrictedusers' => 0, diff --git a/mod/glossary/classes/entry_query_builder.php b/mod/glossary/classes/entry_query_builder.php new file mode 100644 index 00000000000..2b74234d522 --- /dev/null +++ b/mod/glossary/classes/entry_query_builder.php @@ -0,0 +1,471 @@ +. + +/** + * 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() { + $this->fields[] = user_picture::fields('u', null, 'userdataid', 'userdata'); + } + + /** + * 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 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. + * + * @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 = :filterterma OR %s = :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; + } + +} diff --git a/mod/glossary/classes/external.php b/mod/glossary/classes/external.php index 65c8bf3f044..b404d8f320a 100644 --- a/mod/glossary/classes/external.php +++ b/mod/glossary/classes/external.php @@ -23,8 +23,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.1 */ -defined('MOODLE_INTERNAL') || die; + +defined('MOODLE_INTERNAL') || die(); + require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/glossary/lib.php'); /** * Glossary module external functions. @@ -37,6 +40,145 @@ require_once($CFG->libdir . '/externallib.php'); */ 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'), + 'attachment' => new external_value(PARAM_BOOL, 'Whether or not the entry has attachments'), + 'attachments' => new external_multiple_structure( + new external_single_structure(array( + 'filename' => new external_value(PARAM_FILE, 'File name'), + 'mimetype' => new external_value(PARAM_RAW, 'Mime type'), + 'fileurl' => new external_value(PARAM_URL, 'File download URL') + )), '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'), + ); + + 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 = external_format_string($entry->concept, $context->id); + list($entry->definition, $entry->definitionformat) = external_format_text($entry->definition, $entry->definitionformat, + $context->id, 'mod_glossary', 'entry', $entry->id); + + // 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) { + $fs = get_file_storage(); + if ($files = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id, 'filename', false)) { + foreach ($files as $file) { + $filename = $file->get_filename(); + $fileurl = moodle_url::make_webservice_pluginfile_url($context->id, 'mod_glossary', 'attachment', + $entry->id, '/', $filename); + $entry->attachments[] = array( + 'filename' => $filename, + 'mimetype' => $file->get_mimetype(), + 'fileurl' => $fileurl->out(false) + ); + } + } + } + } + + /** + * Validate a glossary via ID. + * + * @param int $id The glossary ID. + * @return array Contains glossary, context, course and cm. + */ + protected 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. * @@ -77,6 +219,7 @@ class mod_glossary_external extends external_api { // Array to store the glossaries to return. $glossaries = array(); + $modes = array(); // Ensure there are courseids to loop through. if (!empty($courseids)) { @@ -89,6 +232,17 @@ class mod_glossary_external extends external_api { $glossary->name = external_format_string($glossary->name, $context->id); list($glossary->intro, $glossary->introformat) = external_format_text($glossary->intro, $glossary->introformat, $context->id, 'mod_glossary', 'intro', null); + + // 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]; } } @@ -152,9 +306,1117 @@ class mod_glossary_external extends external_api { 'visible' => new external_value(PARAM_INT, 'Visible'), 'groupmode' => new external_value(PARAM_INT, 'Group mode'), 'groupingid' => new external_value(PARAM_INT, 'Grouping ID'), + 'browsemodes' => new external_multiple_structure( + new external_value(PARAM_ALPHA, 'Modes of browsing allowed') + ) ), '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 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) = 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'); + } + + // Trigger view. + glossary_entry_view($entry, $context); + + return array( + 'status' => true, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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() + ), + '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; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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() + ), + '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 = external_format_string($category->name, $context->id); + $categories[] = $category; + } + + return array( + 'count' => $count, + 'categories' => $categories, + 'warnings' => array(), + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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 = external_format_string($record->categoryname, $context->id); + } + $entries[] = $record; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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) + ), + '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 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; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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() + ), + '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; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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() + ), + '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; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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() + ), + '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; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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() + ), + '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; + } + $records->close(); + + return array( + 'count' => $count, + 'entries' => $entries, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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() + ), + '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) = 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); + + return array( + 'entry' => $entry, + 'warnings' => $warnings + ); + } + + /** + * Returns the description of the external function return value. + * + * @return 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(), + 'warnings' => new external_warnings() + )); + } + } diff --git a/mod/glossary/db/services.php b/mod/glossary/db/services.php index fe26d6cd5b4..b2e20fc5f86 100644 --- a/mod/glossary/db/services.php +++ b/mod/glossary/db/services.php @@ -35,4 +35,108 @@ $functions = array( 'capabilities' => 'mod/glossary:view' ), + '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' + ), + + 'mod_glossary_view_entry' => array( + 'classname' => 'mod_glossary_external', + 'methodname' => 'view_entry', + 'description' => 'Notify a glossary entry as being viewed.', + 'type' => 'write', + 'capabilities' => 'mod/glossary:view' + ), + + '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' + ), + + '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' + ), + + 'mod_glossary_get_categories' => array( + 'classname' => 'mod_glossary_external', + 'methodname' => 'get_categories', + 'description' => 'Get the categories.', + 'type' => 'read', + 'capabilities' => 'mod/glossary:view' + ), + + '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' + ), + + 'mod_glossary_get_authors' => array( + 'classname' => 'mod_glossary_external', + 'methodname' => 'get_authors', + 'description' => 'Get the authors.', + 'type' => 'read', + 'capabilities' => 'mod/glossary:view' + ), + + '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' + ), + + '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' + ), + + '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' + ), + + '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' + ), + + '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' + ), + + 'mod_glossary_get_entry_by_id' => array( + 'classname' => 'mod_glossary_external', + 'methodname' => 'get_entry_by_id', + 'description' => 'Get an entry by ID', + 'type' => 'read', + 'capabilities' => 'mod/glossary:view' + ), + ); diff --git a/mod/glossary/lib.php b/mod/glossary/lib.php index 55b2d03240f..64ec9551c04 100644 --- a/mod/glossary/lib.php +++ b/mod/glossary/lib.php @@ -3295,3 +3295,597 @@ function glossary_get_visible_tabs($displayformat) { 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, the second the number of entries. + * @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); + } 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. + $qb->limit($from, $limit); + + // Fetching the entries. + $count = $qb->count_records(); + $entries = $qb->get_recordset(); + + 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, the second the number of entries. + * @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_recordset(); + + 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, the second the number of entries. + * @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(); + $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_recordset(); + + 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, the second the number of entries. + * @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_recordset(); + + 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, the second the number of entries. + * @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_recordset(); + + 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, the second the number of entries. + * @since Moodle 3.1 + */ +function glossary_get_authors($glossary, $context, $limit, $from, $options = array()) { + global $DB, $USER; + + $params = array(); + $userfields = user_picture::fields('u', null); + + $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, the second the number of entries. + * @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. + * @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) { + 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 and Oracle + // 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 Oracle and 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) . '%'; + } + } + + // 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, the second the number of entries. + * @since Moodle 3.1 + */ +function glossary_get_entries_by_search($glossary, $context, $query, $fullsearch, $order, $sort, $from, $limit, + $options = array()) { + global $DB, $USER; + + // Remove too little terms. + $terms = explode(' ', $query); + foreach ($terms as $key => $term) { + if (strlen(trim($term, '+-')) < 2) { + unset($terms[$key]); + } + } + + list($searchcond, $params) = glossary_get_search_terms_sql($terms, $fullsearch); + + $userfields = user_picture::fields('u', null, 'userdataid', 'userdata'); + + // 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_recordset_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->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. + $qb->limit($from, $limit); + + // Fetching the entries. + $count = $qb->count_records(); + $entries = $qb->get_recordset(); + + 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); + 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(); + $entries = $qb->get_recordset(); + + return array($entries, $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); +} diff --git a/mod/glossary/print.php b/mod/glossary/print.php index e6f4b9d4b84..145cabc3ffb 100644 --- a/mod/glossary/print.php +++ b/mod/glossary/print.php @@ -187,26 +187,30 @@ echo html_writer::tag('div', $modname, array('class' => 'modname')); if ( $allentries ) { foreach ($allentries as $entry) { - // Setting the pivot for the current entry - $pivot = $entry->glossarypivot; - $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); - } + // Setting the pivot for the current entry. + if ($printpivot) { - // If there's group break - if ( $currentpivot != $upperpivot ) { + if ($userispivot) { + $pivot = $entry->userid; + } else { + $pivot = $pivotfn($entry); + } + $upperpivot = core_text::strtoupper($pivot); + $pivottoshow = core_text::strtoupper(format_string($pivot, true, $fmtoptions)); - // print the group break if apply - if ( $printpivot ) { + // 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 ( isset($entry->userispivot) ) { - // printing the user icon if defined (only when browsing authors) - $user = $DB->get_record("user", array("id"=>$entry->userid)); + 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')); diff --git a/mod/glossary/showentry.php b/mod/glossary/showentry.php index 3b765062776..1bbc6aa9f1c 100644 --- a/mod/glossary/showentry.php +++ b/mod/glossary/showentry.php @@ -61,12 +61,7 @@ if ($entries) { } } $entries[$key]->footer = "

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

"; - $event = \mod_glossary\event\entry_viewed::create(array( - 'objectid' => $entry->id, - 'context' => $modinfo->cms[$entry->cmid]->context - )); - $event->add_record_snapshot('glossary_entries', $entry); - $event->trigger(); + glossary_entry_view($entry, $modinfo->cms[$entry->cmid]->context); } } diff --git a/mod/glossary/showentry_ajax.php b/mod/glossary/showentry_ajax.php index 002cf928b8d..f067d005377 100644 --- a/mod/glossary/showentry_ajax.php +++ b/mod/glossary/showentry_ajax.php @@ -80,12 +80,7 @@ if ($entries) { } $entries[$key]->footer = "

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

"; - $event = \mod_glossary\event\entry_viewed::create(array( - 'objectid' => $entry->id, - 'context' => $modinfo->cms[$entry->cmid]->context - )); - $event->add_record_snapshot('glossary_entries', $entry); - $event->trigger(); + glossary_entry_view($entry, $modinfo->cms[$entry->cmid]->context); } } diff --git a/mod/glossary/sql.php b/mod/glossary/sql.php index 93e9e208e7f..7e6a0c5ce7e 100644 --- a/mod/glossary/sql.php +++ b/mod/glossary/sql.php @@ -6,281 +6,104 @@ * @copyright 2003 **/ -/// Creating the SQL statements +/** + * This file defines, or redefines, the following variables: + * + * bool $userispivot Whether the user is the pivot. + * bool $fullpivot Whether the pivot should be displayed in full. + * bool $printpivot Whether the pivot should be displayed. + * string $pivotkey The property of the record at which the pivot is. + * int $count The number of records matching the request. + * array $allentries The entries matching the request. + * mixed $field Unset in this file. + * mixed $entry Unset in this file. + * mixed $canapprove Unset in this file. + * + * It relies on the following variables: + * + * object $glossary The glossary object. + * context $context The glossary context. + * mixed $hook The hook for the selected tab. + * string $sortkey The key to sort the records. + * string $sortorder The order of the sorting. + * int $offset The number of records to skip. + * int $entriesbypage The number of entries per page. + * string $mode The mode of browsing. + * string $tab The tab selected. + */ -/// Initialise some variables -$sqlorderby = ''; -$sqlsortkey = NULL; +$userispivot = false; +$fullpivot = true; +$pivotkey = 'concept'; -// For cases needing inner view -$sqlwrapheader = ''; -$sqlwrapfooter = ''; - -/// Calculate the SQL sortkey to be used by the SQL statements later -switch ( $sortkey ) { - case "CREATION": - $sqlsortkey = "timecreated"; - break; - case "UPDATE": - $sqlsortkey = "timemodified"; - break; - case "FIRSTNAME": - $sqlsortkey = "firstname"; - break; - case "LASTNAME": - $sqlsortkey = "lastname"; - break; -} -$sqlsortorder = $sortorder; - -/// Pivot is the field that set the break by groups (category, initial, author name, etc) - -/// fullpivot indicate if the whole pivot should be compared agasint the db or just the first letter -/// printpivot indicate if the pivot should be printed or not - -$fullpivot = 1; -$params = array('gid1'=>$glossary->id, 'gid2'=>$glossary->id, 'myid'=>$USER->id, 'hook'=>$hook); - -$userid = ''; -if ( isloggedin() ) { - $userid = "OR ge.userid = :myid"; -} switch ($tab) { - case GLOSSARY_CATEGORY_VIEW: - if ($hook == GLOSSARY_SHOW_ALL_CATEGORIES ) { - $sqlselect = "SELECT gec.id AS cid, ge.*, gec.entryid, gc.name AS glossarypivot"; - $sqlfrom = "FROM {glossary_entries} ge, - {glossary_entries_categories} gec, - {glossary_categories} gc"; - $sqlwhere = "WHERE (ge.glossaryid = :gid1 OR ge.sourceglossaryid = :gid2) AND - ge.id = gec.entryid AND gc.id = gec.categoryid AND - (ge.approved <> 0 $userid)"; - - $sqlorderby = ' ORDER BY gc.name, ge.concept'; - - } elseif ($hook == GLOSSARY_SHOW_NOT_CATEGORISED ) { - - $printpivot = 0; - $sqlselect = "SELECT ge.*, concept AS glossarypivot"; - $sqlfrom = "FROM {glossary_entries} ge LEFT JOIN {glossary_entries_categories} gec - ON ge.id = gec.entryid"; - $sqlwhere = "WHERE (glossaryid = :gid1 OR sourceglossaryid = :gid2) AND - (ge.approved <> 0 $userid) AND gec.entryid IS NULL"; - - - $sqlorderby = ' ORDER BY concept'; - - } else { - - $printpivot = 0; - $sqlselect = "SELECT ge.*, ce.entryid, c.name AS glossarypivot"; - $sqlfrom = "FROM {glossary_entries} ge, {glossary_entries_categories} ce, {glossary_categories} c"; - $sqlwhere = "WHERE ge.id = ce.entryid AND ce.categoryid = :hook AND - ce.categoryid = c.id AND ge.approved != 0 AND - (ge.glossaryid = :gid1 OR ge.sourceglossaryid = :gid2) AND - (ge.approved <> 0 $userid)"; - - $sqlorderby = ' ORDER BY c.name, ge.concept'; - - } - break; case GLOSSARY_AUTHOR_VIEW: - - $where = ''; - $params['hookup'] = core_text::strtoupper($hook); - - if ( $sqlsortkey == 'firstname' ) { - $usernamefield = $DB->sql_fullname('u.firstname' , 'u.lastname'); - } else { - $usernamefield = $DB->sql_fullname('u.lastname' , 'u.firstname'); - } - if ($hook != 'ALL' && ($hookstrlen = core_text::strlen($hook))) { - $where = "AND " . $DB->sql_substr("upper($usernamefield)", 1, core_text::strlen($hook)) . " = :hookup"; - } - - $sqlselect = "SELECT ge.*, $usernamefield AS glossarypivot, 1 AS userispivot "; - $sqlfrom = "FROM {glossary_entries} ge, {user} u"; - $sqlwhere = "WHERE ge.userid = u.id AND - (ge.approved <> 0 $userid) - $where AND - (ge.glossaryid = :gid1 OR ge.sourceglossaryid = :gid2)"; - $sqlorderby = "ORDER BY $usernamefield $sqlsortorder, ge.concept"; + $userispivot = true; + $pivotkey = 'userid'; + $field = ($sortkey == 'LASTNAME' ? 'LASTNAME' : 'FIRSTNAME'); + list($allentries, $count) = glossary_get_entries_by_author($glossary, $context, $hook, + $field, $sortorder, $offset, $entriesbypage); + unset($field); break; - case GLOSSARY_APPROVAL_VIEW: - $fullpivot = 0; - $printpivot = 0; - $where = ''; - $params['hookup'] = core_text::strtoupper($hook); - - if ($hook != 'ALL' and $hook != 'SPECIAL' && ($hookstrlen = core_text::strlen($hook))) { - $where = "AND " . $DB->sql_substr("upper(concept)", 1, $hookstrlen) . " = :hookup"; - } - - $sqlselect = "SELECT ge.*, ge.concept AS glossarypivot"; - $sqlfrom = "FROM {glossary_entries} ge"; - $sqlwhere = "WHERE (ge.glossaryid = :gid1 OR ge.sourceglossaryid = :gid2) AND - ge.approved = 0 $where"; - - if ( $sqlsortkey ) { - $sqlorderby = "ORDER BY $sqlsortkey $sqlsortorder"; - } else { - $sqlorderby = "ORDER BY ge.concept"; + case GLOSSARY_CATEGORY_VIEW: + $hook = (int) $hook; // Make sure it's properly casted to int. + list($allentries, $count) = glossary_get_entries_by_category($glossary, $context, $hook, $offset, $entriesbypage); + $pivotkey = 'categoryname'; + if ($hook != GLOSSARY_SHOW_ALL_CATEGORIES) { + $printpivot = false; } break; + case GLOSSARY_DATE_VIEW: - $printpivot = 0; + $printpivot = false; + $field = ($sortkey == 'CREATION' ? 'CREATION' : 'UPDATE'); + list($allentries, $count) = glossary_get_entries_by_date($glossary, $context, $field, $sortorder, + $offset, $entriesbypage); + unset($field); + break; + + case GLOSSARY_APPROVAL_VIEW: + $fullpivot = false; + $printpivot = false; + list($allentries, $count) = glossary_get_entries_to_approve($glossary, $context, $hook, $sortkey, $sortorder, + $offset, $entriesbypage); + break; + case GLOSSARY_STANDARD_VIEW: default: - $sqlselect = "SELECT ge.*, ge.concept AS glossarypivot"; - $sqlfrom = "FROM {glossary_entries} ge"; - - $where = ''; - $fullpivot = 0; - - switch ( $mode ) { + $fullpivot = false; + switch ($mode) { case 'search': - - if ($DB->sql_regex_supported()) { - $REGEXP = $DB->sql_regex(true); - $NOTREGEXP = $DB->sql_regex(false); - } - - $searchcond = array(); - $alcond = array(); - //$params = array(); - $i = 0; - - $searchterms = explode(" ",$hook); - - foreach ($searchterms as $searchterm) { - $i++; - - $NOT = false; /// Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle - /// 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 Oracle and 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, '+-'); - if (core_text::strlen($searchterm) < 2) { - continue; - } - $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, '+-'); - if (core_text::strlen($searchterm) < 2) { - continue; - } - $searchterm = preg_quote($searchterm, '|'); - $searchcond[] = "$concat $NOTREGEXP :ss$i"; - $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)"; - - } else { - if (core_text::strlen($searchterm) < 2) { - continue; - } - $searchcond[] = $DB->sql_like($concat, ":ss$i", false, true, $NOT); - $params['ss'.$i] = "%$searchterm%"; - } - } - - if (empty($searchcond)) { - $where = "AND 1=2 "; // no search result - - } else { - $searchcond = implode(" AND ", $searchcond); - - // Need one inner view here to avoid distinct + text - $sqlwrapheader = 'SELECT ge.*, ge.concept AS glossarypivot - FROM {glossary_entries} ge - 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"; - $where = "AND ($searchcond)"; - } - + list($allentries, $count) = glossary_get_entries_by_search($glossary, $context, $hook, $fullsearch, + $sortkey, $sortorder, $offset, $entriesbypage); break; case 'term': - $params['hook2'] = $hook; - $printpivot = 0; - $sqlfrom .= " LEFT JOIN {glossary_alias} ga on ge.id = ga.entryid"; - $where = "AND (ge.concept = :hook OR ga.alias = :hook2) "; + $printpivot = false; + list($allentries, $count) = glossary_get_entries_by_term($glossary, $context, $hook, $offset, $entriesbypage); break; case 'entry': - $printpivot = 0; - $where = "AND ge.id = :hook"; + $printpivot = false; + $entry = glossary_get_entry_by_id($hook); + $canapprove = has_capability('mod/glossary:approve', $context); + if ($entry && ($entry->glossaryid == $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': - if ($hook != 'ALL' and $hook != 'SPECIAL' and ($hookstrlen = core_text::strlen($hook))) { - $params['hookup'] = core_text::strtoupper($hook); - $where = "AND " . $DB->sql_substr("upper(concept)", 1, $hookstrlen) . " = :hookup"; - } - if ($hook == 'SPECIAL') { - //Create appropiate IN contents - $alphabet = explode(",", get_string('alphabet', 'langconfig')); - list($nia, $aparams) = $DB->get_in_or_equal($alphabet, SQL_PARAMS_NAMED, $start='a', false); - $params = array_merge($params, $aparams); - $where = "AND " . $DB->sql_substr("upper(concept)", 1, 1) . " $nia"; - } - break; - } - - $sqlwhere = "WHERE (ge.glossaryid = :gid1 or ge.sourceglossaryid = :gid2) AND - (ge.approved <> 0 $userid) - $where"; - switch ( $tab ) { - case GLOSSARY_DATE_VIEW: - $sqlorderby = "ORDER BY $sqlsortkey $sqlsortorder"; - break; - - case GLOSSARY_STANDARD_VIEW: - $sqlorderby = "ORDER BY ge.concept"; default: + list($allentries, $count) = glossary_get_entries_by_letter($glossary, $context, $hook, $offset, $entriesbypage); break; } break; } - -$count = 0; -if ($tab == GLOSSARY_CATEGORY_VIEW && $hook == GLOSSARY_SHOW_ALL_CATEGORIES) { - $count = $DB->count_records_sql("SELECT COUNT(ge.id) $sqlfrom $sqlwhere", $params); -} else { - $count = $DB->count_records_sql("SELECT COUNT(DISTINCT(ge.id)) $sqlfrom $sqlwhere", $params); -} - -$limitfrom = $offset; -$limitnum = 0; - -if ( $offset >= 0 ) { - $limitnum = $entriesbypage; -} - -$query = "$sqlwrapheader $sqlselect $sqlfrom $sqlwhere $sqlwrapfooter $sqlorderby"; -$allentries = $DB->get_records_sql($query, $params, $limitfrom, $limitnum); \ No newline at end of file diff --git a/mod/glossary/tests/external_test.php b/mod/glossary/tests/external_test.php index 405ba93fa17..becf3a14424 100644 --- a/mod/glossary/tests/external_test.php +++ b/mod/glossary/tests/external_test.php @@ -85,4 +85,1007 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase { $this->assertEquals('Third Glossary', $glossaries['glossaries'][0]['name']); } + public function test_view_glossary() { + $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() { + $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->setExpectedException('require_login_exception', 'Activity is hidden'); + mod_glossary_external::view_glossary($g1->id, 'letter'); + } + + public function test_view_entry() { + $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->setExpectedException('require_login_exception', 'Activity is hidden'); + mod_glossary_external::view_entry($e4->id); + } + + public function test_get_entries_by_letter() { + $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)); + $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($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() { + $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() { + 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)); + $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($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, + array('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, + array('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)); + $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() { + $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() { + $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)); + $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($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, + array('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, + array('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, + array('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() { + $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, array('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, array('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() { + $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)); + $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($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, + array('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() { + $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, + array('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, + array('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)); + $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() { + $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')); + $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']); + + // 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, + array('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, + array('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() { + $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')); + $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, array('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->assertEquals($expected, $actual, '', 0.0, 10, true); + + // Search alias. + $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, array('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->assertEquals($expected, $actual, '', 0.0, 10, true); + + // Search including not approved. + $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, array('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->assertEquals($expected, $actual, '', 0.0, 10, true); + + // Pagination. + $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 1, array('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 = 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() { + $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->setExpectedException('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() { + $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' => $c1->id, 'visible' => 0)); + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $ctx = context_module::instance($g1->cmid); + $this->getDataGenerator()->enrol_user($u1->id, $c1->id); + + $e1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); + $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']); + + $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']); + + 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 be 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']); + } + } diff --git a/mod/glossary/tests/lib_test.php b/mod/glossary/tests/lib_test.php new file mode 100644 index 00000000000..f285bf08c13 --- /dev/null +++ b/mod/glossary/tests/lib_test.php @@ -0,0 +1,117 @@ +. + +/** + * 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 + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/glossary/lib.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 + */ +class mod_glossary_lib_testcase extends advanced_testcase { + + public function test_glossary_view() { + 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() { + $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(); + } + +} diff --git a/mod/glossary/version.php b/mod/glossary/version.php index 66ae9ad42ab..6b4073704b6 100644 --- a/mod/glossary/version.php +++ b/mod/glossary/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111601; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2015111605; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2015111000; // Requires this Moodle version $plugin->component = 'mod_glossary'; // Full name of the plugin (used for diagnostics) $plugin->cron = 0; diff --git a/mod/glossary/view.php b/mod/glossary/view.php index a9aee920dcf..178769b8728 100644 --- a/mod/glossary/view.php +++ b/mod/glossary/view.php @@ -255,19 +255,7 @@ break; } // Trigger 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(); - -// Mark as viewed -$completion = new completion_info($course); -$completion->set_module_viewed($cm); +glossary_view($glossary, $course, $cm, $context, $mode); /// Printing the heading $strglossaries = get_string("modulenameplural", "glossary"); @@ -459,31 +447,31 @@ if ($allentries) { foreach ($allentries as $entry) { // Setting the pivot for the current entry - $pivot = $entry->glossarypivot; - $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 ($printpivot) { + $pivot = $entry->{$pivotkey}; + $upperpivot = core_text::strtoupper($pivot); + $pivottoshow = core_text::strtoupper(format_string($pivot, true, $fmtoptions)); - // if there's a group break - if ( $currentpivot != $upperpivot ) { + // Reduce pivot to 1cc if necessary. + if (!$fullpivot) { + $upperpivot = core_text::substr($upperpivot, 0, 1); + $pivottoshow = core_text::substr($pivottoshow, 0, 1); + } - // print the group break if apply - if ( $printpivot ) { + // If there's a group break. + if ($currentpivot != $upperpivot) { $currentpivot = $upperpivot; + // print the group break if apply + echo '
'; echo ''; echo ''; - if ( isset($entry->userispivot) ) { + if ($userispivot) { // printing the user icon if defined (only when browsing authors) echo '
'; - - $user = $DB->get_record("user", array("id"=>$entry->userid)); + $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 { @@ -492,7 +480,6 @@ if ($allentries) { echo $OUTPUT->heading($pivottoshow, 3); echo "
\n"; - } } diff --git a/version.php b/version.php index 93a440c9605..8b2da70858f 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2015123100.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2015123100.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.