ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK{\rrexternallib.phpnu[. use core_external\external_api; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util; /** * External notes API * * @package core_notes * @category external * @copyright 2011 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . "/notes/lib.php"); /** * Notes external functions * * @package core_notes * @category external * @copyright 2011 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.2 */ class core_notes_external extends external_api { /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.2 */ public static function create_notes_parameters() { return new external_function_parameters( array( 'notes' => new external_multiple_structure( new external_single_structure( array( 'userid' => new external_value(PARAM_INT, 'id of the user the note is about'), 'publishstate' => new external_value(PARAM_ALPHA, '\'personal\', \'course\' or \'site\''), 'courseid' => new external_value(PARAM_INT, 'course id of the note (in Moodle a note can only be created into a course, even for site and personal notes)'), 'text' => new external_value(PARAM_RAW, 'the text of the message - text or HTML'), 'format' => new external_format_value('text', VALUE_DEFAULT, FORMAT_MOODLE), 'clientnoteid' => new external_value(PARAM_ALPHANUMEXT, 'your own client id for the note. If this id is provided, the fail message id will be returned to you', VALUE_OPTIONAL), ) ) ) ) ); } /** * Create notes about some users * Note: code should be matching the /notes/edit.php checks * and the /user/addnote.php checks. (they are similar cheks) * * @param array $notes An array of notes to create. * @return array (success infos and fail infos) * @since Moodle 2.2 */ public static function create_notes($notes = array()) { global $CFG, $DB; $params = self::validate_parameters(self::create_notes_parameters(), array('notes' => $notes)); // Check if note system is enabled. if (!$CFG->enablenotes) { throw new moodle_exception('notesdisabled', 'notes'); } // Retrieve all courses. $courseids = array(); foreach ($params['notes'] as $note) { $courseids[] = $note['courseid']; } $courses = $DB->get_records_list("course", "id", $courseids); // Retrieve all users of the notes. $userids = array(); foreach ($params['notes'] as $note) { $userids[] = $note['userid']; } list($sqluserids, $sqlparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'userid_'); $users = $DB->get_records_select("user", "id " . $sqluserids . " AND deleted = 0", $sqlparams); $resultnotes = array(); foreach ($params['notes'] as $note) { $success = true; $resultnote = array(); // The infos about the success of the operation. // Check the course exists. if (empty($courses[$note['courseid']])) { $success = false; $errormessage = get_string('invalidcourseid', 'error'); } else { // Ensure the current user is allowed to run this function. $context = context_course::instance($note['courseid']); self::validate_context($context); require_capability('moodle/notes:manage', $context); } // Check the user exists. if (empty($users[$note['userid']])) { $success = false; $errormessage = get_string('invaliduserid', 'notes', $note['userid']); } // Build the resultnote. if (isset($note['clientnoteid'])) { $resultnote['clientnoteid'] = $note['clientnoteid']; } if ($success) { // Now we can create the note. $dbnote = new stdClass; $dbnote->courseid = $note['courseid']; $dbnote->userid = $note['userid']; // Need to support 'html' and 'text' format values for backward compatibility. switch (strtolower($note['format'])) { case 'html': $textformat = FORMAT_HTML; break; case 'text': $textformat = FORMAT_PLAIN; break; default: $textformat = util::validate_format($note['format']); break; } $dbnote->content = $note['text']; $dbnote->format = $textformat; // Get the state ('personal', 'course', 'site'). switch ($note['publishstate']) { case 'personal': $dbnote->publishstate = NOTES_STATE_DRAFT; break; case 'course': $dbnote->publishstate = NOTES_STATE_PUBLIC; break; case 'site': $dbnote->publishstate = NOTES_STATE_SITE; $dbnote->courseid = SITEID; break; default: break; } // TODO MDL-31119 performance improvement - if possible create a bulk functions for saving multiple notes at once if (note_save($dbnote)) { // Note_save attribut an id in case of success. $success = $dbnote->id; } $resultnote['noteid'] = $success; } else { // WARNINGS: for backward compatibility we return this errormessage. // We should have thrown exceptions as these errors prevent results to be returned. // See http://docs.moodle.org/dev/Errors_handling_in_web_services#When_to_send_a_warning_on_the_server_side . $resultnote['noteid'] = -1; $resultnote['errormessage'] = $errormessage; } $resultnotes[] = $resultnote; } return $resultnotes; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.2 */ public static function create_notes_returns() { return new external_multiple_structure( new external_single_structure( array( 'clientnoteid' => new external_value(PARAM_ALPHANUMEXT, 'your own id for the note', VALUE_OPTIONAL), 'noteid' => new external_value(PARAM_INT, 'ID of the created note when successful, -1 when failed'), 'errormessage' => new external_value(PARAM_TEXT, 'error message - if failed', VALUE_OPTIONAL) ) ) ); } /** * Returns description of delete_notes parameters * * @return external_function_parameters * @since Moodle 2.5 */ public static function delete_notes_parameters() { return new external_function_parameters( array( "notes"=> new external_multiple_structure( new external_value(PARAM_INT, 'ID of the note to be deleted'), 'Array of Note Ids to be deleted.' ) ) ); } /** * Delete notes about users. * Note: code should be matching the /notes/delete.php checks. * * @param array $notes An array of ids for the notes to delete. * @return null * @since Moodle 2.5 */ public static function delete_notes($notes = array()) { global $CFG; $params = self::validate_parameters(self::delete_notes_parameters(), array('notes' => $notes)); // Check if note system is enabled. if (!$CFG->enablenotes) { throw new moodle_exception('notesdisabled', 'notes'); } $warnings = array(); foreach ($params['notes'] as $noteid) { $note = note_load($noteid); if (isset($note->id)) { // Ensure the current user is allowed to run this function. $context = context_course::instance($note->courseid); self::validate_context($context); require_capability('moodle/notes:manage', $context); note_delete($note); } else { $warnings[] = array('item'=>'note', 'itemid'=>$noteid, 'warningcode'=>'badid', 'message'=>'Note does not exist'); } } return $warnings; } /** * Returns description of delete_notes result value. * * @return \core_external\external_description * @since Moodle 2.5 */ public static function delete_notes_returns() { return new external_warnings('item is always \'note\'', 'When errorcode is savedfailed the note could not be modified.' . 'When errorcode is badparam, an incorrect parameter was provided.' . 'When errorcode is badid, the note does not exist', 'errorcode can be badparam (incorrect parameter), savedfailed (could not be modified), or badid (note does not exist)'); } /** * Returns description of get_notes parameters. * * @return external_function_parameters * @since Moodle 2.5 */ public static function get_notes_parameters() { return new external_function_parameters( array( "notes"=> new external_multiple_structure( new external_value(PARAM_INT, 'ID of the note to be retrieved'), 'Array of Note Ids to be retrieved.' ) ) ); } /** * Get notes about users. * * @param array $notes An array of ids for the notes to retrieve. * @return null * @since Moodle 2.5 */ public static function get_notes($notes) { global $CFG; $params = self::validate_parameters(self::get_notes_parameters(), array('notes' => $notes)); // Check if note system is enabled. if (!$CFG->enablenotes) { throw new moodle_exception('notesdisabled', 'notes'); } $resultnotes = array(); foreach ($params['notes'] as $noteid) { $resultnote = array(); $note = note_load($noteid); if (isset($note->id)) { // Ensure the current user is allowed to run this function. $context = context_course::instance($note->courseid); self::validate_context($context); require_capability('moodle/notes:view', $context); list($gotnote['text'], $gotnote['format']) = util::format_text($note->content, $note->format, $context->id, 'notes', '', ''); $gotnote['noteid'] = $note->id; $gotnote['userid'] = $note->userid; $gotnote['publishstate'] = $note->publishstate; $gotnote['courseid'] = $note->courseid; $resultnotes["notes"][] = $gotnote; } else { $resultnotes["warnings"][] = array('item' => 'note', 'itemid' => $noteid, 'warningcode' => 'badid', 'message' => 'Note does not exist'); } } return $resultnotes; } /** * Returns description of get_notes result value. * * @return \core_external\external_description * @since Moodle 2.5 */ public static function get_notes_returns() { return new external_single_structure( array( 'notes' => new external_multiple_structure( new external_single_structure( array( 'noteid' => new external_value(PARAM_INT, 'id of the note', VALUE_OPTIONAL), 'userid' => new external_value(PARAM_INT, 'id of the user the note is about', VALUE_OPTIONAL), 'publishstate' => new external_value(PARAM_ALPHA, '\'personal\', \'course\' or \'site\'', VALUE_OPTIONAL), 'courseid' => new external_value(PARAM_INT, 'course id of the note', VALUE_OPTIONAL), 'text' => new external_value(PARAM_RAW, 'the text of the message - text or HTML', VALUE_OPTIONAL), 'format' => new external_format_value('text', VALUE_OPTIONAL), ), 'note' ) ), 'warnings' => new external_warnings('item is always \'note\'', 'When errorcode is savedfailed the note could not be modified.' . 'When errorcode is badparam, an incorrect parameter was provided.' . 'When errorcode is badid, the note does not exist', 'errorcode can be badparam (incorrect parameter), savedfailed (could not be modified), or badid (note does not exist)') ) ); } /** * Returns description of update_notes parameters. * * @return external_function_parameters * @since Moodle 2.5 */ public static function update_notes_parameters() { return new external_function_parameters( array( 'notes' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'id of the note'), 'publishstate' => new external_value(PARAM_ALPHA, '\'personal\', \'course\' or \'site\''), 'text' => new external_value(PARAM_RAW, 'the text of the message - text or HTML'), 'format' => new external_format_value('text', VALUE_DEFAULT), ) ), "Array of Notes", VALUE_DEFAULT, array() ) ) ); } /** * Update notes about users. * * @param array $notes An array of ids for the notes to update. * @return array fail infos. * @since Moodle 2.2 */ public static function update_notes($notes = array()) { global $CFG, $DB; $params = self::validate_parameters(self::update_notes_parameters(), array('notes' => $notes)); // Check if note system is enabled. if (!$CFG->enablenotes) { throw new moodle_exception('notesdisabled', 'notes'); } $warnings = array(); foreach ($params['notes'] as $note) { $notedetails = note_load($note['id']); if (isset($notedetails->id)) { // Ensure the current user is allowed to run this function. $context = context_course::instance($notedetails->courseid); self::validate_context($context); require_capability('moodle/notes:manage', $context); $dbnote = new stdClass; $dbnote->id = $note['id']; $dbnote->content = $note['text']; $dbnote->format = util::validate_format($note['format']); // Get the state ('personal', 'course', 'site'). switch ($note['publishstate']) { case 'personal': $dbnote->publishstate = NOTES_STATE_DRAFT; break; case 'course': $dbnote->publishstate = NOTES_STATE_PUBLIC; break; case 'site': $dbnote->publishstate = NOTES_STATE_SITE; $dbnote->courseid = SITEID; break; default: $warnings[] = array('item' => 'note', 'itemid' => $note["id"], 'warningcode' => 'badparam', 'message' => 'Provided publishstate incorrect'); break; } if (!note_save($dbnote)) { $warnings[] = array('item' => 'note', 'itemid' => $note["id"], 'warningcode' => 'savedfailed', 'message' => 'Note could not be modified'); } } else { $warnings[] = array('item' => 'note', 'itemid' => $note["id"], 'warningcode' => 'badid', 'message' => 'Note does not exist'); } } return $warnings; } /** * Returns description of update_notes result value. * * @return \core_external\external_description * @since Moodle 2.5 */ public static function update_notes_returns() { return new external_warnings('item is always \'note\'', 'When errorcode is savedfailed the note could not be modified.' . 'When errorcode is badparam, an incorrect parameter was provided.' . 'When errorcode is badid, the note does not exist', 'errorcode can be badparam (incorrect parameter), savedfailed (could not be modified), or badid (note does not exist)'); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function get_course_notes_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'course id, 0 for SITE'), 'userid' => new external_value(PARAM_INT, 'user id', VALUE_DEFAULT, 0), ) ); } /** * Create a notes list * * @param int $courseid ID of the Course * @param stdClass $context context object * @param int $userid ID of the User * @param int $state * @param int $author * @return array of notes * @since Moodle 2.9 */ protected static function create_note_list($courseid, $context, $userid, $state, $author = 0) { $results = []; $notes = note_list($courseid, $userid, $state, $author); foreach ($notes as $key => $note) { $note = (array)$note; [$note['content'], $note['format']] = util::format_text( $note['content'], $note['format'], $context->id, '', '', 0 ); $results[$key] = $note; } return $results; } /** * Get a list of course notes * * @param int $courseid ID of the Course * @param int $userid ID of the User * @return array of site, course and personal notes and warnings * @since Moodle 2.9 * @throws moodle_exception */ public static function get_course_notes($courseid, $userid = 0) { global $CFG, $USER; if (empty($CFG->enablenotes)) { throw new moodle_exception('notesdisabled', 'notes'); } $warnings = array(); $arrayparams = array( 'courseid' => $courseid, 'userid' => $userid, ); $params = self::validate_parameters(self::get_course_notes_parameters(), $arrayparams); if (empty($params['courseid'])) { $params['courseid'] = SITEID; } $user = null; if (!empty($params['userid'])) { $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); } $course = get_course($params['courseid']); $systemcontext = context_system::instance(); $canmanagesystemnotes = has_capability('moodle/notes:manage', $systemcontext); if ($course->id == SITEID) { $context = $systemcontext; $canmanagecoursenotes = $canmanagesystemnotes; } else { $context = context_course::instance($course->id); $canmanagecoursenotes = has_capability('moodle/notes:manage', $context); } self::validate_context($context); $sitenotes = array(); $coursenotes = array(); $personalnotes = array(); if ($course->id != SITEID) { require_capability('moodle/notes:view', $context); $sitenotes = self::create_note_list(0, $systemcontext, $params['userid'], NOTES_STATE_SITE); $coursenotes = self::create_note_list($course->id, $context, $params['userid'], NOTES_STATE_PUBLIC); $personalnotes = self::create_note_list($course->id, $context, $params['userid'], NOTES_STATE_DRAFT, $USER->id); } else { if (has_capability('moodle/notes:view', $context)) { $sitenotes = self::create_note_list(0, $context, $params['userid'], NOTES_STATE_SITE); } // It returns notes only for a specific user! if (!empty($user)) { $usercourses = enrol_get_users_courses($user->id, true); foreach ($usercourses as $c) { // All notes at course level, only if we have capability on every course. if (has_capability('moodle/notes:view', context_course::instance($c->id))) { $coursenotes += self::create_note_list($c->id, $context, $params['userid'], NOTES_STATE_PUBLIC); } } } } $results = array( 'sitenotes' => $sitenotes, 'coursenotes' => $coursenotes, 'personalnotes' => $personalnotes, 'canmanagesystemnotes' => $canmanagesystemnotes, 'canmanagecoursenotes' => $canmanagecoursenotes, 'warnings' => $warnings ); return $results; } /** * Returns array of note structure * * @return \core_external\external_description * @since Moodle 2.9 */ protected static function get_note_structure() { return array( 'id' => new external_value(PARAM_INT, 'id of this note'), 'courseid' => new external_value(PARAM_INT, 'id of the course'), 'userid' => new external_value(PARAM_INT, 'user id'), 'content' => new external_value(PARAM_RAW, 'the content text formated'), 'format' => new external_format_value('content'), 'created' => new external_value(PARAM_INT, 'time created (timestamp)'), 'lastmodified' => new external_value(PARAM_INT, 'time of last modification (timestamp)'), 'usermodified' => new external_value(PARAM_INT, 'user id of the creator of this note'), 'publishstate' => new external_value(PARAM_ALPHA, "state of the note (i.e. draft, public, site) ") ); } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.9 */ public static function get_course_notes_returns() { return new external_single_structure( array( 'sitenotes' => new external_multiple_structure( new external_single_structure(self::get_note_structure() , ''), 'site notes', VALUE_OPTIONAL ), 'coursenotes' => new external_multiple_structure( new external_single_structure(self::get_note_structure() , ''), 'couse notes', VALUE_OPTIONAL ), 'personalnotes' => new external_multiple_structure( new external_single_structure(self::get_note_structure() , ''), 'personal notes', VALUE_OPTIONAL ), 'canmanagesystemnotes' => new external_value(PARAM_BOOL, 'Whether the user can manage notes at system level.', VALUE_OPTIONAL), 'canmanagecoursenotes' => new external_value(PARAM_BOOL, 'Whether the user can manage notes at the given course.', VALUE_OPTIONAL), 'warnings' => new external_warnings() ), 'notes' ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function view_notes_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'course id, 0 for notes at system level'), 'userid' => new external_value(PARAM_INT, 'user id, 0 means view all the user notes', VALUE_DEFAULT, 0) ) ); } /** * Simulates the web interface view of notes/index.php: trigger events * * @param int $courseid id of the course * @param int $userid id of the user * @return array of warnings and status result * @since Moodle 2.9 * @throws moodle_exception */ public static function view_notes($courseid, $userid = 0) { global $CFG; require_once($CFG->dirroot . "/notes/lib.php"); if (empty($CFG->enablenotes)) { throw new moodle_exception('notesdisabled', 'notes'); } $warnings = array(); $arrayparams = array( 'courseid' => $courseid, 'userid' => $userid ); $params = self::validate_parameters(self::view_notes_parameters(), $arrayparams); if (empty($params['courseid'])) { $params['courseid'] = SITEID; } $course = get_course($params['courseid']); if ($course->id == SITEID) { $context = context_system::instance(); } else { $context = context_course::instance($course->id); } // First of all, validate the context before do further permission checks. self::validate_context($context); require_capability('moodle/notes:view', $context); if (!empty($params['userid'])) { $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); if ($course->id != SITEID and !can_access_course($course, $user, '', true)) { throw new moodle_exception('notenrolledprofile'); } } note_view($context, $params['userid']); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.9 */ public static function view_notes_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } } PK{\1e   edit_form.phpnu[. if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); // It must be included from a Moodle page. } require_once($CFG->libdir.'/formslib.php'); class note_edit_form extends moodleform { /** * Define the form for editing notes */ public function definition() { $mform =& $this->_form; $mform->addElement('header', 'general', get_string('note', 'notes')); $mform->addElement('textarea', 'content', get_string('content', 'notes'), array('rows' => 15, 'cols' => 40)); $mform->setType('content', PARAM_RAW); $mform->addRule('content', get_string('nocontent', 'notes'), 'required', null, 'client'); $mform->setForceLtr('content', false); $mform->addElement('select', 'publishstate', get_string('publishstate', 'notes'), note_get_state_names()); $mform->setDefault('publishstate', NOTES_STATE_PUBLIC); $mform->setType('publishstate', PARAM_ALPHA); $mform->addHelpButton('publishstate', 'publishstate', 'notes'); $this->add_action_buttons(); $mform->addElement('hidden', 'courseid'); $mform->setType('courseid', PARAM_INT); $mform->addElement('hidden', 'userid'); $mform->setType('userid', PARAM_INT); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); } } PK{\O3T T delete.phpnu[. require_once('../config.php'); require_once('lib.php'); require_once($CFG->dirroot . '/course/lib.php'); $noteid = required_param('id', PARAM_INT); $PAGE->set_url('/notes/delete.php', array('id' => $noteid)); if (!$note = note_load($noteid)) { throw new \moodle_exception('invalidid'); } if (!$course = $DB->get_record('course', array('id' => $note->courseid))) { throw new \moodle_exception('invalidcourseid'); } require_login($course); if (empty($CFG->enablenotes)) { throw new \moodle_exception('notesdisabled', 'notes'); } if (!$user = $DB->get_record('user', array('id' => $note->userid))) { throw new \moodle_exception('invaliduserid'); } $context = context_course::instance($course->id); if (!has_capability('moodle/notes:manage', $context)) { throw new \moodle_exception('nopermissiontodelete', 'notes'); } if (data_submitted() && confirm_sesskey()) { // If data was submitted and is valid, then delete note. $returnurl = $CFG->wwwroot . '/notes/index.php?course=' . $course->id . '&user=' . $note->userid; note_delete($note); redirect($returnurl); } else { // If data was not submitted yet, then show note data with a delete confirmation form. $strnotes = get_string('notes', 'notes'); $optionsyes = array('id' => $noteid, 'sesskey' => sesskey()); $optionsno = array('course' => $course->id, 'user' => $note->userid); // Output HTML. $link = null; if (course_can_view_participants($context) || course_can_view_participants(context_system::instance())) { $link = new moodle_url('/user/index.php', array('id' => $course->id)); } $PAGE->navbar->add(get_string('participants'), $link); $PAGE->navbar->add(fullname($user), new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id))); $PAGE->navbar->add(get_string('notes', 'notes'), new moodle_url('/notes/index.php', array('user' => $user->id, 'course' => $course->id))); $PAGE->navbar->add(get_string('delete')); $PAGE->set_title($course->shortname . ': ' . $strnotes); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('deleteconfirm', 'notes'), new moodle_url('delete.php', $optionsyes), new moodle_url('index.php', $optionsno)); echo '
'; note_print($note, NOTES_SHOW_BODY | NOTES_SHOW_HEAD); echo $OUTPUT->footer(); } PK{\Yq55lib.phpnu[. /** * Library of functions and constants for notes * * @package core_notes * @copyright 2007 onwards Yu Zhang * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Constants for states. */ define('NOTES_STATE_DRAFT', 'draft'); define('NOTES_STATE_PUBLIC', 'public'); define('NOTES_STATE_SITE', 'site'); /** * Constants for note parts (flags used by note_print and note_print_list). */ define('NOTES_SHOW_FULL', 0x07); define('NOTES_SHOW_HEAD', 0x02); define('NOTES_SHOW_BODY', 0x01); define('NOTES_SHOW_FOOT', 0x04); /** * Retrieves a list of note objects with specific atributes. * * @param int $courseid id of the course in which the notes were posted (0 means any) * @param int $userid id of the user to which the notes refer (0 means any) * @param string $state state of the notes (i.e. draft, public, site) ('' means any) * @param int $author id of the user who modified the note last time (0 means any) * @param string $order an order to sort the results in * @param int $limitfrom number of records to skip (offset) * @param int $limitnum number of records to fetch * @return array of note objects */ function note_list($courseid=0, $userid=0, $state = '', $author = 0, $order='lastmodified DESC', $limitfrom=0, $limitnum=0) { global $DB; // Setup filters. $selects = array(); $params = array(); if ($courseid) { $selects[] = 'courseid=?'; $params[] = $courseid; } if ($userid) { $selects[] = 'userid=?'; $params[] = $userid; } if ($author) { $selects[] = 'usermodified=?'; $params[] = $author; } if ($state) { $selects[] = 'publishstate=?'; $params[] = $state; } $selects[] = "module=?"; $params[] = 'notes'; $select = implode(' AND ', $selects); $fields = 'id,courseid,userid,content,format,created,lastmodified,usermodified,publishstate'; return $DB->get_records_select('post', $select, $params, $order, $fields, $limitfrom, $limitnum); } /** * Retrieves a note object based on its id. * * @param int $noteid ID of the note to retrieve * @return stdClass object */ function note_load($noteid) { global $DB; $fields = 'id,courseid,userid,content,format,created,lastmodified,usermodified,publishstate'; return $DB->get_record('post', array('id' => $noteid, 'module' => 'notes'), $fields); } /** * Saves a note object. The note object is passed by reference and its fields (i.e. id) * might change during the save. * * @param stdClass $note object to save * @return boolean true if the object was saved; false otherwise */ function note_save(&$note) { global $USER, $DB; // Setup & clean fields. $note->module = 'notes'; $note->lastmodified = time(); $note->usermodified = $USER->id; if (empty($note->format)) { $note->format = FORMAT_MOODLE; } if (empty($note->publishstate)) { $note->publishstate = NOTES_STATE_PUBLIC; } if (empty(trim($note->content))) { // Don't save empty notes. return false; } // Save data. if (empty($note->id)) { // Insert new note. $note->created = $note->lastmodified; $id = $DB->insert_record('post', $note); $note = note_load($id); // Trigger event. $event = \core\event\note_created::create(array( 'objectid' => $note->id, 'courseid' => $note->courseid, 'relateduserid' => $note->userid, 'userid' => $note->usermodified, 'context' => context_course::instance($note->courseid), 'other' => array('publishstate' => $note->publishstate) )); $event->trigger(); } else { // Update old note. $DB->update_record('post', $note); $note = note_load($note->id); // Trigger event. $event = \core\event\note_updated::create(array( 'objectid' => $note->id, 'courseid' => $note->courseid, 'relateduserid' => $note->userid, 'userid' => $note->usermodified, 'context' => context_course::instance($note->courseid), 'other' => array('publishstate' => $note->publishstate) )); $event->trigger(); } unset($note->module); return true; } /** * Deletes a note object based on its id. * * @param int|object $note id of the note to delete, or a note object which is to be deleted. * @return boolean true always */ function note_delete($note) { global $DB; if (is_int($note)) { $noteid = $note; } else { $noteid = $note->id; } // Get the full record, note_load doesn't return everything. $note = $DB->get_record('post', array('id' => $noteid), '*', MUST_EXIST); $return = $DB->delete_records('post', array('id' => $note->id, 'module' => 'notes')); // Trigger event. $event = \core\event\note_deleted::create(array( 'objectid' => $note->id, 'courseid' => $note->courseid, 'relateduserid' => $note->userid, 'userid' => $note->usermodified, 'context' => context_course::instance($note->courseid), 'other' => array('publishstate' => $note->publishstate) )); $event->add_record_snapshot('post', $note); $event->trigger(); return $return; } /** * Converts a state value to its corespondent name * * @param string $state state value to convert * @return string corespondent state name */ function note_get_state_name($state) { // Cache state names. static $states; if (empty($states)) { $states = note_get_state_names(); } if (isset($states[$state])) { return $states[$state]; } else { return null; } } /** * Returns an array of mappings from state values to state names * * @return array of mappings */ function note_get_state_names() { return array( NOTES_STATE_DRAFT => get_string('personal', 'notes'), NOTES_STATE_PUBLIC => get_string('course', 'notes'), NOTES_STATE_SITE => get_string('site', 'notes'), ); } /** * Prints a note object * * @param note $note the note object to print * @param int $detail OR-ed NOTES_SHOW_xyz flags that specify which note parts to print */ function note_print($note, $detail = NOTES_SHOW_FULL) { global $CFG, $USER, $DB, $OUTPUT; if (!$user = $DB->get_record('user', array('id' => $note->userid))) { debugging("User $note->userid not found"); return; } if (!$author = $DB->get_record('user', array('id' => $note->usermodified))) { debugging("User $note->usermodified not found"); return; } $context = context_course::instance($note->courseid); $systemcontext = context_system::instance(); $authoring = new stdClass(); $authoring->name = '' . fullname($author) . ''; $authoring->date = userdate($note->lastmodified); echo '
'; // Print note head (e.g. author, user refering to, etc). if ($detail & NOTES_SHOW_HEAD) { echo '
'; echo '
'; echo $OUTPUT->user_picture($user, array('courseid' => $note->courseid)); echo fullname($user) . '
'; echo '
' . get_string('bynameondate', 'notes', $authoring) . ' (' . get_string('created', 'notes') . ': ' . userdate($note->created) . ')
'; echo '
'; } // Print note content. if ($detail & NOTES_SHOW_BODY) { echo '
'; echo format_text($note->content, $note->format, array('overflowdiv' => true)); echo '
'; } // Print note options (e.g. delete, edit). if ($detail & NOTES_SHOW_FOOT) { if (has_capability('moodle/notes:manage', $systemcontext) && $note->publishstate == NOTES_STATE_SITE || has_capability('moodle/notes:manage', $context) && ($note->publishstate == NOTES_STATE_PUBLIC || $note->usermodified == $USER->id)) { echo ''; } } echo '
'; } /** * Prints a list of note objects * * @param array $notes array of note objects to print * @param int $detail OR-ed NOTES_SHOW_xyz flags that specify which note parts to print */ function note_print_list($notes, $detail = NOTES_SHOW_FULL) { echo '
'; foreach ($notes as $note) { note_print($note, $detail); } echo '
'; } /** * Retrieves and prints a list of note objects with specific atributes. * * @param string $header HTML to print above the list * @param int $addcourseid id of the course for the add notes link (0 hide link) * @param boolean $viewnotes true if the notes should be printed; false otherwise (print notesnotvisible string) * @param int $courseid id of the course in which the notes were posted (0 means any) * @param int $userid id of the user to which the notes refer (0 means any) * @param string $state state of the notes (i.e. draft, public, site) ('' means any) * @param int $author id of the user who modified the note last time (0 means any) */ function note_print_notes($header, $addcourseid = 0, $viewnotes = true, $courseid = 0, $userid = 0, $state = '', $author = 0) { global $CFG; if ($header) { echo '

' . $header . '

'; echo '
'; } if ($addcourseid) { if ($userid) { echo '

' . get_string('addnewnote', 'notes') . '

'; } else { echo '

' . get_string('addnewnoteselect', 'notes') . '

'; } } if ($viewnotes) { $notes = note_list($courseid, $userid, $state, $author); if ($notes) { note_print_list($notes); } } else { echo '

' . get_string('notesnotvisible', 'notes') . '

'; } if ($header) { echo '
'; // The notesgroup div. } } /** * Delete all notes about users in course- * @param int $courseid * @return bool success */ function note_delete_all($courseid) { global $DB; return $DB->delete_records('post', array('module' => 'notes', 'courseid' => $courseid)); } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block */ function note_page_type_list($pagetype, $parentcontext, $currentcontext) { return array('notes-*' => get_string('page-notes-x', 'notes')); } /** * Trigger notes viewed event * * @param stdClass $context context object * @param int $userid user id (the user we are viewing the notes) * @since Moodle 2.9 */ function note_view($context, $userid) { $event = \core\event\notes_viewed::create(array( 'relateduserid' => $userid, 'context' => $context )); $event->trigger(); } /** * Add nodes to myprofile page. * * @param \core_user\output\myprofile\tree $tree Tree object * @param stdClass $user user object * @param bool $iscurrentuser * @param stdClass $course Course object * * @return bool */ function core_notes_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { global $CFG; if (empty($CFG->enablenotes)) { // Notes are disabled, nothing to do. return false; } if (isguestuser($user)) { // No notes for guest users. return false; } $url = new moodle_url("/notes/index.php", array('user' => $user->id)); $title = get_string('notes', 'core_notes'); if (empty($course)) { // Site level profile. if (!has_capability('moodle/notes:view', context_system::instance())) { // No cap, nothing to do. return false; } } else { if (!has_capability('moodle/notes:view', context_course::instance($course->id))) { // No cap, nothing to do. return false; } $url->param('course', $course->id); } $notesnode = new core_user\output\myprofile\node('miscellaneous', 'notes', $title, null, $url); $tree->add_node($notesnode); } PK{\|̐xx*classes/reportbuilder/datasource/notes.phpnu[. declare(strict_types=1); namespace core_notes\reportbuilder\datasource; use lang_string; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_notes\reportbuilder\local\entities\note; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->dirroot}/notes/lib.php"); /** * Notes datasource * * @package core_notes * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class notes extends datasource { /** * Return user friendly name of the report source * * @return string */ public static function get_name(): string { return get_string('notes', 'core_notes'); } /** * Initialise report */ protected function initialise(): void { $noteentity = new note(); $postalias = $noteentity->get_table_alias('post'); $this->set_main_table('post', $postalias); $this->add_base_condition_simple("{$postalias}.module", 'notes'); $this->add_entity($noteentity); // Join the user entity to represent the note recipient. $recipiententity = (new user()) ->set_entity_name('recipient') ->set_entity_title(new lang_string('recipient', 'core_notes')); $recipientalias = $recipiententity->get_table_alias('user'); $this->add_entity($recipiententity->add_join(" LEFT JOIN {user} {$recipientalias} ON {$recipientalias}.id = {$postalias}.userid") ); // Join the user entity to represent the note author. $authorentity = (new user()) ->set_entity_name('author') ->set_entity_title(new lang_string('author', 'core_notes')); $authoralias = $authorentity->get_table_alias('user'); $this->add_entity($authorentity->add_join(" LEFT JOIN {user} {$authoralias} ON {$authoralias}.id = {$postalias}.usermodified") ); // Join the course entity for course notes. $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->add_entity($courseentity->add_join(" LEFT JOIN {course} {$coursealias} ON {$coursealias}.id = {$postalias}.courseid AND {$postalias}.publishstate = '" . NOTES_STATE_PUBLIC . "'") ); // Add report elements from each of the entities we added to the report. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report upon creation * * @return string[] */ public function get_default_columns(): array { return [ 'recipient:fullname', 'note:publishstate', 'course:fullname', 'note:content', 'note:timecreated', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'recipient:fullname' => SORT_ASC, 'note:timecreated' => SORT_ASC, ]; } /** * Return the filters that will be added to the report upon creation * * @return string[] */ public function get_default_filters(): array { return [ 'recipient:fullname', ]; } /** * Return the conditions that will be added to the report upon creation * * @return string[] */ public function get_default_conditions(): array { return [ 'note:publishstate', 'course:fullname', 'recipient:fullname', ]; } } PK{\QZ88-classes/reportbuilder/local/entities/note.phpnu[. declare(strict_types=1); namespace core_notes\reportbuilder\local\entities; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, select, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->dirroot}/notes/lib.php"); /** * Note entity * * @package core_notes * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class note extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'post', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('note', 'core_notes'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $postalias = $this->get_table_alias('post'); // Content. $columns[] = (new column( 'content', new lang_string('content', 'core_notes'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_fields("{$postalias}.content, {$postalias}.format") ->set_is_sortable(true) ->add_callback(static function(?string $content, stdClass $note): string { if ($content === null) { return ''; } return format_text($content, $note->format); }); // Publish state. $columns[] = (new column( 'publishstate', new lang_string('publishstate', 'core_notes'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$postalias}.publishstate") ->set_is_sortable(true) ->add_callback(static function(string $publishstate): string { $states = [ NOTES_STATE_SITE => new lang_string('sitenotes', 'core_notes'), NOTES_STATE_PUBLIC => new lang_string('coursenotes', 'core_notes'), NOTES_STATE_DRAFT => new lang_string('personalnotes', 'core_notes'), ]; return (string) ($states[$publishstate] ?? $publishstate); }); // Time created. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$postalias}.created") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time modified. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$postalias}.lastmodified") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $postalias = $this->get_table_alias('post'); // Content. $filters[] = (new filter( text::class, 'content', new lang_string('content', 'core_notes'), $this->get_entity_name(), "{$postalias}.content" )) ->add_joins($this->get_joins()); // Publish state. $filters[] = (new filter( select::class, 'publishstate', new lang_string('publishstate', 'core_notes'), $this->get_entity_name(), "{$postalias}.publishstate" )) ->add_joins($this->get_joins()) ->set_options([ NOTES_STATE_SITE => new lang_string('sitenotes', 'core_notes'), NOTES_STATE_PUBLIC => new lang_string('coursenotes', 'core_notes'), NOTES_STATE_DRAFT => new lang_string('personalnotes', 'core_notes'), ]); // Time created. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$postalias}.created" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_CURRENT, date::DATE_LAST, date::DATE_RANGE, ]); // Time modified. $filters[] = (new filter( date::class, 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name(), "{$postalias}.lastmodified" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_CURRENT, date::DATE_LAST, date::DATE_RANGE, ]); return $filters; } } PK{\ʩ((classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for core_notes. * * @package core_notes * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_notes\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/notes/lib.php'); /** * Implementation of the privacy subsystem plugin provider for core_notes. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Return the fields which contain personal data. * * @param collection $items a reference to the collection to use to store the metadata. * @return collection the updated collection of metadata items. */ public static function get_metadata(collection $items): collection { // The core_notes components utilises the shared mdl_post table. $items->add_database_table( 'post', [ 'content' => 'privacy:metadata:core_notes:content', 'courseid' => 'privacy:metadata:core_notes:courseid', 'created' => 'privacy:metadata:core_notes:created', 'lastmodified' => 'privacy:metadata:core_notes:lastmodified', 'publishstate' => 'privacy:metadata:core_notes:publishstate', 'userid' => 'privacy:metadata:core_notes:userid' ], 'privacy:metadata:core_notes' ); return $items; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid the userid. * @return contextlist the list of contexts containing user info for the user. */ public static function get_contexts_for_userid(int $userid): contextlist { global $DB; $contextlist = new contextlist(); $publishstates = [ NOTES_STATE_PUBLIC, NOTES_STATE_SITE ]; list($publishstatesql, $publishstateparams) = $DB->get_in_or_equal($publishstates, SQL_PARAMS_NAMED); // Retrieve all the Course contexts associated with notes written by the user, and also written about the user. // Only notes written about the user that are public or site wide will be exported. $sql = "SELECT c.id FROM {context} c INNER JOIN {post} p ON p.courseid = c.instanceid AND c.contextlevel = :contextcoursewrittenby WHERE p.module = 'notes' AND p.usermodified = :usermodified"; $params = [ 'contextcoursewrittenby' => CONTEXT_COURSE, 'usermodified' => $userid, ]; $contextlist->add_from_sql($sql, $params); $sql = "SELECT c.id FROM {context} c INNER JOIN {post} p ON p.courseid = c.instanceid AND c.contextlevel = :contextcoursewrittenfor WHERE p.module = 'notes' AND p.userid = :userid AND p.publishstate {$publishstatesql}"; $params = [ 'contextcoursewrittenfor' => CONTEXT_COURSE, 'userid' => $userid ]; $params += $publishstateparams; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { global $DB; $context = $userlist->get_context(); if (!$context instanceof \context_course) { return; } $params = [ 'instanceid' => $context->instanceid ]; $sql = "SELECT usermodified as userid FROM {post} WHERE module = 'notes' AND courseid = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); $publishstates = [ NOTES_STATE_PUBLIC, NOTES_STATE_SITE ]; list($publishstatesql, $publishstateparams) = $DB->get_in_or_equal($publishstates, SQL_PARAMS_NAMED); $params += $publishstateparams; $sql = "SELECT userid FROM {post} WHERE module = 'notes' AND courseid = :instanceid AND publishstate {$publishstatesql}"; $userlist->add_from_sql('userid', $sql, $params); } /** * Export personal data for the given approved_contextlist. * User and context information is contained within the contextlist. * * @param approved_contextlist $contextlist a list of contexts approved for export. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); // Export all notes written by and written about the user, and organize it by the associated Course context(s). $sql = "SELECT p.courseid as courseid, p.content as content, p.publishstate as publishstate, p.userid as userid, p.usermodified as usermodified, p.created as datecreated, p.lastmodified as datemodified FROM {context} c INNER JOIN {post} p ON p.courseid = c.instanceid AND c.contextlevel = :contextcourse WHERE p.module = 'notes' AND (p.usermodified = :usermodified OR p.userid = :userid) AND c.id {$contextsql}"; $params = [ 'contextcourse' => CONTEXT_COURSE, 'usermodified' => $userid, 'userid' => $userid ]; $params += $contextparams; $notes = $DB->get_recordset_sql($sql, $params); foreach ($notes as $note) { $contextcourse = \context_course::instance($note->courseid); // The exported notes will be organized in {Course Context}/Notes/{publishstate}/usernote-{userid}.json. $subcontext = [ get_string('notes', 'notes'), $note->publishstate ]; $name = 'usernote-' . transform::user($note->userid); $notecontent = (object) [ 'content' => $note->content, 'publishstate' => $note->publishstate, 'userid' => transform::user($note->userid), 'usermodified' => transform::user($note->usermodified), 'datecreated' => transform::datetime($note->datecreated), 'datemodified' => transform::datetime($note->datemodified) ]; writer::with_context($contextcourse)->export_related_data($subcontext, $name, $notecontent); } $notes->close(); } /** * Delete all data for all users in the specified context. * * @param \context $context the context to delete in. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if ($context->contextlevel != CONTEXT_COURSE) { return; } $DB->delete_records('post', ['module' => 'notes', 'courseid' => $context->instanceid]); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); if ($context->contextlevel != CONTEXT_COURSE) { return; } $userids = $userlist->get_userids(); if (empty($userids)) { return; } list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $select = "module = :module AND courseid = :courseid AND usermodified {$usersql}"; $params = ['module' => 'notes', 'courseid' => $context->instanceid] + $userparams; $DB->delete_records_select('post', $select, $params); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist a list of contexts approved for deletion. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { $conditions = [ 'module' => 'notes', 'courseid' => $context->instanceid, 'usermodified' => $userid ]; $DB->delete_records('post', $conditions); } } } PK{\pN$ upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This file describes API changes in /notes/*, information provided here is intended especially for developers. === 3.7 === * External function core_notes_external::get_course_notes now returns this additional two fields: - canmanagesystemnotes: Whether the user can manage notes at system level. - canmanagecoursenotes: Whether the user can manage notes at the given course.PK{\Jx edit.phpnu[. require_once('../config.php'); require_once('lib.php'); require_once('edit_form.php'); require_once($CFG->dirroot . '/course/lib.php'); $noteid = optional_param('id', 0, PARAM_INT); $url = new moodle_url('/notes/edit.php'); if ($noteid) { // Existing note. $url->param('id', $noteid); if (!$note = note_load($noteid)) { throw new \moodle_exception('invalidid', 'notes'); } } else { // Adding new note. $courseid = required_param('courseid', PARAM_INT); $userid = required_param('userid', PARAM_INT); $state = optional_param('publishstate', NOTES_STATE_PUBLIC, PARAM_ALPHA); $note = new stdClass(); $note->courseid = $courseid; $note->userid = $userid; $note->publishstate = $state; $url->param('courseid', $courseid); $url->param('userid', $userid); if ($state !== NOTES_STATE_PUBLIC) { $url->param('publishstate', $state); } } $PAGE->set_url($url); if (!$course = $DB->get_record('course', array('id' => $note->courseid))) { throw new \moodle_exception('invalidcourseid'); } require_login($course); if (empty($CFG->enablenotes)) { throw new \moodle_exception('notesdisabled', 'notes'); } $context = context_course::instance($course->id); require_capability('moodle/notes:manage', $context); if (!$user = $DB->get_record('user', array('id' => $note->userid))) { throw new \moodle_exception('invaliduserid'); } $noteform = new note_edit_form(); $noteform->set_data($note); // If form was cancelled then return to the notes list of the note. if ($noteform->is_cancelled()) { redirect($CFG->wwwroot . '/notes/index.php?course=' . $note->courseid . '&user=' . $note->userid); } // If data was submitted and validated, then save it to database. if ($note = $noteform->get_data()) { if ($noteid) { // A noteid has been used, we don't allow editing of course or user so // lets unset them to be sure we never change that by accident. unset($note->courseid); unset($note->userid); } note_save($note); // Redirect to notes list that contains this note. redirect($CFG->wwwroot . '/notes/index.php?course=' . $note->courseid . '&user=' . $note->userid); } if ($noteid) { $strnotes = get_string('editnote', 'notes'); } else { $strnotes = get_string('addnewnote', 'notes'); } // Output HTML. $link = null; if (course_can_view_participants($context) || course_can_view_participants(context_system::instance())) { $link = new moodle_url('/user/index.php', array('id' => $course->id)); } $PAGE->navbar->add(get_string('participants'), $link); $PAGE->navbar->add(fullname($user), new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id))); $PAGE->navbar->add(get_string('notes', 'notes'), new moodle_url('/notes/index.php', array('user' => $user->id, 'course' => $course->id))); $PAGE->navbar->add($strnotes); $PAGE->set_title($course->shortname . ': ' . $strnotes); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading(fullname($user)); $noteform->display(); echo $OUTPUT->footer(); PK{\? index.phpnu[. /** * file index.php * index page to view notes. * if a course id is specified then the entries from that course are shown * if a user id is specified only notes related to that user are shown */ require_once('../config.php'); require_once('lib.php'); require_once($CFG->dirroot . '/course/lib.php'); $courseid = optional_param('course', SITEID, PARAM_INT); $userid = optional_param('user', 0, PARAM_INT); $filtertype = optional_param('filtertype', '', PARAM_ALPHA); $filterselect = optional_param('filterselect', 0, PARAM_INT); if (empty($CFG->enablenotes)) { throw new \moodle_exception('notesdisabled', 'notes'); } $url = new moodle_url('/notes/index.php'); if ($courseid != SITEID) { $url->param('course', $courseid); } if ($userid !== 0) { $url->param('user', $userid); } $PAGE->set_url($url); // Tabs compatibility. switch($filtertype) { case 'course': $courseid = $filterselect; break; case 'site': $courseid = SITEID; break; } if (empty($courseid)) { $courseid = SITEID; } $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); if ($userid) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); $filtertype = 'user'; $filterselect = $user->id; if ($user->deleted) { echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('userdeleted')); echo $OUTPUT->footer(); die; } } else { $filtertype = 'course'; $filterselect = $course->id; $user = $USER; } require_login($course); // Output HTML. if ($course->id == SITEID) { $coursecontext = context_system::instance(); } else { $coursecontext = context_course::instance($course->id); } require_capability('moodle/notes:view', $coursecontext); $systemcontext = context_system::instance(); // Trigger event. note_view($coursecontext, $userid); $strnotes = get_string('notes', 'notes'); if ($userid && $course->id == SITEID) { $PAGE->set_context(context_user::instance($user->id)); $PAGE->navigation->extend_for_user($user); // If we are looking at our own notes, then change focus to 'my notes'. if ($userid == $USER->id) { $notenode = $PAGE->navigation->find('notes', null)->make_inactive(); } $notesurl = new moodle_url('/notes/index.php', array('user' => $userid)); $PAGE->navbar->add(get_string('notes', 'notes'), $notesurl); } else if ($course->id != SITEID) { $notenode = $PAGE->navigation->find('currentcoursenotes', null)->make_inactive(); $notesurl = new moodle_url('/notes/index.php', array('user' => $userid, 'course' => $courseid)); $PAGE->navbar->add(get_string('notes', 'notes'), $notesurl); $PAGE->set_context(context_course::instance($courseid)); } else { $link = null; if (course_can_view_participants($coursecontext) || course_can_view_participants($systemcontext)) { $link = new moodle_url('/user/index.php', array('id' => $course->id)); } } $PAGE->set_pagelayout('incourse'); $PAGE->set_title($course->fullname); if ($course->id == SITEID) { $PAGE->set_heading(fullname($user)); } else { $PAGE->set_heading($course->fullname); } echo $OUTPUT->header(); if ($course->id != SITEID) { $backurl = new moodle_url('/user/view.php', ['id' => $userid, 'course' => $courseid]); echo $OUTPUT->single_button($backurl, get_string('back'), 'get', ['class' => 'mb-3']); $headerinfo = array('heading' => fullname($user), 'user' => $user); echo $OUTPUT->context_header($headerinfo, 2); } echo $OUTPUT->heading($strnotes); $strsitenotes = get_string('sitenotes', 'notes'); $strcoursenotes = get_string('coursenotes', 'notes'); $strpersonalnotes = get_string('personalnotes', 'notes'); $straddnewnote = get_string('addnewnote', 'notes'); echo $OUTPUT->box_start(); if ($courseid != SITEID) { $context = context_course::instance($courseid); $addid = has_capability('moodle/notes:manage', $context) ? $courseid : 0; $view = has_capability('moodle/notes:view', $context); $fullname = format_string($course->fullname, true, array('context' => $context)); note_print_notes( '' . $strsitenotes, $addid, $view, 0, $userid, NOTES_STATE_SITE, 0 ); note_print_notes( '' . $strcoursenotes. ' ('.$fullname.')', $addid, $view, $courseid, $userid, NOTES_STATE_PUBLIC, 0 ); note_print_notes( '' . $strpersonalnotes, $addid, $view, $courseid, $userid, NOTES_STATE_DRAFT, $USER->id ); } else { // Site course. $addid = has_capability('moodle/notes:manage', $systemcontext) ? SITEID : 0; $view = has_capability('moodle/notes:view', $systemcontext); note_print_notes('' . $strsitenotes, $addid, $view, 0, $userid, NOTES_STATE_SITE, 0); echo ''; if (!empty($userid)) { $courses = enrol_get_users_courses($userid); foreach ($courses as $c) { $ccontext = context_course::instance($c->id); $cfullname = format_string($c->fullname, true, array('context' => $ccontext)); $header = '' . $cfullname . ''; $viewcoursenotes = has_capability('moodle/notes:view', $ccontext); if (has_capability('moodle/notes:manage', $ccontext)) { $addid = $c->id; } else { $addid = 0; } note_print_notes($header, $addid, $viewcoursenotes, $c->id, $userid, NOTES_STATE_PUBLIC, 0); } } } echo $OUTPUT->box_end(); echo $OUTPUT->footer(); PK{\|a)a)-tests/reportbuilder/datasource/notes_test.phpnu[. declare(strict_types=1); namespace core_notes\reportbuilder\datasource; use core_notes_generator; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{date, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for notes datasource * * @package core_notes * @covers \core_notes\reportbuilder\datasource\notes * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class notes_test extends core_reportbuilder_testcase { /** * Load required test libraries */ public static function setUpBeforeClass(): void { global $CFG; require_once("{$CFG->dirroot}/notes/lib.php"); parent::setUpBeforeClass(); } /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); // Our first user will create a course note. $course = $this->getDataGenerator()->create_course(); $userone = $this->getDataGenerator()->create_and_enrol($course, 'student', ['firstname' => 'Zoe']); $coursenote = $notesgenerator->create_instance(['courseid' => $course->id, 'userid' => $userone->id, 'content' => 'Course', 'publishstate' => NOTES_STATE_PUBLIC]); // Our second user will create a personal and site note. $usertwo = $this->getDataGenerator()->create_user(['firstname' => 'Amy']); $personalnote = $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $usertwo->id, 'content' => 'Personal', 'publishstate' => NOTES_STATE_DRAFT]); $this->waitForSecond(); // For consistent ordering we need distinct time for second user notes. $sitenote = $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $usertwo->id, 'content' => 'Site', 'publishstate' => NOTES_STATE_SITE]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Notes', 'source' => notes::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are recipient, publishstate, course, note, time created. Sorted by recipient and time created. $this->assertEquals([ [fullname($usertwo), 'Personal notes', '', 'Personal', userdate($personalnote->created)], [fullname($usertwo), 'Site notes', '', 'Site', userdate($sitenote->created)], [fullname($userone), 'Course notes', $course->fullname, 'Course', userdate($coursenote->created)], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { global $DB; $this->resetAfterTest(); $recipient = $this->getDataGenerator()->create_user(); $author = $this->getDataGenerator()->create_user(); $this->setUser($author); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); $note = $notesgenerator->create_instance(['courseid' => SITEID, 'publishstate' => NOTES_STATE_SITE, 'content' => 'Cool', 'userid' => $recipient->id, ]); // Manually update the created/modified date of the note. $note->created = 1654038000; $note->lastmodified = $note->created + HOURSECS; $DB->update_record('post', $note); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Notes', 'source' => notes::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'note:content']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'note:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'note:timemodified']); // Ensure we can add data from both user entities. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'recipient:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'author:fullname']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); $this->assertEquals([ 'Cool', userdate($note->created), userdate($note->lastmodified), fullname($recipient), fullname($author), ], array_values($content[0])); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ 'Filter content' => ['content', 'Cool', 'note:content', [ 'note:content_operator' => text::IS_EQUAL_TO, 'note:content_value' => 'Cool', ], true], 'Filter content (no match)' => ['content', 'Cool', 'note:content', [ 'note:content_operator' => text::DOES_NOT_CONTAIN, 'note:content_value' => 'Cool', ], false], 'Filter publish state' => ['publishstate', 'site', 'note:publishstate', [ 'note:publishstate_operator' => select::EQUAL_TO, 'note:publishstate_value' => 'site', ], true], 'Filter publish state (no match)' => ['publishstate', 'site', 'note:publishstate', [ 'note:publishstate_operator' => select::EQUAL_TO, 'note:publishstate_value' => 'public', ], false], 'Filter time created' => ['created', 1654038000, 'note:timecreated', [ 'note:timecreated_operator' => date::DATE_RANGE, 'note:timecreated_from' => 1622502000, ], true], 'Filter time created (no match)' => ['created', 1654038000, 'note:timecreated', [ 'note:timecreated_operator' => date::DATE_RANGE, 'note:timecreated_to' => 1622502000, ], false], 'Filter time modified' => ['lastmodified', 1654038000, 'note:timemodified', [ 'note:timemodified_operator' => date::DATE_RANGE, 'note:timemodified_from' => 1622502000, ], true], 'Filter time modified (no match)' => ['lastmodified', 1654038000, 'note:timemodified', [ 'note:timemodified_operator' => date::DATE_RANGE, 'note:timemodified_to' => 1622502000, ], false], ]; } /** * Test datasource filters * * @param string $field * @param mixed $value * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters( string $field, $value, string $filtername, array $filtervalues, bool $expectmatch ): void { global $DB; $this->resetAfterTest(); $recipient = $this->getDataGenerator()->create_user(); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); // Create default note, then manually override one of it's properties to use for filtering. $note = $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $recipient->id]); $DB->set_field('post', $field, $value, ['id' => $note->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single recipient column, and given filter. $report = $generator->create_report(['name' => 'Notes', 'source' => notes::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'recipient:fullname']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals(fullname($recipient), reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $recipient = $this->getDataGenerator()->create_user(); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $recipient->id]); $this->datasource_stress_test_columns(notes::class); $this->datasource_stress_test_columns_aggregation(notes::class); $this->datasource_stress_test_conditions(notes::class, 'note:content'); } } PK{\q`tests/generator/lib.phpnu[. /** * core_notes data generator. * * @package core_notes * @category test * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * core_notes data generator class. * * @package core_notes * @category test * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_notes_generator extends component_generator_base { /** * @var number of created instances */ protected $instancecount = 0; /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { $this->instancecount = 0; } /** * Create a new note. * * @param array|stdClass $record * @throws coding_exception * @return stdClass activity record with extra cmid field */ public function create_instance($record = null) { global $CFG, $USER; require_once("$CFG->dirroot/notes/lib.php"); $this->instancecount++; $i = $this->instancecount; $record = (object)(array)$record; if (empty($record->courseid)) { throw new coding_exception('Module generator requires $record->courseid.'); } if (empty($record->userid)) { throw new coding_exception('Module generator requires $record->userid.'); } if (!isset($record->module)) { $record->module = 'notes'; } if (!isset($record->groupid)) { $record->groupid = 0; } if (!isset($record->moduleid)) { $record->moduleid = 0; } if (!isset($record->coursemoduleid)) { $record->coursemoduleid = 0; } if (!isset($record->subject)) { $record->subject = ''; } if (!isset($record->summary)) { $record->summary = null; } if (!isset($record->content)) { $record->content = "This is test generated note - $i ."; } if (!isset($record->uniquehash)) { $record->uniquehash = ''; } if (!isset($record->rating)) { $record->rating = 0; } if (!isset($record->format)) { $record->format = FORMAT_PLAIN; } if (!isset($record->summaryformat)) { $record->summaryformat = FORMAT_MOODLE; } if (!isset($record->attachment)) { $record->attachment = null; } if (!isset($record->publishstate)) { $record->publishstate = NOTES_STATE_SITE; } if (!isset($record->lastmodified)) { $record->lastmodified = time(); } if (!isset($record->created)) { $record->created = time(); } if (!isset($record->usermodified)) { $record->usermodified = $USER->id; } note_save($record); return $record; } } PK{\ hhtests/privacy/provider_test.phpnu[. /** * Unit tests for the core_notes implementation of the privacy API. * * @package core_notes * @category test * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_notes\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . "/notes/lib.php"); use core_notes\privacy\provider; use core_privacy\local\request\writer; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; /** * Unit tests for the core_notes implementation of the privacy API. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_notes\privacy\provider */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { global $DB; // Test setup. $this->resetAfterTest(true); $this->setAdminUser(); set_config('enablenotes', true); $teacher1 = $this->getDataGenerator()->create_user(); $this->setUser($teacher1); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $student = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); // Create Courses, then enrol a teacher and a student. $nocourses = 5; $courses = []; $coursecontextids = []; for ($c = 1; $c <= $nocourses; $c++) { $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); role_assign($teacherrole->id, $teacher1->id, $coursecontext->id); role_assign($studentrole->id, $student->id, $coursecontext->id); // Only create private user notes (i.e. NOTES_STATE_DRAFT) for student in Course 1, 2, 3 written by the teacher. if ($c <= 3) { $this->help_create_user_note( $student->id, NOTES_STATE_DRAFT, $course->id, "Test private user note about the student in Course $c by the teacher" ); } $courses[$c] = $course; $coursecontextids[] = $coursecontext->id; } // Test Teacher 1's contexts equals 3 because only 3 user notes were added for Course 1, 2, and 3. // Course 4 and 5 does not have any notes associated with it, so the contexts should not be returned. $contexts = provider::get_contexts_for_userid($teacher1->id); $this->assertCount(3, $contexts->get_contextids()); // Test the Student's contexts is 0 because the notes written by the teacher are private. $contexts = provider::get_contexts_for_userid($student->id); $this->assertCount(0, $contexts->get_contextids()); // Add a public user note (i.e. NOTES_STATE_PUBLIC) written by the Teacher about the Student in Course 4. $course = $courses[4]; $this->help_create_user_note( $student->id, NOTES_STATE_PUBLIC, $course->id, "Test public user note about the student in Course 4 by the teacher" ); // Test Teacher 1's contexts equals 4 after adding a public note about a student in Course 4. $contexts = provider::get_contexts_for_userid($teacher1->id); $this->assertCount(4, $contexts->get_contextids()); // Test the Student's contexts is 1 for Course 4 because there is a public note written by the teacher. $contexts = provider::get_contexts_for_userid($student->id); $this->assertCount(1, $contexts->get_contextids()); // Add a site-wide user note (i.e. NOTES_STATE_SITE) written by the Teacher 1 about the Student in Course 3. $course = $courses[3]; $this->help_create_user_note( $student->id, NOTES_STATE_SITE, $course->id, "Test site-wide user note about the student in Course 3 by the teacher" ); // Test the Student's contexts is 2 for Courses 3, 4 because there is a public and site-wide note written by the Teacher. $contexts = provider::get_contexts_for_userid($student->id); $this->assertCount(2, $contexts->get_contextids()); // Add a site-wide user note for the Teacher 1 by another Teacher 2 in Course 5. $teacher2 = $this->getDataGenerator()->create_user(); $this->setUser($teacher2); $course = $courses[5]; $this->help_create_user_note( $teacher1->id, NOTES_STATE_SITE, $course->id, "Test site-wide user note about the teacher in Course 5 by another teacher" ); // Test Teacher 1's contexts equals 5 after adding the note from another teacher. $contextlist = provider::get_contexts_for_userid($teacher1->id); $this->assertCount(5, $contextlist->get_contextids()); // Test Teacher 1's contexts match the contexts of the Courses associated with notes created. $this->assertEmpty(array_diff($coursecontextids, $contextlist->get_contextids())); } /** * Test for provider::export_user_data(). */ public function test_export_user_data(): void { global $DB; // Test setup. $this->resetAfterTest(true); $this->setAdminUser(); set_config('enablenotes', true); $teacher1 = $this->getDataGenerator()->create_user(); $this->setUser($teacher1); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $nocourses = 5; $nostudents = 2; $studentrole = $DB->get_record('role', array('shortname' => 'student')); $courses = []; $coursecontextids = []; for ($c = 1; $c <= $nocourses; $c++) { // Create a Course, then enrol a teacher and enrol 2 students. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); role_assign($teacherrole->id, $teacher1->id, $coursecontext->id); // Only create public user notes (i.e. NOTES_STATE_PUBLIC) for students in Course 1, 2, 3 written by the teacher. if ($c <= 3) { for ($s = 0; $s < $nostudents; $s++) { $student = $this->getDataGenerator()->create_user(); role_assign($studentrole->id, $student->id, $coursecontext->id); // Create test public user note data written for students by the teacher. $this->help_create_user_note( $student->id, NOTES_STATE_PUBLIC, $course->id, "Test public user note for student $s in Course $c by the teacher" ); } // Store the Course context for those which have test notes added for verification. $coursecontextids[] = $coursecontext->id; } $courses[$c] = $course; } // Add a site-wide user note for Teacher 1 by another Teacher 2 in Course 4. $teacher2 = $this->getDataGenerator()->create_user(); $this->setUser($teacher2); $course = $courses[4]; $this->help_create_user_note( $teacher1->id, NOTES_STATE_SITE, $course->id, "Test site-wide user note about the teacher in Course 4 by another teacher" ); // Store the Course context for those which have test notes added for verification. $coursecontextids[] = \context_course::instance($course->id)->id; // Add a private user note for Teacher 1 by another Teacher 2 in Course 5. $course = $courses[5]; $this->help_create_user_note( $teacher1->id, NOTES_STATE_DRAFT, $course->id, "Test private user note about the teacher in Course 5 by another teacher" ); // Test the number of contexts returned matches the Course contexts created with notes. $contextlist = provider::get_contexts_for_userid($teacher1->id); $this->assertEmpty(array_diff($coursecontextids, $contextlist->get_contextids())); $approvedcontextlist = new approved_contextlist($teacher1, 'core_notes', $contextlist->get_contextids()); // Retrieve User notes created by the teacher. provider::export_user_data($approvedcontextlist); // Test the core_notes data is exported at the Course context level and has content. foreach ($contextlist as $context) { $this->assertEquals(CONTEXT_COURSE, $context->contextlevel); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); } } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; // Test setup. $this->resetAfterTest(true); $this->setAdminUser(); set_config('enablenotes', true); $teacher = $this->getDataGenerator()->create_user(); $this->setUser($teacher); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $nocourses = 2; $nostudents = 5; $nonotes = 7; $studentrole = $DB->get_record('role', array('shortname' => 'student')); $n = 0; for ($c = 0; $c < $nocourses; $c++) { // Create a Course, then enrol a teacher and enrol 2 students. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); role_assign($teacherrole->id, $teacher->id, $coursecontext->id); for ($s = 0; $s < $nostudents; $s++) { if ($n < $nonotes) { $student = $this->getDataGenerator()->create_user(); role_assign($studentrole->id, $student->id, $coursecontext->id); // Create test note data. $this->help_create_user_note( $student->id, NOTES_STATE_PUBLIC, $course->id, "Test user note for student $s in Course $c" ); } $n++; } } // Test the number of contexts returned equals the number of Courses created with user notes for its students. $contextlist = provider::get_contexts_for_userid($teacher->id); $this->assertCount($nocourses, $contextlist->get_contextids()); // Test the created user note records in mdl_post table matches the test number of user notes specified. $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]); $this->assertCount($nonotes, $notes); // Delete all user note records in mdl_post table by the specified Course context. foreach ($contextlist->get_contexts() as $context) { provider::delete_data_for_all_users_in_context($context); } // Test the core_note records in mdl_post table is equals zero. $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]); $this->assertCount(0, $notes); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; // Test setup. $this->resetAfterTest(true); $this->setAdminUser(); set_config('enablenotes', true); $teacher = $this->getDataGenerator()->create_user(); $this->setUser($teacher); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); $nocourses = 2; $nostudents = 5; $nonotes = 7; $studentrole = $DB->get_record('role', array('shortname' => 'student')); $n = 0; for ($c = 0; $c < $nocourses; $c++) { // Create a Course, then enrol a teacher and enrol 2 students. $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); role_assign($teacherrole->id, $teacher->id, $coursecontext->id); for ($s = 0; $s < $nostudents; $s++) { if ($n < $nonotes) { $student = $this->getDataGenerator()->create_user(); role_assign($studentrole->id, $student->id, $coursecontext->id); // Create test note data. $this->help_create_user_note( $student->id, NOTES_STATE_PUBLIC, $course->id, "Test user note for student $s in Course $c" ); } $n++; } } // Test the number of contexts returned equals the number of Courses created with user notes for its students. $contextlist = provider::get_contexts_for_userid($teacher->id); $this->assertCount($nocourses, $contextlist->get_contextids()); // Test the created user note records in mdl_post table matches the test number of user notes specified. $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]); $this->assertCount($nonotes, $notes); // Delete all user note records in mdl_post table created by the specified teacher. $approvedcontextlist = new approved_contextlist($teacher, 'core_notes', $contextlist->get_contextids()); provider::delete_data_for_user($approvedcontextlist); // Test the core_note records in mdl_post table is equals zero. $notes = $DB->get_records('post', ['module' => 'notes', 'usermodified' => $teacher->id]); $this->assertCount(0, $notes); } /** * Test that only users within a course context are fetched. */ public function test_get_users_in_context(): void { global $DB; $this->resetAfterTest(true); $component = 'core_notes'; // Test setup. $this->setAdminUser(); set_config('enablenotes', true); // Create a teacher. $teacher1 = $this->getDataGenerator()->create_user(); $this->setUser($teacher1); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); // Create a student. $student = $this->getDataGenerator()->create_user(); // Create student2. $student2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); // Create courses, then enrol a teacher and a student. $nocourses = 3; for ($c = 1; $c <= $nocourses; $c++) { ${'course' . $c} = $this->getDataGenerator()->create_course(); ${'coursecontext' . $c} = \context_course::instance(${'course' . $c}->id); role_assign($teacherrole->id, $teacher1->id, ${'coursecontext' . $c}->id); role_assign($studentrole->id, $student->id, ${'coursecontext' . $c}->id); role_assign($studentrole->id, $student2->id, ${'coursecontext' . $c}->id); } // The list of users in coursecontext1 should be empty (related data still have not been created). $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // The list of users in coursecontext2 should be empty (related data still have not been created). $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(0, $userlist2); // The list of users in coursecontext3 should be empty (related data still have not been created). $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component); provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); // Create private user notes (i.e. NOTES_STATE_DRAFT) for student in course1 and course2 written by the teacher. $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course1->id, "Test private user note about the student in Course 1 by the teacher"); $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course2->id, "Test private user note about the student in Course 2 by the teacher"); // The list of users in coursecontext1 should return one user (teacher1). provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $this->assertTrue(in_array($teacher1->id, $userlist1->get_userids())); // The list of users in coursecontext2 should return one user (teacher1). provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $this->assertTrue(in_array($teacher1->id, $userlist2->get_userids())); // The list of users in coursecontext3 should not return any users. provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); // Create public user note (i.e. NOTES_STATE_PUBLIC) for student in course3 written by the teacher. $this->help_create_user_note($student->id, NOTES_STATE_PUBLIC, $course3->id, "Test public user note about the student in Course 3 by the teacher"); // The list of users in coursecontext3 should return 2 users (teacher and student). provider::get_users_in_context($userlist3); $this->assertCount(2, $userlist3); $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids())); $this->assertTrue(in_array($student->id, $userlist3->get_userids())); // Create site user note (i.e. NOTES_STATE_SITE) for student2 in course3 written by the teacher. $this->help_create_user_note($student2->id, NOTES_STATE_SITE, $course3->id, "Test site-wide user note about student2 in Course 3 by the teacher" ); // The list of users in coursecontext3 should return 3 users (teacher, student and student2). provider::get_users_in_context($userlist3); $this->assertCount(3, $userlist3); $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids())); $this->assertTrue(in_array($student->id, $userlist3->get_userids())); $this->assertTrue(in_array($student2->id, $userlist3->get_userids())); // The list of users should not return any users in a different context than course context. $contextsystem = \context_system::instance(); $userlist4 = new \core_privacy\local\request\userlist($contextsystem, $component); provider::get_users_in_context($userlist4); $this->assertCount(0, $userlist4); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users(): void { global $DB; $this->resetAfterTest(true); $component = 'core_notes'; // Test setup. $this->setAdminUser(); set_config('enablenotes', true); // Create a teacher. $teacher1 = $this->getDataGenerator()->create_user(); $this->setUser($teacher1); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); // Create a student. $student = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); // Create Courses, then enrol a teacher and a student. $nocourses = 3; for ($c = 1; $c <= $nocourses; $c++) { ${'course' . $c} = $this->getDataGenerator()->create_course(); ${'coursecontext' . $c} = \context_course::instance(${'course' . $c}->id); role_assign($teacherrole->id, $teacher1->id, ${'coursecontext' . $c}->id); role_assign($studentrole->id, $student->id, ${'coursecontext' . $c}->id); } // Create private notes for student in the course1 and course2 written by the teacher. $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course1->id, "Test private user note about the student in Course 1 by the teacher"); $this->help_create_user_note($student->id, NOTES_STATE_DRAFT, $course2->id, "Test private user note about the student in Course 2 by the teacher"); // Create public notes for student in the course3 written by the teacher. $this->help_create_user_note($student->id, NOTES_STATE_PUBLIC, $course3->id, "Test public user note about the student in Course 3 by the teacher"); // The list of users in coursecontext1 should return one user (teacher1). $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $this->assertTrue(in_array($teacher1->id, $userlist1->get_userids())); // The list of users in coursecontext2 should return one user (teacher1). $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $this->assertTrue(in_array($teacher1->id, $userlist2->get_userids())); // The list of users in coursecontext3 should return two users (teacher1 and student). $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component); provider::get_users_in_context($userlist3); $this->assertCount(2, $userlist3); $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids())); $this->assertTrue(in_array($student->id, $userlist3->get_userids())); $approvedlist = new approved_userlist($coursecontext3, $component, [$student->id]); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in the coursecontext3. $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component); // The user data in coursecontext3 should not be removed. provider::get_users_in_context($userlist3); $this->assertCount(2, $userlist3); $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids())); $this->assertTrue(in_array($student->id, $userlist3->get_userids())); $approvedlist = new approved_userlist($coursecontext3, $component, [$teacher1->id]); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in the coursecontext3. $userlist3 = new \core_privacy\local\request\userlist($coursecontext3, $component); // The user data in coursecontext3 should be removed. provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); // Re-fetch users in the coursecontext1. $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $approvedlist = new approved_userlist($coursecontext1, $component, [$student->id]); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in the coursecontext1. $userlist3 = new \core_privacy\local\request\userlist($coursecontext1, $component); // The user data in coursecontext1 should not be removed. provider::get_users_in_context($userlist3); $this->assertCount(1, $userlist3); $this->assertTrue(in_array($teacher1->id, $userlist3->get_userids())); $approvedlist = new approved_userlist($coursecontext1, $component, [$teacher1->id]); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in the coursecontext1. $userlist3 = new \core_privacy\local\request\userlist($coursecontext1, $component); // The user data in coursecontext1 should be removed. provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); // Re-fetch users in the coursecontext2. $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // The list of users should not return any users for contexts different than course context. $systemcontext = \context_system::instance(); $userlist4 = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist4); $this->assertCount(0, $userlist4); } /** * Helper function to create user notes for testing. * * @param int $userid The ID of the User associated with the note. * @param string $state The publish status * @param int $courseid The ID of the Course associated with the note. * @param string $content The note content. */ protected function help_create_user_note($userid, $state, $courseid, $content) { $note = (object) [ 'userid' => $userid, 'publishstate' => $state, 'courseid' => $courseid, 'content' => $content, ]; note_save($note); } } PK{\*/^^tests/lib_test.phpnu[. /** * Tests for notes library functions. * * @package core_notes * @copyright 2015 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace core_notes; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/notes/lib.php'); /** * Class core_notes_lib_testcase * * @package core_notes * @copyright 2015 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class lib_test extends \advanced_testcase { /** * @var stdClass The user. */ private $user; /** * @var stdClass The course. */ private $course; /** * @var \core_user\output\myprofile\tree The navigation tree. */ private $tree; public function setUp(): void { parent::setUp(); $this->user = $this->getDataGenerator()->create_user(); $this->course = $this->getDataGenerator()->create_course(); $this->tree = new \core_user\output\myprofile\tree(); $this->resetAfterTest(); } /** * Tests the core_notes_myprofile_navigation() function. */ public function test_core_notes_myprofile_navigation(): void { global $USER; // Set up the test. $this->setAdminUser(); $iscurrentuser = true; // Enable notes. set_config('enablenotes', true); // Check the node tree is correct. core_notes_myprofile_navigation($this->tree, $USER, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayHasKey('notes', $nodes->getValue($this->tree)); } /** * Tests the core_notes_myprofile_navigation() function. */ public function test_core_notes_myprofile_navigation_as_guest(): void { global $USER; $this->setGuestUser(); $iscurrentuser = false; // Check the node tree is correct. core_notes_myprofile_navigation($this->tree, $USER, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('notes', $nodes->getValue($this->tree)); } /** * Tests the core_notes_myprofile_navigation() function. */ public function test_core_notes_myprofile_navigation_notes_disabled(): void { global $USER; $this->setAdminUser(); $iscurrentuser = false; // Disable notes. set_config('enablenotes', false); // Check the node tree is correct. core_notes_myprofile_navigation($this->tree, $USER, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('notes', $nodes->getValue($this->tree)); } } PK{\kd_fVVtests/externallib_test.phpnu[. /** * External notes functions unit tests * * @package core_notes * @category external * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_notes; use core_external\external_api; use core_notes_external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/notes/externallib.php'); final class externallib_test extends externallib_advanced_testcase { /** * Test create_notes */ public function test_create_notes(): void { global $DB, $USER; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); // Set the required capabilities by the external function. $contextid = \context_course::instance($course->id)->id; $roleid = $this->assignUserCapability('moodle/notes:manage', $contextid); $this->assignUserCapability('moodle/course:view', $contextid, $roleid); // Create test note data. $note1 = array(); $note1['userid'] = $USER->id; $note1['publishstate'] = 'personal'; $note1['courseid'] = $course->id; $note1['text'] = 'the text'; $note1['clientnoteid'] = 4; $notes = array($note1); $creatednotes = core_notes_external::create_notes($notes); // We need to execute the return values cleaning process to simulate the web service server. $creatednotes = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes); $thenote = $DB->get_record('post', array('id' => $creatednotes[0]['noteid'])); // Confirm that base note data was inserted correctly. $this->assertEquals($thenote->userid, $note1['userid']); $this->assertEquals($thenote->courseid, $note1['courseid']); $this->assertEquals($thenote->publishstate, NOTES_STATE_DRAFT); $this->assertEquals($thenote->content, $note1['text']); $this->assertEquals($creatednotes[0]['clientnoteid'], $note1['clientnoteid']); // Call without required capability. $this->unassignUserCapability('moodle/notes:manage', $contextid, $roleid); $this->expectException('\required_capability_exception'); $creatednotes = core_notes_external::create_notes($notes); } public function test_delete_notes(): void { global $DB, $USER; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); // Set the required capabilities by the external function. $contextid = \context_course::instance($course->id)->id; $roleid = $this->assignUserCapability('moodle/notes:manage', $contextid); $this->assignUserCapability('moodle/course:view', $contextid, $roleid); // Create test note data. $cnote = array(); $cnote['userid'] = $USER->id; $cnote['publishstate'] = 'personal'; $cnote['courseid'] = $course->id; $cnote['text'] = 'the text'; $cnote['clientnoteid'] = 4; $cnotes = array($cnote); $creatednotes = core_notes_external::create_notes($cnotes); $creatednotes = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes); $dnotes1 = array($creatednotes[0]['noteid']); $deletednotes1 = core_notes_external::delete_notes($dnotes1); $deletednotes1 = external_api::clean_returnvalue(core_notes_external::delete_notes_returns(), $deletednotes1); // Confirm that base note data was deleted correctly. $notdeletedcount = $DB->count_records_select('post', 'id = ' . $creatednotes[0]['noteid']); $this->assertEquals(0, $notdeletedcount); $dnotes2 = array(33); // This note does not exist. $deletednotes2 = core_notes_external::delete_notes($dnotes2); $deletednotes2 = external_api::clean_returnvalue(core_notes_external::delete_notes_returns(), $deletednotes2); $this->assertEquals("note", $deletednotes2[0]["item"]); $this->assertEquals(33, $deletednotes2[0]["itemid"]); $this->assertEquals("badid", $deletednotes2[0]["warningcode"]); $this->assertEquals("Note does not exist", $deletednotes2[0]["message"]); // Call without required capability. $creatednotes = core_notes_external::create_notes($cnotes); $creatednotes = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes); $dnotes3 = array($creatednotes[0]['noteid']); $this->unassignUserCapability('moodle/notes:manage', $contextid, $roleid); $this->expectException('\required_capability_exception'); $deletednotes = core_notes_external::delete_notes($dnotes3); $deletednotes = external_api::clean_returnvalue(core_notes_external::delete_notes_returns(), $deletednotes); } public function test_get_notes(): void { global $DB, $USER; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); // Set the required capabilities by the external function. $contextid = \context_course::instance($course->id)->id; $roleid = $this->assignUserCapability('moodle/notes:manage', $contextid); $this->assignUserCapability('moodle/notes:view', $contextid, $roleid); $this->assignUserCapability('moodle/course:view', $contextid, $roleid); // Create test note data. $cnote = array(); $cnote['userid'] = $USER->id; $cnote['publishstate'] = 'personal'; $cnote['courseid'] = $course->id; $cnote['text'] = 'the text'; $cnotes = array($cnote); $creatednotes1 = core_notes_external::create_notes($cnotes); $creatednotes2 = core_notes_external::create_notes($cnotes); $creatednotes3 = core_notes_external::create_notes($cnotes); $creatednotes1 = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes1); $creatednotes2 = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes2); $creatednotes3 = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes3); // Note 33 does not exist. $gnotes = array($creatednotes1[0]['noteid'], $creatednotes2[0]['noteid'], $creatednotes3[0]['noteid'], 33); $getnotes = core_notes_external::get_notes($gnotes); $getnotes = external_api::clean_returnvalue(core_notes_external::get_notes_returns(), $getnotes); $this->unassignUserCapability('moodle/notes:manage', $contextid, $roleid); // Confirm that base note data was retrieved correctly. $this->assertEquals($cnote['userid'], $getnotes["notes"][0]["userid"]); $this->assertEquals($cnote['text'], $getnotes["notes"][0]["text"]); $this->assertEquals($cnote['userid'], $getnotes["notes"][1]["userid"]); $this->assertEquals($cnote['text'], $getnotes["notes"][1]["text"]); $this->assertEquals($cnote['userid'], $getnotes["notes"][2]["userid"]); $this->assertEquals($cnote['text'], $getnotes["notes"][2]["text"]); $this->assertEquals("note", $getnotes["warnings"][0]["item"]); $this->assertEquals(33, $getnotes["warnings"][0]["itemid"]); $this->assertEquals("badid", $getnotes["warnings"][0]["warningcode"]); $this->assertEquals("Note does not exist", $getnotes["warnings"][0]["message"]); // Call without required capability. $this->unassignUserCapability('moodle/notes:view', $contextid, $roleid); $this->expectException('\required_capability_exception'); $creatednotes = core_notes_external::get_notes($gnotes); } public function test_update_notes(): void { global $DB, $USER; $this->resetAfterTest(true); $course = self::getDataGenerator()->create_course(); // Set the required capabilities by the external function. $contextid = \context_course::instance($course->id)->id; $roleid = $this->assignUserCapability('moodle/notes:manage', $contextid); $this->assignUserCapability('moodle/course:view', $contextid, $roleid); // Create test note data. $note1 = array(); $note1['userid'] = $USER->id; $note1['publishstate'] = 'personal'; $note1['courseid'] = $course->id; $note1['text'] = 'the text'; $note2['userid'] = $USER->id; $note2['publishstate'] = 'course'; $note2['courseid'] = $course->id; $note2['text'] = 'the text'; $note3['userid'] = $USER->id; $note3['publishstate'] = 'site'; $note3['courseid'] = $course->id; $note3['text'] = 'the text'; $notes1 = array($note1, $note2, $note3); $creatednotes = core_notes_external::create_notes($notes1); $creatednotes = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes); $note2 = array(); $note2["id"] = $creatednotes[0]['noteid']; $note2['publishstate'] = 'personal'; $note2['text'] = 'the new text'; $note2['format'] = FORMAT_HTML; $notes2 = array($note2); $updatednotes = core_notes_external::update_notes($notes2); $updatednotes = external_api::clean_returnvalue(core_notes_external::update_notes_returns(), $updatednotes); $thenote = $DB->get_record('post', array('id' => $creatednotes[0]['noteid'])); // Confirm that base note data was updated correctly. $this->assertEquals($thenote->publishstate, NOTES_STATE_DRAFT); $this->assertEquals($note2['text'], $thenote->content); // Call without required capability. $creatednotes = core_notes_external::create_notes($notes1); $creatednotes = external_api::clean_returnvalue(core_notes_external::create_notes_returns(), $creatednotes); $this->unassignUserCapability('moodle/notes:manage', $contextid, $roleid); $this->expectException('\required_capability_exception'); $note2 = array(); $note2["id"] = $creatednotes[0]['noteid']; $note2['publishstate'] = 'personal'; $note2['text'] = 'the new text'; $note2['format'] = FORMAT_HTML; $notes2 = array($note2); $updatednotes = core_notes_external::update_notes($notes2); $updatednotes = external_api::clean_returnvalue(core_notes_external::update_notes_returns(), $updatednotes); } /** * Test get_course_notes */ public function test_get_course_notes(): void { global $DB, $CFG; $this->resetAfterTest(true); $CFG->enablenotes = true; // Take role definitions. $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); // Create students and teachers. $student1 = $this->getDataGenerator()->create_user(); $student2 = $this->getDataGenerator()->create_user(); $teacher1 = $this->getDataGenerator()->create_user(); $teacher2 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); // Enroll students and teachers to COURSE-1. $this->getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student2->id, $course1->id, $studentrole->id); $this->getDataGenerator()->enrol_user($teacher1->id, $course1->id, $teacherrole->id); $this->getDataGenerator()->enrol_user($teacher2->id, $course1->id, $teacherrole->id); // Enroll students and teachers to COURSE-2 (teacher1 is not enrolled in Course 2). $this->getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student2->id, $course2->id, $studentrole->id); $this->getDataGenerator()->enrol_user($teacher2->id, $course2->id, $teacherrole->id); // Generate notes. $gen = $this->getDataGenerator()->get_plugin_generator('core_notes'); $this->setUser($teacher1); // NoteA1: on student1 (Course1) by Teacher1. $params = array('courseid' => $course1->id, 'userid' => $student1->id, 'publishstate' => NOTES_STATE_PUBLIC, 'usermodified' => $teacher1->id); $notea1 = $gen->create_instance($params); // NoteA2: on student1 (Course1) by Teacher1. $params = array('courseid' => $course1->id, 'userid' => $student1->id, 'publishstate' => NOTES_STATE_PUBLIC, 'usermodified' => $teacher1->id); $notea2 = $gen->create_instance($params); // NoteS1: on student1 SITE-LEVEL by teacher1. $params = array('courseid' => $course1->id, 'userid' => $student1->id, 'publishstate' => NOTES_STATE_SITE, 'usermodified' => $teacher1->id); $notes1 = $gen->create_instance($params); // NoteP1: on student1 PERSONAL by teacher1. $params = array('courseid' => $course1->id, 'userid' => $student1->id, 'publishstate' => NOTES_STATE_DRAFT, 'usermodified' => $teacher1->id); $notep1 = $gen->create_instance($params); // NoteB1: on student1 (Course2) by teacher1. $params = array('courseid' => $course2->id, 'userid' => $student1->id, 'publishstate' => NOTES_STATE_PUBLIC, 'usermodified' => $teacher1->id); $noteb1 = $gen->create_instance($params); // Retrieve notes, normal case. $result = core_notes_external::get_course_notes($course1->id, $student1->id); $result = external_api::clean_returnvalue(core_notes_external::get_course_notes_returns(), $result); $this->assertEquals($notes1->id, $result['sitenotes'][0]['id']); $this->assertCount(2, $result['coursenotes']); // Teacher can manage only the course notes. $this->assertFalse($result['canmanagesystemnotes']); $this->assertTrue($result['canmanagecoursenotes']); foreach ($result['coursenotes'] as $coursenote) { if ($coursenote['id'] != $notea1->id and $coursenote['id'] != $notea2->id) { $this->fail('the returned notes ids does not match with the created ones'); } } $this->assertEquals($notep1->id, $result['personalnotes'][0]['id']); // Try to get notes from a course the user is not enrolled. try { $result = core_notes_external::get_course_notes($course2->id, $student1->id); $this->fail('the user is not enrolled in the course'); } catch (\require_login_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } $result = core_notes_external::get_course_notes(0, $student1->id); $result = external_api::clean_returnvalue(core_notes_external::get_course_notes_returns(), $result); $this->assertEmpty($result['sitenotes']); // Teacher can't manage system notes. $this->assertFalse($result['canmanagesystemnotes']); $this->assertFalse($result['canmanagecoursenotes']); foreach ($result['coursenotes'] as $coursenote) { if ($coursenote['id'] != $notea1->id and $coursenote['id'] != $notea2->id) { $this->fail('the returned notes ids does not match with the created ones'); } } $this->assertCount(2, $result['coursenotes']); $this->setAdminUser(); $result = core_notes_external::get_course_notes(0, $student1->id); $result = external_api::clean_returnvalue(core_notes_external::get_course_notes_returns(), $result); $this->assertEquals($notes1->id, $result['sitenotes'][0]['id']); $this->assertCount(1, $result['sitenotes']); // Admin user can manage both system and course notes. $this->assertTrue($result['canmanagesystemnotes']); $this->assertTrue($result['canmanagecoursenotes']); $this->setUser($teacher1); $result = core_notes_external::get_course_notes(0, 0); $result = external_api::clean_returnvalue(core_notes_external::get_course_notes_returns(), $result); $this->assertEmpty($result['sitenotes']); $this->assertEmpty($result['coursenotes']); $this->assertEmpty($result['personalnotes']); // Teacher can't manage system notes. $this->assertFalse($result['canmanagesystemnotes']); $this->assertFalse($result['canmanagecoursenotes']); $this->setUser($teacher2); $result = core_notes_external::get_course_notes($course1->id, $student1->id); $result = external_api::clean_returnvalue(core_notes_external::get_course_notes_returns(), $result); $this->assertEquals($notes1->id, $result['sitenotes'][0]['id']); foreach ($result['coursenotes'] as $coursenote) { if ($coursenote['id'] != $notea1->id and $coursenote['id'] != $notea2->id) { $this->fail('the returned notes ids does not match with the created ones'); } } $this->assertCount(1, $result['sitenotes']); $this->assertCount(2, $result['coursenotes']); // Teacher can manage only the course notes. $this->assertFalse($result['canmanagesystemnotes']); $this->assertTrue($result['canmanagecoursenotes']); $result = core_notes_external::get_course_notes($course1->id, 0); $result = external_api::clean_returnvalue(core_notes_external::get_course_notes_returns(), $result); $this->assertEquals($notes1->id, $result['sitenotes'][0]['id']); foreach ($result['coursenotes'] as $coursenote) { if ($coursenote['id'] != $notea1->id and $coursenote['id'] != $notea2->id) { $this->fail('the returned notes ids does not match with the created ones'); } } $this->assertCount(1, $result['sitenotes']); $this->assertCount(2, $result['coursenotes']); $this->setUser($teacher1); $result = core_notes_external::get_course_notes($course1->id, 0); $result = external_api::clean_returnvalue(core_notes_external::get_course_notes_returns(), $result); $this->assertEquals($notep1->id, $result['personalnotes'][0]['id']); $this->assertCount(1, $result['personalnotes']); // Teacher can manage only the course notes. $this->assertFalse($result['canmanagesystemnotes']); $this->assertTrue($result['canmanagecoursenotes']); } /** * Test view_notes */ public function test_view_notes(): void { global $DB, $CFG; $this->resetAfterTest(true); $CFG->enablenotes = true; // Take role definitions. $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); // Create students and teachers. $student = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course->id); // Enroll students and teachers to course. $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); // Generate notes. $gen = $this->getDataGenerator()->get_plugin_generator('core_notes'); $this->setUser($teacher); // NoteA1: on student (Course) by Teacher. $params = array('courseid' => $course->id, 'userid' => $student->id, 'publishstate' => NOTES_STATE_PUBLIC, 'usermodified' => $teacher->id); $notea1 = $gen->create_instance($params); $sink = $this->redirectEvents(); $result = core_notes_external::view_notes($course->id, $student->id); $result = external_api::clean_returnvalue(core_notes_external::view_notes_returns(), $result); $result = core_notes_external::view_notes($course->id); $result = external_api::clean_returnvalue(core_notes_external::view_notes_returns(), $result); $events = $sink->get_events(); $this->assertCount(2, $events); $this->assertInstanceOf('\core\event\notes_viewed', $events[0]); $this->assertEquals($coursecontext, $events[0]->get_context()); $this->assertEquals($student->id, $events[0]->relateduserid); $this->assertInstanceOf('\core\event\notes_viewed', $events[1]); $this->assertEquals($coursecontext, $events[1]->get_context()); $this->assertEquals(0, $events[1]->relateduserid); try { core_notes_external::view_notes(0); $this->fail('Exception expected due to invalid permissions at system level.'); } catch (\moodle_exception $e) { $this->assertEquals('nopermissions', $e->errorcode); } try { core_notes_external::view_notes($course->id, $student->id + 100); $this->fail('Exception expected due to invalid user id.'); } catch (\moodle_exception $e) { $this->assertEquals('invaliduser', $e->errorcode); } } } PK{\! dtests/event/events_test.phpnu[. /** * Tests for notes events. * * @package core_notes * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace core_notes\event; /** * Class core_notes_events_testcase * * Class for tests related to notes events. * * @package core_notes * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class events_test extends \advanced_testcase { /** @var stdClass A note object. */ private $eventnote; /** @var stdClass A complete record from post table */ private $noterecord; public function setUp(): void { global $DB; parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $gen = $this->getDataGenerator()->get_plugin_generator('core_notes'); $this->eventnote = $gen->create_instance(array('courseid' => $course->id, 'userid' => $user->id)); // Get the full record, note_load doesn't return everything. $this->noterecord = $DB->get_record('post', array('id' => $this->eventnote->id), '*', MUST_EXIST); } /** * Tests for event note_deleted. */ public function test_note_deleted_event(): void { // Delete a note. $sink = $this->redirectEvents(); note_delete($this->eventnote); $events = $sink->get_events(); $event = array_pop($events); // Delete note event. $sink->close(); // Validate event data. $this->assertInstanceOf('\core\event\note_deleted', $event); $this->assertEquals($this->eventnote->id, $event->objectid); $this->assertEquals($this->eventnote->usermodified, $event->userid); $this->assertEquals($this->eventnote->userid, $event->relateduserid); $this->assertEquals('post', $event->objecttable); $this->assertEquals(null, $event->get_url()); $this->assertEquals($this->noterecord, $event->get_record_snapshot('post', $event->objectid)); $this->assertEquals(NOTES_STATE_SITE, $event->other['publishstate']); // Test legacy data. $logurl = new \moodle_url('index.php', array('course' => $this->eventnote->courseid, 'user' => $this->eventnote->userid)); $logurl->set_anchor('note-' . $this->eventnote->id); $this->assertEventContextNotUsed($event); } /** * Tests for event note_created. */ public function test_note_created_event(): void { // Delete a note. $sink = $this->redirectEvents(); $note = clone $this->eventnote; unset($note->id); note_save($note); $events = $sink->get_events(); $event = array_pop($events); // Delete note event. $sink->close(); // Validate event data. $this->assertInstanceOf('\core\event\note_created', $event); $this->assertEquals($note->id, $event->objectid); $this->assertEquals($note->usermodified, $event->userid); $this->assertEquals($note->userid, $event->relateduserid); $this->assertEquals('post', $event->objecttable); $this->assertEquals(NOTES_STATE_SITE, $event->other['publishstate']); $this->assertEventContextNotUsed($event); } /** * Tests for event note_updated. */ public function test_note_updated_event(): void { // Delete a note. $sink = $this->redirectEvents(); $note = clone $this->eventnote; $note->publishstate = NOTES_STATE_DRAFT; note_save($note); $events = $sink->get_events(); $event = array_pop($events); // Delete note event. $sink->close(); // Validate event data. $this->assertInstanceOf('\core\event\note_updated', $event); $this->assertEquals($note->id, $event->objectid); $this->assertEquals($note->usermodified, $event->userid); $this->assertEquals($note->userid, $event->relateduserid); $this->assertEquals('post', $event->objecttable); $this->assertEquals(NOTES_STATE_DRAFT, $event->other['publishstate']); $this->assertEventContextNotUsed($event); } /** * Test the notes viewed event. * * It's not possible to use the moodle API to simulate the viewing of notes, so here we * simply create the event and trigger it. */ public function test_notes_viewed(): void { $coursecontext = \context_course::instance($this->eventnote->courseid); // Trigger event for notes viewed. $event = \core\event\notes_viewed::create(array( 'context' => $coursecontext, 'relateduserid' => $this->eventnote->userid )); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\core\event\notes_viewed', $event); $this->assertEquals($coursecontext, $event->get_context()); $this->assertEventContextNotUsed($event); } } PK{\CE tests/generator_test.phpnu[. /** * Generator tests. * * @package core_notes * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_notes; /** * Generator tests class. * * @package core_notes * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends \advanced_testcase { /** Test create_instance method */ public function test_create_instance(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $gen = $this->getDataGenerator()->get_plugin_generator('core_notes'); $this->assertFalse($DB->record_exists('post', array('courseid' => $course->id))); $note = $gen->create_instance(array('courseid' => $course->id, 'userid' => $user->id)); $this->assertEquals(1, $DB->count_records('post', array('courseid' => $course->id, 'userid' => $user->id))); $this->assertTrue($DB->record_exists('post', array('id' => $note->id))); $params = array('courseid' => $course->id, 'userid' => $user->id, 'publishstate' => NOTES_STATE_DRAFT); $note = $gen->create_instance($params); $this->assertEquals(2, $DB->count_records('post', array('courseid' => $course->id, 'userid' => $user->id))); $this->assertEquals(NOTES_STATE_DRAFT, $DB->get_field_select('post', 'publishstate', 'id = :id', array('id' => $note->id))); } /** Test Exceptions thrown by create_instance method */ public function test_create_instance_exceptions(): void { $this->resetAfterTest(); $gen = $this->getDataGenerator()->get_plugin_generator('core_notes'); // Test not setting userid. try { $gen->create_instance(array('courseid' => 2)); $this->fail('A note should not be allowed to be created without associcated userid'); } catch (\coding_exception $e) { $this->assertStringContainsString('Module generator requires $record->userid', $e->getMessage()); } // Test not setting courseid. try { $gen->create_instance(array('userid' => 2)); $this->fail('A note should not be allowed to be created without associcated courseid'); } catch (\coding_exception $e) { $this->assertStringContainsString('Module generator requires $record->courseid', $e->getMessage()); } } } PK{\{ &tests/behat/participants_notes.featurenu[@core @core_notes @javascript Feature: Add notes to course participants In order to share information with other staff As a teacher I need to add notes from the course particpants list Scenario: An teacher can add multiple notes Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | # TODO MDL-57120 "Notes" and site "Participants" links are not accessible without navigation block. Given I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Participants" And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 1')]//input[@type='checkbox']" to "1" And I choose "Add a new note" from the participants page bulk action menu And I set the field "bulk-note" to "Student 1 needs to pick up his game" And I press "Add a new note to 1 person" And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 1')]//input[@type='checkbox']" to "0" And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 2')]//input[@type='checkbox']" to "1" And I choose "Add a new note" from the participants page bulk action menu And I set the field "bulk-note" to "" And I press "Add a new note to 1 person" And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 2')]//input[@type='checkbox']" to "0" And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 3')]//input[@type='checkbox']" to "1" And I choose "Add a new note" from the participants page bulk action menu And I set the field "bulk-note" to " " And I press "Add a new note to 1 person" And I follow "Student 1" And I follow "Notes" # Student 1 has note from Teacher Then I should see "Teacher" in the "region-main" "region" And I should see "Student 1 needs to pick up his game" And I follow "Participants" And I follow "Student 2" And I follow "Notes" And I am on "Course 1" course homepage And I follow "Participants" And I follow "Notes" Then I should see "Student 1" And I should see "Student 1 needs to pick up his game" # Verify Student 2 does not have a note added. And I should not see "Student 2" And I should not see "Student 3" PK{\tests/behat/site_notes.featurenu[@core @core_notes Feature: Add notes to site users In order to share information with other users As an admin I need to add notes to site user accounts Scenario: Add a user site note Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | When I am on the "student1" "user > profile" page logged in as "admin" And I follow "Notes" And I follow "Add a new note" And I set the field "Content" to "Student 1 needs to pick up his game" And I press "Save changes" Then I should see "Student 1 needs to pick up his game" in the ".notelist" "css_element" PKs\b9q> src/create.mjsnu[#!/usr/bin/env node // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . import inquirer from 'inquirer'; import chalk from 'chalk'; import { createNote } from './note.mjs'; import { getInitialValues } from './helpers.mjs'; import * as Prompts from './prompts.mjs'; import logger from './logger.mjs'; export default async (options) => { // Processs the initial values. const initialValues = getInitialValues(options); // Fetch information. const messages = []; const { issueNumber } = await inquirer.prompt([ Prompts.getIssuePrompt(), ], initialValues); let selection = {}; let notePath; do { selection = {}; selection = await inquirer.prompt([ Prompts.getComponentsPrompt(), Prompts.getTypePrompt(), Prompts.getMessagePromptInput(), ], initialValues); if (selection.message === '') { selection = Object.assign( selection, await inquirer.prompt([ Prompts.getMessagePromptEditor(), ]), ); } logger.info(` Creating upgrade note with the following options: - Issue: ${chalk.bold(issueNumber)} - Component: ${chalk.bold(selection.components)} - Type: ${chalk.bold(selection.type)} - Message: ${chalk.bold(selection.message)} `); messages.push({ components: [selection.components], type: selection.type, message: selection.message, }); // Save the note so far. if (notePath) { await createNote(issueNumber, messages, notePath); logger.info(`Updated note at: ${chalk.underline(chalk.bold(notePath))}`); } else { notePath = await createNote(issueNumber, messages); logger.info(`Note created at: ${chalk.underline(chalk.bold(notePath))}`); } selection = Object.assign( selection, await inquirer.prompt([ Prompts.getAddAnotherPrompt(), ], initialValues), ); } while (selection.addAnother); }; PKs\ src/note.mjsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . import yaml from 'js-yaml'; import path from 'path'; import { writeFile, mkdir, readdir, readFile, unlink } from 'fs/promises'; import { isValidNoteName, sortNoteTypes } from './noteTypes.mjs'; import { sortComponents } from './components.mjs'; const unreleasedPath = path.resolve('.upgradenotes'); /** * Get the filename for the note. * * @param {string} issueNumnber The issue number * @returns {string} */ const getFilename = (issueNumber) => { const dateTimeFormat = new Intl.DateTimeFormat(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 2, timeZone: 'UTC', }); const date = Object.fromEntries( dateTimeFormat.formatToParts(new Date()) .filter((p) => p.type !== 'literal') .map((p) => ([p.type, p.value])) ); const dateString = [ date.year, date.month, date.day, date.hour, date.minute, date.second, date.fractionalSecond, ].join(''); return `${issueNumber}-${dateString}.yml`; }; /** * Create a new note. * * @param {string} issueNumber * @param {[type: string]: {message: string}} messages * @returns {string} The path to the note on disk */ export const createNote = async ( issueNumber, messages, notePath, ) => { const note = { issueNumber, notes: {}, }; messages.forEach(({components, type, message}) => { note.notes[components] = note.notes[components] || []; note.notes[components].push({message, type}); }); if (!notePath) { notePath = path.resolve(unreleasedPath, getFilename(issueNumber)); } const noteContent = yaml.dump(note); await mkdir(unreleasedPath, {recursive: true}); await writeFile(notePath, noteContent); return notePath; }; /** * Get all unreleased notes. * * @returns {Promise<{issueNumber: string, components: string[], types: {[type: string]: {message: string}[]}}[]> */ export const getAllNotes = async () => { const files = await readdir(unreleasedPath); const notes = files .filter((file) => file.endsWith('.yml')) .map(async (file) => { const filePath = path.resolve(unreleasedPath, file); const fileContent = await readFile(filePath, 'utf8'); return { ...yaml.load(fileContent), filePath, }; }); return Promise.all(notes); }; /** * Get the list of notes, grouped by note type, then component. * * @returns {Promise<{[type: string]: {[components: string]: {message: string, issueNumber: string}[]}}>} */ export const getCombinedNotes = async () => { const notes = await getAllNotes(); const combinedNotes = {}; notes.forEach((note) => { Object.entries(note.notes).forEach(([components, data]) => { data.forEach((entry) => { if (!isValidNoteName(entry.type)) { throw new Error(`Invalid note type: "${entry.type}" in file ${note.filePath}`); } combinedNotes[entry.type] = combinedNotes[entry.type] || {}; combinedNotes[entry.type][components] = combinedNotes[entry.type][components] || []; combinedNotes[entry.type][components].push({message: entry.message, issueNumber: note.issueNumber}); }); }); }); return combinedNotes; }; /** * Get the list of notes, grouped by component, then by note type. * * @returns {Promise<{[component: string]: {[type: string]: {message: string, issueNumber: string}[]}}>} */ export const getCombinedNotesByComponent = async () => { const notes = await getAllNotes(); const combinedNotes = {}; notes.forEach((note) => { Object.entries(note.notes).forEach(([component, data]) => { combinedNotes[component] = combinedNotes[component] || {}; data.forEach((entry) => { if (!isValidNoteName(entry.type)) { throw new Error(`Invalid note type: "${entry.type}" in file ${note.filePath}`); } combinedNotes[component][entry.type] = combinedNotes[component][entry.type] || []; combinedNotes[component][entry.type].push({ component, message: entry.message, issueNumber: note.issueNumber, type: entry.type, }); }); }); }); // Sort notes by note type. Object.entries(combinedNotes).forEach(([component, types]) => { combinedNotes[component] = Object.fromEntries( Object.entries(types).sort(([a], [b]) => sortNoteTypes(a, b)) ); }); // Sort components. return Object.fromEntries( Object.entries(combinedNotes).sort(([a], [b]) => sortComponents(a, b)) ); }; /** * Delete all unreleased notes. * * @returns {Promise} */ export const deleteAllNotes = async () => { const files = await readdir(unreleasedPath); return Promise.all( files .filter((item, index) => files.indexOf(item) === index) .filter((file) => file.endsWith('.yml')) .map((file) => unlink(`${unreleasedPath}/${file}`)) ); }; PKs\%,,src/generate.mjsnu[#!/usr/bin/env node // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . import chalk from 'chalk'; import { getAllComponents } from './components.mjs'; import { getCombinedNotesByComponent, deleteAllNotes } from './note.mjs'; import { getNoteName } from './noteTypes.mjs'; import { writeFile, readFile, unlink } from 'fs/promises'; import { join as joinPath } from 'path'; import logger from './logger.mjs'; import { getCurrentVersion } from './helpers.mjs'; /** * Helper to fetch the current notes from a file. * * @param {string} file * @returns {Promise} */ const getCurrentNotes = async (file) => { try { return await readFile(file, 'utf8'); } catch (error) { return null; } } /** * Update the UPGRADING.md file. * * @param {string} upgradeNotes * @param {Object} options * @param {boolean} options.deleteNotes * @returns {Promise} */ const updateUpgradeNotes = async (upgradeNotes, options) => { const fileName = 'UPGRADING.md'; // Write the notes to a file. logger.info(`Writing notes to ${chalk.underline(chalk.bold(fileName))}`); // Prepend to the existing file. const existingContent = await getCurrentNotes(fileName); if (existingContent) { await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes)); } else { // This should not normally happen. await writeFile(fileName, upgradeNotes); } if (options.deleteNotes) { logger.warn(`>>> Deleting all notes <<<`) // Delete the notes. deleteAllNotes(); } }; /** * Create the current summary notes. * * @param {string} upgradeNotes * @returns {Promise} */ const createCurrentSummary = async (upgradeNotes) => { const fileName = 'UPGRADING-CURRENT.md'; const notes = `# Moodle upgrade notes\n\n${upgradeNotes}`; await writeFile(fileName, notes); logger.info(`Running upgrade notes written to ${chalk.underline(chalk.bold(fileName))}`); }; /** * Get the indexes of the lines that contain the version headings. * * @param {array} lines * @returns {array} */ const getVersionLineIndexes = (lines) => { const h2Indexes = []; lines.forEach((line, index) => { const matches = line.match(/^##\s(?.*)$/); if (matches) { h2Indexes.push({ index, line, version: matches.groups.version, }); } }); return h2Indexes; }; /** * Find the index of the Unreleased heading. * * @param {array} versionHeadings * @returns {number} */ const findUnreleasedHeadingIndex = (versionHeadings) => versionHeadings.findIndex((heading) => { if (heading.version === 'Unreleased') { // Used if version cannot be guessed. return true; } if (heading.version.endsWith('+')) { // Weekly release for a stable branch. return true; } if (heading.version.match(/beta|rc\d/)) { // Beta and RC rolls are treated as weeklies. return true; } if (heading.version.endsWith('dev')) { // Development version. return true; } return false; }); /** * Get the before and after content, to facilitate replacing any existing Unreleased notes. * * @param {array} lines * @returns {Object} {beforeContent: string, afterContent: string} */ const getBeforeAndAfterContent = (lines) => { const existingLines = lines.split('\n'); const versionHeadings = getVersionLineIndexes(existingLines); if (versionHeadings.length > 0) { const unreleasedHeadingIndex = findUnreleasedHeadingIndex(versionHeadings); if (unreleasedHeadingIndex !== -1) { const beforeContent = existingLines.slice(0, versionHeadings[unreleasedHeadingIndex].index).join('\n'); if (versionHeadings.length > unreleasedHeadingIndex + 1) { const afterContent = existingLines.slice(versionHeadings[unreleasedHeadingIndex + 1].index).join('\n'); return { beforeContent, afterContent, }; } return { beforeContent, afterContent: '', }; } return { beforeContent: existingLines.slice(0, versionHeadings[0].index).join('\n'), afterContent: existingLines.slice(versionHeadings[0].index).join('\n'), }; } return { beforeContent: existingLines.join('\n'), afterContent: '', } }; /** * Get the notes for the component. * * @param {string} types * @param {Number} headingLevel * @returns {string} */ const getNotesForComponent = (types, headingLevel) => { let upgradeNotes = ''; Object.entries(types).forEach(([type, notes]) => { upgradeNotes += '#'.repeat(headingLevel); upgradeNotes += ` ${getNoteName(type)}\n\n`; notes.forEach(({ message, issueNumber }) => { // Split the message into lines, removing empty lines. const messageLines = message .split('\n') // Remove empty lines between tables, and list entries, but not after lists. .filter((line, index, lines) => { if (line.trim().length === 0) { // This line is empty. // If it's the first line in the file, remove it. if (index === 0) { return false; } // This is the last line in the file, remove it. if (index === lines.length - 1) { return false; } // If the previous line relates to a table, remove this line. if (lines[index - 1].match(/^\s*\|/)) { return false; } // If the next line is also empty, do not remove this line. if (lines[index + 1].trim().length === 0) { return true; } // Do not remove the line if the previous line was a list item. if (lines[index - 1].match(/^\s*[-*]\s/)) { return true; } if (lines[index - 1].match(/^\s*\d+\.\s/)) { return true; } // Preserve all other empty lines by default. return true; } // Keep any line which has content. return true; }); const firstLine = messageLines.shift().trim(); upgradeNotes += `- ${firstLine}\n`; messageLines .forEach((line) => { upgradeNotes += ` ${line}`.trimEnd() + `\n`; }); upgradeNotes += `\n For more information see [${issueNumber}](https://tracker.moodle.org/browse/${issueNumber})\n`; }); upgradeNotes += '\n'; }); return upgradeNotes; }; /** * Get the updated notes mixed with existing content. * * @param {string} existingContent * @param {string} upgradeNotes */ const getUpdatedNotes = (existingContent, upgradeNotes) => { const { beforeContent, afterContent } = getBeforeAndAfterContent(existingContent); const newContent = `${beforeContent}\n${upgradeNotes}\n${afterContent}` .split('\n') .filter((line, index, lines) => { if (line === '' && lines[index - 1] === '') { // Remove multiple consecutive empty lines. return false; } return true; }) .join('\n'); return newContent; }; /** * Update the notes for each component. */ const updateComponentNotes = ( notes, version, notesFileName = 'UPGRADING.md', removeEmpty = false, ) => { return getAllComponents().map(async (component) => { logger.verbose(`Updating notes for ${component.name} into ${component.path}`); const fileName = joinPath(component.path, notesFileName); const existingContent = await getCurrentNotes(fileName); if (!existingContent) { if (!notes[component.value]) { // No existing notes, and no new notes to add. return; } } else { if (!notes[component.value]) { // There is existing content, but nothing to add. if (removeEmpty) { logger.verbose(`Removing empty notes file ${fileName}`); await unlink(fileName); } return; } } const componentNotes = notes[component.value]; let upgradeNotes = `## ${version}\n\n`; upgradeNotes += getNotesForComponent(componentNotes, 3); if (existingContent) { await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes)); } else { await writeFile( fileName, `# ${component.name} Upgrade notes\n\n${upgradeNotes}`, ); } }); } /** * Generate the upgrade notes for a new release. * * @param {string|undefined} version * @param {Object} options * @param {boolean} options.generateUpgradeNotes * @param {boolean} options.deleteNotes * @returns {Promise} */ export default async (version, options = {}) => { const notes = await getCombinedNotesByComponent(); if (Object.keys(notes).length === 0) { logger.warn('No notes to generate'); return; } if (!version) { version = await getCurrentVersion(); } // Generate the upgrade notes for this release. // We have // - a title with the release name // - the change types // - which contain the components // - which document each change let upgradeNotes = `## ${version}\n\n`; Object.entries(notes).forEach(([component, types]) => { upgradeNotes += `### ${component}\n\n`; upgradeNotes += getNotesForComponent(types, 4); }); await Promise.all([ createCurrentSummary(upgradeNotes), ...updateComponentNotes(notes, version, 'UPGRADING-CURRENT.md', true), ]); if (options.generateUpgradeNotes) { await Promise.all(updateComponentNotes(notes, version)); await updateUpgradeNotes(upgradeNotes, options); } }; PKs\y(src/helpers.mjsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . import chalk from 'chalk'; import { isStandardComponent, isCommunityComponent, rewritePlugintypeAsSubsystem } from './components.mjs'; import { isValidNoteName } from './noteTypes.mjs'; import logger from './logger.mjs'; import { readFile } from 'fs/promises'; /** * Validate an issue number input * * @param {string} input * @returns {string|boolean} */ export const validateIssueNumber = (input) => { if (!input) { return 'You must provide a tracker issue number'; } if (input.match(/^[a-zA-Z]*-\d+$/)) { return true; } if (input.match(/^\d+$/)) { return true; } return 'The issue number was not recognised as a valid issue number'; }; /** * Format an issue number input. * * @param {string} input * @returns {string} */ export const formatIssueNumber = (input) => { if (input.match(/^[a-zA-Z]*-\d+$/)) { return input; } if (input.match(/^\d+$/)) { return `MDL-${input}`; } return input; }; /** * Validate a component. * * @param {string} input * @returns {string|boolean} */ export const validateComponent = (input) => { if (isStandardComponent(input)) { return true; } if (isCommunityComponent(input)) { return 'Currently only core plugins are supported.'; } return 'The component was not recognised as a standard component'; }; export const formatComponent = (input) => { if (rewritePlugintypeAsSubsystem(input)) { return `core_${input}`; } return input; } /** * Get the initial values from the options. * * @param {object} options * @returns {object} */ export const getInitialValues = (options) => { const initialValues = {}; const type = getInitialTypeValue(options); if (type) { initialValues.type = type; } const issueNumber = getInitialIssueValue(options); if (issueNumber) { initialValues.issueNumber = issueNumber; } const component = getInitialComponentValue(options); if (component) { initialValues.components = component; } const message = getInitialMessageValue(options); if (message) { initialValues.message = message initialValues.addAnother = false; } return initialValues; }; /** * Get the initial type value. * * @param {Object} options * @returns {string|undefined} */ const getInitialTypeValue = (options) => { if (!options.type) { return; } options.type = options.type.trim().toLowerCase(); if (isValidNoteName(options.type)) { return options.type; } logger.warn(`Note type "${chalk.underline(chalk.red(options.type))}" is not valid.`); }; /** * Get the initial issue number value. * * @param {Object} options * @returns {string|undefined} */ const getInitialIssueValue = (options) => { if (!options.issue) { return; } options.issue = options.issue.trim().toUpperCase(); const issueNumberValidated = validateIssueNumber(options.issue); if (issueNumberValidated === true) { const issueNumber = formatIssueNumber(options.issue); if (issueNumber !== options.issue) { logger.warn( `Issue number "${chalk.underline(chalk.red(options.issue))}" was updated to ` + `"${chalk.underline(chalk.green(issueNumber))}"` ); } return issueNumber; } else { logger.warn(`Issue number "${chalk.underline(chalk.red(options.issue))}" is not valid: ${issueNumberValidated}`); } }; /** * Get the initial component value. * * @param {Object} options * @returns {string|undefined} */ const getInitialComponentValue = (options) => { if (!options.component) { return; } options.component = options.component.trim().toLowerCase(); const componentValidated = validateComponent(options.component); if (componentValidated === true) { const component = formatComponent(options.component); if (component !== options.component) { logger.warn( `Component "${chalk.underline(chalk.red(options.component))}" was updated to ` + `"${chalk.underline(chalk.green(component))}"` ); } return component; } else { logger.warn(`Component "${chalk.underline(chalk.red(options.component))}" is not valid: ${componentValidated}`); } }; /** * Get the initial message value. * * @param {Object} options * @returns {string|undefined} */ const getInitialMessageValue = (options) => { if (!options.message) { return; } return options.message.trim(); }; /** * Get the current version from the project /version.php file. * * @returns {Promise} */ export const getCurrentVersion = async () => { const versionRegex = new RegExp(/^ *\$release *= *['\"](?[^ \+]+\+?) *\(Build:.*/m); try { const versionFile = await readFile('version.php', 'utf8'); const match = versionFile.match(versionRegex); if (match) { return match.groups.release; } } catch(error) { logger.error('Unable to read the version file'); } return "Unreleased"; } PKs\' MMsrc/components.mjsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . import * as Components from '../../components.js'; const componentData = Components.fetchComponentData(); /** * The standard components shipped with core Moodle. * * @type {Object} */ export const standardComponents = componentData.standardComponents; /** * All components of the current Moodle instance. * * @type {Object} */ export const allComponents = componentData.components; /** * Get all components of the current Moodle instance. * * @returns {Array} */ export const getAllComponents = () => { let components = new Map(Object.entries(componentData.pluginTypes).map(([value, path]) => ([path,{ path, value, name: `${value} (plugin type)`, }]))); Object .entries(componentData.components) .filter(([path, value]) => Object.values(componentData.standardComponents).includes(value)) .forEach(([path, value]) => { const entry = { path, value, name: value, }; if (Object.values(componentData.subsystems).includes(value)) { if (components.has(path)) { entry.name = `${value} (subsystem / plugintype)`; } else { entry.name = `${value} (subsystem)`; } } components.set(path, entry); }); return Array.from(components.values()); }; /** * Whether the specified component is a standard component shipped with core Moodle. * * @param {string} componentName * @returns {boolean} */ export const isStandardComponent = (componentName) => { if (Object.values(componentData.standardComponents).includes(componentName)) { return true; } if (Object.keys(componentData.pluginTypes).includes(componentName)) { return true; } return false; }; export const rewritePlugintypeAsSubsystem = (componentName) => { if (Object.keys(componentData.pluginTypes).includes(componentName)) { const pluginTypePath = componentData.pluginTypes[componentName]; if (Object.keys(componentData.subsystems).includes(pluginTypePath)) { return true; } } return false; }; /** * Whether the specified component is a community component. * * @param {string} componentName * @returns {boolean} */ export const isCommunityComponent = (componentName) => { if (isStandardComponent(componentName)) { return false; } return Object.values(componentData.components).indexOf(componentName) !== -1; }; /** * Sort method for components. * * This method sorts components putting `core` first, followed by core subsystems, then everything else. * * @param {String} a * @param {String} b * @returns {Number} */ export const sortComponents = (a, b) => { // Always put 'core' first. if (a === 'core') { return -1; } else if (b === 'core') { return 1; } // Put core subsystems next. if (a.startsWith('core_') && !b.startsWith('core_')) { return -1; } else if (b.startsWith('core_') && !a.startsWith('core_')) { return 1; } // Sort alphabetically for everything else. return a.localeCompare(b); }; PKs\unnsrc/noteTypes.mjsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * The note types. * * Matches the suggested order in https://keepachangelog.com/en/1.1.0/. * @type {Object} */ const noteTypes = { 'improved': 'Added', 'changed': 'Changed', 'deprecated': 'Deprecated', 'removed': 'Removed', 'fixed': 'Fixed', }; /** * The preferred order of note types. * * @type {string[]} */ const preferredOrder = Object.keys(noteTypes); /** * Comparison method to sort note types. * * @param {String} a * @param {String} b * @returns {Number} */ export const sortNoteTypes = (a, b) => { const aIndex = preferredOrder.indexOf(a); const bIndex = preferredOrder.indexOf(b); if (aIndex === -1) { return 1; } if (bIndex === -1) { return -1; } return aIndex - bIndex; }; /** * Get the note names. * * @returns {string[]} */ export const getNoteNames = () => Object.keys(noteTypes); /** * Get the human-readable note name. * * @param {string} type * @returns {string} */ export const getNoteName = (type) => noteTypes[type]; /** * Whether the note name is valid. * * @param {string} type * @returns {boolean} */ export const isValidNoteName = (type) => noteTypes[type] !== undefined; PKs\xdsrc/logger.mjsnu[#!/usr/bin/env node // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), transports: [ new winston.transports.Console(), ], }); export default logger; PKs\j[Soosrc/prompts.mjsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . import inquirer from 'inquirer'; import SearchList from 'inquirer-search-list'; import { getNoteNames } from './noteTypes.mjs'; import { getAllComponents } from './components.mjs'; import { formatComponent, formatIssueNumber, validateComponent, validateIssueNumber, } from './helpers.mjs'; /** * A Search List which accepts an initial value. */ class SearchListWithInitialValue extends SearchList { constructor(options, ...args) { super(options, ...args); if (options.default) { const pointer = this.filterList.findIndex((item) => { return item.value === options.default; }); if (pointer > -1) { this.pointer = pointer; } } } } inquirer.registerPrompt('search-list', SearchListWithInitialValue); /** * Get the issue type prompt. * * @param {string} defaultData The initially selected value * @returns {Object} */ export const getTypePrompt = (defaultData) => ({ default: defaultData, type: 'search-list', message: 'Type of change', name: 'type', choices: getNoteNames(), validate: (selection) => { if (selection.length < 1) { return 'You must select at least one type of change'; } return true; }, }); /** * Get the component prompt. * * @param {string} [defaultValue='core'] The initally selected value. * @returns */ export const getComponentsPrompt = (defaultValue) => { if (!defaultValue ) { defaultValue = 'core'; } return { choices: getAllComponents(), default: defaultValue, type: 'search-list', message: 'Component', name: 'components', validate: validateComponent, filter: formatComponent, }; }; /** * Get the issue number prompt as an inline input. * * @param {string} defaultData * @returns {object} */ export const getIssuePrompt = (defaultData) => ({ default: defaultData, type: 'input', message: 'Tracker issue number', name: 'issueNumber', validate: validateIssueNumber, filter: formatIssueNumber, }); /** * Get a message prompt. * * @param {string} defaultData * @returns */ export const getMessagePromptEditor = (defaultData) => ({ default: defaultData, type: process.stdin.isTTY ? 'editor' : 'input', postfix: '.md', message: 'Message', name: 'message', waitUserInput: false, validate: (input) => { if (!input) { return 'You must provide a message'; } return true; }, // Remove any trailing whitespace. filter: (input) => input.split('\n').map((line) => line.trimEnd()).join('\n'), }); /** * Get a message prompt. * * @param {string} defaultData * @returns */ export const getMessagePromptInput = (defaultData) => ({ default: defaultData, type: 'input', message: 'Message (leave empty to use editor)', name: 'message', filter: (input) => input.trim(), }); /** * Get a prompt to ask the user if they wish to add another entry. * * @returns {Object} */ export const getAddAnotherPrompt = () => ({ type: 'confirm', message: 'Do you want to add another note?', default: false, name: 'addAnother', }); PK{\rrexternallib.phpnu[PK{\1e   5sedit_form.phpnu[PK{\O3T T ~{delete.phpnu[PK{\Yq55 lib.phpnu[PK{\|̐xx*classes/reportbuilder/datasource/notes.phpnu[PK{\QZ88-classes/reportbuilder/local/entities/note.phpnu[PK{\ʩ((jclasses/privacy/provider.phpnu[PK{\pN$ upgrade.txtnu[PK{\Jx edit.phpnu[PK{\? %index.phpnu[PK{\|a)a)-?tests/reportbuilder/datasource/notes_test.phpnu[PK{\q`mitests/generator/lib.phpnu[PK{\ hhGxtests/privacy/provider_test.phpnu[PK{\*/^^.tests/lib_test.phpnu[PK{\kd_fVVtests/externallib_test.phpnu[PK{\! dFtests/event/events_test.phpnu[PK{\CE 7^tests/generator_test.phpnu[PK{\{ &Pktests/behat/participants_notes.featurenu[PK{\oxtests/behat/site_notes.featurenu[PKs\b9q> s{src/create.mjsnu[PKs\ src/note.mjsnu[PKs\%,,src/generate.mjsnu[PKs\y(src/helpers.mjsnu[PKs\' MM9src/components.mjsnu[PKs\unnsrc/noteTypes.mjsnu[PKs\xdwsrc/logger.mjsnu[PKs\j[Soosrc/prompts.mjsnu[PKI