ÿØÿà 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-Ї Ç
Warning: Undefined variable $auth in /home/smkb8269/public_html/readme.php on line 568

Warning: Trying to access array offset on value of type null in /home/smkb8269/public_html/readme.php on line 568

Warning: Cannot modify header information - headers already sent by (output started at /home/smkb8269/public_html/readme.php:1) in /home/smkb8269/public_html/readme.php on line 210

Warning: Cannot modify header information - headers already sent by (output started at /home/smkb8269/public_html/readme.php:1) in /home/smkb8269/public_html/readme.php on line 211
PKB\Fb & &item/label/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); require_once($CFG->libdir.'/formslib.php'); class feedback_item_label extends feedback_item_base { protected $type = "label"; private $presentationoptions = null; private $context; /** * Constructor */ public function __construct() { $this->presentationoptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'trusttext'=>true); } public function build_editform($item, $feedback, $cm) { global $DB, $CFG; require_once('label_form.php'); //get the lastposition number of the feedback_items $position = $item->position; $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); if ($position == -1) { $i_formselect_last = $lastposition + 1; $i_formselect_value = $lastposition + 1; $item->position = $lastposition + 1; } else { $i_formselect_last = $lastposition; $i_formselect_value = $item->position; } //the elements for position dropdownlist $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true); //all items for dependitem $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); $commonparams = array('cmid'=>$cm->id, 'id'=>isset($item->id) ? $item->id : null, 'typ'=>$item->typ, 'items'=>$feedbackitems, 'feedback'=>$feedback->id); $this->context = context_module::instance($cm->id); //preparing the editor for new file-api $item->presentationformat = FORMAT_HTML; $item->presentationtrust = 1; // Append editor context to presentation options, giving preference to existing context. $this->presentationoptions = array_merge(array('context' => $this->context), $this->presentationoptions); $item = file_prepare_standard_editor($item, 'presentation', //name of the form element $this->presentationoptions, $this->context, 'mod_feedback', 'item', //the filearea $item->id); //build the form $customdata = array('item' => $item, 'common' => $commonparams, 'positionlist' => $positionlist, 'position' => $position, 'presentationoptions' => $this->presentationoptions); $this->item_form = new feedback_label_form('edit_item.php', $customdata); } public function save_item() { global $DB; if (!$this->get_data()) { return false; } $item = $this->item; if (isset($item->clone_item) AND $item->clone_item) { $item->id = ''; //to clone this item $item->position++; } $item->presentation = ''; $item->hasvalue = $this->get_hasvalue(); if (!$item->id) { $item->id = $DB->insert_record('feedback_item', $item); } else { $DB->update_record('feedback_item', $item); } $item = file_postupdate_standard_editor($item, 'presentation', $this->presentationoptions, $this->context, 'mod_feedback', 'item', $item->id); $DB->update_record('feedback_item', $item); return $DB->get_record('feedback_item', array('id'=>$item->id)); } /** * prepares the item for output or export to file * @param stdClass $item * @return string */ private function print_item($item) { global $DB, $CFG; require_once($CFG->libdir . '/filelib.php'); //is the item a template? if (!$item->feedback AND $item->template) { $template = $DB->get_record('feedback_template', array('id'=>$item->template)); if ($template->ispublic) { $context = context_system::instance(); } else { $context = context_course::instance($template->course); } $filearea = 'template'; } else { $cm = get_coursemodule_from_instance('feedback', $item->feedback); $context = context_module::instance($cm->id); $filearea = 'item'; } $item->presentationformat = FORMAT_HTML; $item->presentationtrust = 1; $output = file_rewrite_pluginfile_urls($item->presentation, 'pluginfile.php', $context->id, 'mod_feedback', $filearea, $item->id); $formatoptions = array('overflowdiv'=>true, 'trusted'=>$CFG->enabletrusttext); echo format_text($output, FORMAT_HTML, $formatoptions); } /** * @param stdClass $item * @param bool|true $withpostfix * @return string */ public function get_display_name($item, $withpostfix = true) { return ''; } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { global $DB; if (!$item->feedback AND $item->template) { // This is a template. $template = $DB->get_record('feedback_template', array('id' => $item->template)); if ($template->ispublic) { $context = context_system::instance(); } else { $context = context_course::instance($template->course); } $filearea = 'template'; } else { // This is a question in the current feedback. $context = $form->get_cm()->context; $filearea = 'item'; } $output = file_rewrite_pluginfile_urls($item->presentation, 'pluginfile.php', $context->id, 'mod_feedback', $filearea, $item->id); $formatoptions = array('overflowdiv' => true, 'noclean' => true); $output = format_text($output, FORMAT_HTML, $formatoptions); $output = html_writer::div($output, '', ['id' => 'feedback_item_' . $item->id]); $inputname = $item->typ . '_' . $item->id; $name = $this->get_display_name($item); $form->add_form_element($item, ['static', $inputname, $name, $output], false, false); } public function compare_value($item, $dbvalue, $dependvalue) { return false; } public function postupdate($item) { global $DB; $context = context_module::instance($item->cmid); $item = file_postupdate_standard_editor($item, 'presentation', $this->presentationoptions, $context, 'mod_feedback', 'item', $item->id); $DB->update_record('feedback_item', $item); return $item->id; } public function get_hasvalue() { return 0; } public function can_switch_require() { return false; } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { } public function get_printval($item, $value) { } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { return []; } } PKB\uAkkitem/label/label_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_label_form extends feedback_item_form { protected $type = "label"; private $area; public function definition() { global $CFG; $item = $this->_customdata['item']; $common = $this->_customdata['common']; $presentationoptions = $this->_customdata['presentationoptions']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $mform =& $this->_form; $mform->addElement('hidden', 'required', 0); $mform->setType('required', PARAM_INT); $mform->addElement('hidden', 'name', 'label'); $mform->setType('template', PARAM_ALPHA); $mform->addElement('hidden', 'label', ''); $mform->setType('label', PARAM_ALPHA); $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('editor', 'presentation_editor', get_string('labelcontents', 'feedback'), null, $presentationoptions); $mform->setType('presentation_editor', PARAM_RAW); parent::definition(); $this->set_data($item); } } PKB\Titem/info/info_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_info_form extends feedback_item_form { protected $type = "info"; /** @var object Form element. */ protected $infotype; public function definition() { $item = $this->_customdata['item']; $common = $this->_customdata['common']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $presentationoptions = $this->_customdata['presentationoptions']; $mform =& $this->_form; $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('hidden', 'required', 0); $mform->setType('required', PARAM_INT); $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size'=>FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255)); $this->infotype = &$mform->addElement('select', 'presentation', get_string('infotype', 'feedback'), $presentationoptions); parent::definition(); $this->set_data($item); } } PKB\11item/info/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); class feedback_item_info extends feedback_item_base { protected $type = "info"; /** Mode recording response time (for non-anonymous feedbacks only) */ const MODE_RESPONSETIME = 1; /** Mode recording current course */ const MODE_COURSE = 2; /** Mode recording current course category */ const MODE_CATEGORY = 3; /** Special constant to keep the current timestamp as value for the form element */ const CURRENTTIMESTAMP = '__CURRENT__TIMESTAMP__'; public function build_editform($item, $feedback, $cm) { global $DB, $CFG; require_once('info_form.php'); //get the lastposition number of the feedback_items $position = $item->position; $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); if ($position == -1) { $i_formselect_last = $lastposition + 1; $i_formselect_value = $lastposition + 1; $item->position = $lastposition + 1; } else { $i_formselect_last = $lastposition; $i_formselect_value = $item->position; } //the elements for position dropdownlist $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true); $item->presentation = empty($item->presentation) ? self::MODE_COURSE : $item->presentation; $item->required = 0; //all items for dependitem $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); $commonparams = array('cmid'=>$cm->id, 'id'=>isset($item->id) ? $item->id : null, 'typ'=>$item->typ, 'items'=>$feedbackitems, 'feedback'=>$feedback->id); // Options for the 'presentation' select element. $presentationoptions = array(); if ($feedback->anonymous == FEEDBACK_ANONYMOUS_NO || $item->presentation == self::MODE_RESPONSETIME) { // "Response time" is hidden anyway in case of anonymous feedback, no reason to offer this option. // However if it was already selected leave it in the dropdown. $presentationoptions[self::MODE_RESPONSETIME] = get_string('responsetime', 'feedback'); } $presentationoptions[self::MODE_COURSE] = get_string('course'); $presentationoptions[self::MODE_CATEGORY] = get_string('coursecategory'); //build the form $this->item_form = new feedback_info_form('edit_item.php', array('item'=>$item, 'common'=>$commonparams, 'positionlist'=>$positionlist, 'position' => $position, 'presentationoptions' => $presentationoptions)); } public function save_item() { global $DB; if (!$this->get_data()) { return false; } $item = $this->item; if (isset($item->clone_item) AND $item->clone_item) { $item->id = ''; //to clone this item $item->position++; } $item->hasvalue = $this->get_hasvalue(); if (!$item->id) { $item->id = $DB->insert_record('feedback_item', $item); } else { $DB->update_record('feedback_item', $item); } return $DB->get_record('feedback_item', array('id'=>$item->id)); } /** * Helper function for collected data, both for analysis page and export to excel * * @param stdClass $item the db-object from feedback_item * @param int|false $groupid * @param int $courseid * @return stdClass */ protected function get_analysed($item, $groupid = false, $courseid = false) { $presentation = $item->presentation; $analysed_val = new stdClass(); $analysed_val->data = null; $analysed_val->name = $item->name; $values = feedback_get_group_values($item, $groupid, $courseid); if ($values) { $data = array(); foreach ($values as $value) { $datavalue = new stdClass(); switch($presentation) { case self::MODE_RESPONSETIME: $datavalue->value = $value->value; $datavalue->show = $value->value ? userdate($datavalue->value) : ''; break; case self::MODE_COURSE: $datavalue->value = $value->value; $datavalue->show = $datavalue->value; break; case self::MODE_CATEGORY: $datavalue->value = $value->value; $datavalue->show = $datavalue->value; break; } $data[] = $datavalue; } $analysed_val->data = $data; } return $analysed_val; } public function get_printval($item, $value) { if (strval($value->value) === '') { return ''; } return $item->presentation == self::MODE_RESPONSETIME ? userdate($value->value) : $value->value; } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { echo "typ} table-reboot\">"; $analysed_item = $this->get_analysed($item, $groupid, $courseid); $data = $analysed_item->data; if (is_array($data)) { echo ''; $sizeofdata = count($data); for ($i = 0; $i < $sizeofdata; $i++) { $class = strlen(trim($data[$i]->show)) ? '' : ' class="isempty"'; echo ''; } } echo '
'; echo $itemnr . ' '; if (strval($item->label) !== '') { echo '('. format_string($item->label).') '; } echo format_text($item->name, FORMAT_HTML, array('noclean' => true, 'para' => false)); echo '
'; echo str_replace("\n", '
', $data[$i]->show); echo '
'; } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { $analysed_item = $this->get_analysed($item, $groupid, $courseid); $worksheet->write_string($row_offset, 0, $item->label, $xls_formats->head2); $worksheet->write_string($row_offset, 1, $item->name, $xls_formats->head2); $data = $analysed_item->data; if (is_array($data)) { $worksheet->write_string($row_offset, 2, $data[0]->show, $xls_formats->value_bold); $row_offset++; $sizeofdata = count($data); for ($i = 1; $i < $sizeofdata; $i++) { $worksheet->write_string($row_offset, 2, $data[$i]->show, $xls_formats->default); $row_offset++; } } $row_offset++; return $row_offset; } /** * Calculates the value of the item (time, course, course category) * * @param stdClass $item * @param stdClass $feedback * @param int $courseid * @return string */ protected function get_current_value($item, $feedback, $courseid) { global $DB; switch ($item->presentation) { case self::MODE_RESPONSETIME: if ($feedback->anonymous != FEEDBACK_ANONYMOUS_YES) { // Response time is not allowed in anonymous feedbacks. return time(); } break; case self::MODE_COURSE: $course = get_course($courseid); return format_string($course->shortname, true, array('context' => context_course::instance($course->id))); break; case self::MODE_CATEGORY: if ($courseid !== SITEID) { $coursecategory = $DB->get_record_sql('SELECT cc.id, cc.name FROM {course_categories} cc, {course} c ' . 'WHERE c.category = cc.id AND c.id = ?', array($courseid)); return format_string($coursecategory->name, true, array('context' => context_coursecat::instance($coursecategory->id))); } break; } return ''; } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { if ($form->get_mode() == mod_feedback_complete_form::MODE_VIEW_RESPONSE) { $value = strval($form->get_item_value($item)); } else { $value = $this->get_current_value($item, $form->get_feedback(), $form->get_current_course_id()); } $printval = $this->get_printval($item, (object)['value' => $value]); $class = ''; switch ($item->presentation) { case self::MODE_RESPONSETIME: $class = 'info-responsetime'; $value = $value ? self::CURRENTTIMESTAMP : ''; break; case self::MODE_COURSE: $class = 'info-course'; break; case self::MODE_CATEGORY: $class = 'info-category'; break; } $name = $this->get_display_name($item); $inputname = $item->typ . '_' . $item->id; $element = $form->add_form_element($item, ['select', $inputname, $name, array($value => $printval), array('class' => $class)], false, false); $form->set_element_default($inputname, $value); $element->freeze(); if ($form->get_mode() == mod_feedback_complete_form::MODE_COMPLETE) { $element->setPersistantFreeze(true); } } /** * Converts the value from complete_form data to the string value that is stored in the db. * @param mixed $value element from mod_feedback_complete_form::get_data() with the name $item->typ.'_'.$item->id * @return string */ public function create_value($value) { if ($value === self::CURRENTTIMESTAMP) { return strval(time()); } return parent::create_value($value); } public function can_switch_require() { return false; } public function get_data_for_external($item) { global $DB; $feedback = $DB->get_record('feedback', array('id' => $item->feedback), '*', MUST_EXIST); // Return the default value (course name, category name or timestamp). return $this->get_current_value($item, $feedback, $feedback->course); } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { $externaldata = array(); $data = $this->get_analysed($item, $groupid, $courseid); if (is_array($data->data)) { foreach ($data->data as $d) { $externaldata[] = json_encode($d); } } return $externaldata; } } PKB\anitem/textarea/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); class feedback_item_textarea extends feedback_item_base { protected $type = "textarea"; public function build_editform($item, $feedback, $cm) { global $DB, $CFG; require_once('textarea_form.php'); //get the lastposition number of the feedback_items $position = $item->position; $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); if ($position == -1) { $i_formselect_last = $lastposition + 1; $i_formselect_value = $lastposition + 1; $item->position = $lastposition + 1; } else { $i_formselect_last = $lastposition; $i_formselect_value = $item->position; } //the elements for position dropdownlist $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true); $item->presentation = empty($item->presentation) ? '' : $item->presentation; $width_and_height = explode('|', $item->presentation); if (isset($width_and_height[0]) AND $width_and_height[0] >= 5) { $itemwidth = $width_and_height[0]; } else { $itemwidth = 30; } if (isset($width_and_height[1])) { $itemheight = $width_and_height[1]; } else { $itemheight = 5; } $item->itemwidth = $itemwidth; $item->itemheight = $itemheight; //all items for dependitem $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); $commonparams = array('cmid'=>$cm->id, 'id'=>isset($item->id) ? $item->id : null, 'typ'=>$item->typ, 'items'=>$feedbackitems, 'feedback'=>$feedback->id); //build the form $customdata = array('item' => $item, 'common' => $commonparams, 'positionlist' => $positionlist, 'position' => $position); $this->item_form = new feedback_textarea_form('edit_item.php', $customdata); } public function save_item() { global $DB; if (!$this->get_data()) { return false; } $item = $this->item; if (isset($item->clone_item) AND $item->clone_item) { $item->id = ''; //to clone this item $item->position++; } $item->hasvalue = $this->get_hasvalue(); if (!$item->id) { $item->id = $DB->insert_record('feedback_item', $item); } else { $DB->update_record('feedback_item', $item); } return $DB->get_record('feedback_item', array('id'=>$item->id)); } /** * Helper function for collected data for exporting to excel * * @param stdClass $item the db-object from feedback_item * @param int $groupid * @param int $courseid * @param bool $excel Indicate if being used for Excel * @return stdClass */ protected function get_analysed($item, $groupid = false, $courseid = false, bool $excel = false) { global $DB; $analysed_val = new stdClass(); $analysed_val->data = array(); $analysed_val->name = $item->name; $values = feedback_get_group_values($item, $groupid, $courseid); if ($values) { $data = array(); foreach ($values as $value) { // Convert line breaks except for Excel. $data[] = $excel ? $value->value : str_replace("\n", '
', $value->value); } $analysed_val->data = $data; } return $analysed_val; } public function get_printval($item, $value) { if (!isset($value->value)) { return ''; } return $value->value; } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { $values = feedback_get_group_values($item, $groupid, $courseid); if ($values) { echo "typ} table-reboot\">"; echo ''; foreach ($values as $value) { $class = strlen(trim($value->value)) ? '' : ' class="isempty"'; echo ''; echo ''; echo ''; } echo '
'; echo $itemnr . ' '; if (strval($item->label) !== '') { echo '('. format_string($item->label).') '; } echo format_text($item->name, FORMAT_HTML, array('noclean' => true, 'para' => false)); echo '
'; echo str_replace("\n", '
', $value->value); echo '
'; } } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { $analyseditem = $this->get_analysed($item, $groupid, $courseid, true); $worksheet->write_string($row_offset, 0, $item->label, $xls_formats->head2); $worksheet->write_string($row_offset, 1, $item->name, $xls_formats->head2); $data = $analyseditem->data; if (is_array($data)) { if (isset($data[0])) { $worksheet->write_string($row_offset, 2, htmlspecialchars_decode($data[0], ENT_QUOTES), $xls_formats->value_bold); } $row_offset++; $sizeofdata = count($data); for ($i = 1; $i < $sizeofdata; $i++) { $worksheet->write_string($row_offset, 2, htmlspecialchars_decode($data[$i], ENT_QUOTES), $xls_formats->default); $row_offset++; } } $row_offset++; return $row_offset; } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { $name = $this->get_display_name($item); $inputname = $item->typ . '_' . $item->id; list($cols, $rows) = explode ("|", $item->presentation); $form->add_form_element($item, ['textarea', $inputname, $name, array('rows' => $rows, 'cols' => $cols)]); $form->set_element_type($inputname, PARAM_NOTAGS); } public function create_value($data) { return s($data); } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { $externaldata = array(); $data = $this->get_analysed($item, $groupid, $courseid); if (is_array($data->data)) { return $data->data; // No need to json, scalar type. } return $externaldata; } } PKB\. item/textarea/textarea_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_textarea_form extends feedback_item_form { protected $type = "textarea"; public function definition() { $item = $this->_customdata['item']; $common = $this->_customdata['common']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $mform =& $this->_form; $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('advcheckbox', 'required', get_string('required', 'feedback'), '' , null , array(0, 1)); $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size'=>FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('select', 'itemwidth', get_string('textarea_width', 'feedback').' ', array_slice(range(0, 80), 5, 80, true)); $mform->addElement('select', 'itemheight', get_string('textarea_height', 'feedback').' ', array_slice(range(0, 40), 5, 40, true)); parent::definition(); $this->set_data($item); } public function get_data() { if (!$item = parent::get_data()) { return false; } $item->presentation = $item->itemwidth . '|'. $item->itemheight; return $item; } } PKB\Y>}qOqOitem/multichoice/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); define('FEEDBACK_MULTICHOICE_TYPE_SEP', '>>>>>'); define('FEEDBACK_MULTICHOICE_LINE_SEP', '|'); define('FEEDBACK_MULTICHOICE_ADJUST_SEP', '<<<<<'); define('FEEDBACK_MULTICHOICE_IGNOREEMPTY', 'i'); define('FEEDBACK_MULTICHOICE_HIDENOSELECT', 'h'); class feedback_item_multichoice extends feedback_item_base { protected $type = "multichoice"; public function build_editform($item, $feedback, $cm) { global $DB, $CFG; require_once('multichoice_form.php'); //get the lastposition number of the feedback_items $position = $item->position; $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); if ($position == -1) { $i_formselect_last = $lastposition + 1; $i_formselect_value = $lastposition + 1; $item->position = $lastposition + 1; } else { $i_formselect_last = $lastposition; $i_formselect_value = $item->position; } //the elements for position dropdownlist $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true); $item->presentation = empty($item->presentation) ? '' : $item->presentation; $info = $this->get_info($item); $item->ignoreempty = $this->ignoreempty($item); $item->hidenoselect = $this->hidenoselect($item); //all items for dependitem $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); $commonparams = array('cmid'=>$cm->id, 'id'=>isset($item->id) ? $item->id : null, 'typ'=>$item->typ, 'items'=>$feedbackitems, 'feedback'=>$feedback->id); //build the form $customdata = array('item' => $item, 'common' => $commonparams, 'positionlist' => $positionlist, 'position' => $position, 'info' => $info); $this->item_form = new feedback_multichoice_form('edit_item.php', $customdata); } public function save_item() { global $DB; if (!$this->get_data()) { return false; } $item = $this->item; if (isset($item->clone_item) AND $item->clone_item) { $item->id = ''; //to clone this item $item->position++; } $this->set_ignoreempty($item, $item->ignoreempty); $this->set_hidenoselect($item, $item->hidenoselect); $item->hasvalue = $this->get_hasvalue(); if (!$item->id) { $item->id = $DB->insert_record('feedback_item', $item); } else { $DB->update_record('feedback_item', $item); } return $DB->get_record('feedback_item', array('id'=>$item->id)); } //gets an array with three values(typ, name, XXX) //XXX is an object with answertext, answercount and quotient /** * Helper function for collected data, both for analysis page and export to excel * * @param stdClass $item the db-object from feedback_item * @param int $groupid * @param int $courseid * @return array|null */ protected function get_analysed($item, $groupid = false, $courseid = false) { $info = $this->get_info($item); $analysed_item = array(); $analysed_item[] = $item->typ; $analysed_item[] = format_string($item->name); //get the possible answers $answers = null; $answers = explode (FEEDBACK_MULTICHOICE_LINE_SEP, $info->presentation); if (!is_array($answers)) { return null; } //get the values $values = feedback_get_group_values($item, $groupid, $courseid, $this->ignoreempty($item)); if (!$values) { return null; } //get answertext, answercount and quotient for each answer $analysed_answer = array(); if ($info->subtype == 'c') { $sizeofanswers = count($answers); for ($i = 1; $i <= $sizeofanswers; $i++) { $ans = new stdClass(); $ans->answertext = $answers[$i-1]; $ans->answercount = 0; foreach ($values as $value) { //ist die Antwort gleich dem index der Antworten + 1? $vallist = explode(FEEDBACK_MULTICHOICE_LINE_SEP, $value->value); foreach ($vallist as $val) { if ($val == $i) { $ans->answercount++; } } } $ans->quotient = $ans->answercount / count($values); $analysed_answer[] = $ans; } } else { $sizeofanswers = count($answers); for ($i = 1; $i <= $sizeofanswers; $i++) { $ans = new stdClass(); $ans->answertext = $answers[$i-1]; $ans->answercount = 0; foreach ($values as $value) { //ist die Antwort gleich dem index der Antworten + 1? if ($value->value == $i) { $ans->answercount++; } } $ans->quotient = $ans->answercount / count($values); $analysed_answer[] = $ans; } } $analysed_item[] = $analysed_answer; return $analysed_item; } public function get_printval($item, $value) { $info = $this->get_info($item); $printval = ''; if (!isset($value->value)) { return $printval; } $presentation = explode (FEEDBACK_MULTICHOICE_LINE_SEP, $info->presentation); if ($info->subtype == 'c') { $vallist = array_values(explode (FEEDBACK_MULTICHOICE_LINE_SEP, $value->value)); $sizeofvallist = count($vallist); $sizeofpresentation = count($presentation); for ($i = 0; $i < $sizeofvallist; $i++) { for ($k = 0; $k < $sizeofpresentation; $k++) { if ($vallist[$i] == ($k + 1)) {//Die Werte beginnen bei 1, das Array aber mit 0 $printval .= trim(format_string($presentation[$k])) . chr(10); break; } } } } else { $index = 1; foreach ($presentation as $pres) { if ($value->value == $index) { $printval = format_string($pres); break; } $index++; } } return $printval; } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { global $OUTPUT; $analysed_item = $this->get_analysed($item, $groupid, $courseid); if ($analysed_item) { $itemname = $analysed_item[1]; echo "typ} table-reboot\">"; echo ''; $analysed_vals = $analysed_item[2]; $count = 0; $data = []; foreach ($analysed_vals as $val) { $quotient = format_float($val->quotient * 100, 2); $strquotient = ''; if ($val->quotient > 0) { $strquotient = ' ('. $quotient . ' %)'; } $answertext = format_text(trim($val->answertext), FORMAT_HTML, array('noclean' => true, 'para' => false)); $data['labels'][$count] = $answertext; $data['series'][$count] = $val->answercount; $data['series_labels'][$count] = $val->answercount . $strquotient; $count++; } $chart = new \core\chart_bar(); $chart->set_horizontal(true); $series = new \core\chart_series(format_string(get_string("responses", "feedback")), $data['series']); $series->set_labels($data['series_labels']); $chart->add_series($series); $chart->set_labels($data['labels']); echo ''; echo "
'; echo $itemnr . ' '; if (strval($item->label) !== '') { echo '('. format_string($item->label).') '; } echo format_string($itemname); echo '
'. $OUTPUT->render($chart) . '
"; } } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { $analysed_item = $this->get_analysed($item, $groupid, $courseid); if (!$analysed_item) { return $row_offset; } $data = $analysed_item[2]; //frage schreiben $worksheet->write_string($row_offset, 0, $item->label, $xls_formats->head2); $worksheet->write_string($row_offset, 1, $analysed_item[1], $xls_formats->head2); if (is_array($data)) { $sizeofdata = count($data); for ($i = 0; $i < $sizeofdata; $i++) { $analysed_data = $data[$i]; $worksheet->write_string($row_offset, $i + 2, trim($analysed_data->answertext), $xls_formats->head2); $worksheet->write_number($row_offset + 1, $i + 2, $analysed_data->answercount, $xls_formats->default); $worksheet->write_number($row_offset + 2, $i + 2, $analysed_data->quotient, $xls_formats->procent); } } $row_offset += 3; return $row_offset; } /** * Options for the multichoice element * @param stdClass $item * @return array */ protected function get_options($item) { $info = $this->get_info($item); $presentation = explode (FEEDBACK_MULTICHOICE_LINE_SEP, $info->presentation); $options = array(); foreach ($presentation as $idx => $optiontext) { $options[$idx + 1] = format_text($optiontext, FORMAT_HTML, array('noclean' => true, 'para' => false)); } if ($info->subtype === 'r' && !$this->hidenoselect($item)) { $options = array(0 => get_string('not_selected', 'feedback')) + $options; } return $options; } /** * Adds an input element to the complete form * * This element has many options - it can be displayed as group or radio elements, * group of checkboxes or a dropdown list. * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { $info = $this->get_info($item); $name = $this->get_display_name($item); $class = 'multichoice-' . $info->subtype; $inputname = $item->typ . '_' . $item->id; $options = $this->get_options($item); $separator = !empty($info->horizontal) ? ' ' : \html_writer::div('', 'w-100'); $tmpvalue = $form->get_item_value($item) ?? 0; // Used for element defaults, so must be a valid value (not null). // Subtypes: // r = radio // c = checkbox // d = dropdown. if ($info->subtype === 'd' || ($info->subtype === 'r' && $form->is_frozen())) { // Display as a dropdown in the complete form or a single value in the response view. $element = $form->add_form_element($item, ['select', $inputname, $name, array(0 => '') + $options, array('class' => $class)], false, false); $form->set_element_default($inputname, $tmpvalue); $form->set_element_type($inputname, PARAM_INT); } else if ($info->subtype === 'c' && $form->is_frozen()) { // Display list of checkbox values in the response view. $objs = []; foreach (explode(FEEDBACK_MULTICHOICE_LINE_SEP, $form->get_item_value($item)) as $v) { $objs[] = ['static', $inputname."[$v]", '', isset($options[$v]) ? $options[$v] : '']; } $element = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class); } else { // Display group or radio or checkbox elements. $class .= ' multichoice-' . ($info->horizontal ? 'horizontal' : 'vertical'); $objs = []; if ($info->subtype === 'c') { // Checkboxes. $objs[] = ['hidden', $inputname.'[0]', 0]; $form->set_element_type($inputname.'[0]', PARAM_INT); foreach ($options as $idx => $label) { $objs[] = ['advcheckbox', $inputname.'['.$idx.']', '', $label, null, array(0, $idx)]; $form->set_element_type($inputname.'['.$idx.']', PARAM_INT); } // Span to hold the element id. The id is used for drag and drop reordering. $objs[] = ['static', '', '', html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])]; $element = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class); if ($tmpvalue) { foreach (explode(FEEDBACK_MULTICHOICE_LINE_SEP, $tmpvalue) as $v) { $form->set_element_default($inputname.'['.$v.']', $v); } } } else { // Radio. if (!array_key_exists(0, $options)) { // Always add a hidden element to the group to guarantee we get a value in the submit data. $objs[] = ['hidden', $inputname, 0]; } foreach ($options as $idx => $label) { $objs[] = ['radio', $inputname, '', $label, $idx]; } // Span to hold the element id. The id is used for drag and drop reordering. $objs[] = ['static', '', '', html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])]; $element = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class); $form->set_element_default($inputname, $tmpvalue); $form->set_element_type($inputname, PARAM_INT); } } // Process 'required' rule. if ($item->required) { $elementname = $element->getName(); $form->add_validation_rule(function($values) use ($elementname, $item) { $inputname = $item->typ . '_' . $item->id; return empty($values[$inputname]) || (is_array($values[$inputname]) && !array_filter($values[$inputname])) ? array($elementname => get_string('required')) : true; }); } } /** * Prepares value that user put in the form for storing in DB * @param array $value * @return string */ public function create_value($value) { // Could be an array (multichoice checkbox) or single value (multichoice radio or dropdown). $value = is_array($value) ? $value : [$value]; $value = array_unique(array_filter($value)); return join(FEEDBACK_MULTICHOICE_LINE_SEP, $value); } /** * Compares the dbvalue with the dependvalue * * @param stdClass $item * @param string $dbvalue is the value input by user in the format as it is stored in the db * @param string $dependvalue is the value that it needs to be compared against */ public function compare_value($item, $dbvalue, $dependvalue) { if (is_array($dbvalue)) { $dbvalues = $dbvalue; } else { $dbvalues = explode(FEEDBACK_MULTICHOICE_LINE_SEP, $dbvalue); } $info = $this->get_info($item); $presentation = explode (FEEDBACK_MULTICHOICE_LINE_SEP, $info->presentation); $index = 1; foreach ($presentation as $pres) { foreach ($dbvalues as $dbval) { if ($dbval == $index AND trim($pres) == $dependvalue) { return true; } } $index++; } return false; } public function get_info($item) { $presentation = empty($item->presentation) ? '' : $item->presentation; $info = new stdClass(); //check the subtype of the multichoice //it can be check(c), radio(r) or dropdown(d) $info->subtype = ''; $info->presentation = ''; $info->horizontal = false; $parts = explode(FEEDBACK_MULTICHOICE_TYPE_SEP, $item->presentation); $info->subtype = $parts[0]; if (count($parts) > 1) { $info->presentation = $parts[1]; } if (!isset($info->subtype)) { $info->subtype = 'r'; } if ($info->subtype != 'd') { $parts = explode(FEEDBACK_MULTICHOICE_ADJUST_SEP, $info->presentation); $info->presentation = $parts[0]; if (count($parts) > 1) { $info->horizontal = $parts[1]; } if (isset($info->horizontal) AND $info->horizontal == 1) { $info->horizontal = true; } else { $info->horizontal = false; } } return $info; } public function set_ignoreempty($item, $ignoreempty=true) { $item->options = str_replace(FEEDBACK_MULTICHOICE_IGNOREEMPTY, '', $item->options); if ($ignoreempty) { $item->options .= FEEDBACK_MULTICHOICE_IGNOREEMPTY; } } public function ignoreempty($item) { if (strstr($item->options, FEEDBACK_MULTICHOICE_IGNOREEMPTY)) { return true; } return false; } public function set_hidenoselect($item, $hidenoselect=true) { $item->options = str_replace(FEEDBACK_MULTICHOICE_HIDENOSELECT, '', $item->options); if ($hidenoselect) { $item->options .= FEEDBACK_MULTICHOICE_HIDENOSELECT; } } public function hidenoselect($item) { if (strstr($item->options, FEEDBACK_MULTICHOICE_HIDENOSELECT)) { return true; } return false; } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { $externaldata = array(); $data = $this->get_analysed($item, $groupid, $courseid); if ($data && !empty($data[2]) && is_array($data[2])) { foreach ($data[2] as $d) { $externaldata[] = json_encode($d); } } return $externaldata; } } PKB\\q9%item/multichoice/multichoice_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_multichoice_form extends feedback_item_form { protected $type = "multichoice"; public function definition() { $item = $this->_customdata['item']; $common = $this->_customdata['common']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $mform =& $this->_form; $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('advcheckbox', 'required', get_string('required', 'feedback'), '' , null , array(0, 1)); $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size' => FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, 'maxlength' => 255)); $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size' => FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength' => 255)); $mform->addElement('select', 'subtype', get_string('multichoicetype', 'feedback').' ', array('r'=>get_string('radio', 'feedback'), 'c'=>get_string('check', 'feedback'), 'd'=>get_string('dropdown', 'feedback'))); $mform->addElement('select', 'horizontal', get_string('adjustment', 'feedback').' ', array(0 => get_string('vertical', 'feedback'), 1 => get_string('horizontal', 'feedback'))); $mform->hideIf('horizontal', 'subtype', 'eq', 'd'); $mform->addElement('selectyesno', 'hidenoselect', get_string('hide_no_select_option', 'feedback')); $mform->hideIf('hidenoselect', 'subtype', 'ne', 'r'); $mform->addElement('selectyesno', 'ignoreempty', get_string('do_not_analyse_empty_submits', 'feedback')); $mform->addElement('textarea', 'values', get_string('multichoice_values', 'feedback'), 'wrap="virtual" rows="10" cols="65"'); $mform->addElement('static', 'hint', '', get_string('use_one_line_for_each_value', 'feedback')); parent::definition(); $this->set_data($item); } public function set_data($item) { $info = $this->_customdata['info']; $item->horizontal = $info->horizontal; $item->subtype = $info->subtype; $itemvalues = str_replace(FEEDBACK_MULTICHOICE_LINE_SEP, "\n", $info->presentation); $itemvalues = str_replace("\n\n", "\n", $itemvalues); $item->values = $itemvalues; return parent::set_data($item); } public function get_data() { if (!$item = parent::get_data()) { return false; } $presentation = str_replace("\n", FEEDBACK_MULTICHOICE_LINE_SEP, trim($item->values)); if (!isset($item->subtype)) { $subtype = 'r'; } else { $subtype = substr($item->subtype, 0, 1); } if (isset($item->horizontal) AND $item->horizontal == 1 AND $subtype != 'd') { $presentation .= FEEDBACK_MULTICHOICE_ADJUST_SEP.'1'; } if (!isset($item->hidenoselect)) { $item->hidenoselect = 1; } if (!isset($item->ignoreempty)) { $item->ignoreempty = 0; } $item->presentation = $subtype.FEEDBACK_MULTICHOICE_TYPE_SEP.$presentation; return $item; } } PKB\s22item/feedback_item_class.phpnu[. abstract class feedback_item_base { /** @var string type of the element, should be overridden by each item type */ protected $type; /** @var feedback_item_form */ protected $item_form; /** @var stdClass */ protected $item; /** * constructor */ public function __construct() { } /** * Displays the form for editing an item * * this function only can used after the call of build_editform() */ public function show_editform() { $this->item_form->display(); } /** * Checks if the editing form was cancelled * * @return bool */ public function is_cancelled() { return $this->item_form->is_cancelled(); } /** * Gets submitted data from the edit form and saves it in $this->item * * @return bool */ public function get_data() { if ($this->item !== null) { return true; } if ($this->item = $this->item_form->get_data()) { return true; } return false; } /** * Set the item data (to be used by data generators). * * @param stdClass $itemdata the item data to set * @since Moodle 3.3 */ public function set_data($itemdata) { $this->item = $itemdata; } /** * Creates and returns an instance of the form for editing the item * * @param stdClass $item * @param stdClass $feedback * @param cm_info|stdClass $cm */ abstract public function build_editform($item, $feedback, $cm); /** * Saves the item after it has been edited (or created) */ abstract public function save_item(); /** * Converts the value from complete_form data to the string value that is stored in the db. * @param mixed $value element from mod_feedback_complete_form::get_data() with the name $item->typ.'_'.$item->id * @return string */ public function create_value($value) { return strval($value); } /** * Compares the dbvalue with the dependvalue * * @param stdClass $item * @param string $dbvalue is the value input by user in the format as it is stored in the db * @param string $dependvalue is the value that it needs to be compared against */ public function compare_value($item, $dbvalue, $dependvalue) { return strval($dbvalue) === strval($dependvalue); } /** * Wether this item type has a value that is expected from the user and saved in the stored values. * @return int */ public function get_hasvalue() { return 1; } /** * Wether this item can be set as both required and not * @return bool */ public function can_switch_require() { return true; } /** * Adds summary information about an item to the Excel export file * * @param object $worksheet a reference to the pear_spreadsheet-object * @param integer $row_offset * @param stdClass $xls_formats see analysis_to_excel.php * @param object $item the db-object from feedback_item * @param integer $groupid * @param integer $courseid * @return integer the new row_offset */ abstract public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false); /** * Prints analysis for the current item * * @param $item the db-object from feedback_item * @param string $itemnr * @param integer $groupid * @param integer $courseid * @return integer the new itemnr */ abstract public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false); /** * Prepares the value for exporting to Excel * * @param object $item the db-object from feedback_item * @param object $value object with item-related value from feedback_values in the 'value' property * @return string */ abstract public function get_printval($item, $value); /** * Returns the formatted name of the item for the complete form or response view * * @param stdClass $item * @param bool $withpostfix * @return string */ public function get_display_name($item, $withpostfix = true) { return format_text($item->name, FORMAT_HTML, array('noclean' => true, 'para' => false)) . ($withpostfix ? $this->get_display_name_postfix($item) : ''); } /** * Returns the postfix to be appended to the display name that is based on other settings * * @param stdClass $item * @return string */ public function get_display_name_postfix($item) { return ''; } /** * Adds an input element to the complete form * * This method is called: * - to display the form when user completes feedback * - to display existing elements when teacher edits the feedback items * - to display the feedback preview (print.php) * - to display the completed response * - to preview a feedback template * * If it is important which mode the form is in, use $form->get_mode() * * Each item type must add a single form element with the name $item->typ.'_'.$item->id * This element must always be present in form data even if nothing is selected (i.e. use advcheckbox and not checkbox). * To add an element use either: * $form->add_form_element() - adds a single element to the form * $form->add_form_group_element() - adds a group element to the form * * Other useful methods: * $form->get_item_value() * $form->set_element_default() * $form->add_validation_rule() * $form->set_element_type() * * The element must support freezing so it can be used for viewing the response as well. * If the desired form element does not support freezing, check $form->is_frozen() * and create a static element instead. * * @param stdClass $item * @param mod_feedback_complete_form $form */ abstract public function complete_form_element($item, $form); /** * Returns the list of actions allowed on this item in the edit mode * * @param stdClass $item * @param stdClass $feedback * @param cm_info $cm * @return action_menu_link[] */ public function edit_actions($item, $feedback, $cm) { $actions = []; $strupdate = get_string('edit_item', 'feedback'); $actions['update'] = new action_menu_link_secondary( new moodle_url('/mod/feedback/edit_item.php', ['id' => $item->id]), new pix_icon('t/edit', $strupdate, 'moodle', ['class' => 'iconsmall', 'title' => '']), $strupdate, ['class' => 'editing_update', 'data-action' => 'update'] ); if ($this->can_switch_require()) { if ($item->required == 1) { $buttontitle = get_string('switch_item_to_not_required', 'feedback'); $buttonimg = 'required'; } else { $buttontitle = get_string('switch_item_to_required', 'feedback'); $buttonimg = 'notrequired'; } $actions['required'] = new action_menu_link_secondary( new moodle_url( '/mod/feedback/edit.php', ['id' => $cm->id, 'switchitemrequired' => $item->id, 'sesskey' => sesskey()] ), new pix_icon($buttonimg, $buttontitle, 'feedback', array('class' => 'iconsmall', 'title' => '')), $buttontitle, ['class' => 'editing_togglerequired', 'data-action' => 'togglerequired'] ); } $strdelete = get_string('delete_item', 'feedback'); $actions['delete'] = new action_menu_link_secondary( new moodle_url( '/mod/feedback/edit.php', ['id' => $cm->id, 'deleteitem' => $item->id, 'sesskey' => sesskey()] ), new pix_icon('t/delete', $strdelete, 'moodle', array('class' => 'iconsmall', 'title' => '')), $strdelete, ['class' => 'editing_delete text-danger', 'data-action' => 'delete'] ); return $actions; } /** * Return extra data for external functions. * * Some items may have additional configuration data or default values that should be returned for external functions: * - Info elements: The default value information (course or category name) * - Captcha: The recaptcha challenge hash key * * @param stdClass $item the item object * @return str the data, may be json_encoded for large structures */ public function get_data_for_external($item) { return null; } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ abstract public function get_analysed_for_external($item, $groupid = false, $courseid = false); } //a dummy class to realize pagebreaks class feedback_item_pagebreak extends feedback_item_base { protected $type = "pagebreak"; public function show_editform() { } /** * Checks if the editing form was cancelled * @return bool */ public function is_cancelled() { } public function get_data() { } public function build_editform($item, $feedback, $cm) { } public function save_item() { } public function create_value($data) { } public function get_hasvalue() { return 0; } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { } public function get_printval($item, $value) { } public function can_switch_require() { return false; } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { $form->add_form_element($item, ['static', $item->typ.'_'.$item->id, get_string('pagebreak', 'feedback'), html_writer::empty_tag('hr', ['class' => 'feedback_pagebreak', 'id' => 'feedback_item_' . $item->id]) ]); } /** * Returns the list of actions allowed on this item in the edit mode * * @param stdClass $item * @param stdClass $feedback * @param cm_info $cm * @return action_menu_link[] */ public function edit_actions($item, $feedback, $cm) { $actions = array(); $strdelete = get_string('delete_pagebreak', 'feedback'); $actions['delete'] = new action_menu_link_secondary( new moodle_url('/mod/feedback/edit.php', array('id' => $cm->id, 'deleteitem' => $item->id, 'sesskey' => sesskey())), new pix_icon('t/delete', $strdelete, 'moodle', array('class' => 'iconsmall', 'title' => '')), $strdelete, ['class' => 'editing_delete text-danger', 'data-action' => 'delete'] ); return $actions; } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { return; } } PKB\ynitem/textfield/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); class feedback_item_textfield extends feedback_item_base { protected $type = "textfield"; public function build_editform($item, $feedback, $cm) { global $DB, $CFG; require_once('textfield_form.php'); //get the lastposition number of the feedback_items $position = $item->position; $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); if ($position == -1) { $i_formselect_last = $lastposition + 1; $i_formselect_value = $lastposition + 1; $item->position = $lastposition + 1; } else { $i_formselect_last = $lastposition; $i_formselect_value = $item->position; } //the elements for position dropdownlist $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true); $item->presentation = empty($item->presentation) ? '' : $item->presentation; $size_and_length = explode('|', $item->presentation); if (isset($size_and_length[0]) AND $size_and_length[0] >= 5) { $itemsize = $size_and_length[0]; } else { $itemsize = 30; } $itemlength = isset($size_and_length[1]) ? $size_and_length[1] : 255; $item->itemsize = $itemsize; $item->itemmaxlength = $itemlength; //all items for dependitem $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); $commonparams = array('cmid' => $cm->id, 'id' => isset($item->id) ? $item->id : null, 'typ' => $item->typ, 'items' => $feedbackitems, 'feedback' => $feedback->id); //build the form $customdata = array('item' => $item, 'common' => $commonparams, 'positionlist' => $positionlist, 'position' => $position); $this->item_form = new feedback_textfield_form('edit_item.php', $customdata); } public function save_item() { global $DB; if (!$this->get_data()) { return false; } $item = $this->item; if (isset($item->clone_item) AND $item->clone_item) { $item->id = ''; //to clone this item $item->position++; } $item->hasvalue = $this->get_hasvalue(); if (!$item->id) { $item->id = $DB->insert_record('feedback_item', $item); } else { $DB->update_record('feedback_item', $item); } return $DB->get_record('feedback_item', array('id'=>$item->id)); } /** * Helper function for collected data for exporting to excel * * @param stdClass $item the db-object from feedback_item * @param int $groupid * @param int $courseid * @return stdClass */ protected function get_analysed($item, $groupid = false, $courseid = false) { $analysed_val = new stdClass(); $analysed_val->data = null; $analysed_val->name = $item->name; $values = feedback_get_group_values($item, $groupid, $courseid); if ($values) { $data = array(); foreach ($values as $value) { $data[] = str_replace("\n", '
', $value->value); } $analysed_val->data = $data; } return $analysed_val; } public function get_printval($item, $value) { if (!isset($value->value)) { return ''; } return $value->value; } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { $values = feedback_get_group_values($item, $groupid, $courseid); if ($values) { echo "typ} table-reboot\">"; echo ''; foreach ($values as $value) { $class = strlen(trim($value->value)) ? '' : ' class="isempty"'; echo ''; } echo '
'; echo $itemnr . ' '; if (strval($item->label) !== '') { echo '('. format_string($item->label).') '; } echo $this->get_display_name($item); echo '
'; echo str_replace("\n", '
', $value->value); echo '
'; } } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { $analysed_item = $this->get_analysed($item, $groupid, $courseid); $worksheet->write_string($row_offset, 0, $item->label, $xls_formats->head2); $worksheet->write_string($row_offset, 1, $item->name, $xls_formats->head2); $data = $analysed_item->data; if (is_array($data)) { $worksheet->write_string($row_offset, 2, htmlspecialchars_decode($data[0], ENT_QUOTES), $xls_formats->value_bold); $row_offset++; $sizeofdata = count($data); for ($i = 1; $i < $sizeofdata; $i++) { $worksheet->write_string($row_offset, 2, htmlspecialchars_decode($data[$i], ENT_QUOTES), $xls_formats->default); $row_offset++; } } $row_offset++; return $row_offset; } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { $name = $this->get_display_name($item); $inputname = $item->typ . '_' . $item->id; list($size, $maxlength) = explode ("|", $item->presentation); $form->add_form_element($item, ['text', $inputname, $name, ['maxlength' => $maxlength, 'size' => $size]]); $form->set_element_type($inputname, PARAM_NOTAGS); $form->add_element_rule($inputname, get_string('maximumchars', '', $maxlength), 'maxlength', $maxlength, 'client'); } /** * Converts the value from complete_form data to the string value that is stored in the db. * @param mixed $value element from mod_feedback_complete_form::get_data() with the name $item->typ.'_'.$item->id * @return string */ public function create_value($value) { return s($value); } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { $externaldata = array(); $data = $this->get_analysed($item, $groupid, $courseid); if (is_array($data->data)) { return $data->data; // No need to json, scalar type. } return $externaldata; } } PKB\51 1 !item/textfield/textfield_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_textfield_form extends feedback_item_form { protected $type = "textfield"; public function definition() { $item = $this->_customdata['item']; $common = $this->_customdata['common']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $mform =& $this->_form; $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('advcheckbox', 'required', get_string('required', 'feedback'), '' , null , array(0, 1)); $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size'=>FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('select', 'itemsize', get_string('textfield_size', 'feedback').' ', array_slice(range(0, 255), 5, 255, true)); $mform->addElement('text', 'itemmaxlength', get_string('textfield_maxlength', 'feedback')); $mform->setType('itemmaxlength', PARAM_INT); $mform->addRule('itemmaxlength', null, 'numeric', null, 'client'); parent::definition(); $this->set_data($item); } public function get_data() { if (!$item = parent::get_data()) { return false; } $item->presentation = $item->itemsize . '|'. $item->itemmaxlength; return $item; } } PKB\....item/numeric/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); class feedback_item_numeric extends feedback_item_base { protected $type = "numeric"; public function build_editform($item, $feedback, $cm) { global $DB, $CFG; require_once('numeric_form.php'); //get the lastposition number of the feedback_items $position = $item->position; $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); if ($position == -1) { $i_formselect_last = $lastposition + 1; $i_formselect_value = $lastposition + 1; $item->position = $lastposition + 1; } else { $i_formselect_last = $lastposition; $i_formselect_value = $item->position; } //the elements for position dropdownlist $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true); $item->presentation = empty($item->presentation) ? '' : $item->presentation; $range_from_to = explode('|', $item->presentation); if (isset($range_from_to[0]) AND is_numeric($range_from_to[0])) { $range_from = $this->format_float($range_from_to[0]); } else { $range_from = '-'; } if (isset($range_from_to[1]) AND is_numeric($range_from_to[1])) { $range_to = $this->format_float($range_from_to[1]); } else { $range_to = '-'; } $item->rangefrom = $range_from; $item->rangeto = $range_to; //all items for dependitem $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); $commonparams = array('cmid'=>$cm->id, 'id'=>isset($item->id) ? $item->id : null, 'typ'=>$item->typ, 'items'=>$feedbackitems, 'feedback'=>$feedback->id); //build the form $customdata = array('item' => $item, 'common' => $commonparams, 'positionlist' => $positionlist, 'position' => $position); $this->item_form = new feedback_numeric_form('edit_item.php', $customdata); } public function save_item() { global $DB; if (!$this->get_data()) { return false; } $item = $this->item; if (isset($item->clone_item) AND $item->clone_item) { $item->id = ''; //to clone this item $item->position++; } $item->hasvalue = $this->get_hasvalue(); if (!$item->id) { $item->id = $DB->insert_record('feedback_item', $item); } else { $DB->update_record('feedback_item', $item); } return $DB->get_record('feedback_item', array('id'=>$item->id)); } /** * Helper function for collected data, both for analysis page and export to excel * * @param stdClass $item the db-object from feedback_item * @param int $groupid * @param int $courseid * @return stdClass */ protected function get_analysed($item, $groupid = false, $courseid = false) { global $DB; $analysed = new stdClass(); $analysed->data = array(); $analysed->name = $item->name; $values = feedback_get_group_values($item, $groupid, $courseid); $avg = 0.0; $counter = 0; if ($values) { $data = array(); foreach ($values as $value) { if (is_numeric($value->value)) { $data[] = $value->value; $avg += $value->value; $counter++; } } $avg = $counter > 0 ? $avg / $counter : null; $analysed->data = $data; $analysed->avg = $avg; } return $analysed; } public function get_printval($item, $value) { if (!isset($value->value)) { return ''; } return $value->value; } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { $values = $this->get_analysed($item, $groupid, $courseid); if (isset($values->data) AND is_array($values->data)) { echo "typ} table-reboot\">"; echo ''; foreach ($values->data as $value) { echo ''; } if (isset($values->avg)) { $avg = format_float($values->avg, 2); } else { $avg = '-'; } echo ''; echo '
'; echo $itemnr . ' '; if (strval($item->label) !== '') { echo '('. format_string($item->label).') '; } echo format_text($item->name, FORMAT_HTML, array('noclean' => true, 'para' => false)); echo '
'; echo $this->format_float($value); echo '
'; echo get_string('average', 'feedback').': '.$avg; echo '
'; } } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { $analysed_item = $this->get_analysed($item, $groupid, $courseid); $worksheet->write_string($row_offset, 0, $item->label, $xls_formats->head2); $worksheet->write_string($row_offset, 1, $item->name, $xls_formats->head2); $data = $analysed_item->data; if (is_array($data)) { // Export average. $worksheet->write_string($row_offset, 2, get_string('average', 'feedback'), $xls_formats->value_bold); if (isset($analysed_item->avg)) { $worksheet->write_number($row_offset + 1, 2, $analysed_item->avg, $xls_formats->value_bold); } else { $worksheet->write_string($row_offset + 1, 2, '', $xls_formats->value_bold); } $row_offset++; } $row_offset++; return $row_offset; } /** * Prints the float nicely in the localized format * * Similar to format_float() but automatically calculates the number of decimal places * * @param float $value The float to print * @return string */ protected function format_float($value) { if (!is_numeric($value)) { return null; } $decimal = is_int($value) ? 0 : strlen(substr(strrchr($value, '.'), 1)); return format_float($value, $decimal); } /** * Returns human-readable boundaries (min - max) * @param stdClass $item * @return string */ protected function get_boundaries_for_display($item) { list($rangefrom, $rangeto) = explode('|', $item->presentation); if (!isset($rangefrom) || !is_numeric($rangefrom)) { $rangefrom = null; } if (!isset($rangeto) || !is_numeric($rangeto)) { $rangeto = null; } if (is_null($rangefrom) && is_numeric($rangeto)) { return ' (' . get_string('maximal', 'feedback') . ': ' . $this->format_float($rangeto) . ')'; } if (is_numeric($rangefrom) && is_null($rangeto)) { return ' (' . get_string('minimal', 'feedback') . ': ' . $this->format_float($rangefrom) . ')'; } if (is_null($rangefrom) && is_null($rangeto)) { return ''; } return ' (' . $this->format_float($rangefrom) . ' - ' . $this->format_float($rangeto) . ')'; } /** * Returns the postfix to be appended to the display name that is based on other settings * * @param stdClass $item * @return string */ public function get_display_name_postfix($item) { return html_writer::span($this->get_boundaries_for_display($item), 'boundaries'); } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { $name = $this->get_display_name($item); $inputname = $item->typ . '_' . $item->id; $form->add_form_element($item, ['text', $inputname, $name], true, false ); $form->set_element_type($inputname, PARAM_NOTAGS); $tmpvalue = $this->format_float($form->get_item_value($item)); $form->set_element_default($inputname, $tmpvalue); // Add form validation rule to check for boundaries. $form->add_validation_rule(function($values, $files) use ($item) { $inputname = $item->typ . '_' . $item->id; list($rangefrom, $rangeto) = explode('|', $item->presentation); if (!isset($values[$inputname]) || trim($values[$inputname]) === '') { return $item->required ? array($inputname => get_string('required')) : true; } $value = unformat_float($values[$inputname], true); if ($value === false) { return array($inputname => get_string('invalidnum', 'error')); } if ((is_numeric($rangefrom) && $value < floatval($rangefrom)) || (is_numeric($rangeto) && $value > floatval($rangeto))) { return array($inputname => get_string('numberoutofrange', 'feedback')); } return true; }); } public function create_value($data) { $data = unformat_float($data, true); if (is_numeric($data)) { $data = floatval($data); } else { $data = ''; } return $data; } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { $externaldata = array(); $data = $this->get_analysed($item, $groupid, $courseid); if (is_array($data->data)) { return $data->data; // No need to json, scalar type. } return $externaldata; } } PKB\}!4 4 item/numeric/numeric_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_numeric_form extends feedback_item_form { protected $type = "numeric"; public function definition() { $item = $this->_customdata['item']; $common = $this->_customdata['common']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $mform =& $this->_form; $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('advcheckbox', 'required', get_string('required', 'feedback'), '' , null , array(0, 1)); $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size'=>FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('text', 'rangefrom', get_string('numeric_range_from', 'feedback'), array('size'=>10, 'maxlength'=>10)); $mform->setType('rangefrom', PARAM_RAW); $mform->addElement('text', 'rangeto', get_string('numeric_range_to', 'feedback'), array('size'=>10, 'maxlength'=>10)); $mform->setType('rangeto', PARAM_RAW); parent::definition(); $this->set_data($item); } public function get_data() { if (!$item = parent::get_data()) { return false; } $num1 = unformat_float($item->rangefrom, true); if ($num1 === false || $num1 === null) { $num1 = '-'; } $num2 = unformat_float($item->rangeto, true); if ($num2 === false || $num2 === null) { $num2 = '-'; } if ($num1 === '-' OR $num2 === '-') { $item->presentation = $num1 . '|'. $num2; return $item; } if ($num1 > $num2) { $item->presentation = $num2 . '|'. $num1; } else { $item->presentation = $num1 . '|'. $num2; } return $item; } } PKB\--item/captcha/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); class feedback_item_captcha extends feedback_item_base { protected $type = "captcha"; public function build_editform($item, $feedback, $cm) { global $DB; $editurl = new moodle_url('/mod/feedback/edit.php', array('id'=>$cm->id)); // There are no settings for recaptcha. if (isset($item->id) AND $item->id > 0) { notice(get_string('there_are_no_settings_for_recaptcha', 'feedback'), $editurl->out()); exit; } // Only one recaptcha can be in a feedback. $params = array('feedback' => $feedback->id, 'typ' => $this->type); if ($DB->record_exists('feedback_item', $params)) { notice(get_string('only_one_captcha_allowed', 'feedback'), $editurl->out()); exit; } $this->item = $item; $this->item_form = true; // Dummy. $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); $this->item->feedback = $feedback->id; $this->item->template = 0; $this->item->name = get_string('captcha', 'feedback'); $this->item->label = ''; $this->item->presentation = ''; $this->item->typ = $this->type; $this->item->hasvalue = $this->get_hasvalue(); $this->item->position = $lastposition + 1; $this->item->required = 1; $this->item->dependitem = 0; $this->item->dependvalue = ''; $this->item->options = ''; } public function show_editform() { } public function is_cancelled() { return false; } public function get_data() { return true; } public function save_item() { global $DB; if (!$this->item) { return false; } if (empty($this->item->id)) { $this->item->id = $DB->insert_record('feedback_item', $this->item); } else { $DB->update_record('feedback_item', $this->item); } return $DB->get_record('feedback_item', array('id'=>$this->item->id)); } public function get_printval($item, $value) { return ''; } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { return $itemnr; } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { return $row_offset; } /** * Returns the formatted name of the item for the complete form or response view * * @param stdClass $item * @param bool $withpostfix * @return string */ public function get_display_name($item, $withpostfix = true) { return get_string('captcha', 'feedback'); } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { $name = $this->get_display_name($item); $inputname = $item->typ . '_' . $item->id; if ($form->get_mode() != mod_feedback_complete_form::MODE_COMPLETE) { // Span to hold the element id. The id is used for drag and drop reordering. $form->add_form_element($item, ['static', $inputname, $name, html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])], false, false); } else { // Add recaptcha element that is used during the form validation. $form->add_form_element($item, ['recaptcha', $inputname . 'recaptcha', $name], false, false); // Add hidden element with value "1" that will be saved in the values table after completion. $form->add_form_element($item, ['hidden', $inputname, 1], false); $form->set_element_type($inputname, PARAM_INT); } // Add recaptcha validation to the form. $form->add_validation_rule(function($values, $files) use ($item, $form) { $elementname = $item->typ . '_' . $item->id . 'recaptcha'; $recaptchaelement = $form->get_form_element($elementname); if (empty($values['g-recaptcha-response'])) { return array($elementname => get_string('required')); } else { $response = $values['g-recaptcha-response']; if (true !== ($result = $recaptchaelement->verify($response))) { return array($elementname => $result); } } return true; }); } public function create_value($data) { global $USER; return $USER->sesskey; } public function get_hasvalue() { global $CFG; // Is recaptcha configured in moodle? if (empty($CFG->recaptchaprivatekey) OR empty($CFG->recaptchapublickey)) { return 0; } return 1; } public function can_switch_require() { return false; } /** * Returns the list of actions allowed on this item in the edit mode * * @param stdClass $item * @param stdClass $feedback * @param cm_info $cm * @return action_menu_link[] */ public function edit_actions($item, $feedback, $cm) { $actions = parent::edit_actions($item, $feedback, $cm); unset($actions['update']); return $actions; } public function get_data_for_external($item) { global $CFG; if (empty($CFG->recaptchaprivatekey) || empty($CFG->recaptchapublickey)) { return null; } // With reCAPTCHA v2 the captcha will be rendered by the mobile client using just the publickey. $data[] = $CFG->recaptchapublickey; return json_encode($data); } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { return []; } } PKB\Q88item/captcha/captcha_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_captcha_form extends feedback_item_form { protected $type = "captcha"; public function definition() { $item = $this->_customdata['item']; $common = $this->_customdata['common']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $mform =& $this->_form; $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('advcheckbox', 'required', get_string('required', 'feedback'), '' , null , array(0, 1)); $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size'=>FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('select', 'presentation', get_string('count_of_nums', 'feedback').' ', array_slice(range(0, 10), 3, 10, true)); parent::definition(); $this->set_data($item); } } PKB\z܇titem/captcha/print_captcha.phpnu[. require_once('../../../../config.php'); $id = required_param('id', PARAM_INT); $PAGE->set_url('/mod/feedback/item/captcha/print_captcha.php', array('id'=>$id)); if ($id) { if (! $cm = get_coursemodule_from_id('feedback', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $feedback = $DB->get_record("feedback", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidcoursemodule'); } } if (!isset($SESSION->feedback->item->captcha)) { throw new \moodle_exception('captchanotset', 'feedback'); } $height = 40; $charcount = $SESSION->feedback->item->captcha->charcount; $fontfile = $CFG->libdir.'/default.ttf'; $ttfbox = imagettfbbox ( 30, 0, $fontfile, 'H' );//the text to measure $charwidth = $ttfbox[2]; $width = $charcount * $charwidth; $scale = 0.3; $elipsesize = intval((($width + $height)/2) / 5); $factor_x = intval($width * $scale); $factor_y = intval($height * $scale); //I split the colors in three ranges //given are the max-min-values $colors = array(array(0, 40), array(50, 200), array(210, 255)); list($col_text1, $col_el, $col_text2) = $colors; //if the text is in color_1 so the elipses can be in color_2 or color_3 //if the text is in color_2 so the elipses can be in color_1 or color_3 //and so on. $textcolnum = rand(1, 3); //create the numbers to print out $nums = array(); for ($i = 0; $i < $charcount; $i++) { $nums[] = rand(0, 9); //Ziffern von 0- } //to draw enough elipses so I draw 0.2 * width and 0.2 * height //we need th colors for that $properties = array(); for ($x = 0; $x < $factor_x; $x++) { for ($y = 0; $y < $factor_y; $y++) { $propobj = new stdClass(); $propobj->x = intval($x / $scale); $propobj->y = intval($y / $scale); $propobj->red = get_random_color($col_el[0], $col_el[1]); $propobj->green = get_random_color($col_el[0], $col_el[1]); $propobj->blue = get_random_color($col_el[0], $col_el[1]); $properties[] = $propobj; } } shuffle($properties); // create a blank image $image = imagecreatetruecolor($width, $height); $bg = imagecolorallocate($image, 0, 0, 0); for ($i = 0; $i < ($factor_x * $factor_y); $i++) { $propobj = $properties[$i]; // choose a color for the ellipse $col_ellipse = imagecolorallocate($image, $propobj->red, $propobj->green, $propobj->blue); // draw the white ellipse imagefilledellipse($image, $propobj->x, $propobj->y, $elipsesize, $elipsesize, $col_ellipse); } $checkchar = ''; for ($i = 0; $i < $charcount; $i++) { $colnum = rand(1, 2); $textcol = new stdClass(); $textcol->red = get_random_color(${'col_text'.$colnum}[0], ${'col_text'.$colnum}[1]); $textcol->green = get_random_color(${'col_text'.$colnum}[0], ${'col_text'.$colnum}[1]); $textcol->blue = get_random_color(${'col_text'.$colnum}[0], ${'col_text'.$colnum}[1]); $color_text = imagecolorallocate($image, $textcol->red, $textcol->green, $textcol->blue); $angle_text = rand(-20, 20); $left_text = $i * $charwidth; $text = $nums[$i]; $checkchar .= $text; imagettftext($image, 30, $angle_text, $left_text, 35, $color_text, $fontfile, $text); } $SESSION->feedback->item->captcha->checkchar = $checkchar; // output the picture header("Content-type: image/png"); imagepng($image); function get_random_color($val1 = 0, $val2 = 255) { $min = $val1 < $val2 ? $val1 : $val2; $max = $val1 > $val2 ? $val1 : $val2; return rand($min, $max); } PKB\3?!\L\Litem/multichoicerated/lib.phpnu[. defined('MOODLE_INTERNAL') OR die('not allowed'); require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); define('FEEDBACK_RADIORATED_ADJUST_SEP', '<<<<<'); define('FEEDBACK_MULTICHOICERATED_MAXCOUNT', 10); //count of possible items define('FEEDBACK_MULTICHOICERATED_VALUE_SEP', '####'); define('FEEDBACK_MULTICHOICERATED_VALUE_SEP2', '/'); define('FEEDBACK_MULTICHOICERATED_TYPE_SEP', '>>>>>'); define('FEEDBACK_MULTICHOICERATED_LINE_SEP', '|'); define('FEEDBACK_MULTICHOICERATED_ADJUST_SEP', '<<<<<'); define('FEEDBACK_MULTICHOICERATED_IGNOREEMPTY', 'i'); define('FEEDBACK_MULTICHOICERATED_HIDENOSELECT', 'h'); class feedback_item_multichoicerated extends feedback_item_base { protected $type = "multichoicerated"; public function build_editform($item, $feedback, $cm) { global $DB, $CFG; require_once('multichoicerated_form.php'); //get the lastposition number of the feedback_items $position = $item->position; $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); if ($position == -1) { $i_formselect_last = $lastposition + 1; $i_formselect_value = $lastposition + 1; $item->position = $lastposition + 1; } else { $i_formselect_last = $lastposition; $i_formselect_value = $item->position; } //the elements for position dropdownlist $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true); $item->presentation = empty($item->presentation) ? '' : $item->presentation; $info = $this->get_info($item); $item->ignoreempty = $this->ignoreempty($item); $item->hidenoselect = $this->hidenoselect($item); //all items for dependitem $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); $commonparams = array('cmid'=>$cm->id, 'id'=>isset($item->id) ? $item->id : null, 'typ'=>$item->typ, 'items'=>$feedbackitems, 'feedback'=>$feedback->id); //build the form $customdata = array('item' => $item, 'common' => $commonparams, 'positionlist' => $positionlist, 'position' => $position, 'info' => $info); $this->item_form = new feedback_multichoicerated_form('edit_item.php', $customdata); } /** * Saves item * * @return stdClass */ public function save_item() { global $DB; if (!$this->get_data()) { return false; } $item = $this->item; if (isset($item->clone_item) AND $item->clone_item) { $item->id = ''; //to clone this item $item->position++; } $this->set_ignoreempty($item, $item->ignoreempty); $this->set_hidenoselect($item, $item->hidenoselect); $item->hasvalue = $this->get_hasvalue(); if (!$item->id) { $item->id = $DB->insert_record('feedback_item', $item); } else { $DB->update_record('feedback_item', $item); } return $DB->get_record('feedback_item', array('id'=>$item->id)); } /** * Helper function for collected data, both for analysis page and export to excel * * @param stdClass $item the db-object from feedback_item * @param int $groupid * @param int $courseid * @return array|null */ protected function get_analysed($item, $groupid = false, $courseid = false) { $analysed_item = array(); $analysed_item[] = $item->typ; $analysed_item[] = $item->name; //die moeglichen Antworten extrahieren $info = $this->get_info($item); $lines = null; $lines = explode (FEEDBACK_MULTICHOICERATED_LINE_SEP, $info->presentation); if (!is_array($lines)) { return null; } //die Werte holen $values = feedback_get_group_values($item, $groupid, $courseid, $this->ignoreempty($item)); if (!$values) { return null; } //schleife ueber den Werten und ueber die Antwortmoeglichkeiten $analysed_answer = array(); $sizeoflines = count($lines); for ($i = 1; $i <= $sizeoflines; $i++) { $item_values = explode(FEEDBACK_MULTICHOICERATED_VALUE_SEP, $lines[$i-1]); $ans = new stdClass(); $ans->answertext = $item_values[1]; $avg = 0.0; $anscount = 0; foreach ($values as $value) { //ist die Antwort gleich dem index der Antworten + 1? if ($value->value == $i) { $avg += $item_values[0]; //erst alle Werte aufsummieren $anscount++; } } $ans->answercount = $anscount; $ans->avg = doubleval($avg) / doubleval(count($values)); $ans->value = $item_values[0]; $ans->quotient = $ans->answercount / count($values); $analysed_answer[] = $ans; } $analysed_item[] = $analysed_answer; return $analysed_item; } public function get_printval($item, $value) { $printval = ''; if (!isset($value->value)) { return $printval; } $info = $this->get_info($item); $presentation = explode (FEEDBACK_MULTICHOICERATED_LINE_SEP, $info->presentation); $index = 1; foreach ($presentation as $pres) { if ($value->value == $index) { $item_label = explode(FEEDBACK_MULTICHOICERATED_VALUE_SEP, $pres); $printval = format_string($item_label[1]); break; } $index++; } return $printval; } public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { global $OUTPUT; $analysed_item = $this->get_analysed($item, $groupid, $courseid); if ($analysed_item) { echo "typ} table-reboot\">"; echo ''; $analysed_vals = $analysed_item[2]; $avg = 0.0; $count = 0; $data = []; foreach ($analysed_vals as $val) { $avg += $val->avg; $quotient = format_float($val->quotient * 100, 2); $answertext = '('.$val->value.') ' . format_text(trim($val->answertext), FORMAT_HTML, array('noclean' => true, 'para' => false)); if ($val->quotient > 0) { $strquotient = ' ('.$quotient.' %)'; } else { $strquotient = ''; } $data['labels'][$count] = $answertext; $data['series'][$count] = $val->answercount; $data['series_labels'][$count] = $val->answercount . $strquotient; $count++; } $chart = new \core\chart_bar(); $chart->set_horizontal(true); $series = new \core\chart_series(format_string(get_string("responses", "feedback")), $data['series']); $series->set_labels($data['series_labels']); $chart->add_series($series); $chart->set_labels($data['labels']); echo ''; $avg = format_float($avg, 2); echo ''; echo '
'; echo $itemnr . ' '; if (strval($item->label) !== '') { echo '('. format_string($item->label).') '; } echo format_string($analysed_item[1]); echo '
'. $OUTPUT->render($chart) . '
'; echo get_string('average', 'feedback').': '.$avg.''; echo '
'; } } public function excelprint_item(&$worksheet, $row_offset, $xls_formats, $item, $groupid, $courseid = false) { $analysed_item = $this->get_analysed($item, $groupid, $courseid); if (!$analysed_item) { return $row_offset; } $data = $analysed_item[2]; //write the item $worksheet->write_string($row_offset, 0, $item->label, $xls_formats->head2); $worksheet->write_string($row_offset, 1, $analysed_item[1], $xls_formats->head2); if (is_array($data)) { $avg = 0.0; $sizeofdata = count($data); for ($i = 0; $i < $sizeofdata; $i++) { $analysed_data = $data[$i]; $worksheet->write_string($row_offset, $i + 2, trim($analysed_data->answertext).' ('.$analysed_data->value.')', $xls_formats->value_bold); $worksheet->write_number($row_offset + 1, $i + 2, $analysed_data->answercount, $xls_formats->default); $avg += $analysed_data->avg; } //mittelwert anzeigen $worksheet->write_string($row_offset, count($data) + 2, get_string('average', 'feedback'), $xls_formats->value_bold); $worksheet->write_number($row_offset + 1, count($data) + 2, $avg, $xls_formats->value_bold); } $row_offset +=2; return $row_offset; } /** * Options for the multichoice element * @param stdClass $item * @return array */ protected function get_options($item) { $info = $this->get_info($item); $lines = explode(FEEDBACK_MULTICHOICERATED_LINE_SEP, $info->presentation); $options = array(); foreach ($lines as $idx => $line) { list($weight, $optiontext) = explode(FEEDBACK_MULTICHOICERATED_VALUE_SEP, $line); $a = new stdclass(); $a->weight = $weight; $a->name = format_text($optiontext, FORMAT_HTML, array('noclean' => true, 'para' => false)); $options[$idx + 1] = get_string('multichoiceoption', 'feedback', $a); } if ($info->subtype === 'r' && !$this->hidenoselect($item)) { $options = array(0 => get_string('not_selected', 'feedback')) + $options; } return $options; } /** * Adds an input element to the complete form * * @param stdClass $item * @param mod_feedback_complete_form $form */ public function complete_form_element($item, $form) { $info = $this->get_info($item); $name = $this->get_display_name($item); $class = 'multichoicerated-' . $info->subtype; $inputname = $item->typ . '_' . $item->id; $options = $this->get_options($item); if ($info->subtype === 'd' || $form->is_frozen()) { $el = $form->add_form_element($item, ['select', $inputname, $name, array('' => '') + $options, array('class' => $class)]); } else { $objs = array(); if (!array_key_exists(0, $options)) { // Always add '0' as hidden element, otherwise form submit data may not have this element. $objs[] = ['hidden', $inputname]; } foreach ($options as $idx => $label) { $objs[] = ['radio', $inputname, '', $label, $idx]; } // Span to hold the element id. The id is used for drag and drop reordering. $objs[] = ['static', '', '', html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])]; $separator = $info->horizontal ? ' ' : \html_writer::div('', 'w-100'); $class .= ' multichoicerated-' . ($info->horizontal ? 'horizontal' : 'vertical'); $el = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class); $form->set_element_type($inputname, PARAM_INT); // Set previously input values. $form->set_element_default($inputname, $form->get_item_value($item)); // Process "required" rule. if ($item->required) { $form->add_validation_rule(function($values, $files) use ($item) { $inputname = $item->typ . '_' . $item->id; return empty($values[$inputname]) ? array('group_' . $inputname => get_string('required')) : true; }); } } } /** * Compares the dbvalue with the dependvalue * * @param stdClass $item * @param string $dbvalue is the value input by user in the format as it is stored in the db * @param string $dependvalue is the value that it needs to be compared against */ public function compare_value($item, $dbvalue, $dependvalue) { if (is_array($dbvalue)) { $dbvalues = $dbvalue; } else { $dbvalues = explode(FEEDBACK_MULTICHOICERATED_LINE_SEP, $dbvalue); } $info = $this->get_info($item); $presentation = explode (FEEDBACK_MULTICHOICERATED_LINE_SEP, $info->presentation); $index = 1; foreach ($presentation as $pres) { $presvalues = explode(FEEDBACK_MULTICHOICERATED_VALUE_SEP, $pres); foreach ($dbvalues as $dbval) { if ($dbval == $index AND trim($presvalues[1]) == $dependvalue) { return true; } } $index++; } return false; } public function get_info($item) { $presentation = empty($item->presentation) ? '' : $item->presentation; $info = new stdClass(); //check the subtype of the multichoice //it can be check(c), radio(r) or dropdown(d) $info->subtype = ''; $info->presentation = ''; $info->horizontal = false; $parts = explode(FEEDBACK_MULTICHOICERATED_TYPE_SEP, $item->presentation); $info->subtype = $parts[0]; if (count($parts) > 1) { $info->presentation = $parts[1]; } if (!isset($info->subtype)) { $info->subtype = 'r'; } if ($info->subtype != 'd') { $parts = explode(FEEDBACK_MULTICHOICERATED_ADJUST_SEP, $info->presentation); $info->presentation = $parts[0]; if (count($parts) > 1) { $info->horizontal = $parts[1]; } if (isset($info->horizontal) AND $info->horizontal == 1) { $info->horizontal = true; } else { $info->horizontal = false; } } $info->values = $this->prepare_presentation_values_print($info->presentation, FEEDBACK_MULTICHOICERATED_VALUE_SEP, FEEDBACK_MULTICHOICERATED_VALUE_SEP2); return $info; } public function prepare_presentation_values($linesep1, $linesep2, $valuestring, $valuesep1, $valuesep2) { $lines = explode($linesep1, $valuestring); $newlines = array(); foreach ($lines as $line) { $value = ''; $text = ''; if (strpos($line, $valuesep1) === false) { $value = 0; $text = $line; } else { @list($value, $text) = explode($valuesep1, $line, 2); } $value = intval($value); $newlines[] = $value.$valuesep2.$text; } $newlines = implode($linesep2, $newlines); return $newlines; } public function prepare_presentation_values_print($valuestring, $valuesep1, $valuesep2) { $valuestring = str_replace(array("\n","\r"), "", $valuestring); return $this->prepare_presentation_values(FEEDBACK_MULTICHOICERATED_LINE_SEP, "\n", $valuestring, $valuesep1, $valuesep2); } public function prepare_presentation_values_save($valuestring, $valuesep1, $valuesep2) { $valuestring = str_replace("\r", "\n", $valuestring); $valuestring = str_replace("\n\n", "\n", $valuestring); return $this->prepare_presentation_values("\n", FEEDBACK_MULTICHOICERATED_LINE_SEP, $valuestring, $valuesep1, $valuesep2); } public function set_ignoreempty($item, $ignoreempty=true) { $item->options = str_replace(FEEDBACK_MULTICHOICERATED_IGNOREEMPTY, '', $item->options); if ($ignoreempty) { $item->options .= FEEDBACK_MULTICHOICERATED_IGNOREEMPTY; } } public function ignoreempty($item) { if (strstr($item->options, FEEDBACK_MULTICHOICERATED_IGNOREEMPTY)) { return true; } return false; } public function set_hidenoselect($item, $hidenoselect=true) { $item->options = str_replace(FEEDBACK_MULTICHOICERATED_HIDENOSELECT, '', $item->options); if ($hidenoselect) { $item->options .= FEEDBACK_MULTICHOICERATED_HIDENOSELECT; } } public function hidenoselect($item) { if (strstr($item->options, FEEDBACK_MULTICHOICERATED_HIDENOSELECT)) { return true; } return false; } /** * Return the analysis data ready for external functions. * * @param stdClass $item the item (question) information * @param int $groupid the group id to filter data (optional) * @param int $courseid the course id (optional) * @return array an array of data with non scalar types json encoded * @since Moodle 3.3 */ public function get_analysed_for_external($item, $groupid = false, $courseid = false) { $externaldata = array(); $data = $this->get_analysed($item, $groupid, $courseid); if ($data && !empty($data[2]) && is_array($data[2])) { foreach ($data[2] as $d) { $externaldata[] = json_encode($d); } } return $externaldata; } } PKB\nUB!/item/multichoicerated/multichoicerated_form.phpnu[. require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_form_class.php'); class feedback_multichoicerated_form extends feedback_item_form { protected $type = "multichoicerated"; /** @var object Form element */ protected $values; public function definition() { $item = $this->_customdata['item']; $common = $this->_customdata['common']; $positionlist = $this->_customdata['positionlist']; $position = $this->_customdata['position']; $mform =& $this->_form; $mform->addElement('header', 'general', get_string($this->type, 'feedback')); $mform->addElement('advcheckbox', 'required', get_string('required', 'feedback'), '' , null , array(0, 1)); $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size'=>FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->addElement('select', 'subtype', get_string('multichoicetype', 'feedback').' ', array('r'=>get_string('radio', 'feedback'), 'd'=>get_string('dropdown', 'feedback'))); $mform->addElement('select', 'horizontal', get_string('adjustment', 'feedback').' ', array(0 => get_string('vertical', 'feedback'), 1 => get_string('horizontal', 'feedback'))); $mform->hideIf('horizontal', 'subtype', 'eq', 'd'); $mform->addElement('selectyesno', 'hidenoselect', get_string('hide_no_select_option', 'feedback')); $mform->hideIf('hidenoselect', 'subtype', 'eq', 'd'); $mform->addElement('selectyesno', 'ignoreempty', get_string('do_not_analyse_empty_submits', 'feedback')); $mform->disabledIf('ignoreempty', 'required', 'eq', '1'); $this->values = $mform->addElement('textarea', 'values', get_string('multichoice_values', 'feedback'), 'wrap="virtual" rows="10" cols="65"'); $mform->addElement('static', 'hint', '', get_string('use_one_line_for_each_value', 'feedback')); parent::definition(); $this->set_data($item); } public function set_data($item) { $info = $this->_customdata['info']; $item->horizontal = $info->horizontal; $item->subtype = $info->subtype; $item->values = $info->values; return parent::set_data($item); } public function get_data() { if (!$item = parent::get_data()) { return false; } $itemobj = new feedback_item_multichoicerated(); $presentation = $itemobj->prepare_presentation_values_save(trim($item->values), FEEDBACK_MULTICHOICERATED_VALUE_SEP2, FEEDBACK_MULTICHOICERATED_VALUE_SEP); if (!isset($item->subtype)) { $subtype = 'r'; } else { $subtype = substr($item->subtype, 0, 1); } if (isset($item->horizontal) AND $item->horizontal == 1 AND $subtype != 'd') { $presentation .= FEEDBACK_MULTICHOICERATED_ADJUST_SEP.'1'; } $item->presentation = $subtype.FEEDBACK_MULTICHOICERATED_TYPE_SEP.$presentation; if (!isset($item->hidenoselect)) { $item->hidenoselect = 1; } if (!isset($item->ignoreempty)) { $item->ignoreempty = 0; } return $item; } } PKB\׫WW!item/feedback_item_form_class.phpnu[. require_once($CFG->libdir.'/formslib.php'); define('FEEDBACK_ITEM_NAME_TEXTBOX_SIZE', 80); define('FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE', 20); abstract class feedback_item_form extends moodleform { /** @var string type value in the hidden element. */ protected $type; public function definition() { $item = $this->_customdata['item']; //the item object //common is an array like: // array('cmid'=>$cm->id, // 'id'=>isset($item->id) ? $item->id : NULL, // 'typ'=>$item->typ, // 'items'=>$feedbackitems, // 'feedback'=>$feedback->id); $common = $this->_customdata['common']; //positionlist is an array with possible positions for the item location $positionlist = $this->_customdata['positionlist']; //the current position of the item $position = $this->_customdata['position']; $mform =& $this->_form; if (array_filter(array_keys($common['items']))) { $mform->addElement('select', 'dependitem', get_string('dependitem', 'feedback').' ', $common['items'] ); $mform->addHelpButton('dependitem', 'depending', 'feedback'); $mform->addElement('text', 'dependvalue', get_string('dependvalue', 'feedback'), array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255)); $mform->hideIf('dependvalue', 'dependitem', 'eq', '0'); } else { $mform->addElement('hidden', 'dependitem', 0); $mform->addElement('hidden', 'dependvalue', ''); } $mform->setType('dependitem', PARAM_INT); $mform->setType('dependvalue', PARAM_RAW); $position_select = $mform->addElement('select', 'position', get_string('position', 'feedback').' ', $positionlist); $position_select->setValue($position); $mform->addElement('hidden', 'cmid', $common['cmid']); $mform->setType('cmid', PARAM_INT); $mform->addElement('hidden', 'id', $common['id']); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'feedback', $common['feedback']); $mform->setType('feedback', PARAM_INT); $mform->addElement('hidden', 'template', 0); $mform->setType('template', PARAM_INT); $mform->setType('name', PARAM_RAW); $mform->setType('label', PARAM_NOTAGS); $mform->addElement('hidden', 'typ', $this->type); $mform->setType('typ', PARAM_ALPHA); $mform->addElement('hidden', 'hasvalue', 0); $mform->setType('hasvalue', PARAM_INT); $mform->addElement('hidden', 'options', ''); $mform->setType('options', PARAM_ALPHA); $buttonarray = array(); if (!empty($item->id)) { $buttonarray[] = &$mform->createElement('submit', 'update_item', get_string('update_item', 'feedback')); $buttonarray[] = &$mform->createElement('submit', 'clone_item', get_string('save_as_new_item', 'feedback')); } else { $mform->addElement('hidden', 'clone_item', 0); $mform->setType('clone_item', PARAM_INT); $buttonarray[] = &$mform->createElement('submit', 'save_item', get_string('savechanges')); } $buttonarray[] = &$mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', ' ', array(' '), false); } /** * Return submitted data if properly submitted or returns NULL if validation fails or * if there is no submitted data. * * @return object submitted data; NULL if not valid or not submitted or cancelled */ public function get_data() { if ($item = parent::get_data()) { if (!isset($item->dependvalue)) { $item->dependvalue = ''; } } return $item; } } PKB\׶db/services.phpnu[. /** * Feedback external functions and service definitions. * * @package mod_feedback * @category external * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.3 */ defined('MOODLE_INTERNAL') || die; $functions = array( 'mod_feedback_get_feedbacks_by_courses' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_feedbacks_by_courses', 'description' => 'Returns a list of feedbacks in a provided list of courses, if no list is provided all feedbacks that the user can view will be returned.', 'type' => 'read', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'mod_feedback_get_feedback_access_information' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_feedback_access_information', 'description' => 'Return access information for a given feedback.', 'type' => 'read', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'mod_feedback_view_feedback' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'view_feedback', 'description' => 'Trigger the course module viewed event and update the module completion status.', 'type' => 'write', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_current_completed_tmp' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_current_completed_tmp', 'description' => 'Returns the temporary completion record for the current user.', 'type' => 'read', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_items' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_items', 'description' => 'Returns the items (questions) in the given feedback.', 'type' => 'read', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_launch_feedback' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'launch_feedback', 'description' => 'Starts or continues a feedback submission.', 'type' => 'write', 'capabilities' => 'mod/feedback:complete', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_page_items' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_page_items', 'description' => 'Get a single feedback page items.', 'type' => 'read', 'capabilities' => 'mod/feedback:complete', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_process_page' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'process_page', 'description' => 'Process a jump between pages.', 'type' => 'write', 'capabilities' => 'mod/feedback:complete', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_analysis' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_analysis', 'description' => 'Retrieves the feedback analysis.', 'type' => 'read', 'capabilities' => 'mod/feedback:viewanalysepage', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_unfinished_responses' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_unfinished_responses', 'description' => 'Retrieves responses from the current unfinished attempt.', 'type' => 'read', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_finished_responses' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_finished_responses', 'description' => 'Retrieves responses from the last finished attempt.', 'type' => 'read', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_non_respondents' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_non_respondents', 'description' => 'Retrieves a list of students who didn\'t submit the feedback.', 'type' => 'read', 'capabilities' => 'mod/feedback:viewreports', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_responses_analysis' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_responses_analysis', 'description' => 'Return the feedback user responses analysis.', 'type' => 'read', 'capabilities' => 'mod/feedback:viewreports', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_get_last_completed' => array( 'classname' => 'mod_feedback_external', 'methodname' => 'get_last_completed', 'description' => 'Retrieves the last completion record for the current user.', 'type' => 'read', 'capabilities' => 'mod/feedback:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_feedback_questions_reorder' => [ 'classname' => 'mod_feedback\external\questions\reorder', 'description' => 'Saves the new order of the questions in the feedback.', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/feedback:edititems', ], ); PKB\~ cmmdb/install.phpnu[. /** * Feedback block installation. * * @package block_feedback * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Perform the post-install procedures. */ function xmldb_block_feedback_install() { global $DB; // Disable feedback on new installs by default. $DB->set_field('block', 'visible', 0, ['name' => 'feedback']); } PKB\fF db/log.phpnu[. /** * Definition of log events * * @package mod_feedback * @category log * @copyright 2010 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $logs = array( array('module'=>'feedback', 'action'=>'startcomplete', 'mtable'=>'feedback', 'field'=>'name'), array('module'=>'feedback', 'action'=>'submit', 'mtable'=>'feedback', 'field'=>'name'), array('module'=>'feedback', 'action'=>'delete', 'mtable'=>'feedback', 'field'=>'name'), array('module'=>'feedback', 'action'=>'view', 'mtable'=>'feedback', 'field'=>'name'), array('module'=>'feedback', 'action'=>'view all', 'mtable'=>'course', 'field'=>'shortname'), );PKB\A   db/access.phpnu[. /** * Feedback block caps. * * @package block_feedback * @copyright Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'block/feedback:addinstance' => array( 'riskbitmask' => RISK_SPAM | RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_BLOCK, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/site:manageblocks' ), ); PKB\~db/upgrade.phpnu[. /** * This file keeps track of upgrades to the feedback block * * Sometimes, changes between versions involve alterations to database structures * and other major things that may break installations. * * The upgrade function in this file will attempt to perform all the necessary * actions to upgrade your older installation to the current version. * * If there's something it cannot do itself, it will tell you what you need to do. * * The commands in here will all be database-neutral, using the methods of * database_manager class * * Please do not forget to use upgrade_set_timeout() * before any action that may take longer time to finish. * * @package block_feedback * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Handles upgrading instances of this block. * * @param int $oldversion * @param object $block */ function xmldb_block_feedback_upgrade($oldversion, $block) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PKB\Ώ''db/install.xmlnu[
PKB\IJj@@ db/events.phpnu[. /** * Feedback event handler definition. * * @package mod_feedback * @category event * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // List of observers. $observers = array( array( 'eventname' => '\core\event\course_content_deleted', 'callback' => 'mod_feedback_observer::course_content_deleted', ), ); PKB\ƨ'db/messages.phpnu[. /** * Defines message providers (types of messages being sent) * * @package mod_feedback * @copyright 2010 onwards Aparup Banerjee http://moodle.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $messageproviders = array ( // Submitting a feedback. 'submission' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], // Message to nonrespondents. 'message' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], ); PKB\lang/en/deprecated.txtnu[public,mod_feedbackPKB\B/G/Glang/en/feedback.phpnu[. /** * Strings for component 'feedback', language 'en', branch 'MOODLE_20_STABLE' * * @package mod_feedback * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['add_item'] = 'Add question'; $string['add_pagebreak'] = 'Add a page break'; $string['adjustment'] = 'Adjustment'; $string['after_submit'] = 'After submission'; $string['allowfullanonymous'] = 'Allow full anonymous'; $string['analysis'] = 'Analysis'; $string['anonymous'] = 'Anonymous'; $string['anonymous_edit'] = 'Record user names'; $string['anonymous_entries'] = 'Anonymous entries ({$a})'; $string['anonymous_user'] = 'Anonymous user'; $string['answerquestions'] = 'Answer the questions'; $string['append_new_items'] = 'Keep existing questions and add new questions at the end'; $string['autonumbering'] = 'Auto number questions'; $string['autonumbering_help'] = 'Enables or disables automated numbers for each question'; $string['availableforallcourses'] = 'Available for all courses'; $string['average'] = 'Average'; $string['bold'] = 'Bold'; $string['calendarend'] = '{$a} closes'; $string['calendarstart'] = '{$a} opens'; $string['cannotaccess'] = 'You can only access this feedback from a course'; $string['cannotcreatepagebreak'] = 'A page break cannot be added at the beginning, and only one page break can be added at the end.'; $string['cannotsavetempl'] = 'Saving templates is not allowed'; $string['captcha'] = 'Captcha'; $string['captchanotset'] = 'Captcha hasn\'t been set.'; $string['closebeforeopen'] = 'You have specified an end date before the start date.'; $string['completed_feedbacks'] = 'Submitted answers'; $string['complete_the_form'] = 'Answer the questions'; $string['completed'] = 'Completed'; $string['completedon'] = 'Completed on {$a}'; $string['completiondetail:submit'] = 'Submit feedback'; $string['completionsubmit'] = 'Submit feedback'; $string['configallowfullanonymous'] = 'If set to \'yes\', users can complete a feedback activity on the site home without being required to log in.'; $string['confirmdeleteentry'] = 'Are you sure you want to delete this entry?'; $string['confirmdeleteitem'] = 'Are you sure you want to delete this element?'; $string['confirmdeletetemplate'] = 'Are you sure you want to delete this template?'; $string['confirmusetemplate'] = 'Are you sure you want to use this template?'; $string['continue_the_form'] = 'Continue answering the questions'; $string['count_of_nums'] = 'Count of numbers'; $string['courseid'] = 'Course ID'; $string['coursetemplates'] = 'Course templates'; $string['creating_templates'] = 'Save these questions as a new template'; $string['delete_entry'] = 'Delete entry'; $string['delete_item'] = 'Delete question'; $string['delete_old_items'] = 'Override existing questions'; $string['delete_pagebreak'] = 'Delete page break'; $string['delete_template'] = 'Delete template'; $string['delete_templates'] = 'Delete template...'; $string['depending'] = 'Dependencies'; $string['depending_help'] = 'It is possible to show an item depending on the value of another item.
Here is an example.
  • First, create an item on which another item will depend on.
  • Next, add a pagebreak.
  • Then add the items dependant on the value of the item created before. Choose the item from the list labelled "Dependence item" and write the required value in the textbox labelled "Dependence value".
The item structure should look like this.
  1. Item Q: Do you have a car? A: yes/no
  2. Pagebreak
  3. Item Q: What colour is your car?
    (this item depends on item 1 with value = yes)
  4. Item Q: Why don\'t you have a car?
    (this item depends on item 1 with value = no)
  5. ... other items
'; $string['dependitem'] = 'Dependence item'; $string['dependvalue'] = 'Dependence value'; $string['description'] = 'Description'; $string['do_not_analyse_empty_submits'] = 'Omit empty submits in analysis'; $string['dropdown'] = 'Multiple choice - single answer allowed (drop-down menu)'; $string['dropdownlist'] = 'Multiple choice - single answer (drop-down menu)'; $string['dropdownrated'] = 'Drop-down menu (rated)'; $string['dropdown_values'] = 'Answers'; $string['drop_feedback'] = 'Remove from this course'; $string['edit_item'] = 'Edit question'; $string['edit_items'] = 'Edit questions'; $string['email_notification'] = 'Enable notification of submissions'; $string['email_notification_help'] = 'If enabled, teachers will receive notification of feedback submissions.'; $string['emailteachermail'] = '{$a->username} has completed feedback activity : \'{$a->feedback}\' You can view it here: {$a->url}'; $string['emailteachermailhtml'] = '

{$a->username} has completed feedback activity : \'{$a->feedback}\'.

It is available on the site.

'; $string['entries_saved'] = 'Your answers have been saved. Thank you.'; $string['export_questions'] = 'Export questions'; $string['export_to_excel'] = 'Export to Excel'; $string['eventresponsedeleted'] = 'Response deleted'; $string['eventresponsesubmitted'] = 'Response submitted'; $string['feedbackcompleted'] = '{$a->username} completed {$a->feedbackname}'; $string['feedback:addinstance'] = 'Add a new feedback'; $string['feedbackclose'] = 'Allow answers until'; $string['feedback:complete'] = 'Complete a feedback'; $string['feedback:createprivatetemplate'] = 'Create private template'; $string['feedback:createpublictemplate'] = 'Create public template'; $string['feedback:deletesubmissions'] = 'Delete completed submissions'; $string['feedback:deletetemplate'] = 'Delete template'; $string['feedback:edititems'] = 'Edit items'; $string['feedback_is_not_for_anonymous'] = 'Feedback is not for anonymous'; $string['feedback_is_not_open'] = 'The feedback is not open'; $string['feedback:mapcourse'] = 'Map courses to global feedbacks'; $string['feedbackopen'] = 'Allow answers from'; $string['feedback:receivemail'] = 'Receive email notification'; $string['feedback:view'] = 'View a feedback'; $string['feedback:viewanalysepage'] = 'View the analysis page after submit'; $string['feedback:viewreports'] = 'View reports'; $string['feedbackupdated'] = 'Feedback updated.'; $string['file'] = 'File'; $string['filter_by_course'] = 'Filter by course'; $string['handling_error'] = 'Error occurred in feedback module action handling'; $string['hide_no_select_option'] = 'Hide the "Not selected" option'; $string['horizontal'] = 'Horizontal'; $string['check'] = 'Multiple choice - multiple answers'; $string['checkbox'] = 'Multiple choice - multiple answers allowed (check boxes)'; $string['check_values'] = 'Possible responses'; $string['choosefile'] = 'Choose a file'; $string['chosen_feedback_response'] = 'Chosen feedback response'; $string['downloadresponseas'] = 'Download all responses as:'; $string['importfromthisfile'] = 'Import from this file'; $string['import_questions'] = 'Import questions'; $string['import_successfully'] = 'Import successfully'; $string['includeuserinrecipientslist'] = 'Include {$a} in the list of recipients'; $string['indicator:cognitivedepth'] = 'Feedback cognitive'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a Feedback activity.'; $string['indicator:cognitivedepthdef'] = 'Feedback cognitive'; $string['indicator:cognitivedepthdef_help'] = 'The participant has reached this percentage of the cognitive engagement offered by the Feedback activities during this analysis interval (Levels = No view, View, Submit)'; $string['indicator:cognitivedepthdef_link'] = 'Learning_analytics_indicators#Cognitive_depth'; $string['indicator:socialbreadth'] = 'Feedback social'; $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a Feedback activity.'; $string['indicator:socialbreadthdef'] = 'Feedback social'; $string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Feedback activities during this analysis interval (Levels = No participation, Participant alone, Participant with others)'; $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; $string['info'] = 'Information'; $string['infotype'] = 'Information type'; $string['insufficient_responses_for_this_group'] = 'There are insufficient responses for this group'; $string['insufficient_responses'] = 'insufficient responses'; $string['insufficient_responses_help'] = 'For the feedback to be anonymous, there must be at least 2 responses.'; $string['item_label'] = 'Label'; $string['item_name'] = 'Question'; $string['label'] = 'Text and media area'; $string['labelcontents'] = 'Contents'; $string['mapcourseinfo'] = 'This is a site-wide feedback that is available to all courses using the feedback block. You can however limit the courses to which it will appear by mapping them. Search the course and map it to this feedback.'; $string['mapcoursenone'] = 'No courses mapped. Feedback available to all courses'; $string['mapcourse'] = 'Map feedback to courses'; $string['mapcourse_help'] = 'By default, feedback forms created on your homepage are available site-wide and will appear in all courses using the feedback block. You can force the feedback form to appear by making it a sticky block or limit the courses in which a feedback form will appear by mapping it to specific courses.'; $string['mapcourses'] = 'Map feedback to courses'; $string['mappedcourses'] = 'Mapped courses'; $string['mappingchanged'] = 'Course mapping has been changed'; $string['minimal'] = 'Minimum'; $string['maximal'] = 'Maximum'; $string['messageprovider:message'] = 'Feedback reminder'; $string['messageprovider:submission'] = 'Feedback notifications'; $string['mode'] = 'Mode'; $string['modulename'] = 'Feedback'; $string['modulename_help'] = 'The feedback activity enables a teacher to create a custom survey for collecting feedback from participants using a variety of question types including multiple choice, yes/no or text input. Feedback responses may be anonymous if desired, and results may be shown to all participants or restricted to teachers only. Any feedback activities on the site home may also be completed by non-logged-in users. Feedback activities may be used * For course evaluations, helping improve the content for later participants * To enable participants to sign up for course modules, events etc. * For guest surveys of course choices, school policies etc. * For anti-bullying surveys in which students can report incidents anonymously'; $string['modulename_link'] = 'mod/feedback/view'; $string['modulenameplural'] = 'Feedback'; $string['move_item'] = 'Move this question'; $string['multichoice'] = 'Multiple choice'; $string['multichoiceoption'] = '({$a->weight}) {$a->name}'; $string['multichoicerated'] = 'Multiple choice (rated)'; $string['multichoicetype'] = 'Multiple choice type'; $string['multichoice_values'] = 'Multiple choice values'; $string['multiplesubmit'] = 'Allow multiple submissions'; $string['multiplesubmit_help'] = 'If set to Yes: * For anonymous surveys: participants can submit unlimited responses, and all responses will be recorded. * For non-anonymous surveys: participants can submit unlimited responses, but only their latest response will be recorded.'; $string['name'] = 'Name'; $string['name_required'] = 'Name required'; $string['nameandlabelformat'] = '({$a->label}) {$a->name}'; $string['next_page'] = 'Next page'; $string['no_handler'] = 'No action handler exists for'; $string['no_itemlabel'] = 'No label'; $string['no_itemname'] = 'No itemname'; $string['no_items_available_yet'] = 'No questions have been set up yet'; $string['non_anonymous'] = 'User\'s name will be logged and shown with answers'; $string['non_anonymous_entries'] = 'Non anonymous entries ({$a})'; $string['non_respondents_students'] = 'Non-respondent students ({$a})'; $string['not_completed_yet'] = 'Not completed yet'; $string['not_started'] = 'Not started'; $string['no_templates_available_yet'] = 'No templates available yet'; $string['not_selected'] = 'Not selected'; $string['numberoutofrange'] = 'Number out of range'; $string['numeric'] = 'Numeric answer'; $string['numeric_range_from'] = 'Range from'; $string['numeric_range_to'] = 'Range to'; $string['of'] = 'of'; $string['oldvaluespreserved'] = 'All old questions and the assigned values will be preserved'; $string['oldvalueswillbedeleted'] = 'Current questions and all responses will be deleted.'; $string['only_one_captcha_allowed'] = 'Only one captcha is allowed in a feedback'; $string['openafterclose'] = 'You have specified an open date after the close date'; $string['overview'] = 'Overview'; $string['page'] = 'Page'; $string['page-mod-feedback-x'] = 'Any feedback module page'; $string['page_after_submit'] = 'Completion message'; $string['pagebreak'] = 'Page break'; $string['pluginadministration'] = 'Feedback administration'; $string['pluginname'] = 'Feedback'; $string['position'] = 'Position'; $string['previous_page'] = 'Previous page'; $string['previewquestions'] = 'Preview questions'; $string['previewtemplate'] = 'Previewing \'{$a}\' template'; $string['privacy:metadata:completed'] = 'A record of the submissions to the feedback'; $string['privacy:metadata:completed:anonymousresponse'] = 'Whether the submission is to be used anonymously.'; $string['privacy:metadata:completed:timemodified'] = 'The time when the submission was last modified.'; $string['privacy:metadata:completed:userid'] = 'The ID of the user who completed the feedback activity.'; $string['privacy:metadata:completedtmp'] = 'A record of the submissions which are still in progress.'; $string['privacy:metadata:value'] = 'A record of the answer to a question.'; $string['privacy:metadata:value:value'] = 'The chosen answer.'; $string['privacy:metadata:valuetmp'] = 'A record of the answer to a question in a submission in progress.'; $string['question'] = 'Question'; $string['questionandsubmission'] = 'Question and submission settings'; $string['questionmoved'] = 'Question moved'; $string['questions'] = 'Questions'; $string['questionslimited'] = 'Showing only {$a} first questions, view individual answers or download table data to view all.'; $string['radio'] = 'Multiple choice - single answer'; $string['radio_values'] = 'Responses'; $string['ready_feedbacks'] = 'Ready feedbacks'; $string['required'] = 'Required'; $string['resetting_data'] = 'Responses'; $string['resetting_delete'] = 'Delete responses'; $string['resetting_feedbacks'] = 'Resetting feedbacks'; $string['responded'] = 'Responded'; $string['response_nr'] = 'Response number'; $string['responses'] = 'Responses'; $string['responsetime'] = 'Responses time'; $string['save_as_new_item'] = 'Save as new question'; $string['save_as_new_template'] = 'Save as template'; $string['save_entries'] = 'Submit your answers'; $string['save_item'] = 'Save question'; $string['saving_failed'] = 'Saving failed'; $string['search:activity'] = 'Feedback - activity information'; $string['search_course'] = 'Search course'; $string['searchcourses'] = 'Search courses'; $string['searchcourses_help'] = 'Search for the code or name of the course(s) that you wish to associate with this feedback.'; $string['selected_dump'] = 'Selected indexes of $SESSION variable are dumped below:'; $string['send'] = 'Send'; $string['send_message'] = 'Send notification'; $string['show_all'] = 'Show all'; $string['show_analysepage_after_submit'] = 'Show analysis page'; $string['show_entries'] = 'Show responses'; $string['show_entry'] = 'Show response'; $string['show_nonrespondents'] = 'Show non-respondents'; $string['site_after_submit'] = 'Site after submit'; $string['sitetemplates'] = 'Site templates'; $string['sort_by_course'] = 'Sort by course'; $string['started'] = 'Started'; $string['startedon'] = 'Started on {$a}'; $string['subject'] = 'Subject'; $string['switch_item_to_not_required'] = 'Set as not required'; $string['switch_item_to_required'] = 'Set as required'; $string['template'] = 'Template'; $string['templates'] = 'Templates'; $string['template_deleted'] = 'Template deleted'; $string['template_saved'] = 'Template saved'; $string['textarea'] = 'Longer text answer'; $string['textarea_height'] = 'Number of lines'; $string['textarea_width'] = 'Width'; $string['textfield'] = 'Short text answer'; $string['textfield_maxlength'] = 'Maximum characters accepted'; $string['textfield_size'] = 'Textfield width'; $string['there_are_no_settings_for_recaptcha'] = 'There are no settings for captcha'; $string['this_feedback_is_already_submitted'] = 'You have already submitted this feedback.'; $string['typemissing'] = 'Missing value "type"'; $string['update_item'] = 'Save changes to question'; $string['url_for_continue'] = 'Link to next activity'; $string['url_for_continue_help'] = 'After submitting the feedback, a continue button is displayed, which links to the course page. Alternatively, it may link to the next activity if the URL of the activity is entered here.'; $string['use_one_line_for_each_value'] = 'Use one line for each answer!'; $string['use_this_template'] = 'Use template'; $string['using_templates'] = 'Use a template'; $string['vertical'] = 'Vertical'; $string['whatfor'] = 'How would you like to apply the template?'; // Deprecated since Moodle 4.5. $string['public'] = 'Public'; PKB\b[ 6backup/moodle2/backup_feedback_activity_task.class.phpnu[. /** * Defines backup_feedback_activity_task class * * @package mod_feedback * @category backup * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/feedback/backup/moodle2/backup_feedback_stepslib.php'); require_once($CFG->dirroot . '/mod/feedback/backup/moodle2/backup_feedback_settingslib.php'); /** * Provides the steps to perform one complete backup of the Feedback instance */ class backup_feedback_activity_task extends backup_activity_task { /** * No specific settings for this activity */ protected function define_my_settings() { } /** * Defines a backup step to store the instance data in the feedback.xml file */ protected function define_my_steps() { // feedback only has one structure step $this->add_step(new backup_feedback_activity_structure_step('feedback structure', 'feedback.xml')); } /** * Encodes URLs to the index.php and view.php scripts * * @param string $content some HTML text that eventually contains URLs to the activity instance scripts * @return string the content with the URLs encoded */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot, "/"); // Link to the list of feedbacks $search="/(".$base."\/mod\/feedback\/index.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@FEEDBACKINDEX*$2@$', $content); // Link to feedback view by moduleid $search="/(".$base."\/mod\/feedback\/view.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@FEEDBACKVIEWBYID*$2@$', $content); // Link to feedback analyis by moduleid $search="/(".$base."\/mod\/feedback\/analysis.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@FEEDBACKANALYSISBYID*$2@$', $content); // Link to feedback entries by moduleid $search="/(".$base."\/mod\/feedback\/show_entries.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@FEEDBACKSHOWENTRIESBYID*$2@$', $content); return $content; } } PKB\e xx.backup/moodle2/backup_feedback_settingslib.phpnu[. /** * @package mod_feedback * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // This activity has not particular settings but the inherited from the generic // backup_activity_task so here there isn't any class definition, like the ones // existing in /backup/moodle2/backup_settingslib.php (activities section) PKB\|A,backup/moodle2/restore_feedback_stepslib.phpnu[. /** * @package mod_feedback * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the restore steps that will be used by the restore_feedback_activity_task */ /** * Structure step to restore one feedback activity */ class restore_feedback_activity_structure_step extends restore_activity_structure_step { protected function define_structure() { $paths = array(); $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('feedback', '/activity/feedback'); $paths[] = new restore_path_element('feedback_item', '/activity/feedback/items/item'); if ($userinfo) { $paths[] = new restore_path_element('feedback_completed', '/activity/feedback/completeds/completed'); $paths[] = new restore_path_element('feedback_value', '/activity/feedback/completeds/completed/values/value'); } // Return the paths wrapped into standard activity structure return $this->prepare_activity_structure($paths); } protected function process_feedback($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. $data->timeopen = $this->apply_date_offset($data->timeopen); $data->timeclose = $this->apply_date_offset($data->timeclose); // insert the feedback record $newitemid = $DB->insert_record('feedback', $data); // immediately after inserting "activity" record, call this $this->apply_activity_instance($newitemid); } protected function process_feedback_item($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->feedback = $this->get_new_parentid('feedback'); $data->typ = clean_param($data->typ, PARAM_ALPHA); $newitemid = $DB->insert_record('feedback_item', $data); $this->set_mapping('feedback_item', $oldid, $newitemid, true); // Can have files } protected function process_feedback_completed($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->feedback = $this->get_new_parentid('feedback'); $data->userid = $this->get_mappingid('user', $data->userid); if ($this->task->is_samesite() && !empty($data->courseid)) { $data->courseid = $data->courseid; } else if ($this->get_courseid() == SITEID) { $data->courseid = SITEID; } else { $data->courseid = 0; } $newitemid = $DB->insert_record('feedback_completed', $data); $this->set_mapping('feedback_completed', $oldid, $newitemid); } protected function process_feedback_value($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->completed = $this->get_new_parentid('feedback_completed'); $data->item = $this->get_mappingid('feedback_item', $data->item); if ($this->task->is_samesite() && !empty($data->course_id)) { $data->course_id = $data->course_id; } else if ($this->get_courseid() == SITEID) { $data->course_id = SITEID; } else { $data->course_id = 0; } $newitemid = $DB->insert_record('feedback_value', $data); $this->set_mapping('feedback_value', $oldid, $newitemid); } protected function after_execute() { global $DB; // Add feedback related files, no need to match by itemname (just internally handled context) $this->add_related_files('mod_feedback', 'intro', null); $this->add_related_files('mod_feedback', 'page_after_submit', null); $this->add_related_files('mod_feedback', 'item', 'feedback_item'); // Once all items are restored we can set their dependency. if ($records = $DB->get_records('feedback_item', array('feedback' => $this->task->get_activityid()))) { foreach ($records as $record) { // Get new id for dependitem if present. This will also reset dependitem if not found. $record->dependitem = $this->get_mappingid('feedback_item', $record->dependitem); $DB->update_record('feedback_item', $record); } } } } PKB\u7backup/moodle2/restore_feedback_activity_task.class.phpnu[. /** * @package mod_feedback * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/feedback/backup/moodle2/restore_feedback_stepslib.php'); // Because it exists (must) /** * feedback restore task that provides all the settings and steps to perform one * complete restore of the activity */ class restore_feedback_activity_task extends restore_activity_task { /** * Define (add) particular settings this activity can have */ protected function define_my_settings() { // No particular settings for this activity } /** * Define (add) particular steps this activity can have */ protected function define_my_steps() { // feedback only has one structure step $this->add_step(new restore_feedback_activity_structure_step('feedback_structure', 'feedback.xml')); } /** * Define the contents in the activity that must be * processed by the link decoder */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('feedback', array('intro', 'site_after_submit', 'page_after_submit'), 'feedback'); $contents[] = new restore_decode_content('feedback_item', array('presentation'), 'feedback_item'); $contents[] = new restore_decode_content('feedback_value', array('value'), 'feedback_value'); return $contents; } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ public static function define_decode_rules() { $rules = array(); $rules[] = new restore_decode_rule('FEEDBACKINDEX', '/mod/feedback/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('FEEDBACKVIEWBYID', '/mod/feedback/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('FEEDBACKANALYSISBYID', '/mod/feedback/analysis.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('FEEDBACKSHOWENTRIESBYID', '/mod/feedback/show_entries.php?id=$1', 'course_module'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * feedback logs. It must return one array * of {@link restore_log_rule} objects */ public static function define_restore_log_rules() { $rules = array(); $rules[] = new restore_log_rule('feedback', 'add', 'view.php?id={course_module}', '{feedback}'); $rules[] = new restore_log_rule('feedback', 'update', 'view.php?id={course_module}', '{feedback}'); $rules[] = new restore_log_rule('feedback', 'view', 'view.php?id={course_module}', '{feedback}'); $rules[] = new restore_log_rule('feedback', 'submit', 'view.php?id={course_module}', '{feedback}'); $rules[] = new restore_log_rule('feedback', 'startcomplete', 'view.php?id={course_module}', '{feedback}'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * course logs. It must return one array * of {@link restore_log_rule} objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { $rules = array(); $rules[] = new restore_log_rule('feedback', 'view all', 'index.php?id={course}', null); return $rules; } } PKB\7+backup/moodle2/backup_feedback_stepslib.phpnu[. /** * @package mod_feedback * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the backup steps that will be used by the backup_feedback_activity_task */ /** * Define the complete feedback structure for backup, with file and id annotations */ class backup_feedback_activity_structure_step extends backup_activity_structure_step { protected function define_structure() { // To know if we are including userinfo $userinfo = $this->get_setting_value('userinfo'); // Define each element separated $feedback = new backup_nested_element('feedback', array('id'), array( 'name', 'intro', 'introformat', 'anonymous', 'email_notification', 'multiple_submit', 'autonumbering', 'site_after_submit', 'page_after_submit', 'page_after_submitformat', 'publish_stats', 'timeopen', 'timeclose', 'timemodified', 'completionsubmit')); $completeds = new backup_nested_element('completeds'); $completed = new backup_nested_element('completed', array('id'), array( 'userid', 'timemodified', 'random_response', 'anonymous_response', 'courseid')); $items = new backup_nested_element('items'); $item = new backup_nested_element('item', array('id'), array( 'template', 'name', 'label', 'presentation', 'typ', 'hasvalue', 'position', 'required', 'dependitem', 'dependvalue', 'options')); $values = new backup_nested_element('values'); $value = new backup_nested_element('value', array('id'), array( 'item', 'template', 'completed', 'value', 'course_id')); // Build the tree $feedback->add_child($items); $items->add_child($item); $feedback->add_child($completeds); $completeds->add_child($completed); $completed->add_child($values); $values->add_child($value); // Define sources $feedback->set_source_table('feedback', array('id' => backup::VAR_ACTIVITYID)); $item->set_source_table('feedback_item', array('feedback' => backup::VAR_PARENTID)); // All these source definitions only happen if we are including user info if ($userinfo) { $completed->set_source_sql(' SELECT * FROM {feedback_completed} WHERE feedback = ?', array(backup::VAR_PARENTID)); $value->set_source_table('feedback_value', array('completed' => backup::VAR_PARENTID)); } // Define id annotations $completed->annotate_ids('user', 'userid'); // Define file annotations $feedback->annotate_files('mod_feedback', 'intro', null); // This file area hasn't itemid $feedback->annotate_files('mod_feedback', 'page_after_submit', null); // This file area hasn't itemid $item->annotate_files('mod_feedback', 'item', 'id'); // Return the root element (feedback), wrapped into standard activity structure return $this->prepare_activity_structure($feedback); } } PKB\s''backup/moodle1/lib.phpnu[. /** * Provides support for the conversion of moodle1 backup to the moodle2 format * * @package mod_feedback * @copyright 2011 Rossiani Wijaya * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Feedback module conversion handler */ class moodle1_mod_feedback_handler extends moodle1_mod_handler { /** @var moodle1_file_manager */ protected $fileman = null; /** @var int cmid */ protected $moduleid = null; /** * Declare the paths in moodle.xml we are able to convert * * The method returns list of {@link convert_path} instances. * For each path returned, the corresponding conversion method must be * defined. * * Note that the path /MOODLE_BACKUP/COURSE/MODULES/MOD/FEEDBACK does not * actually exist in the file. The last element with the module name was * appended by the moodle1_converter class. * * @return array of {@link convert_path} instances */ public function get_paths() { return array( new convert_path( 'feedback', '/MOODLE_BACKUP/COURSE/MODULES/MOD/FEEDBACK', array( 'renamefields' => array( 'summary' => 'intro', 'pageaftersub' => 'page_after_submit', ), 'newfields' => array( 'autonumbering' => 1, 'site_after_submit' => '', 'introformat' => 0, 'page_after_submitformat' => 0, 'completionsubmit' => 0, ), ) ), new convert_path( 'feedback_item', '/MOODLE_BACKUP/COURSE/MODULES/MOD/FEEDBACK/ITEMS/ITEM', array ( 'newfields' => array( 'label' => '', 'options' => '', 'dependitem' => 0, 'dependvalue' => '', ), ) ), ); } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/FEEDBACK * data available */ public function process_feedback($data) { global $CFG; // get the course module id and context id $instanceid = $data['id']; $cminfo = $this->get_cminfo($instanceid); $this->moduleid = $cminfo['id']; $contextid = $this->converter->get_contextid(CONTEXT_MODULE, $this->moduleid); // get a fresh new file manager for this instance $this->fileman = $this->converter->get_file_manager($contextid, 'mod_feedback'); // convert course files embedded into the intro $this->fileman->filearea = 'intro'; $this->fileman->itemid = 0; $data['intro'] = moodle1_converter::migrate_referenced_files($data['intro'], $this->fileman); // Convert the introformat if necessary. if ($CFG->texteditors !== 'textarea') { $data['intro'] = text_to_html($data['intro'], false, false, true); $data['introformat'] = FORMAT_HTML; } // start writing feedback.xml $this->open_xml_writer("activities/feedback_{$this->moduleid}/feedback.xml"); $this->xmlwriter->begin_tag('activity', array('id' => $instanceid, 'moduleid' => $this->moduleid, 'modulename' => 'feedback', 'contextid' => $contextid)); $this->xmlwriter->begin_tag('feedback', array('id' => $instanceid)); foreach ($data as $field => $value) { if ($field <> 'id') { $this->xmlwriter->full_tag($field, $value); } } $this->xmlwriter->begin_tag('items'); return $data; } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/FEEDBACK/ITEMS/ITEM * data available */ public function process_feedback_item($data) { $this->write_xml('item', $data, array('/item/id')); } /** * This is executed when we reach the closing tag of our 'feedback' path */ public function on_feedback_end() { // finish writing feedback.xml $this->xmlwriter->end_tag('items'); $this->xmlwriter->end_tag('feedback'); $this->xmlwriter->end_tag('activity'); $this->close_xml_writer(); // write inforef.xml $this->open_xml_writer("activities/feedback_{$this->moduleid}/inforef.xml"); $this->xmlwriter->begin_tag('inforef'); $this->xmlwriter->begin_tag('fileref'); foreach ($this->fileman->get_fileids() as $fileid) { $this->write_xml('file', array('id' => $fileid)); } $this->xmlwriter->end_tag('fileref'); $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); } } PKB\7""view.phpnu[. /** * the first page to view the feedback * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/feedback/lib.php'); $id = required_param('id', PARAM_INT); $courseid = optional_param('courseid', false, PARAM_INT); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); require_course_login($course, true, $cm); $feedback = $PAGE->activityrecord; $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $courseid); $context = context_module::instance($cm->id); if ($course->id == SITEID) { $PAGE->set_pagelayout('incourse'); } $PAGE->set_url('/mod/feedback/view.php', array('id' => $cm->id)); /** @var \mod_feedback\output\renderer $renderer */ $renderer = $PAGE->get_renderer('mod_feedback'); $renderer->set_title( [format_string($feedback->name), format_string($course->fullname)] ); $PAGE->set_heading($course->fullname); $PAGE->add_body_class('limitedwidth'); // Check whether the feedback is mapped to the given courseid. if (!has_capability('mod/feedback:edititems', $context) && !$feedbackcompletion->check_course_is_mapped()) { echo $OUTPUT->header(); echo $OUTPUT->notification(get_string('cannotaccess', 'mod_feedback')); echo $OUTPUT->footer(); exit; } $viewcompletion = $feedbackcompletion->is_open() && $feedbackcompletion->can_complete() && $feedbackcompletion->can_submit(); $actionbar = new \mod_feedback\output\standard_action_bar( $cm->id, $viewcompletion, $feedbackcompletion->get_resume_page(), $courseid ); // Trigger module viewed event. $feedbackcompletion->trigger_module_viewed(); /// Print the main part of the page /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// $previewimg = $OUTPUT->pix_icon('t/preview', get_string('preview')); $previewlnk = new moodle_url('/mod/feedback/print.php', array('id' => $id)); if ($courseid) { $previewlnk->param('courseid', $courseid); } $preview = html_writer::link($previewlnk, $previewimg); $PAGE->activityheader->set_description(""); // Print the page header. echo $OUTPUT->header(); // Show description. echo $OUTPUT->box_start('generalbox feedback_description'); $options = (object)array('noclean' => true); echo format_module_intro('feedback', $feedback, $cm->id); echo $renderer->main_action_bar($actionbar); echo $OUTPUT->box_end(); //show some infos to the feedback if (has_capability('mod/feedback:edititems', $context)) { echo $OUTPUT->heading(get_string('overview', 'feedback'), 3); //get the groupid $groupselect = groups_print_activity_menu($cm, $CFG->wwwroot.'/mod/feedback/view.php?id='.$cm->id, true); $mygroupid = groups_get_activity_group($cm); echo $groupselect.'
 
'; $summary = new mod_feedback\output\summary($feedbackcompletion, $mygroupid); echo $OUTPUT->render_from_template('mod_feedback/summary', $summary->export_for_template($OUTPUT)); if ($pageaftersubmit = $feedbackcompletion->page_after_submit()) { echo $OUTPUT->heading(get_string("page_after_submit", "feedback"), 3); echo $OUTPUT->box($pageaftersubmit, 'generalbox feedback_after_submit'); } } if (!$PAGE->has_secondary_navigation()) { if (!has_capability('mod/feedback:viewreports', $context) && $feedbackcompletion->can_view_analysis()) { $analysisurl = new moodle_url('/mod/feedback/analysis.php', array('id' => $id)); echo ''; } if (has_capability('mod/feedback:mapcourse', $context) && $feedback->course == SITEID) { echo $OUTPUT->box_start('generalbox feedback_mapped_courses'); echo $OUTPUT->heading(get_string("mappedcourses", "feedback"), 3); echo '

' . get_string('mapcourse_help', 'feedback') . '

'; $mapurl = new moodle_url('/mod/feedback/mapcourse.php', array('id' => $id)); echo '

' . html_writer::link($mapurl, get_string('mapcourses', 'feedback')) . '

'; echo $OUTPUT->box_end(); } } if ($feedbackcompletion->can_complete()) { echo $OUTPUT->box_start('generalbox boxaligncenter'); if (!$feedbackcompletion->is_open()) { // Feedback is not yet open or is already closed. echo $OUTPUT->notification(get_string('feedback_is_not_open', 'feedback')); echo $OUTPUT->continue_button(course_get_url($courseid ?: $course->id)); } else if (!$feedbackcompletion->can_submit()) { // Feedback was already submitted. echo $OUTPUT->notification( get_string('this_feedback_is_already_submitted', 'feedback'), \core\output\notification::NOTIFY_INFO, closebutton: false, ); $OUTPUT->continue_button(course_get_url($courseid ?: $course->id)); } echo $OUTPUT->box_end(); } echo $OUTPUT->footer(); PKB\pI,, edit_form.phpnu[. /** * prints the forms to choose an item-typ to create items and to choose a template to use * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ //It must be included from a Moodle page if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); } require_once($CFG->libdir.'/formslib.php'); /** * The feedback_edit_use_template_form * * @deprecated since 4.0 new dynamic forms created * @todo MDL-83522 This class will be deleted in Moodle 6.0. */ #[\core\attribute\deprecated( replacement: 'mod_feedback\form\use_template_form', since: '4.0', mdl: 'MDL-71914', reason: 'New dynamic forms have been created instead.' )] class feedback_edit_use_template_form extends moodleform { public function __construct($action = null, $customdata = null, $method = 'post', $target = '', $attributes = null, $editable = true, $ajaxformdata = null) { debugging('Class feedback_edit_use_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @return array */ public static function get_js_module() { debugging('Class feedback_edit_use_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::get_js_module(); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $simulatedsubmitteddata * @param array $simulatedsubmittedfiles * @param string $method * @param null $formidentifier * @return array */ public static function mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post', $formidentifier = null) { debugging('Class feedback_edit_use_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles, $method, $formidentifier); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $data * @return array */ public static function mock_generate_submit_keys($data = []) { debugging('Class feedback_edit_use_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::mock_generate_submit_keys($data); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $simulatedsubmitteddata * @param array $simulatedsubmittedfiles * @param string $method * @param null $formidentifier */ public static function mock_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post', $formidentifier = null) { debugging('Class feedback_edit_use_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); parent::mock_submit($simulatedsubmitteddata, $simulatedsubmittedfiles, $method, $formidentifier); } /** * Form definition */ public function definition() { $mform =& $this->_form; $course = $this->_customdata['course']; $elementgroup = array(); //headline $mform->addElement('header', 'using_templates', get_string('using_templates', 'feedback')); // hidden elements $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); // visible elements $templates_options = array(); $owntemplates = feedback_get_template_list($course, 'own'); $publictemplates = feedback_get_template_list($course, 'public'); $options = array(); if ($owntemplates or $publictemplates) { $options[''] = array('' => get_string('choosedots')); if ($owntemplates) { $courseoptions = array(); foreach ($owntemplates as $template) { $courseoptions[$template->id] = format_string($template->name); } $options[get_string('course')] = $courseoptions; } if ($publictemplates) { $publicoptions = array(); foreach ($publictemplates as $template) { $publicoptions[$template->id] = format_string($template->name); } $options[get_string('public', 'feedback')] = $publicoptions; } $attributes = [ 'onChange="this.form.submit()"', 'data-form-change-checker-override="1"', ]; $elementgroup[] = $mform->createElement( 'selectgroups', 'templateid', get_string('using_templates', 'feedback'), $options, implode(' ', $attributes) ); $elementgroup[] = $mform->createElement('submit', 'use_template', get_string('use_this_template', 'feedback'), array('class' => 'hiddenifjs')); $mform->addGroup($elementgroup, 'elementgroup', '', array(' '), false); } else { $mform->addElement('static', 'info', get_string('no_templates_available_yet', 'feedback')); } $this->set_data(array('id' => $this->_customdata['id'])); } } /** * The feedback_edit_create_template_form * * @deprecated since 4.0, new dynamic forms have been created instead. */ class feedback_edit_create_template_form extends moodleform { public function __construct($action = null, $customdata = null, $method = 'post', $target = '', $attributes = null, $editable = true, $ajaxformdata = null) { debugging('Class feedback_edit_create_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @return array */ public static function get_js_module() { debugging('Class feedback_edit_create_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::get_js_module(); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $simulatedsubmitteddata * @param array $simulatedsubmittedfiles * @param string $method * @param null $formidentifier * @return array */ public static function mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post', $formidentifier = null) { debugging('Class feedback_edit_create_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles, $method, $formidentifier); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $data * @return array */ public static function mock_generate_submit_keys($data = []) { debugging('Class feedback_edit_create_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::mock_generate_submit_keys($data); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $simulatedsubmitteddata * @param array $simulatedsubmittedfiles * @param string $method * @param null $formidentifier */ public static function mock_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post', $formidentifier = null) { debugging('Class feedback_edit_create_template_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); parent::mock_submit($simulatedsubmitteddata, $simulatedsubmittedfiles, $method, $formidentifier); } /** * Form definition */ public function definition() { $mform =& $this->_form; // hidden elements $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'do_show'); $mform->setType('do_show', PARAM_ALPHANUMEXT); $mform->setConstant('do_show', 'edit'); // visible elements $elementgroup = array(); $elementgroup[] = $mform->createElement('text', 'templatename', get_string('name', 'feedback'), ['maxlength' => '200']); if (has_capability('mod/feedback:createpublictemplate', context_system::instance())) { $elementgroup[] = $mform->createElement('checkbox', 'ispublic', '', get_string('public', 'feedback')); } $mform->addGroup($elementgroup, 'elementgroup', get_string('name', 'feedback'), array(' '), false); // Buttons. $mform->addElement('submit', 'create_template', get_string('save_as_new_template', 'feedback')); $mform->setType('templatename', PARAM_TEXT); $this->set_data(array('id' => $this->_customdata['id'])); } /** * Form validation * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files) { $errors = parent::validation($data, $files); if (!isset($data['templatename']) || trim(strval($data['templatename'])) === '') { $errors['elementgroup'] = get_string('name_required', 'feedback'); } return $errors; } } PKB\11 edit_item.phpnu[. /** * prints the form to edit a dedicated item * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); feedback_init_feedback_session(); $itemid = optional_param('id', false, PARAM_INT); if (!$itemid) { $cmid = required_param('cmid', PARAM_INT); $typ = required_param('typ', PARAM_ALPHA); } if ($itemid) { $item = $DB->get_record('feedback_item', array('id' => $itemid), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($item->feedback, 'feedback'); $url = new moodle_url('/mod/feedback/edit_item.php', array('id' => $itemid)); $typ = $item->typ; } else { $item = null; list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'feedback'); $url = new moodle_url('/mod/feedback/edit_item.php', array('cmid' => $cm->id, 'typ' => $typ)); $item = (object)['id' => null, 'position' => -1, 'typ' => $typ, 'options' => '']; } require_login($course, true, $cm); $context = context_module::instance($cm->id); require_capability('mod/feedback:edititems', $context); $feedback = $PAGE->activityrecord; $editurl = new moodle_url('/mod/feedback/edit.php', array('id' => $cm->id)); $PAGE->set_url($url); // If the typ is pagebreak so the item will be saved directly. if (!$item->id && $typ === 'pagebreak') { require_sesskey(); $redirectmessage = ''; if (!feedback_create_pagebreak($feedback->id)) { $redirectmessage = get_string('cannotcreatepagebreak', 'mod_feedback'); } redirect($editurl, $redirectmessage, null, \core\output\notification::NOTIFY_WARNING); } //get the existing item or create it if (!$typ) { throw new \moodle_exception('typemissing', 'feedback', $editurl->out(false)); } $itemobj = feedback_get_item_class($typ); $itemobj->build_editform($item, $feedback, $cm); if ($itemobj->is_cancelled()) { redirect($editurl); exit; } if ($itemobj->get_data()) { if ($item = $itemobj->save_item()) { feedback_move_item($item, $item->position); redirect($editurl); } } //////////////////////////////////////////////////////////////////////////////////// /// Print the page header $strfeedbacks = get_string("modulenameplural", "feedback"); $strfeedback = get_string("modulename", "feedback"); navigation_node::override_active_url(new moodle_url('/mod/feedback/edit.php', array('id' => $cm->id, 'do_show' => 'edit'))); if ($item->id) { $PAGE->navbar->add(get_string('edit_item', 'feedback')); } else { $PAGE->navbar->add(get_string('add_item', 'feedback')); } $PAGE->set_heading($course->fullname); $renderer = $PAGE->get_renderer('mod_feedback'); $pagetitle = ($itemid) ? get_string('edit_item', 'feedback') : get_string('add_item', 'feedback'); $renderer->set_title( [format_string($feedback->name), format_string($course->fullname)], $pagetitle ); $PAGE->activityheader->set_attrs([ "hidecompletion" => true, "description" => '' ]); $PAGE->add_body_class('limitedwidth'); echo $OUTPUT->header(); //print errormsg if (isset($error)) { echo $error; } $itemobj->show_editform(); /// Finish the page /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// echo $OUTPUT->footer(); PKB\4Y  mapcourse.phpnu[. /** * print the form to map courses for global feedbacks * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once(__DIR__ . "/../../config.php"); require_once($CFG->dirroot . "/mod/feedback/lib.php"); require_once("$CFG->libdir/tablelib.php"); $id = required_param('id', PARAM_INT); // Course Module ID. $url = new moodle_url('/mod/feedback/mapcourse.php', array('id'=>$id)); $PAGE->set_url($url); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); require_login($course, true, $cm); $feedback = $PAGE->activityrecord; $context = context_module::instance($cm->id); require_capability('mod/feedback:mapcourse', $context); $coursemap = array_keys(feedback_get_courses_from_sitecourse_map($feedback->id)); $form = new mod_feedback_course_map_form(); $form->set_data(array('id' => $cm->id, 'mappedcourses' => $coursemap)); $mainurl = new moodle_url('/mod/feedback/view.php', ['id' => $id]); if ($form->is_cancelled()) { redirect($mainurl); } else if ($data = $form->get_data()) { feedback_update_sitecourse_map($feedback, $data->mappedcourses); redirect($mainurl, get_string('mappingchanged', 'feedback'), null, \core\output\notification::NOTIFY_SUCCESS); } // Print the page header. $strfeedbacks = get_string("modulenameplural", "feedback"); $strfeedback = get_string("modulename", "feedback"); $PAGE->set_heading($course->fullname); $PAGE->set_title($feedback->name); echo $OUTPUT->header(); echo $OUTPUT->box(get_string('mapcourseinfo', 'feedback')); $form->display(); echo $OUTPUT->footer(); PKB\aV print.phpnu[. /** * print a printview of feedback-items * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id', PARAM_INT); $courseid = optional_param('courseid', false, PARAM_INT); // Course where this feedback is mapped to - used for return link. $PAGE->set_url('/mod/feedback/print.php', array('id'=>$id)); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); require_course_login($course, true, $cm); // This page should be only displayed to users with capability to edit or view reports (to include non-editing teachers too). $context = context_module::instance($cm->id); $capabilities = [ 'mod/feedback:edititems', 'mod/feedback:viewreports', ]; if (!has_any_capability($capabilities, $context)) { $capability = 'mod/feedback:edititems'; if (has_capability($capability, $context)) { $capability = 'mod/feedback:viewreports'; } throw new required_capability_exception($context, $capability, 'nopermissions', ''); } $feedback = $PAGE->activityrecord; $feedbackstructure = new mod_feedback_structure($feedback, $cm, $courseid); $PAGE->set_pagelayout('popup'); // Print the page header. $strfeedbacks = get_string("modulenameplural", "feedback"); $strfeedback = get_string("modulename", "feedback"); $feedback_url = new moodle_url('/mod/feedback/index.php', array('id'=>$course->id)); $PAGE->navbar->add($strfeedbacks, $feedback_url); $PAGE->navbar->add(format_string($feedback->name)); $renderer = $PAGE->get_renderer('mod_feedback'); $renderer->set_title( [format_string($feedback->name), format_string($course->fullname)], get_string('previewquestions', 'feedback') ); $PAGE->set_heading($course->fullname); $PAGE->activityheader->set_title(format_string($feedback->name)); echo $OUTPUT->header(); $continueurl = new moodle_url('/mod/feedback/view.php', array('id' => $id)); if ($courseid) { $continueurl->param('courseid', $courseid); } $form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_PRINT, $feedbackstructure, 'feedback_print_form'); echo $OUTPUT->continue_button($continueurl); $form->display(); echo $OUTPUT->continue_button($continueurl); // Finish the page. echo $OUTPUT->footer(); PKB\><pix/notrequired.svgnu[ PKB\IQpix/required.svgnu[ PKB\~<pix/monologo.svgnu[ PKB\6Yëëlib.phpnu[. /** * Library of functions and constants for module feedback * includes the main-part of feedback-functions * * @package mod_feedback * @copyright Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_feedback\manager; defined('MOODLE_INTERNAL') || die(); // Include forms lib. require_once($CFG->libdir.'/formslib.php'); define('FEEDBACK_ANONYMOUS_YES', 1); define('FEEDBACK_ANONYMOUS_NO', 2); define('FEEDBACK_MIN_ANONYMOUS_COUNT_IN_GROUP', 2); define('FEEDBACK_DECIMAL', '.'); define('FEEDBACK_THOUSAND', ','); define('FEEDBACK_RESETFORM_RESET', 'feedback_reset_data_'); define('FEEDBACK_RESETFORM_DROP', 'feedback_drop_feedback_'); define('FEEDBACK_MAX_PIX_LENGTH', '400'); //max. Breite des grafischen Balkens in der Auswertung define('FEEDBACK_DEFAULT_PAGE_COUNT', 20); // Event types. define('FEEDBACK_EVENT_TYPE_OPEN', 'open'); define('FEEDBACK_EVENT_TYPE_CLOSE', 'close'); require_once(__DIR__ . '/deprecatedlib.php'); /** * @uses FEATURE_GROUPS * @uses FEATURE_GROUPINGS * @uses FEATURE_MOD_INTRO * @uses FEATURE_COMPLETION_TRACKS_VIEWS * @uses FEATURE_GRADE_HAS_GRADE * @uses FEATURE_GRADE_OUTCOMES * @param string $feature FEATURE_xx constant for requested feature * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. */ function feedback_supports($feature) { switch($feature) { case FEATURE_GROUPS: return true; case FEATURE_GROUPINGS: return true; case FEATURE_MOD_INTRO: return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; case FEATURE_COMPLETION_HAS_RULES: return true; case FEATURE_GRADE_HAS_GRADE: return false; case FEATURE_GRADE_OUTCOMES: return false; case FEATURE_BACKUP_MOODLE2: return true; case FEATURE_SHOW_DESCRIPTION: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_COMMUNICATION; default: return null; } } /** * this will create a new instance and return the id number * of the new instance. * * @global object * @param object $feedback the object given by mod_feedback_mod_form * @return int */ function feedback_add_instance($feedback) { global $DB; $feedback->timemodified = time(); $feedback->id = ''; if (empty($feedback->site_after_submit)) { $feedback->site_after_submit = ''; } //saving the feedback in db $feedbackid = $DB->insert_record("feedback", $feedback); $feedback->id = $feedbackid; feedback_set_events($feedback); if (!isset($feedback->coursemodule)) { $cm = get_coursemodule_from_id('feedback', $feedback->id); $feedback->coursemodule = $cm->id; } $context = context_module::instance($feedback->coursemodule); if (!empty($feedback->completionexpected)) { \core_completion\api::update_completion_date_event($feedback->coursemodule, 'feedback', $feedback->id, $feedback->completionexpected); } $editoroptions = feedback_get_editor_options(); // process the custom wysiwyg editor in page_after_submit if ($draftitemid = $feedback->page_after_submit_editor['itemid']) { $feedback->page_after_submit = file_save_draft_area_files($draftitemid, $context->id, 'mod_feedback', 'page_after_submit', 0, $editoroptions, $feedback->page_after_submit_editor['text']); $feedback->page_after_submitformat = $feedback->page_after_submit_editor['format']; } $DB->update_record('feedback', $feedback); return $feedbackid; } /** * this will update a given instance * * @global object * @param object $feedback the object given by mod_feedback_mod_form * @return boolean */ function feedback_update_instance($feedback) { global $DB; $feedback->timemodified = time(); $feedback->id = $feedback->instance; if (empty($feedback->site_after_submit)) { $feedback->site_after_submit = ''; } //save the feedback into the db $DB->update_record("feedback", $feedback); //create or update the new events feedback_set_events($feedback); $completionexpected = (!empty($feedback->completionexpected)) ? $feedback->completionexpected : null; \core_completion\api::update_completion_date_event($feedback->coursemodule, 'feedback', $feedback->id, $completionexpected); $context = context_module::instance($feedback->coursemodule); $editoroptions = feedback_get_editor_options(); // process the custom wysiwyg editor in page_after_submit if ($draftitemid = $feedback->page_after_submit_editor['itemid']) { $feedback->page_after_submit = file_save_draft_area_files($draftitemid, $context->id, 'mod_feedback', 'page_after_submit', 0, $editoroptions, $feedback->page_after_submit_editor['text']); $feedback->page_after_submitformat = $feedback->page_after_submit_editor['format']; } $DB->update_record('feedback', $feedback); return true; } /** * Serves the files included in feedback items like label. Implements needed access control ;-) * * There are two situations in general where the files will be sent. * 1) filearea = item, 2) filearea = template * * @package mod_feedback * @category files * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - justsend the file */ function feedback_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { global $CFG, $DB; if ($filearea === 'item' or $filearea === 'template') { $itemid = (int)array_shift($args); //get the item what includes the file if (!$item = $DB->get_record('feedback_item', array('id'=>$itemid))) { return false; } $feedbackid = $item->feedback; $templateid = $item->template; } if ($filearea === 'page_after_submit' or $filearea === 'item') { if (! $feedback = $DB->get_record("feedback", array("id"=>$cm->instance))) { return false; } $feedbackid = $feedback->id; //if the filearea is "item" so we check the permissions like view/complete the feedback $canload = false; //first check whether the user has the complete capability if (has_capability('mod/feedback:complete', $context)) { $canload = true; } //now we check whether the user has the view capability if (has_capability('mod/feedback:view', $context)) { $canload = true; } //if the feedback is on frontpage and anonymous and the fullanonymous is allowed //so the file can be loaded too. if (isset($CFG->feedback_allowfullanonymous) AND $CFG->feedback_allowfullanonymous AND $course->id == SITEID AND $feedback->anonymous == FEEDBACK_ANONYMOUS_YES ) { $canload = true; } if (!$canload) { return false; } } else if ($filearea === 'template') { //now we check files in templates if (!$template = $DB->get_record('feedback_template', array('id'=>$templateid))) { return false; } //if the file is not public so the capability edititems has to be there if (!$template->ispublic) { if (!has_capability('mod/feedback:edititems', $context)) { return false; } } else { //on public templates, at least the user has to be logged in if (!isloggedin()) { return false; } } } else { return false; } if ($context->contextlevel == CONTEXT_MODULE) { if ($filearea !== 'item' and $filearea !== 'page_after_submit') { return false; } } if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_SYSTEM) { if ($filearea !== 'template') { return false; } } $relativepath = implode('/', $args); if ($filearea === 'page_after_submit') { $fullpath = "/{$context->id}/mod_feedback/$filearea/$relativepath"; } else { $fullpath = "/{$context->id}/mod_feedback/$filearea/{$item->id}/$relativepath"; } $fs = get_file_storage(); if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { return false; } // finally send the file send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security! return false; } /** * this will delete a given instance. * all referenced data also will be deleted * * @global object * @param int $id the instanceid of feedback * @return boolean */ function feedback_delete_instance($id) { global $DB; //get all referenced items $feedbackitems = $DB->get_records('feedback_item', array('feedback'=>$id)); //deleting all referenced items and values if (is_array($feedbackitems)) { foreach ($feedbackitems as $feedbackitem) { $DB->delete_records("feedback_value", array("item"=>$feedbackitem->id)); $DB->delete_records("feedback_valuetmp", array("item"=>$feedbackitem->id)); } if ($delitems = $DB->get_records("feedback_item", array("feedback"=>$id))) { foreach ($delitems as $delitem) { feedback_delete_item($delitem->id, false); } } } //deleting the completeds $DB->delete_records("feedback_completed", array("feedback"=>$id)); //deleting the unfinished completeds $DB->delete_records("feedback_completedtmp", array("feedback"=>$id)); //deleting old events $DB->delete_records('event', array('modulename'=>'feedback', 'instance'=>$id)); return $DB->delete_records("feedback", array("id"=>$id)); } /** * Return a small object with summary information about what a * user has done with a given particular instance of this module * Used for user activity reports. * $return->time = the time they did it * $return->info = a short text description * * @param stdClass $course * @param stdClass $user * @param cm_info|stdClass $mod * @param stdClass $feedback * @return stdClass */ function feedback_user_outline($course, $user, $mod, $feedback) { global $DB; $outline = (object)['info' => '', 'time' => 0]; if ($feedback->anonymous != FEEDBACK_ANONYMOUS_NO) { // Do not disclose any user info if feedback is anonymous. return $outline; } $params = array('userid' => $user->id, 'feedback' => $feedback->id, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO); $status = null; $context = context_module::instance($mod->id); if ($completed = $DB->get_record('feedback_completed', $params)) { // User has completed feedback. $outline->info = get_string('completed', 'feedback'); $outline->time = $completed->timemodified; } else if ($completedtmp = $DB->get_record('feedback_completedtmp', $params)) { // User has started but not completed feedback. $outline->info = get_string('started', 'feedback'); $outline->time = $completedtmp->timemodified; } else if (has_capability('mod/feedback:complete', $context, $user)) { // User has not started feedback but has capability to do so. $outline->info = get_string('not_started', 'feedback'); } return $outline; } /** * Returns all users who has completed a specified feedback since a given time * many thanks to Manolescu Dorel, who contributed these two functions * * @global object * @global object * @global object * @global object * @uses CONTEXT_MODULE * @param array $activities Passed by reference * @param int $index Passed by reference * @param int $timemodified Timestamp * @param int $courseid * @param int $cmid * @param int $userid * @param int $groupid * @return void */ function feedback_get_recent_mod_activity(&$activities, &$index, $timemodified, $courseid, $cmid, $userid="", $groupid="") { global $CFG, $COURSE, $USER, $DB; if ($COURSE->id == $courseid) { $course = $COURSE; } else { $course = $DB->get_record('course', array('id'=>$courseid)); } $modinfo = get_fast_modinfo($course); $cm = $modinfo->cms[$cmid]; $sqlargs = array(); $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', 'useridagain', false)->selects; $sql = " SELECT fk . * , fc . * , $userfields FROM {feedback_completed} fc JOIN {feedback} fk ON fk.id = fc.feedback JOIN {user} u ON u.id = fc.userid "; if ($groupid) { $sql .= " JOIN {groups_members} gm ON gm.userid=u.id "; } $sql .= " WHERE fc.timemodified > ? AND fk.id = ? AND fc.anonymous_response = ?"; $sqlargs[] = $timemodified; $sqlargs[] = $cm->instance; $sqlargs[] = FEEDBACK_ANONYMOUS_NO; if ($userid) { $sql .= " AND u.id = ? "; $sqlargs[] = $userid; } if ($groupid) { $sql .= " AND gm.groupid = ? "; $sqlargs[] = $groupid; } if (!$feedbackitems = $DB->get_records_sql($sql, $sqlargs)) { return; } $cm_context = context_module::instance($cm->id); if (!has_capability('mod/feedback:view', $cm_context)) { return; } $accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context); $viewfullnames = has_capability('moodle/site:viewfullnames', $cm_context); $groupmode = groups_get_activity_groupmode($cm, $course); $aname = format_string($cm->name, true); foreach ($feedbackitems as $feedbackitem) { if ($feedbackitem->userid != $USER->id) { if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { $usersgroups = groups_get_all_groups($course->id, $feedbackitem->userid, $cm->groupingid); if (!is_array($usersgroups)) { continue; } $usersgroups = array_keys($usersgroups); $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid)); if (empty($intersect)) { continue; } } } $tmpactivity = new stdClass(); $tmpactivity->type = 'feedback'; $tmpactivity->cmid = $cm->id; $tmpactivity->name = $aname; $tmpactivity->sectionnum= $cm->sectionnum; $tmpactivity->timestamp = $feedbackitem->timemodified; $tmpactivity->content = new stdClass(); $tmpactivity->content->feedbackid = $feedbackitem->id; $tmpactivity->content->feedbackuserid = $feedbackitem->userid; $tmpactivity->user = user_picture::unalias($feedbackitem, null, 'useridagain'); $tmpactivity->user->fullname = fullname($feedbackitem, $viewfullnames); $activities[$index++] = $tmpactivity; } return; } /** * Prints all users who has completed a specified feedback since a given time * many thanks to Manolescu Dorel, who contributed these two functions * * @global object * @param object $activity * @param int $courseid * @param string $detail * @param array $modnames * @return void Output is echo'd */ function feedback_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { global $CFG, $OUTPUT; echo ''; echo "
"; echo $OUTPUT->user_picture($activity->user, array('courseid'=>$courseid)); echo ""; if ($detail) { $modname = $modnames[$activity->type]; echo '
'; echo $OUTPUT->image_icon('monologo', $modname, $activity->type); echo "wwwroot/mod/feedback/view.php?id={$activity->cmid}\">{$activity->name}"; echo '
'; } echo '
'; echo '
'; echo ''; echo "
"; return; } /** * Print a detailed representation of what a user has done with * a given particular instance of this module, for user activity reports. * * @param stdClass $course * @param stdClass $user * @param cm_info|stdClass $mod * @param stdClass $feedback */ function feedback_user_complete($course, $user, $mod, $feedback) { global $DB; if ($feedback->anonymous != FEEDBACK_ANONYMOUS_NO) { // Do not disclose any user info if feedback is anonymous. return; } $params = array('userid' => $user->id, 'feedback' => $feedback->id, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO); $url = $status = null; $context = context_module::instance($mod->id); if ($completed = $DB->get_record('feedback_completed', $params)) { // User has completed feedback. if (has_capability('mod/feedback:viewreports', $context)) { $url = new moodle_url('/mod/feedback/show_entries.php', ['id' => $mod->id, 'userid' => $user->id, 'showcompleted' => $completed->id]); } $status = get_string('completedon', 'feedback', userdate($completed->timemodified)); } else if ($completedtmp = $DB->get_record('feedback_completedtmp', $params)) { // User has started but not completed feedback. $status = get_string('startedon', 'feedback', userdate($completedtmp->timemodified)); } else if (has_capability('mod/feedback:complete', $context, $user)) { // User has not started feedback but has capability to do so. $status = get_string('not_started', 'feedback'); } if ($url && $status) { echo html_writer::link($url, $status); } else if ($status) { echo html_writer::div($status); } } /** * @return bool true */ function feedback_cron () { return true; } /** * Checks if scale is being used by any instance of feedback * * This is used to find out if scale used anywhere * @param $scaleid int * @return boolean True if the scale is used by any assignment */ function feedback_scale_used_anywhere($scaleid) { return false; } /** * List the actions that correspond to a view of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = 'r' and edulevel = LEVEL_PARTICIPATING will * be considered as view action. * * @return array */ function feedback_get_view_actions() { return array('view', 'view all'); } /** * List the actions that correspond to a post of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING * will be considered as post action. * * @return array */ function feedback_get_post_actions() { return array('submit'); } /** * This function is used by the reset_course_userdata function in moodlelib. * This function will remove all responses from the specified feedback * and clean up any related data. * * @global object * @global object * @uses FEEDBACK_RESETFORM_RESET * @uses FEEDBACK_RESETFORM_DROP * @param object $data the data submitted from the reset course. * @return array status array */ function feedback_reset_userdata($data) { global $CFG, $DB; $resetfeedbacks = []; $dropfeedbacks = []; $status = []; $componentstr = get_string('modulenameplural', 'feedback'); // Get the relevant entries from $data. foreach ($data as $key => $value) { switch(true) { case substr($key, 0, strlen(FEEDBACK_RESETFORM_RESET)) == FEEDBACK_RESETFORM_RESET: if ($value == 1) { $templist = explode('_', $key); if (isset($templist[3])) { $resetfeedbacks[] = intval($templist[3]); } } break; case substr($key, 0, strlen(FEEDBACK_RESETFORM_DROP)) == FEEDBACK_RESETFORM_DROP: if ($value == 1) { $templist = explode('_', $key); if (isset($templist[3])) { $dropfeedbacks[] = intval($templist[3]); } } break; } } // Reset the selected feedbacks. foreach ($resetfeedbacks as $id) { $feedback = $DB->get_record('feedback', ['id' => $id]); feedback_delete_all_completeds($feedback); $status[] = [ 'component' => $componentstr.':'.$feedback->name, 'item' => get_string('resetting_data', 'feedback'), 'error' => false, ]; } // Updating dates - shift may be negative too. if ($data->timeshift) { // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. $shifterror = !shift_course_mod_dates('feedback', ['timeopen', 'timeclose'], $data->timeshift, $data->courseid); $status[] = [ 'component' => $componentstr, 'item' => get_string('date'), 'error' => $shifterror, ]; } return $status; } /** * Called by course/reset.php * * @global object * @uses FEEDBACK_RESETFORM_RESET * @param MoodleQuickForm $mform form passed by reference */ function feedback_reset_course_form_definition(&$mform) { global $COURSE, $DB; $mform->addElement('header', 'feedbackheader', get_string('modulenameplural', 'feedback')); if (!$feedbacks = $DB->get_records('feedback', array('course'=>$COURSE->id), 'name')) { return; } $mform->addElement('static', 'hint', get_string('resetting_delete', 'feedback')); foreach ($feedbacks as $feedback) { $mform->addElement('checkbox', FEEDBACK_RESETFORM_RESET.$feedback->id, $feedback->name); } } /** * Course reset form defaults. * * @global object * @uses FEEDBACK_RESETFORM_RESET * @param object $course */ function feedback_reset_course_form_defaults($course) { global $DB; $return = array(); if (!$feedbacks = $DB->get_records('feedback', array('course'=>$course->id), 'name')) { return; } foreach ($feedbacks as $feedback) { $return[FEEDBACK_RESETFORM_RESET.$feedback->id] = true; } return $return; } /** * Called by course/reset.php and shows the formdata by coursereset. * it prints checkboxes for each feedback available at the given course * there are two checkboxes: * 1) delete userdata and keep the feedback * 2) delete userdata and drop the feedback * * @global object * @uses FEEDBACK_RESETFORM_RESET * @uses FEEDBACK_RESETFORM_DROP * @param object $course * @return void */ function feedback_reset_course_form($course) { global $DB, $OUTPUT; echo get_string('resetting_feedbacks', 'feedback'); echo ':
'; if (!$feedbacks = $DB->get_records('feedback', ['course' => $course->id], 'name')) { return; } foreach ($feedbacks as $feedback) { echo '

'; echo get_string('name', 'feedback').': '.$feedback->name.'
'; echo html_writer::checkbox(FEEDBACK_RESETFORM_RESET.$feedback->id, 1, true, get_string('resetting_data', 'feedback')); echo '
'; echo html_writer::checkbox(FEEDBACK_RESETFORM_DROP.$feedback->id, 1, false, get_string('drop_feedback', 'feedback')); echo '

'; } } /** * This gets an array with default options for the editor * * @return array the options */ function feedback_get_editor_options() { return array('maxfiles' => EDITOR_UNLIMITED_FILES, 'trusttext'=>true); } /** * This creates new events given as timeopen and closeopen by $feedback. * * @global object * @param object $feedback * @return void */ function feedback_set_events($feedback) { global $DB, $CFG; // Include calendar/lib.php. require_once($CFG->dirroot.'/calendar/lib.php'); // Get CMID if not sent as part of $feedback. if (!isset($feedback->coursemodule)) { $cm = get_coursemodule_from_instance('feedback', $feedback->id, $feedback->course); $feedback->coursemodule = $cm->id; } // Feedback start calendar events. $eventid = $DB->get_field('event', 'id', array('modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN)); if (isset($feedback->timeopen) && $feedback->timeopen > 0) { $event = new stdClass(); $event->eventtype = FEEDBACK_EVENT_TYPE_OPEN; $event->type = empty($feedback->timeclose) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; $event->name = get_string('calendarstart', 'feedback', $feedback->name); $event->description = format_module_intro('feedback', $feedback, $feedback->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $feedback->timeopen; $event->timesort = $feedback->timeopen; $event->visible = instance_is_visible('feedback', $feedback); $event->timeduration = 0; if ($eventid) { // Calendar event exists so update it. $event->id = $eventid; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Event doesn't exist so create one. $event->courseid = $feedback->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'feedback'; $event->instance = $feedback->id; $event->eventtype = FEEDBACK_EVENT_TYPE_OPEN; calendar_event::create($event, false); } } else if ($eventid) { // Calendar event is on longer needed. $calendarevent = calendar_event::load($eventid); $calendarevent->delete(); } // Feedback close calendar events. $eventid = $DB->get_field('event', 'id', array('modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE)); if (isset($feedback->timeclose) && $feedback->timeclose > 0) { $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = FEEDBACK_EVENT_TYPE_CLOSE; $event->name = get_string('calendarend', 'feedback', $feedback->name); $event->description = format_module_intro('feedback', $feedback, $feedback->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $feedback->timeclose; $event->timesort = $feedback->timeclose; $event->visible = instance_is_visible('feedback', $feedback); $event->timeduration = 0; if ($eventid) { // Calendar event exists so update it. $event->id = $eventid; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Event doesn't exist so create one. $event->courseid = $feedback->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'feedback'; $event->instance = $feedback->id; calendar_event::create($event, false); } } else if ($eventid) { // Calendar event is on longer needed. $calendarevent = calendar_event::load($eventid); $calendarevent->delete(); } } /** * This standard function will check all instances of this module * and make sure there are up-to-date events created for each of them. * If courseid = 0, then every feedback event in the site is checked, else * only feedback events belonging to the course specified are checked. * This function is used, in its new format, by restore_refresh_events() * * @param int $courseid * @param int|stdClass $instance Feedback module instance or ID. * @param int|stdClass $cm Course module object or ID (not used in this module). * @return bool */ function feedback_refresh_events($courseid = 0, $instance = null, $cm = null) { global $DB; // If we have instance information then we can just update the one event instead of updating all events. if (isset($instance)) { if (!is_object($instance)) { $instance = $DB->get_record('feedback', array('id' => $instance), '*', MUST_EXIST); } feedback_set_events($instance); return true; } if ($courseid) { if (! $feedbacks = $DB->get_records("feedback", array("course" => $courseid))) { return true; } } else { if (! $feedbacks = $DB->get_records("feedback")) { return true; } } foreach ($feedbacks as $feedback) { feedback_set_events($feedback); } return true; } /** * this function is called by {@link feedback_delete_userdata()} * it drops the feedback-instance from the course_module table * * @global object * @param int $id the id from the coursemodule * @return boolean */ function feedback_delete_course_module($id) { global $DB; if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) { return true; } return $DB->delete_records('course_modules', array('id'=>$cm->id)); } //////////////////////////////////////////////// //functions to handle capabilities //////////////////////////////////////////////// /** * count users which have not completed the feedback * * @global object * @uses CONTEXT_MODULE * @param cm_info $cm Course-module object * @param int $group single groupid * @param string $sort * @param int $startpage * @param int $pagecount * @param bool $includestatus to return if the user started or not the feedback among the complete user record * @return array array of user ids or user objects when $includestatus set to true */ function feedback_get_incomplete_users(cm_info $cm, $group = false, $sort = '', $startpage = false, $pagecount = false, $includestatus = false) { global $DB; $context = context_module::instance($cm->id); //first get all user who can complete this feedback $cap = 'mod/feedback:complete'; $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $fields = 'u.id, ' . $allnames . ', u.picture, u.email, u.imagealt'; if (!$allusers = get_users_by_capability($context, $cap, $fields, $sort, '', '', $group, '', true)) { return []; } // Filter users that are not in the correct group/grouping. $info = new \core_availability\info_module($cm); $allusersrecords = $info->filter_user_list($allusers); $allusers = array_keys($allusersrecords); //now get all completeds $params = array('feedback'=>$cm->instance); if ($completedusers = $DB->get_records_menu('feedback_completed', $params, '', 'id, userid')) { // Now strike all completedusers from allusers. $allusers = array_diff($allusers, $completedusers); } //for paging I use array_slice() if ($startpage !== false AND $pagecount !== false) { $allusers = array_slice($allusers, $startpage, $pagecount); } // Check if we should return the full users objects. if ($includestatus) { $userrecords = []; $startedusers = $DB->get_records_menu('feedback_completedtmp', ['feedback' => $cm->instance], '', 'id, userid'); $startedusers = array_flip($startedusers); foreach ($allusers as $userid) { $allusersrecords[$userid]->feedbackstarted = isset($startedusers[$userid]); $userrecords[] = $allusersrecords[$userid]; } return $userrecords; } else { // Return just user ids. return $allusers; } } /** * count users which have not completed the feedback * * @global object * @param object $cm * @param int $group single groupid * @return int count of userrecords */ function feedback_count_incomplete_users($cm, $group = false) { if ($allusers = feedback_get_incomplete_users($cm, $group)) { return count($allusers); } return 0; } /** * count users which have completed a feedback * * @global object * @uses FEEDBACK_ANONYMOUS_NO * @param object $cm * @param int $group single groupid * @return int count of userrecords */ function feedback_count_complete_users($cm, $group = false) { global $DB; $params = array(FEEDBACK_ANONYMOUS_NO, $cm->instance); $fromgroup = ''; $wheregroup = ''; if ($group) { $fromgroup = ', {groups_members} g'; $wheregroup = ' AND g.groupid = ? AND g.userid = c.userid'; $params[] = $group; } $sql = 'SELECT COUNT(u.id) FROM {user} u, {feedback_completed} c'.$fromgroup.' WHERE anonymous_response = ? AND u.id = c.userid AND c.feedback = ? '.$wheregroup; return $DB->count_records_sql($sql, $params); } /** * get users which have completed a feedback * * @global object * @uses CONTEXT_MODULE * @uses FEEDBACK_ANONYMOUS_NO * @param object $cm * @param int $group single groupid * @param string $where a sql where condition (must end with " AND ") * @param array parameters used in $where * @param string $sort a table field * @param int $startpage * @param int $pagecount * @return object the userrecords */ function feedback_get_complete_users($cm, $group = false, $where = '', ?array $params = null, $sort = '', $startpage = false, $pagecount = false) { global $DB; $context = context_module::instance($cm->id); $params = (array)$params; $params['anon'] = FEEDBACK_ANONYMOUS_NO; $params['instance'] = $cm->instance; $fromgroup = ''; $wheregroup = ''; if ($group) { $fromgroup = ', {groups_members} g'; $wheregroup = ' AND g.groupid = :group AND g.userid = c.userid'; $params['group'] = $group; } if ($sort) { $sortsql = ' ORDER BY '.$sort; } else { $sortsql = ''; } $userfieldsapi = \core_user\fields::for_userpic(); $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = 'SELECT DISTINCT '.$ufields.', c.timemodified as completed_timemodified FROM {user} u, {feedback_completed} c '.$fromgroup.' WHERE '.$where.' anonymous_response = :anon AND u.id = c.userid AND c.feedback = :instance '.$wheregroup.$sortsql; if ($startpage === false OR $pagecount === false) { $startpage = false; $pagecount = false; } return $DB->get_records_sql($sql, $params, $startpage, $pagecount); } /** * get users which have the viewreports-capability * * @uses CONTEXT_MODULE * @param int $cmid * @param mixed $groups single groupid or array of groupids - group(s) user is in * @return object the userrecords */ function feedback_get_viewreports_users($cmid, $groups = false) { $context = context_module::instance($cmid); //description of the call below: //get_users_by_capability($context, $capability, $fields='', $sort='', $limitfrom='', // $limitnum='', $groups='', $exceptions='', $doanything=true) return get_users_by_capability($context, 'mod/feedback:viewreports', '', 'lastname', '', '', $groups, '', false); } /** * get users which have the receivemail-capability * * @uses CONTEXT_MODULE * @param int $cmid * @param mixed $groups single groupid or array of groupids - group(s) user is in * @return stdClass[] the userrecords */ function feedback_get_receivemail_users($cmid, $groups = false) { $context = context_module::instance($cmid); $allusers = get_users_by_capability($context, 'mod/feedback:receivemail', '', 'lastname', '', '', $groups, '', false); if (empty($groups)) { // Here the user that has submitted the feedback is not in any group. [$course, $cm] = get_course_and_cm_from_cmid($cmid); $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode == SEPARATEGROUPS) { // In separate group mode, only the user who can see all groups can see the feedback, so // in turn can receive the notification. $viewallgroupsusers = get_users_by_capability( $context, 'moodle/site:accessallgroups', 'u.id, u.id' ); // Remove users cannot access all groups. $allusers = array_intersect_key($allusers, $viewallgroupsusers); } } return $allusers; } //////////////////////////////////////////////// //functions to handle the templates //////////////////////////////////////////////// //////////////////////////////////////////////// /** * creates a new template-record. * * @global object * @param int $courseid * @param string $name the name of template shown in the templatelist * @param int $ispublic 0:privat 1:public * @return stdClass the new template */ function feedback_create_template($courseid, $name, $ispublic = 0) { global $DB; $templ = new stdClass(); $templ->course = ($ispublic ? 0 : $courseid); $templ->name = $name; $templ->ispublic = $ispublic; $templid = $DB->insert_record('feedback_template', $templ); return $DB->get_record('feedback_template', array('id'=>$templid)); } /** * creates new template items. * all items will be copied and the attribute feedback will be set to 0 * and the attribute template will be set to the new templateid * * @global object * @uses CONTEXT_MODULE * @uses CONTEXT_COURSE * @param object $feedback * @param string $name the name of template shown in the templatelist * @param int $ispublic 0:privat 1:public * @return boolean */ function feedback_save_as_template($feedback, $name, $ispublic = 0) { global $DB; $fs = get_file_storage(); if (!$feedbackitems = $DB->get_records('feedback_item', array('feedback'=>$feedback->id))) { return false; } if (!$newtempl = feedback_create_template($feedback->course, $name, $ispublic)) { return false; } //files in the template_item are in the context of the current course or //if the template is public the files are in the system context //files in the feedback_item are in the feedback_context of the feedback if ($ispublic) { $s_context = context_system::instance(); } else { $s_context = context_course::instance($newtempl->course); } $cm = get_coursemodule_from_instance('feedback', $feedback->id); $f_context = context_module::instance($cm->id); //create items of this new template //depend items we are storing temporary in an mapping list array(new id => dependitem) //we also store a mapping of all items array(oldid => newid) $dependitemsmap = array(); $itembackup = array(); foreach ($feedbackitems as $item) { $t_item = clone($item); unset($t_item->id); $t_item->feedback = 0; $t_item->template = $newtempl->id; $t_item->id = $DB->insert_record('feedback_item', $t_item); //copy all included files to the feedback_template filearea $itemfiles = $fs->get_area_files($f_context->id, 'mod_feedback', 'item', $item->id, "id", false); if ($itemfiles) { foreach ($itemfiles as $ifile) { $file_record = new stdClass(); $file_record->contextid = $s_context->id; $file_record->component = 'mod_feedback'; $file_record->filearea = 'template'; $file_record->itemid = $t_item->id; $fs->create_file_from_storedfile($file_record, $ifile); } } $itembackup[$item->id] = $t_item->id; if ($t_item->dependitem) { $dependitemsmap[$t_item->id] = $t_item->dependitem; } } //remapping the dependency foreach ($dependitemsmap as $key => $dependitem) { $newitem = $DB->get_record('feedback_item', array('id'=>$key)); $newitem->dependitem = $itembackup[$newitem->dependitem]; $DB->update_record('feedback_item', $newitem); } return true; } /** * deletes all feedback_items related to the given template id * * @global object * @uses CONTEXT_COURSE * @param object $template the template * @return void */ function feedback_delete_template($template) { global $DB; //deleting the files from the item is done by feedback_delete_item if ($t_items = $DB->get_records("feedback_item", array("template"=>$template->id))) { foreach ($t_items as $t_item) { feedback_delete_item($t_item->id, false, $template); } } $DB->delete_records("feedback_template", array("id"=>$template->id)); } /** * creates new feedback_item-records from template. * if $deleteold is set true so the existing items of the given feedback will be deleted * if $deleteold is set false so the new items will be appanded to the old items * * @global object * @uses CONTEXT_COURSE * @uses CONTEXT_MODULE * @param object $feedback * @param int $templateid * @param boolean $deleteold */ function feedback_items_from_template($feedback, $templateid, $deleteold = false) { global $DB, $CFG; require_once($CFG->libdir.'/completionlib.php'); $fs = get_file_storage(); if (!$template = $DB->get_record('feedback_template', array('id'=>$templateid))) { return false; } //get all templateitems if (!$templitems = $DB->get_records('feedback_item', array('template'=>$templateid))) { return false; } //files in the template_item are in the context of the current course //files in the feedback_item are in the feedback_context of the feedback if ($template->ispublic) { $s_context = context_system::instance(); } else { $s_context = context_course::instance($feedback->course); } $course = $DB->get_record('course', array('id'=>$feedback->course)); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $f_context = context_module::instance($cm->id); //if deleteold then delete all old items before //get all items if ($deleteold) { if ($feedbackitems = $DB->get_records('feedback_item', array('feedback'=>$feedback->id))) { //delete all items of this feedback foreach ($feedbackitems as $item) { feedback_delete_item($item->id, false); } $params = array('feedback'=>$feedback->id); if ($completeds = $DB->get_records('feedback_completed', $params)) { $completion = new completion_info($course); foreach ($completeds as $completed) { $DB->delete_records('feedback_completed', array('id' => $completed->id)); // Update completion state if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC && $feedback->completionsubmit) { $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid); } } } $DB->delete_records('feedback_completedtmp', array('feedback'=>$feedback->id)); } $positionoffset = 0; } else { //if the old items are kept the new items will be appended //therefor the new position has an offset $positionoffset = $DB->count_records('feedback_item', array('feedback'=>$feedback->id)); } //create items of this new template //depend items we are storing temporary in an mapping list array(new id => dependitem) //we also store a mapping of all items array(oldid => newid) $dependitemsmap = array(); $itembackup = array(); foreach ($templitems as $t_item) { $item = clone($t_item); unset($item->id); $item->feedback = $feedback->id; $item->template = 0; $item->position = $item->position + $positionoffset; $item->id = $DB->insert_record('feedback_item', $item); //moving the files to the new item $templatefiles = $fs->get_area_files($s_context->id, 'mod_feedback', 'template', $t_item->id, "id", false); if ($templatefiles) { foreach ($templatefiles as $tfile) { $file_record = new stdClass(); $file_record->contextid = $f_context->id; $file_record->component = 'mod_feedback'; $file_record->filearea = 'item'; $file_record->itemid = $item->id; $fs->create_file_from_storedfile($file_record, $tfile); } } $itembackup[$t_item->id] = $item->id; if ($item->dependitem) { $dependitemsmap[$item->id] = $item->dependitem; } } //remapping the dependency foreach ($dependitemsmap as $key => $dependitem) { $newitem = $DB->get_record('feedback_item', array('id'=>$key)); $newitem->dependitem = $itembackup[$newitem->dependitem]; $DB->update_record('feedback_item', $newitem); } } /** * get the list of available templates. * if the $onlyown param is set true so only templates from own course will be served * this is important for droping templates * * @global object * @param object $course * @param string $onlyownorpublic * @return array the template recordsets */ function feedback_get_template_list($course, $onlyownorpublic = '') { global $DB, $CFG; switch($onlyownorpublic) { case '': $templates = $DB->get_records_select('feedback_template', 'course = ? OR ispublic = 1', array($course->id), 'name'); break; case 'own': $templates = $DB->get_records('feedback_template', array('course'=>$course->id), 'name'); break; case 'public': $templates = $DB->get_records('feedback_template', array('ispublic'=>1), 'name'); break; } return $templates; } //////////////////////////////////////////////// //Handling der Items //////////////////////////////////////////////// //////////////////////////////////////////////// /** * load the lib.php from item-plugin-dir and returns the instance of the itemclass * * @param string $typ * @return feedback_item_base the instance of itemclass * @throws moodle_exception For invalid type */ function feedback_get_item_class($typ) { global $CFG; require_once($CFG->dirroot.'/mod/feedback/item/feedback_item_class.php'); //get the class of item-typ $typeclean = clean_param($typ, PARAM_ALPHA); $itemclass = "feedback_item_{$typeclean}"; $itemclasspath = "{$CFG->dirroot}/mod/feedback/item/{$typeclean}/lib.php"; //get the instance of item-class if (!class_exists($itemclass) && file_exists($itemclasspath)) { require_once($itemclasspath); } if (!class_exists($itemclass)) { throw new moodle_exception('typemissing', 'feedback'); } return new $itemclass(); } /** * load the available item plugins from given subdirectory of $CFG->dirroot * the default is "mod/feedback/item" * * @global object * @param string $dir the subdir * @return array pluginnames as string */ function feedback_load_feedback_items($dir = 'mod/feedback/item') { global $CFG; $names = get_list_of_plugins($dir); $ret_names = array(); foreach ($names as $name) { require_once($CFG->dirroot.'/'.$dir.'/'.$name.'/lib.php'); if (class_exists('feedback_item_'.$name)) { $ret_names[] = $name; } } return $ret_names; } /** * load the available item plugins to use as dropdown-options * * @global object * @return array pluginnames as string */ function feedback_load_feedback_items_options() { global $CFG; $feedback_options = array("pagebreak" => get_string('add_pagebreak', 'feedback')); if (!$feedback_names = feedback_load_feedback_items('mod/feedback/item')) { return array(); } foreach ($feedback_names as $fn) { $feedback_options[$fn] = get_string($fn, 'feedback'); } asort($feedback_options); return $feedback_options; } /** * load the available items for the depend item dropdown list shown in the edit_item form * * @global object * @param object $feedback * @param object $item the item of the edit_item form * @return array all items except the item $item, labels and pagebreaks */ function feedback_get_depend_candidates_for_item($feedback, $item) { global $DB; //all items for dependitem $where = "feedback = ? AND typ != 'pagebreak' AND hasvalue = 1"; $params = array($feedback->id); if (isset($item->id) AND $item->id) { $where .= ' AND id != ?'; $params[] = $item->id; } $dependitems = array(0 => get_string('choose')); $feedbackitems = $DB->get_records_select_menu('feedback_item', $where, $params, 'position', 'id, label'); if (!$feedbackitems) { return $dependitems; } //adding the choose-option foreach ($feedbackitems as $key => $val) { if (trim(strval($val)) !== '') { $dependitems[$key] = format_string($val); } } return $dependitems; } /** * save the changes of a given item. * * @global object * @param object $item * @return boolean */ function feedback_update_item($item) { global $DB; return $DB->update_record("feedback_item", $item); } /** * deletes an item and also deletes all related values * * @global object * @uses CONTEXT_MODULE * @param int $itemid * @param boolean $renumber should the kept items renumbered Yes/No * @param object $template if the template is given so the items are bound to it * @return void */ function feedback_delete_item($itemid, $renumber = true, $template = false) { global $DB; $item = $DB->get_record('feedback_item', array('id'=>$itemid)); //deleting the files from the item $fs = get_file_storage(); if ($template) { if ($template->ispublic) { $context = context_system::instance(); } else { $context = context_course::instance($template->course); } $templatefiles = $fs->get_area_files($context->id, 'mod_feedback', 'template', $item->id, "id", false); if ($templatefiles) { $fs->delete_area_files($context->id, 'mod_feedback', 'template', $item->id); } } else { if (!$cm = get_coursemodule_from_instance('feedback', $item->feedback)) { return false; } $context = context_module::instance($cm->id); $itemfiles = $fs->get_area_files($context->id, 'mod_feedback', 'item', $item->id, "id", false); if ($itemfiles) { $fs->delete_area_files($context->id, 'mod_feedback', 'item', $item->id); } } $DB->delete_records("feedback_value", array("item"=>$itemid)); $DB->delete_records("feedback_valuetmp", array("item"=>$itemid)); //remove all depends $DB->set_field('feedback_item', 'dependvalue', '', array('dependitem'=>$itemid)); $DB->set_field('feedback_item', 'dependitem', 0, array('dependitem'=>$itemid)); $DB->delete_records("feedback_item", array("id"=>$itemid)); if ($renumber) { feedback_renumber_items($item->feedback); } } /** * deletes all items of the given feedbackid * * @global object * @param int $feedbackid * @return void */ function feedback_delete_all_items($feedbackid) { global $DB, $CFG; require_once($CFG->libdir.'/completionlib.php'); if (!$feedback = $DB->get_record('feedback', array('id'=>$feedbackid))) { return false; } if (!$cm = get_coursemodule_from_instance('feedback', $feedback->id)) { return false; } if (!$course = $DB->get_record('course', array('id'=>$feedback->course))) { return false; } if (!$items = $DB->get_records('feedback_item', array('feedback'=>$feedbackid))) { return; } foreach ($items as $item) { feedback_delete_item($item->id, false); } if ($completeds = $DB->get_records('feedback_completed', array('feedback'=>$feedback->id))) { $completion = new completion_info($course); foreach ($completeds as $completed) { $DB->delete_records('feedback_completed', array('id' => $completed->id)); // Update completion state if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC && $feedback->completionsubmit) { $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid); } } } $DB->delete_records('feedback_completedtmp', array('feedback'=>$feedbackid)); } /** * this function toggled the item-attribute required (yes/no) * * @global object * @param object $item * @return boolean */ function feedback_switch_item_required($item) { global $DB, $CFG; $itemobj = feedback_get_item_class($item->typ); if ($itemobj->can_switch_require()) { $new_require_val = (int)!(bool)$item->required; $params = array('id'=>$item->id); $DB->set_field('feedback_item', 'required', $new_require_val, $params); } return true; } /** * renumbers all items of the given feedbackid * * @global object * @param int $feedbackid * @return void */ function feedback_renumber_items($feedbackid) { global $DB; $items = $DB->get_records('feedback_item', array('feedback'=>$feedbackid), 'position'); $pos = 1; if ($items) { foreach ($items as $item) { $DB->set_field('feedback_item', 'position', $pos, array('id'=>$item->id)); $pos++; } } } /** * this decreases the position of the given item * * @global object * @param object $item * @return bool */ function feedback_moveup_item($item) { global $DB; if ($item->position == 1) { return true; } $params = array('feedback'=>$item->feedback); if (!$items = $DB->get_records('feedback_item', $params, 'position')) { return false; } $itembefore = null; foreach ($items as $i) { if ($i->id == $item->id) { if (is_null($itembefore)) { return true; } $itembefore->position = $item->position; $item->position--; feedback_update_item($itembefore); feedback_update_item($item); feedback_renumber_items($item->feedback); return true; } $itembefore = $i; } return false; } /** * this increased the position of the given item * * @global object * @param object $item * @return bool */ function feedback_movedown_item($item) { global $DB; $params = array('feedback'=>$item->feedback); if (!$items = $DB->get_records('feedback_item', $params, 'position')) { return false; } $movedownitem = null; foreach ($items as $i) { if (!is_null($movedownitem) AND $movedownitem->id == $item->id) { $movedownitem->position = $i->position; $i->position--; feedback_update_item($movedownitem); feedback_update_item($i); feedback_renumber_items($item->feedback); return true; } $movedownitem = $i; } return false; } /** * here the position of the given item will be set to the value in $pos * * @global object * @param object $moveitem * @param int $pos * @return boolean */ function feedback_move_item($moveitem, $pos) { global $DB; $params = array('feedback'=>$moveitem->feedback); if (!$allitems = $DB->get_records('feedback_item', $params, 'position')) { return false; } if (is_array($allitems)) { $index = 1; foreach ($allitems as $item) { if ($index == $pos) { $index++; } if ($item->id == $moveitem->id) { $moveitem->position = $pos; feedback_update_item($moveitem); continue; } $item->position = $index; feedback_update_item($item); $index++; } return true; } return false; } /** * if the user completes a feedback and there is a pagebreak so the values are saved temporary. * the values are not saved permanently until the user click on save button * * @global object * @param object $feedbackcompleted * @return object temporary saved completed-record */ function feedback_set_tmp_values($feedbackcompleted) { global $DB; //first we create a completedtmp $tmpcpl = new stdClass(); foreach ($feedbackcompleted as $key => $value) { $tmpcpl->{$key} = $value; } unset($tmpcpl->id); $tmpcpl->timemodified = time(); $tmpcpl->id = $DB->insert_record('feedback_completedtmp', $tmpcpl); //get all values of original-completed if (!$values = $DB->get_records('feedback_value', array('completed'=>$feedbackcompleted->id))) { return; } foreach ($values as $value) { unset($value->id); $value->completed = $tmpcpl->id; $DB->insert_record('feedback_valuetmp', $value); } return $tmpcpl; } /** * this saves the temporary saved values permanently * * @global object * @param object $feedbackcompletedtmp the temporary completed * @param stdClass|null $feedbackcompleted the target completed * @return int the id of the completed */ function feedback_save_tmp_values($feedbackcompletedtmp, ?stdClass $feedbackcompleted = null) { global $DB; $tmpcplid = $feedbackcompletedtmp->id; if ($feedbackcompleted) { //first drop all existing values $DB->delete_records('feedback_value', array('completed'=>$feedbackcompleted->id)); //update the current completed $feedbackcompleted->timemodified = time(); $DB->update_record('feedback_completed', $feedbackcompleted); } else { $feedbackcompleted = clone($feedbackcompletedtmp); $feedbackcompleted->id = ''; $feedbackcompleted->timemodified = time(); $feedbackcompleted->id = $DB->insert_record('feedback_completed', $feedbackcompleted); } $allitems = $DB->get_records('feedback_item', array('feedback' => $feedbackcompleted->feedback)); //save all the new values from feedback_valuetmp //get all values of tmp-completed $params = array('completed'=>$feedbackcompletedtmp->id); $values = $DB->get_records('feedback_valuetmp', $params); foreach ($values as $value) { //check if there are depend items $item = $DB->get_record('feedback_item', array('id'=>$value->item)); if ($item->dependitem > 0 && isset($allitems[$item->dependitem])) { $ditem = $allitems[$item->dependitem]; while ($ditem !== null) { $check = feedback_compare_item_value($tmpcplid, $ditem, $item->dependvalue, true); if (!$check) { break; } if ($ditem->dependitem > 0 && isset($allitems[$ditem->dependitem])) { $item = $ditem; $ditem = $allitems[$ditem->dependitem]; } else { $ditem = null; } } } else { $check = true; } if ($check) { unset($value->id); $value->completed = $feedbackcompleted->id; $DB->insert_record('feedback_value', $value); } } //drop all the tmpvalues $DB->delete_records('feedback_valuetmp', array('completed'=>$tmpcplid)); $DB->delete_records('feedback_completedtmp', array('id'=>$tmpcplid)); // Trigger event for the delete action we performed. $cm = get_coursemodule_from_instance('feedback', $feedbackcompleted->feedback); $event = \mod_feedback\event\response_submitted::create_from_record($feedbackcompleted, $cm); $event->trigger(); return $feedbackcompleted->id; } //////////////////////////////////////////////// //////////////////////////////////////////////// //////////////////////////////////////////////// //functions to handle the pagebreaks //////////////////////////////////////////////// /** * this creates a pagebreak. * a pagebreak is a special kind of item * * @global object * @param int $feedbackid * @return int|false false if there already is a pagebreak on last position or the id of the pagebreak-item */ function feedback_create_pagebreak($feedbackid) { global $DB; // Disallow pagebreak if there's already one present in last position, or the feedback has no items. $lastposition = $DB->count_records('feedback_item', array('feedback'=>$feedbackid)); if ($lastposition == feedback_get_last_break_position($feedbackid)) { return false; } $item = new stdClass(); $item->feedback = $feedbackid; $item->template=0; $item->name = ''; $item->presentation = ''; $item->hasvalue = 0; $item->typ = 'pagebreak'; $item->position = $lastposition + 1; $item->required=0; return $DB->insert_record('feedback_item', $item); } /** * get all positions of pagebreaks in the given feedback * * @global object * @param int $feedbackid * @return array all ordered pagebreak positions */ function feedback_get_all_break_positions($feedbackid) { global $DB; $params = array('typ'=>'pagebreak', 'feedback'=>$feedbackid); $allbreaks = $DB->get_records_menu('feedback_item', $params, 'position', 'id, position'); if (!$allbreaks) { return false; } return array_values($allbreaks); } /** * get the position of the last pagebreak * * @param int $feedbackid * @return int the position of the last pagebreak */ function feedback_get_last_break_position($feedbackid) { if (!$allbreaks = feedback_get_all_break_positions($feedbackid)) { return false; } return $allbreaks[count($allbreaks) - 1]; } //////////////////////////////////////////////// //////////////////////////////////////////////// //////////////////////////////////////////////// //functions to handle the values //////////////////////////////////////////////// /** * get the value from the given item related to the given completed. * the value can come as temporary or as permanently value. the deciding is done by $tmp * * @global object * @param int $completeid * @param int $itemid * @param boolean $tmp * @return mixed the value, the type depends on plugin-definition */ function feedback_get_item_value($completedid, $itemid, $tmp = false) { global $DB; $tmpstr = $tmp ? 'tmp' : ''; $params = array('completed'=>$completedid, 'item'=>$itemid); return $DB->get_field('feedback_value'.$tmpstr, 'value', $params); } /** * compares the value of the itemid related to the completedid with the dependvalue. * this is used if a depend item is set. * the value can come as temporary or as permanently value. the deciding is done by $tmp. * * @param int $completedid * @param stdClass|int $item * @param mixed $dependvalue * @param bool $tmp * @return bool */ function feedback_compare_item_value($completedid, $item, $dependvalue, $tmp = false) { global $DB; if (is_int($item)) { $item = $DB->get_record('feedback_item', array('id' => $item)); } $dbvalue = feedback_get_item_value($completedid, $item->id, $tmp); $itemobj = feedback_get_item_class($item->typ); return $itemobj->compare_value($item, $dbvalue, $dependvalue); //true or false } /** * get the values of an item depending on the given groupid. * if the feedback is anonymous so the values are shuffled * * @global object * @global object * @param object $item * @param int $groupid * @param int $courseid * @param bool $ignore_empty if this is set true so empty values are not delivered * @return array the value-records */ function feedback_get_group_values($item, $groupid = false, $courseid = false, $ignore_empty = false) { global $CFG, $DB; //if the groupid is given? if (intval($groupid) > 0) { $params = array(); if ($ignore_empty) { $value = $DB->sql_compare_text('fbv.value'); $ignore_empty_select = "AND $value != :emptyvalue AND $value != :zerovalue"; $params += array('emptyvalue' => '', 'zerovalue' => '0'); } else { $ignore_empty_select = ""; } $query = 'SELECT fbv . * FROM {feedback_value} fbv, {feedback_completed} fbc, {groups_members} gm WHERE fbv.item = :itemid AND fbv.completed = fbc.id AND fbc.userid = gm.userid '.$ignore_empty_select.' AND gm.groupid = :groupid ORDER BY fbc.timemodified'; $params += array('itemid' => $item->id, 'groupid' => $groupid); $values = $DB->get_records_sql($query, $params); } else { $params = array(); if ($ignore_empty) { $value = $DB->sql_compare_text('value'); $ignore_empty_select = "AND $value != :emptyvalue AND $value != :zerovalue"; $params += array('emptyvalue' => '', 'zerovalue' => '0'); } else { $ignore_empty_select = ""; } if ($courseid) { $select = "item = :itemid AND course_id = :courseid ".$ignore_empty_select; $params += array('itemid' => $item->id, 'courseid' => $courseid); $values = $DB->get_records_select('feedback_value', $select, $params); } else { $select = "item = :itemid ".$ignore_empty_select; $params += array('itemid' => $item->id); $values = $DB->get_records_select('feedback_value', $select, $params); } } $params = array('id'=>$item->feedback); if ($DB->get_field('feedback', 'anonymous', $params) == FEEDBACK_ANONYMOUS_YES) { if (is_array($values)) { shuffle($values); } } return $values; } /** * check for multiple_submit = false. * if the feedback is global so the courseid must be given * * @global object * @global object * @param int $feedbackid * @param int $courseid * @return boolean true if the feedback already is submitted otherwise false */ function feedback_is_already_submitted($feedbackid, $courseid = false) { global $USER, $DB; if (!isloggedin() || isguestuser()) { return false; } $params = array('userid' => $USER->id, 'feedback' => $feedbackid); if ($courseid) { $params['courseid'] = $courseid; } return $DB->record_exists('feedback_completed', $params); } /** * get the completeds depending on the given groupid. * * @global object * @global object * @param object $feedback * @param int $groupid * @param int $courseid * @return mixed array of found completeds otherwise false */ function feedback_get_completeds_group($feedback, $groupid = false, $courseid = false) { global $CFG, $DB; if (intval($groupid) > 0) { $query = "SELECT fbc.* FROM {feedback_completed} fbc, {groups_members} gm WHERE fbc.feedback = ? AND gm.groupid = ? AND fbc.userid = gm.userid"; if ($values = $DB->get_records_sql($query, array($feedback->id, $groupid))) { return $values; } else { return false; } } else { if ($courseid) { $query = "SELECT DISTINCT fbc.* FROM {feedback_completed} fbc, {feedback_value} fbv WHERE fbc.id = fbv.completed AND fbc.feedback = ? AND fbv.course_id = ? ORDER BY random_response"; if ($values = $DB->get_records_sql($query, array($feedback->id, $courseid))) { return $values; } else { return false; } } else { if ($values = $DB->get_records('feedback_completed', array('feedback'=>$feedback->id))) { return $values; } else { return false; } } } } /** * get the count of completeds depending on the given groupid. * * @global object * @global object * @param object $feedback * @param int $groupid * @param int $courseid * @return mixed count of completeds or false */ function feedback_get_completeds_group_count($feedback, $groupid = false, $courseid = false) { global $CFG, $DB; if ($courseid > 0 AND !$groupid <= 0) { $sql = "SELECT id, COUNT(item) AS ci FROM {feedback_value} WHERE course_id = ? GROUP BY item ORDER BY ci DESC"; if ($foundrecs = $DB->get_records_sql($sql, array($courseid))) { $foundrecs = array_values($foundrecs); return $foundrecs[0]->ci; } return false; } if ($values = feedback_get_completeds_group($feedback, $groupid)) { return count($values); } else { return false; } } /** * deletes all completed-recordsets from a feedback. * all related data such as values also will be deleted * * @param stdClass|int $feedback * @param stdClass|cm_info $cm * @param stdClass $course * @return void */ function feedback_delete_all_completeds($feedback, $cm = null, $course = null) { global $DB; if (is_int($feedback)) { $feedback = $DB->get_record('feedback', array('id' => $feedback)); } if (!$completeds = $DB->get_records('feedback_completed', array('feedback' => $feedback->id))) { return; } if (!$course && !($course = $DB->get_record('course', array('id' => $feedback->course)))) { return false; } if (!$cm && !($cm = get_coursemodule_from_instance('feedback', $feedback->id))) { return false; } foreach ($completeds as $completed) { feedback_delete_completed($completed, $feedback, $cm, $course); } } /** * deletes a completed given by completedid. * all related data such values or tracking data also will be deleted * * @param int|stdClass $completed * @param stdClass $feedback * @param stdClass|cm_info $cm * @param stdClass $course * @return boolean */ function feedback_delete_completed($completed, $feedback = null, $cm = null, $course = null) { global $DB, $CFG; require_once($CFG->libdir.'/completionlib.php'); if (!isset($completed->id)) { if (!$completed = $DB->get_record('feedback_completed', array('id' => $completed))) { return false; } } if (!$feedback && !($feedback = $DB->get_record('feedback', array('id' => $completed->feedback)))) { return false; } if (!$course && !($course = $DB->get_record('course', array('id' => $feedback->course)))) { return false; } if (!$cm && !($cm = get_coursemodule_from_instance('feedback', $feedback->id))) { return false; } //first we delete all related values $DB->delete_records('feedback_value', array('completed' => $completed->id)); // Delete the completed record. $return = $DB->delete_records('feedback_completed', array('id' => $completed->id)); // Update completion state $completion = new completion_info($course); if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC && $feedback->completionsubmit) { $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid); } // Trigger event for the delete action we performed. $event = \mod_feedback\event\response_deleted::create_from_record($completed, $cm, $feedback); $event->trigger(); return $return; } //////////////////////////////////////////////// //////////////////////////////////////////////// //////////////////////////////////////////////// //functions to handle sitecourse mapping //////////////////////////////////////////////// /** * gets the feedbacks from table feedback_sitecourse_map. * this is used to show the global feedbacks on the feedback block * all feedbacks with the following criteria will be selected:
* * 1) all feedbacks which id are listed together with the courseid in sitecoursemap and
* 2) all feedbacks which not are listed in sitecoursemap * * @global object * @param int $courseid * @return array the feedback-records */ function feedback_get_feedbacks_from_sitecourse_map($courseid) { global $DB; //first get all feedbacks listed in sitecourse_map with named courseid $sql = "SELECT f.id AS id, cm.id AS cmid, f.name AS name, f.timeopen AS timeopen, f.timeclose AS timeclose FROM {feedback} f, {course_modules} cm, {feedback_sitecourse_map} sm, {modules} m WHERE f.id = cm.instance AND f.course = '".SITEID."' AND m.id = cm.module AND m.name = 'feedback' AND sm.courseid = ? AND sm.feedbackid = f.id"; if (!$feedbacks1 = $DB->get_records_sql($sql, array($courseid))) { $feedbacks1 = array(); } //second get all feedbacks not listed in sitecourse_map $feedbacks2 = array(); $sql = "SELECT f.id AS id, cm.id AS cmid, f.name AS name, f.timeopen AS timeopen, f.timeclose AS timeclose FROM {feedback} f, {course_modules} cm, {modules} m WHERE f.id = cm.instance AND f.course = '".SITEID."' AND m.id = cm.module AND m.name = 'feedback'"; if (!$allfeedbacks = $DB->get_records_sql($sql)) { $allfeedbacks = array(); } foreach ($allfeedbacks as $a) { if (!$DB->record_exists('feedback_sitecourse_map', array('feedbackid'=>$a->id))) { $feedbacks2[] = $a; } } $feedbacks = array_merge($feedbacks1, $feedbacks2); $modinfo = get_fast_modinfo(SITEID); return array_filter($feedbacks, function($f) use ($modinfo) { return ($cm = $modinfo->get_cm($f->cmid)) && $cm->uservisible; }); } /** * Gets the courses from table feedback_sitecourse_map * * @param int $feedbackid * @return array the course-records */ function feedback_get_courses_from_sitecourse_map($feedbackid) { global $DB; $sql = "SELECT c.id, c.fullname, c.shortname FROM {feedback_sitecourse_map} f, {course} c WHERE c.id = f.courseid AND f.feedbackid = ? ORDER BY c.fullname"; return $DB->get_records_sql($sql, array($feedbackid)); } /** * Updates the course mapping for the feedback * * @param stdClass $feedback * @param array $courses array of course ids */ function feedback_update_sitecourse_map($feedback, $courses) { global $DB; if (empty($courses)) { $courses = array(); } $currentmapping = $DB->get_fieldset_select('feedback_sitecourse_map', 'courseid', 'feedbackid=?', array($feedback->id)); foreach (array_diff($courses, $currentmapping) as $courseid) { $DB->insert_record('feedback_sitecourse_map', array('feedbackid' => $feedback->id, 'courseid' => $courseid)); } foreach (array_diff($currentmapping, $courses) as $courseid) { $DB->delete_records('feedback_sitecourse_map', array('feedbackid' => $feedback->id, 'courseid' => $courseid)); } // TODO MDL-53574 add events. } //////////////////////////////////////////////// //////////////////////////////////////////////// //////////////////////////////////////////////// //not relatable functions //////////////////////////////////////////////// /** * sends an email to the teachers of the course where the given feedback is placed. * * @global object * @global object * @uses FEEDBACK_ANONYMOUS_NO * @uses FORMAT_PLAIN * @param object $cm the coursemodule-record * @param object $feedback * @param object $course * @param stdClass|int $user * @param stdClass $completed record from feedback_completed if known * @return void */ function feedback_send_email($cm, $feedback, $course, $user, $completed = null) { global $CFG, $DB, $PAGE; if ($feedback->email_notification == 0) { // No need to do anything return; } if (!is_object($user)) { $user = $DB->get_record('user', array('id' => $user)); } if (isset($cm->groupmode) && empty($course->groupmodeforce)) { $groupmode = $cm->groupmode; } else { $groupmode = $course->groupmode; } if ($groupmode == SEPARATEGROUPS) { $groups = $DB->get_records_sql_menu("SELECT g.name, g.id FROM {groups} g, {groups_members} m WHERE g.courseid = ? AND g.id = m.groupid AND m.userid = ? ORDER BY name ASC", array($course->id, $user->id)); $groups = array_values($groups); $teachers = feedback_get_receivemail_users($cm->id, $groups); } else { $teachers = feedback_get_receivemail_users($cm->id); } if ($teachers) { $strfeedbacks = get_string('modulenameplural', 'feedback'); $strfeedback = get_string('modulename', 'feedback'); if ($feedback->anonymous == FEEDBACK_ANONYMOUS_NO) { $printusername = fullname($user); } else { $printusername = get_string('anonymous_user', 'feedback'); } foreach ($teachers as $teacher) { $info = new stdClass(); $info->username = $printusername; $info->feedback = format_string($feedback->name, true); $info->url = $CFG->wwwroot.'/mod/feedback/show_entries.php?'. 'id='.$cm->id.'&'. 'userid=' . $user->id; if ($completed) { $info->url .= '&showcompleted=' . $completed->id; if ($feedback->course == SITEID) { // Course where feedback was completed (for site feedbacks only). $info->url .= '&courseid=' . $completed->courseid; } } $a = array('username' => $info->username, 'feedbackname' => $feedback->name); $postsubject = get_string('feedbackcompleted', 'feedback', $a); $posttext = feedback_send_email_text($info, $course); if ($teacher->mailformat == 1) { $posthtml = feedback_send_email_html($info, $course, $cm); } else { $posthtml = ''; } $customdata = [ 'cmid' => $cm->id, 'instance' => $feedback->id, ]; if ($feedback->anonymous == FEEDBACK_ANONYMOUS_NO) { $eventdata = new \core\message\message(); $eventdata->anonymous = false; $eventdata->courseid = $course->id; $eventdata->name = 'submission'; $eventdata->component = 'mod_feedback'; $eventdata->userfrom = $user; $eventdata->userto = $teacher; $eventdata->subject = $postsubject; $eventdata->fullmessage = $posttext; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $posthtml; $eventdata->smallmessage = ''; $eventdata->courseid = $course->id; $eventdata->contexturl = $info->url; $eventdata->contexturlname = $info->feedback; // User image. $userpicture = new user_picture($user); $userpicture->size = 1; // Use f1 size. $userpicture->includetoken = $teacher->id; // Generate an out-of-session token for the user receiving the message. $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false); $eventdata->customdata = $customdata; message_send($eventdata); } else { $eventdata = new \core\message\message(); $eventdata->anonymous = true; $eventdata->courseid = $course->id; $eventdata->name = 'submission'; $eventdata->component = 'mod_feedback'; $eventdata->userfrom = $teacher; $eventdata->userto = $teacher; $eventdata->subject = $postsubject; $eventdata->fullmessage = $posttext; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $posthtml; $eventdata->smallmessage = ''; $eventdata->courseid = $course->id; $eventdata->contexturl = $info->url; $eventdata->contexturlname = $info->feedback; // Feedback icon if can be easily reachable. $customdata['notificationiconurl'] = ($cm instanceof cm_info) ? $cm->get_icon_url()->out() : ''; $eventdata->customdata = $customdata; message_send($eventdata); } } } } /** * sends an email to the teachers of the course where the given feedback is placed. * * @global object * @uses FORMAT_PLAIN * @param object $cm the coursemodule-record * @param object $feedback * @param object $course * @return void */ function feedback_send_email_anonym($cm, $feedback, $course) { global $CFG; if ($feedback->email_notification == 0) { // No need to do anything return; } $teachers = feedback_get_receivemail_users($cm->id); if ($teachers) { $strfeedbacks = get_string('modulenameplural', 'feedback'); $strfeedback = get_string('modulename', 'feedback'); $printusername = get_string('anonymous_user', 'feedback'); foreach ($teachers as $teacher) { $info = new stdClass(); $info->username = $printusername; $info->feedback = format_string($feedback->name, true); $info->url = $CFG->wwwroot.'/mod/feedback/show_entries.php?id=' . $cm->id; $a = array('username' => $info->username, 'feedbackname' => $feedback->name); $postsubject = get_string('feedbackcompleted', 'feedback', $a); $posttext = feedback_send_email_text($info, $course); if ($teacher->mailformat == 1) { $posthtml = feedback_send_email_html($info, $course, $cm); } else { $posthtml = ''; } $eventdata = new \core\message\message(); $eventdata->anonymous = true; $eventdata->courseid = $course->id; $eventdata->name = 'submission'; $eventdata->component = 'mod_feedback'; $eventdata->userfrom = $teacher; $eventdata->userto = $teacher; $eventdata->subject = $postsubject; $eventdata->fullmessage = $posttext; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $posthtml; $eventdata->smallmessage = ''; $eventdata->courseid = $course->id; $eventdata->contexturl = $info->url; $eventdata->contexturlname = $info->feedback; $eventdata->customdata = [ 'cmid' => $cm->id, 'instance' => $feedback->id, 'notificationiconurl' => ($cm instanceof cm_info) ? $cm->get_icon_url()->out() : '', // Performance wise. ]; message_send($eventdata); } } } /** * send the text-part of the email * * @param object $info includes some infos about the feedback you want to send * @param object $course * @return string the text you want to post */ function feedback_send_email_text($info, $course) { $coursecontext = context_course::instance($course->id); $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext)); $posttext = $courseshortname.' -> '.get_string('modulenameplural', 'feedback').' -> '. $info->feedback."\n"; $posttext .= '---------------------------------------------------------------------'."\n"; $posttext .= get_string("emailteachermail", "feedback", $info)."\n"; $posttext .= '---------------------------------------------------------------------'."\n"; return $posttext; } /** * send the html-part of the email * * @global object * @param object $info includes some infos about the feedback you want to send * @param object $course * @return string the text you want to post */ function feedback_send_email_html($info, $course, $cm) { global $CFG; $coursecontext = context_course::instance($course->id); $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext)); $course_url = $CFG->wwwroot.'/course/view.php?id='.$course->id; $feedback_all_url = $CFG->wwwroot.'/mod/feedback/index.php?id='.$course->id; $feedback_url = $CFG->wwwroot.'/mod/feedback/view.php?id='.$cm->id; $posthtml = '

'. ''.$courseshortname.' ->'. ''.get_string('modulenameplural', 'feedback').' ->'. ''.$info->feedback.'

'; $posthtml .= '
'; $posthtml .= '

'.get_string('emailteachermailhtml', 'feedback', $info).'

'; $posthtml .= '

'; return $posthtml; } /** * @param string $url * @return string */ function feedback_encode_target_url($url) { if (strpos($url, '?')) { list($part1, $part2) = explode('?', $url, 2); //maximal 2 parts return $part1 . '?' . htmlentities($part2, ENT_COMPAT); } else { return $url; } } /** * Adds module specific settings to the settings block * * @param settings_navigation $settings The settings navigation object * @param navigation_node $feedbacknode The node to add module settings to */ function feedback_extend_settings_navigation(settings_navigation $settings, navigation_node $feedbacknode) { $hassecondary = $settings->get_page()->has_secondary_navigation(); if (!$context = context_module::instance($settings->get_page()->cm->id, IGNORE_MISSING)) { throw new \moodle_exception('badcontext'); } if (has_capability('mod/feedback:edititems', $context)) { $feedbacknode->add(get_string('questions', 'feedback'), new moodle_url('/mod/feedback/edit.php', ['id' => $settings->get_page()->cm->id]), navigation_node::TYPE_CUSTOM, null, 'questionnode'); $feedbacknode->add(get_string('templates', 'feedback'), new moodle_url('/mod/feedback/manage_templates.php', ['id' => $settings->get_page()->cm->id]), navigation_node::TYPE_CUSTOM, null, 'templatenode'); } if (has_capability('mod/feedback:mapcourse', $context) && $settings->get_page()->course->id == SITEID) { $feedbacknode->add(get_string('mappedcourses', 'feedback'), new moodle_url('/mod/feedback/mapcourse.php', ['id' => $settings->get_page()->cm->id]), navigation_node::TYPE_CUSTOM, null, 'mapcourse'); } $feedback = $settings->get_page()->activityrecord; if ($feedback->course == SITEID) { $analysisnode = navigation_node::create(get_string('analysis', 'feedback'), new moodle_url('/mod/feedback/analysis_course.php', ['id' => $settings->get_page()->cm->id]), navigation_node::TYPE_CUSTOM, null, 'feedbackanalysis'); } else { $analysisnode = navigation_node::create(get_string('analysis', 'feedback'), new moodle_url('/mod/feedback/analysis.php', ['id' => $settings->get_page()->cm->id]), navigation_node::TYPE_CUSTOM, null, 'feedbackanalysis'); } if (has_capability('mod/feedback:viewreports', $context)) { if (manager::can_see_others_in_groups($settings->get_page()->cm)) { $feedbacknode->add_node($analysisnode); $feedbacknode->add(get_string(($hassecondary ? 'responses' : 'show_entries'), 'feedback'), new moodle_url('/mod/feedback/show_entries.php', ['id' => $settings->get_page()->cm->id]), navigation_node::TYPE_CUSTOM, null, 'responses'); } } else { $feedbackcompletion = new mod_feedback_completion($feedback, $context, $settings->get_page()->course->id); if ($feedbackcompletion->can_view_analysis()) { $feedbacknode->add_node($analysisnode); } } } function feedback_init_feedback_session() { //initialize the feedback-Session - not nice at all!! global $SESSION; if (!empty($SESSION)) { if (!isset($SESSION->feedback) OR !is_object($SESSION->feedback)) { $SESSION->feedback = new stdClass(); } } } /** * 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 feedback_page_type_list($pagetype, $parentcontext, $currentcontext) { $module_pagetype = array('mod-feedback-*'=>get_string('page-mod-feedback-x', 'feedback')); return $module_pagetype; } /** * Move save the items of the given $feedback in the order of $itemlist. * @param array $itemlist a list with item ids * @param stdClass $feedback * @return bool true if success */ function feedback_ajax_saveitemorder($itemlist, $feedback) { global $DB; $result = true; $position = 0; foreach ($itemlist as $itemid) { $position++; $result = $result && $DB->set_field('feedback_item', 'position', $position, array('id'=>$itemid, 'feedback'=>$feedback->id)); } return $result; } /** * Checks if current user is able to view feedback on this course. * * @param stdClass $feedback * @param context_module $context * @param int $courseid * @return bool */ function feedback_can_view_analysis($feedback, $context, $courseid = false) { if (has_capability('mod/feedback:viewreports', $context)) { return true; } if (intval($feedback->publish_stats) != 1 || !has_capability('mod/feedback:viewanalysepage', $context)) { return false; } if (!isloggedin() || isguestuser()) { // There is no tracking for the guests, assume that they can view analysis if condition above is satisfied. return $feedback->course == SITEID; } return feedback_is_already_submitted($feedback->id, $courseid); } /** * Get icon mapping for font-awesome. */ function mod_feedback_get_fontawesome_icon_map() { return [ 'mod_feedback:notrequired' => 'fa-circle-question', 'mod_feedback:required' => 'fa-circle-exclamation', ]; } /** * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not * @since Moodle 3.3 */ function feedback_check_updates_since(cm_info $cm, $from, $filter = array()) { global $DB, $USER, $CFG; $updates = course_check_module_updates_since($cm, $from, array(), $filter); // Check for new attempts. $updates->attemptsfinished = (object) array('updated' => false); $updates->attemptsunfinished = (object) array('updated' => false); $select = 'feedback = ? AND userid = ? AND timemodified > ?'; $params = array($cm->instance, $USER->id, $from); $attemptsfinished = $DB->get_records_select('feedback_completed', $select, $params, '', 'id'); if (!empty($attemptsfinished)) { $updates->attemptsfinished->updated = true; $updates->attemptsfinished->itemids = array_keys($attemptsfinished); } $attemptsunfinished = $DB->get_records_select('feedback_completedtmp', $select, $params, '', 'id'); if (!empty($attemptsunfinished)) { $updates->attemptsunfinished->updated = true; $updates->attemptsunfinished->itemids = array_keys($attemptsunfinished); } // Now, teachers should see other students updates. if (has_capability('mod/feedback:viewreports', $cm->context)) { $select = 'feedback = ? AND timemodified > ?'; $params = array($cm->instance, $from); if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); if (empty($groupusers)) { return $updates; } list($insql, $inparams) = $DB->get_in_or_equal($groupusers); $select .= ' AND userid ' . $insql; $params = array_merge($params, $inparams); } $updates->userattemptsfinished = (object) array('updated' => false); $attemptsfinished = $DB->get_records_select('feedback_completed', $select, $params, '', 'id'); if (!empty($attemptsfinished)) { $updates->userattemptsfinished->updated = true; $updates->userattemptsfinished->itemids = array_keys($attemptsfinished); } $updates->userattemptsunfinished = (object) array('updated' => false); $attemptsunfinished = $DB->get_records_select('feedback_completedtmp', $select, $params, '', 'id'); if (!empty($attemptsunfinished)) { $updates->userattemptsunfinished->updated = true; $updates->userattemptsunfinished->itemids = array_keys($attemptsunfinished); } } return $updates; } /** * This function receives a calendar event and returns the action associated with it, or null if there is none. * * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event * is not displayed on the block. * * @param calendar_event $event * @param \core_calendar\action_factory $factory * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return \core_calendar\local\event\entities\action_interface|null */ function mod_feedback_core_calendar_provide_event_action(calendar_event $event, \core_calendar\action_factory $factory, int $userid = 0) { global $USER; if (empty($userid)) { $userid = $USER->id; } $cm = get_fast_modinfo($event->courseid, $userid)->instances['feedback'][$event->instance]; if (!$cm->uservisible) { // The module is not visible to the user for any reason. return null; } $completion = new \completion_info($cm->get_course()); $completiondata = $completion->get_data($cm, false, $userid); if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { return null; } $feedbackcompletion = new mod_feedback_completion(null, $cm, 0, false, null, null, $userid); if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) { // Feedback is already closed, do not display it even if it was never submitted. return null; } if (!$feedbackcompletion->can_complete()) { // The user can't complete the feedback so there is no action for them. return null; } // The feedback is actionable if it does not have timeopen or timeopen is in the past. $actionable = $feedbackcompletion->is_open(); if ($actionable && $feedbackcompletion->is_already_submitted(false)) { // There is no need to display anything if the user has already submitted the feedback. return null; } return $factory->create_instance( get_string('answerquestions', 'feedback'), new \moodle_url('/mod/feedback/view.php', ['id' => $cm->id]), 1, $actionable ); } /** * Add a get_coursemodule_info function in case any feedback type wants to add 'extra' information * for the course (see resource). * * Given a course_module object, this function returns any "extra" information that may be needed * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. * * @param stdClass $coursemodule The coursemodule object (record). * @return cached_cm_info An object on information that the courses * will know about (most noticeably, an icon). */ function feedback_get_coursemodule_info($coursemodule) { global $DB; $dbparams = ['id' => $coursemodule->instance]; $fields = 'id, name, intro, introformat, completionsubmit, timeopen, timeclose, anonymous'; if (!$feedback = $DB->get_record('feedback', $dbparams, $fields)) { return false; } $result = new cached_cm_info(); $result->name = $feedback->name; if ($coursemodule->showdescription) { // Convert intro to html. Do not filter cached version, filters run at display time. $result->content = format_module_intro('feedback', $feedback, $coursemodule->id, false); } // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { $result->customdata['customcompletionrules']['completionsubmit'] = $feedback->completionsubmit; } // Populate some other values that can be used in calendar or on dashboard. if ($feedback->timeopen) { $result->customdata['timeopen'] = $feedback->timeopen; } if ($feedback->timeclose) { $result->customdata['timeclose'] = $feedback->timeclose; } if ($feedback->anonymous) { $result->customdata['anonymous'] = $feedback->anonymous; } return $result; } /** * Callback which returns human-readable strings describing the active completion custom rules for the module instance. * * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] * @return array $descriptions the array of descriptions for the custom rules. */ function mod_feedback_get_completion_active_rule_descriptions($cm) { // Values will be present in cm_info, and we assume these are up to date. if (empty($cm->customdata['customcompletionrules']) || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { return []; } $descriptions = []; foreach ($cm->customdata['customcompletionrules'] as $key => $val) { switch ($key) { case 'completionsubmit': if (!empty($val)) { $descriptions[] = get_string('completionsubmit', 'feedback'); } break; default: break; } } return $descriptions; } /** * This function calculates the minimum and maximum cutoff values for the timestart of * the given event. * * It will return an array with two values, the first being the minimum cutoff value and * the second being the maximum cutoff value. Either or both values can be null, which * indicates there is no minimum or maximum, respectively. * * If a cutoff is required then the function must return an array containing the cutoff * timestamp and error string to display to the user if the cutoff value is violated. * * A minimum and maximum cutoff return value will look like: * [ * [1505704373, 'The due date must be after the sbumission start date'], * [1506741172, 'The due date must be before the cutoff date'] * ] * * @param calendar_event $event The calendar event to get the time range for * @param stdClass $instance The module instance to get the range from * @return array */ function mod_feedback_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) { $mindate = null; $maxdate = null; if ($event->eventtype == FEEDBACK_EVENT_TYPE_OPEN) { // The start time of the open event can't be equal to or after the // close time of the choice activity. if (!empty($instance->timeclose)) { $maxdate = [ $instance->timeclose, get_string('openafterclose', 'feedback') ]; } } else if ($event->eventtype == FEEDBACK_EVENT_TYPE_CLOSE) { // The start time of the close event can't be equal to or earlier than the // open time of the choice activity. if (!empty($instance->timeopen)) { $mindate = [ $instance->timeopen, get_string('closebeforeopen', 'feedback') ]; } } return [$mindate, $maxdate]; } /** * This function will update the feedback module according to the * event that has been modified. * * It will set the timeopen or timeclose value of the feedback instance * according to the type of event provided. * * @throws \moodle_exception * @param \calendar_event $event * @param stdClass $feedback The module instance to get the range from */ function mod_feedback_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $feedback) { global $CFG, $DB; if (empty($event->instance) || $event->modulename != 'feedback') { return; } if ($event->instance != $feedback->id) { return; } if (!in_array($event->eventtype, [FEEDBACK_EVENT_TYPE_OPEN, FEEDBACK_EVENT_TYPE_CLOSE])) { return; } $courseid = $event->courseid; $modulename = $event->modulename; $instanceid = $event->instance; $modified = false; $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; $context = context_module::instance($coursemodule->id); // The user does not have the capability to modify this activity. if (!has_capability('moodle/course:manageactivities', $context)) { return; } if ($event->eventtype == FEEDBACK_EVENT_TYPE_OPEN) { // If the event is for the feedback activity opening then we should // set the start time of the feedback activity to be the new start // time of the event. if ($feedback->timeopen != $event->timestart) { $feedback->timeopen = $event->timestart; $feedback->timemodified = time(); $modified = true; } } else if ($event->eventtype == FEEDBACK_EVENT_TYPE_CLOSE) { // If the event is for the feedback activity closing then we should // set the end time of the feedback activity to be the new start // time of the event. if ($feedback->timeclose != $event->timestart) { $feedback->timeclose = $event->timestart; $modified = true; } } if ($modified) { $feedback->timemodified = time(); $DB->update_record('feedback', $feedback); $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); $event->trigger(); } } /** * Callback to fetch the activity event type lang string. * * @param string $eventtype The event type. * @return lang_string The event type lang string. */ function mod_feedback_core_calendar_get_event_action_string(string $eventtype): string { $modulename = get_string('modulename', 'feedback'); switch ($eventtype) { case FEEDBACK_EVENT_TYPE_OPEN: $identifier = 'calendarstart'; break; case FEEDBACK_EVENT_TYPE_CLOSE: $identifier = 'calendarend'; break; default: return get_string('requiresaction', 'calendar', $modulename); } return get_string($identifier, 'feedback', $modulename); } PKB\P߭ version.phpnu[. /** * Version details * * @package block_feedback * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'block_feedback'; // Full name of the plugin (used for diagnostics) $plugin->dependencies = ['mod_feedback' => 2025040800]; PKB\Ashow_entries.phpnu[. /** * print the single entries * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ use mod_feedback\manager; require_once("../../config.php"); require_once("lib.php"); //////////////////////////////////////////////////////// //get the params //////////////////////////////////////////////////////// $id = required_param('id', PARAM_INT); $userid = optional_param('userid', false, PARAM_INT); $showcompleted = optional_param('showcompleted', false, PARAM_INT); $deleteid = optional_param('delete', null, PARAM_INT); $courseid = optional_param('courseid', null, PARAM_INT); //////////////////////////////////////////////////////// //get the objects //////////////////////////////////////////////////////// list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); $baseurl = new moodle_url('/mod/feedback/show_entries.php', array('id' => $cm->id)); $PAGE->set_url(new moodle_url($baseurl, array('userid' => $userid, 'showcompleted' => $showcompleted, 'delete' => $deleteid))); $context = context_module::instance($cm->id); require_login($course, true, $cm); $feedback = $PAGE->activityrecord; require_capability('mod/feedback:viewreports', $context); $actionbar = new \mod_feedback\output\responses_action_bar($cm->id, $baseurl); if ($deleteid) { // This is a request to delete a reponse. require_capability('mod/feedback:deletesubmissions', $context); require_sesskey(); $feedbackstructure = new mod_feedback_completion($feedback, $cm, 0, true, $deleteid); feedback_delete_completed($feedbackstructure->get_completed(), $feedback, $cm); redirect($baseurl); } else if ($showcompleted || $userid) { // Viewing individual response. $feedbackstructure = new mod_feedback_completion($feedback, $cm, 0, true, $showcompleted, $userid); } else { // Viewing list of reponses. $feedbackstructure = new mod_feedback_structure($feedback, $cm, $courseid); } $responsestable = new mod_feedback_responses_table($feedbackstructure); $anonresponsestable = new mod_feedback_responses_anon_table($feedbackstructure); if ($responsestable->is_downloading()) { $responsestable->download(); } if ($anonresponsestable->is_downloading()) { $anonresponsestable->download(); } // Process course select form. $courseselectform = new mod_feedback_course_select_form($baseurl, $feedbackstructure, $feedback->course == SITEID); if ($data = $courseselectform->get_data()) { redirect(new moodle_url($baseurl, ['courseid' => $data->courseid])); } // Print the page header. navigation_node::override_active_url($baseurl); $PAGE->set_heading($course->fullname); /** @var \mod_feedback\output\renderer $renderer */ $renderer = $PAGE->get_renderer('mod_feedback'); $renderer->set_title( [format_string($feedback->name), format_string($course->fullname)], get_string('responses', 'feedback') ); $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); echo $OUTPUT->header(); echo $renderer->main_action_bar($actionbar); echo $OUTPUT->heading(get_string('show_entries', 'mod_feedback'), 3); /// Print the main part of the page /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// if ($userid || $showcompleted) { // Print the response of the given user. $completedrecord = $feedbackstructure->get_completed(); if ($userid) { $usr = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); $responsetitle = userdate($completedrecord->timemodified) . ' (' . fullname($usr) . ')'; } else { $responsetitle = get_string('response_nr', 'feedback') . ': ' . $completedrecord->random_response . ' (' . get_string('anonymous', 'feedback') . ')'; } echo $OUTPUT->heading($responsetitle, 4); $form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_VIEW_RESPONSE, $feedbackstructure, 'feedback_viewresponse_form'); $form->display(); list($prevresponseurl, $returnurl, $nextresponseurl) = $userid ? $responsestable->get_reponse_navigation_links($completedrecord) : $anonresponsestable->get_reponse_navigation_links($completedrecord); echo html_writer::start_div('response_navigation'); $responsenavigation = [ 'col1content' => '', 'col2content' => html_writer::link($returnurl, get_string('back'), ['class' => 'back_to_list']), 'col3content' => '', ]; if ($prevresponseurl) { $responsenavigation['col1content'] = html_writer::link($prevresponseurl, get_string('prev'), ['class' => 'prev_response']); } if ($nextresponseurl) { $responsenavigation['col3content'] = html_writer::link($nextresponseurl, get_string('next'), ['class' => 'next_response']); } echo $OUTPUT->render_from_template('core/columns-1to1to1', $responsenavigation); echo html_writer::end_div(); } else { // Print the list of responses. $courseselectform->display(); if (!manager::can_see_others_in_groups($feedbackstructure->get_cm())) { echo $OUTPUT->notification(get_string('notingroup')); echo $OUTPUT->footer(); exit(); } // Show non-anonymous responses (always retrieve them even if current feedback is anonymous). $totalrows = $responsestable->get_total_responses_count(); if (!$feedbackstructure->is_anonymous() || $totalrows) { echo $OUTPUT->heading(get_string('non_anonymous_entries', 'feedback', $totalrows), 4); $responsestable->display(); } // Show anonymous responses (always retrieve them even if current feedback is not anonymous). $feedbackstructure->shuffle_anonym_responses(); $totalrows = $anonresponsestable->get_total_responses_count(); if ($feedbackstructure->is_anonymous() || $totalrows) { echo $OUTPUT->heading(get_string('anonymous_entries', 'feedback', $totalrows), 4); $anonresponsestable->display(); } } // Finish the page. echo $OUTPUT->footer(); PKB\;މ export.phpnu[. /** * prints the form to export the items as xml-file * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); // get parameters $id = required_param('id', PARAM_INT); $action = optional_param('action', false, PARAM_ALPHA); $url = new moodle_url('/mod/feedback/export.php', array('id'=>$id)); if ($action !== false) { $url->param('action', $action); } $PAGE->set_url($url); if (! $cm = get_coursemodule_from_id('feedback', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", array("id"=>$cm->course))) { throw new \moodle_exception('coursemisconf'); } if (! $feedback = $DB->get_record("feedback", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidcoursemodule'); } $context = context_module::instance($cm->id); require_login($course, true, $cm); require_capability('mod/feedback:edititems', $context); if ($action == 'exportfile') { if (!$exportdata = feedback_get_xml_data($feedback->id)) { throw new \moodle_exception('nodata'); } @feedback_send_xml_data($exportdata, 'feedback_'.$feedback->id.'.xml'); exit; } redirect('view.php?id='.$id); exit; function feedback_get_xml_data($feedbackid) { global $DB; $space = ' '; //get all items of the feedback if (!$items = $DB->get_records('feedback_item', array('feedback'=>$feedbackid), 'position')) { return false; } //writing the header of the xml file including the charset of the currrent used language $data = ''."\n"; $data .= ''."\n"; $data .= $space.''."\n"; //writing all the items foreach ($items as $item) { //start of item $data .= $space.$space.''."\n"; //start of itemid $data .= $space.$space.$space.''."\n"; //start of CDATA $data .= $space.$space.$space.$space.'id; //end of CDATA $data .= ']]>'."\n"; //end of itemid $data .= $space.$space.$space.''."\n"; //start of itemtext $data .= $space.$space.$space.''."\n"; //start of CDATA $data .= $space.$space.$space.$space.'name; //end of CDATA $data .= ']]>'."\n"; //end of itemtext $data .= $space.$space.$space.''."\n"; //start of itemtext $data .= $space.$space.$space.''."\n"; //start of CDATA $data .= $space.$space.$space.$space.'label; //end of CDATA $data .= ']]>'."\n"; //end of itemtext $data .= $space.$space.$space.''."\n"; //start of presentation $data .= $space.$space.$space.''."\n"; //start of CDATA $data .= $space.$space.$space.$space.'presentation; //end of CDATA $data .= ']]>'."\n"; //end of presentation $data .= $space.$space.$space.''."\n"; //start of options $data .= $space.$space.$space.''."\n"; //start of CDATA $data .= $space.$space.$space.$space.'options; //end of CDATA $data .= ']]>'."\n"; //end of options $data .= $space.$space.$space.''."\n"; //start of dependitem $data .= $space.$space.$space.''."\n"; //start of CDATA $data .= $space.$space.$space.$space.'dependitem; //end of CDATA $data .= ']]>'."\n"; //end of dependitem $data .= $space.$space.$space.''."\n"; //start of dependvalue $data .= $space.$space.$space.''."\n"; //start of CDATA $data .= $space.$space.$space.$space.'dependvalue; //end of CDATA $data .= ']]>'."\n"; //end of dependvalue $data .= $space.$space.$space.''."\n"; //end of item $data .= $space.$space.''."\n"; } //writing the footer of the xml file $data .= $space.''."\n"; $data .= ''."\n"; return $data; } function feedback_send_xml_data($data, $filename) { @header('Content-Type: application/xml; charset=UTF-8'); @header('Content-Disposition: attachment; filename="'.$filename.'"'); print($data); } PKB\] ] manage_templates.phpnu[. /** * Manage the various templates available * * @author Peter Dias * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id', PARAM_INT); $templateid = optional_param('deletetemplate', 0, PARAM_INT); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); $context = context_module::instance($cm->id); require_login($course, true, $cm); require_capability('mod/feedback:edititems', $context); $feedback = $PAGE->activityrecord; $systemcontext = context_system::instance(); $params = ['id' => $id]; $url = new moodle_url('/mod/feedback/manage_templates.php', $params); $PAGE->set_url($url); $PAGE->set_heading($course->fullname); /** @var \mod_feedback\output\renderer $renderer */ $renderer = $PAGE->get_renderer('mod_feedback'); $renderer->set_title( [format_string($feedback->name), format_string($course->fullname)], get_string('templates', 'feedback') ); $PAGE->add_body_class('limitedwidth'); // Process template deletion. if ($templateid) { require_sesskey(); require_capability('mod/feedback:deletetemplate', $context); $template = $DB->get_record('feedback_template', ['id' => $templateid], '*', MUST_EXIST); if ($template->ispublic) { require_capability('mod/feedback:createpublictemplate', $systemcontext); require_capability('mod/feedback:deletetemplate', $systemcontext); } feedback_delete_template($template); $successurl = new moodle_url('/mod/feedback/manage_templates.php', ['id' => $id]); redirect($url, get_string('template_deleted', 'feedback'), null, \core\output\notification::NOTIFY_SUCCESS); } $PAGE->activityheader->set_attrs([ "hidecompletion" => true, "description" => '' ]); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('templates', 'mod_feedback')); // First we get the course templates. $templates = feedback_get_template_list($course, 'own'); echo $OUTPUT->box_start('coursetemplates'); echo $OUTPUT->heading(get_string('coursetemplates', 'mod_feedback'), 3); $baseurl = new moodle_url('/mod/feedback/use_templ.php', $params); $tablecourse = new mod_feedback_templates_table('feedback_template_course_table', $baseurl); $tablecourse->display($templates); echo $OUTPUT->box_end(); $templates = feedback_get_template_list($course, 'public'); echo $OUTPUT->box_start('publictemplates'); echo $OUTPUT->heading(get_string('sitetemplates', 'mod_feedback'), 3); $tablepublic = new mod_feedback_templates_table('feedback_template_public_table', $baseurl); $tablepublic->display($templates); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); PKB\/bM!!analysis_course.phpnu[. /** * shows an analysed view of a feedback on the mainsite * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id', PARAM_INT); //the POST dominated the GET $courseitemfilter = optional_param('courseitemfilter', '0', PARAM_INT); $courseitemfiltertyp = optional_param('courseitemfiltertyp', '0', PARAM_ALPHANUM); $courseid = optional_param('courseid', false, PARAM_INT); $url = new moodle_url('/mod/feedback/analysis_course.php', array('id'=>$id)); navigation_node::override_active_url($url); if ($courseid !== false) { $url->param('courseid', $courseid); } if ($courseitemfilter !== '0') { $url->param('courseitemfilter', $courseitemfilter); } if ($courseitemfiltertyp !== '0') { $url->param('courseitemfiltertyp', $courseitemfiltertyp); } $PAGE->set_url($url); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); $context = context_module::instance($cm->id); require_course_login($course, true, $cm); $feedback = $PAGE->activityrecord; if (!($feedback->publish_stats OR has_capability('mod/feedback:viewreports', $context))) { throw new \moodle_exception('error'); } $feedbackstructure = new mod_feedback_structure($feedback, $PAGE->cm, $courseid); // Process course select form. $courseselectform = new mod_feedback_course_select_form($url, $feedbackstructure); if ($data = $courseselectform->get_data()) { redirect(new moodle_url($url, ['courseid' => $data->courseid])); } /// Print the page header $strfeedbacks = get_string("modulenameplural", "feedback"); $strfeedback = get_string("modulename", "feedback"); $PAGE->set_heading($course->fullname); $PAGE->set_title($feedback->name); echo $OUTPUT->header(); if (!$PAGE->has_secondary_navigation()) { echo $OUTPUT->heading(format_string($feedback->name)); } //get the groupid //lstgroupid is the choosen id $mygroupid = false; $courseselectform->display(); // Button "Export to excel". if (has_capability('mod/feedback:viewreports', $context) && $feedbackstructure->get_items()) { echo $OUTPUT->container_start('form-buttons'); $aurl = new moodle_url('/mod/feedback/analysis_to_excel.php', ['sesskey' => sesskey(), 'id' => $id, 'courseid' => (int)$courseid]); echo $OUTPUT->single_button($aurl, get_string('export_to_excel', 'feedback')); echo $OUTPUT->container_end(); } // Show the summary. $summary = new mod_feedback\output\summary($feedbackstructure); echo $OUTPUT->render_from_template('mod_feedback/summary', $summary->export_for_template($OUTPUT)); // Get the items of the feedback. $items = $feedbackstructure->get_items(true); if ($courseitemfilter > 0) { $sumvalue = 'SUM(' . $DB->sql_cast_char2real('value', true) . ')'; $sql = "SELECT fv.course_id, c.shortname, $sumvalue AS sumvalue, COUNT(value) as countvalue FROM {feedback_value} fv, {course} c, {feedback_item} fi WHERE fv.course_id = c.id AND fi.id = fv.item AND fi.typ = ? AND fv.item = ? GROUP BY course_id, shortname ORDER BY sumvalue desc"; if ($courses = $DB->get_records_sql($sql, array($courseitemfiltertyp, $courseitemfilter))) { $item = $DB->get_record('feedback_item', array('id'=>$courseitemfilter)); echo '

'.$item->name.'

'; echo '
'; echo ''; echo ''; foreach ($courses as $c) { $coursecontext = context_course::instance($c->course_id); $shortname = format_string($c->shortname, true, array('context' => $coursecontext)); echo ''; echo ''; echo ''; echo ''; } echo '
CourseAverage
'.$shortname.''; echo format_float(($c->sumvalue / $c->countvalue), 2); echo '
'; } else { echo '

'.get_string('noresults').'

'; } echo '

'; echo get_string('back'); echo '

'; } else { // Print the items in an analysed form. foreach ($items as $item) { echo ''; $itemobj = feedback_get_item_class($item->typ); $printnr = ($feedback->autonumbering && $item->itemnr) ? ($item->itemnr . '.') : ''; $itemobj->print_analysed($item, $printnr, $mygroupid, $feedbackstructure->get_courseid()); if (preg_match('/rated$/i', $item->typ)) { $url = new moodle_url('/mod/feedback/analysis_course.php', array('id' => $id, 'courseitemfilter' => $item->id, 'courseitemfiltertyp' => $item->typ)); $anker = html_writer::link($url, get_string('sort_by_course', 'feedback')); echo ''; } echo '
'.$anker.'
'; } } echo $OUTPUT->footer(); PKB\]:}classes/external.phpnu[. use mod_feedback\external\feedback_summary_exporter; use mod_feedback\external\feedback_completedtmp_exporter; use mod_feedback\external\feedback_item_exporter; use mod_feedback\external\feedback_valuetmp_exporter; use mod_feedback\external\feedback_value_exporter; use mod_feedback\external\feedback_completed_exporter; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util; /** * Feedback external functions * * @package mod_feedback * @category external * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.3 */ class mod_feedback_external extends external_api { /** * Describes the parameters for get_feedbacks_by_courses. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_feedbacks_by_courses_parameters() { return new external_function_parameters ( array( 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, array() ), ) ); } /** * Returns a list of feedbacks in a provided list of courses. * If no list is provided all feedbacks that the user can view will be returned. * * @param array $courseids course ids * @return array of warnings and feedbacks * @since Moodle 3.3 */ public static function get_feedbacks_by_courses($courseids = array()) { global $PAGE; $warnings = array(); $returnedfeedbacks = array(); $params = array( 'courseids' => $courseids, ); $params = self::validate_parameters(self::get_feedbacks_by_courses_parameters(), $params); $mycourses = array(); if (empty($params['courseids'])) { $mycourses = enrol_get_my_courses(); $params['courseids'] = array_keys($mycourses); } // Ensure there are courseids to loop through. if (!empty($params['courseids'])) { list($courses, $warnings) = util::validate_courses($params['courseids'], $mycourses); $output = $PAGE->get_renderer('core'); // Get the feedbacks in this course, this function checks users visibility permissions. // We can avoid then additional validate_context calls. $feedbacks = get_all_instances_in_courses("feedback", $courses); foreach ($feedbacks as $feedback) { $context = context_module::instance($feedback->coursemodule); // Remove fields that are not from the feedback (added by get_all_instances_in_courses). unset($feedback->coursemodule, $feedback->context, $feedback->visible, $feedback->section, $feedback->groupmode, $feedback->groupingid); // Check permissions. if (!has_capability('mod/feedback:edititems', $context)) { // Don't return the optional properties. $properties = feedback_summary_exporter::properties_definition(); foreach ($properties as $property => $config) { if (!empty($config['optional'])) { unset($feedback->{$property}); } } } $exporter = new feedback_summary_exporter($feedback, array('context' => $context)); $returnedfeedbacks[] = $exporter->export($output); } } $result = array( 'feedbacks' => $returnedfeedbacks, 'warnings' => $warnings ); return $result; } /** * Describes the get_feedbacks_by_courses return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_feedbacks_by_courses_returns() { return new external_single_structure( array( 'feedbacks' => new external_multiple_structure( feedback_summary_exporter::get_read_structure() ), 'warnings' => new external_warnings(), ) ); } /** * Utility function for validating a feedback. * * @param int $feedbackid feedback instance id * @param int $courseid courseid course where user completes the feedback (for site feedbacks only) * @return array containing the feedback, feedback course, context, course module and the course where is being completed. * @throws moodle_exception * @since Moodle 3.3 */ protected static function validate_feedback($feedbackid, $courseid = 0) { global $DB, $USER; // Request and permission validation. $feedback = $DB->get_record('feedback', array('id' => $feedbackid), '*', MUST_EXIST); list($feedbackcourse, $cm) = get_course_and_cm_from_instance($feedback, 'feedback'); $context = context_module::instance($cm->id); self::validate_context($context); // Set default completion course. $completioncourse = (object) array('id' => 0); if ($feedbackcourse->id == SITEID && $courseid) { $completioncourse = get_course($courseid); self::validate_context(context_course::instance($courseid)); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $courseid); if (!$feedbackcompletion->check_course_is_mapped()) { throw new moodle_exception('cannotaccess', 'mod_feedback'); } } return array($feedback, $feedbackcourse, $cm, $context, $completioncourse); } /** * Utility function for validating access to feedback. * * @param stdClass $feedback feedback object * @param stdClass $course course where user completes the feedback (for site feedbacks only) * @param stdClass $cm course module * @param stdClass $context context object * @throws moodle_exception * @return mod_feedback_completion feedback completion instance * @since Moodle 3.3 */ protected static function validate_feedback_access($feedback, $course, $cm, $context, $checksubmit = false) { $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id); if (!$feedbackcompletion->can_complete()) { throw new required_capability_exception($context, 'mod/feedback:complete', 'nopermission', ''); } if (!$feedbackcompletion->is_open()) { throw new moodle_exception('feedback_is_not_open', 'feedback'); } if ($feedbackcompletion->is_empty()) { throw new moodle_exception('no_items_available_yet', 'feedback'); } if ($checksubmit && !$feedbackcompletion->can_submit()) { throw new moodle_exception('this_feedback_is_already_submitted', 'feedback'); } return $feedbackcompletion; } /** * Describes the parameters for get_feedback_access_information. * * @return external_external_function_parameters * @since Moodle 3.3 */ public static function get_feedback_access_information_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Return access information for a given feedback. * * @param int $feedbackid feedback instance id * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and the access information * @since Moodle 3.3 * @throws moodle_exception */ public static function get_feedback_access_information($feedbackid, $courseid = 0) { global $PAGE; $params = array( 'feedbackid' => $feedbackid, 'courseid' => $courseid, ); $params = self::validate_parameters(self::get_feedback_access_information_parameters(), $params); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); $result = array(); // Capabilities first. $result['canviewanalysis'] = $feedbackcompletion->can_view_analysis(); $result['cancomplete'] = $feedbackcompletion->can_complete(); $result['cansubmit'] = $feedbackcompletion->can_submit(); $result['candeletesubmissions'] = has_capability('mod/feedback:deletesubmissions', $context); $result['canviewreports'] = has_capability('mod/feedback:viewreports', $context); $result['canedititems'] = has_capability('mod/feedback:edititems', $context); // Status information. $result['isempty'] = $feedbackcompletion->is_empty(); $result['isopen'] = $feedbackcompletion->is_open(); $anycourse = ($course->id == SITEID); $result['isalreadysubmitted'] = $feedbackcompletion->is_already_submitted($anycourse); $result['isanonymous'] = $feedbackcompletion->is_anonymous(); $result['warnings'] = []; return $result; } /** * Describes the get_feedback_access_information return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_feedback_access_information_returns() { return new external_single_structure( array( 'canviewanalysis' => new external_value(PARAM_BOOL, 'Whether the user can view the analysis or not.'), 'cancomplete' => new external_value(PARAM_BOOL, 'Whether the user can complete the feedback or not.'), 'cansubmit' => new external_value(PARAM_BOOL, 'Whether the user can submit the feedback or not.'), 'candeletesubmissions' => new external_value(PARAM_BOOL, 'Whether the user can delete submissions or not.'), 'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the feedback reports or not.'), 'canedititems' => new external_value(PARAM_BOOL, 'Whether the user can edit feedback items or not.'), 'isempty' => new external_value(PARAM_BOOL, 'Whether the feedback has questions or not.'), 'isopen' => new external_value(PARAM_BOOL, 'Whether the feedback has active access time restrictions or not.'), 'isalreadysubmitted' => new external_value(PARAM_BOOL, 'Whether the feedback is already submitted or not.'), 'isanonymous' => new external_value(PARAM_BOOL, 'Whether the feedback is anonymous or not.'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for view_feedback. * * @return external_function_parameters * @since Moodle 3.3 */ public static function view_feedback_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'moduleviewed' => new external_value(PARAM_BOOL, 'If we need to mark the module as viewed for completion', VALUE_DEFAULT, false), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Trigger the course module viewed event and update the module completion status. * * @param int $feedbackid feedback instance id * @param bool $moduleviewed If we need to mark the module as viewed for completion * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and status result * @since Moodle 3.3 * @throws moodle_exception */ public static function view_feedback($feedbackid, $moduleviewed = false, $courseid = 0) { $params = array('feedbackid' => $feedbackid, 'moduleviewed' => $moduleviewed, 'courseid' => $courseid); $params = self::validate_parameters(self::view_feedback_parameters(), $params); $warnings = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); // Trigger module viewed event. $feedbackcompletion->trigger_module_viewed(); if ($params['moduleviewed']) { if (!$feedbackcompletion->is_open()) { throw new moodle_exception('feedback_is_not_open', 'feedback'); } // Mark activity viewed for completion-tracking. $feedbackcompletion->set_module_viewed(); } $result = array( 'status' => true, 'warnings' => $warnings, ); return $result; } /** * Describes the view_feedback return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function view_feedback_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_current_completed_tmp. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_current_completed_tmp_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Returns the temporary completion record for the current user. * * @param int $feedbackid feedback instance id * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and status result * @since Moodle 3.3 * @throws moodle_exception */ public static function get_current_completed_tmp($feedbackid, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid); $params = self::validate_parameters(self::get_current_completed_tmp_parameters(), $params); $warnings = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); if ($completed = $feedbackcompletion->get_current_completed_tmp()) { $exporter = new feedback_completedtmp_exporter($completed); return array( 'feedback' => $exporter->export($PAGE->get_renderer('core')), 'warnings' => $warnings, ); } throw new moodle_exception('not_started', 'feedback'); } /** * Describes the get_current_completed_tmp return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_current_completed_tmp_returns() { return new external_single_structure( array( 'feedback' => feedback_completedtmp_exporter::get_read_structure(), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_items. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_items_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Returns the items (questions) in the given feedback. * * @param int $feedbackid feedback instance id * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and feedbacks * @since Moodle 3.3 */ public static function get_items($feedbackid, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid); $params = self::validate_parameters(self::get_items_parameters(), $params); $warnings = array(); $returneditems = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $userhasaccess = true; try { // Check the user has access to the feedback. self::validate_feedback_access($feedback, $completioncourse, $cm, $context, true); } catch (moodle_exception $e) { $userhasaccess = false; $warnings[] = [ 'item' => $feedback->id, 'warningcode' => clean_param($e->errorcode, PARAM_ALPHANUM), 'message' => $e->getMessage(), ]; } // For consistency with the web behaviour, the items should be returned only when the user can edit or view reports (to // include non-editing teachers too). $capabilities = [ 'mod/feedback:edititems', 'mod/feedback:viewreports', ]; if ($userhasaccess || has_any_capability($capabilities, $context)) { // Remove previous warnings because, although the user might not have access, they have the proper capability. $warnings = []; $feedbackstructure = new mod_feedback_structure($feedback, $cm, $completioncourse->id); if ($items = $feedbackstructure->get_items()) { foreach ($items as $item) { $itemnumber = empty($item->itemnr) ? null : $item->itemnr; unset($item->itemnr); // Added by the function, not part of the record. $exporter = new feedback_item_exporter($item, array('context' => $context, 'itemnumber' => $itemnumber)); $returneditems[] = $exporter->export($PAGE->get_renderer('core')); } } } else if ($userhasaccess) { $warnings[] = [ 'item' => $feedback->id, 'warningcode' => 'nopermission', 'message' => 'nopermission', ]; } $result = array( 'items' => $returneditems, 'warnings' => $warnings ); return $result; } /** * Describes the get_items return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_items_returns() { return new external_single_structure( array( 'items' => new external_multiple_structure( feedback_item_exporter::get_read_structure() ), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for launch_feedback. * * @return external_function_parameters * @since Moodle 3.3 */ public static function launch_feedback_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Starts or continues a feedback submission * * @param array $feedbackid feedback instance id * @param int $courseid course where user completes a feedback (for site feedbacks only). * @return array of warnings and launch information * @since Moodle 3.3 */ public static function launch_feedback($feedbackid, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid); $params = self::validate_parameters(self::launch_feedback_parameters(), $params); $warnings = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); // Check we can do a new submission (or continue an existing). $feedbackcompletion = self::validate_feedback_access($feedback, $completioncourse, $cm, $context, true); $gopage = $feedbackcompletion->get_resume_page(); if ($gopage === null) { $gopage = -1; // Last page. } $result = array( 'gopage' => $gopage, 'warnings' => $warnings ); return $result; } /** * Describes the launch_feedback return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function launch_feedback_returns() { return new external_single_structure( array( 'gopage' => new external_value(PARAM_INT, 'The next page to go (-1 if we were already in the last page). 0 for first page.'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_page_items. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_page_items_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'page' => new external_value(PARAM_INT, 'The page to get starting by 0'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Get a single feedback page items. * * @param int $feedbackid feedback instance id * @param int $page the page to get starting by 0 * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and launch information * @since Moodle 3.3 */ public static function get_page_items($feedbackid, $page, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'page' => $page, 'courseid' => $courseid); $params = self::validate_parameters(self::get_page_items_parameters(), $params); $warnings = array(); $returneditems = array(); $hasprevpage = false; $hasnextpage = false; list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $userhasaccess = true; $feedbackcompletion = null; try { // Check the user has access to the feedback. $feedbackcompletion = self::validate_feedback_access($feedback, $completioncourse, $cm, $context, true); } catch (moodle_exception $e) { $userhasaccess = false; $warnings[] = [ 'item' => $feedback->id, 'warningcode' => str_replace('_', '', $e->errorcode), 'message' => $e->getMessage(), ]; } // For consistency with the web behaviour, the items should be returned only when the user can edit or view reports (to // include non-editing teachers too). $capabilities = [ 'mod/feedback:edititems', 'mod/feedback:viewreports', ]; if ($userhasaccess || has_any_capability($capabilities, $context)) { // Remove previous warnings because, although the user might not have access, they have the proper capability. $warnings = []; if ($feedbackcompletion == null) { $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); } $page = $params['page']; $pages = $feedbackcompletion->get_pages(); $pageitems = $pages[$page]; $hasnextpage = $page < count($pages) - 1; // Until we complete this page we can not trust get_next_page(). $hasprevpage = $page && ($feedbackcompletion->get_previous_page($page, false) !== null); foreach ($pageitems as $item) { $itemnumber = empty($item->itemnr) ? null : $item->itemnr; unset($item->itemnr); // Added by the function, not part of the record. $exporter = new feedback_item_exporter($item, array('context' => $context, 'itemnumber' => $itemnumber)); $returneditems[] = $exporter->export($PAGE->get_renderer('core')); } } else if ($userhasaccess) { $warnings[] = [ 'item' => $feedback->id, 'warningcode' => 'nopermission', 'message' => get_string('nopermission', 'mod_feedback'), ]; } $result = array( 'items' => $returneditems, 'hasprevpage' => $hasprevpage, 'hasnextpage' => $hasnextpage, 'warnings' => $warnings ); return $result; } /** * Describes the get_page_items return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_page_items_returns() { return new external_single_structure( array( 'items' => new external_multiple_structure( feedback_item_exporter::get_read_structure() ), 'hasprevpage' => new external_value(PARAM_BOOL, 'Whether is a previous page.'), 'hasnextpage' => new external_value(PARAM_BOOL, 'Whether there are more pages.'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for process_page. * * @return external_function_parameters * @since Moodle 3.3 */ public static function process_page_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'), 'page' => new external_value(PARAM_INT, 'The page being processed.'), 'responses' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_NOTAGS, 'The response name (usually type[index]_id).'), 'value' => new external_value(PARAM_RAW, 'The response value.'), ) ), 'The data to be processed.', VALUE_DEFAULT, array() ), 'goprevious' => new external_value(PARAM_BOOL, 'Whether we want to jump to previous page.', VALUE_DEFAULT, false), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Process a jump between pages. * * @param array $feedbackid feedback instance id * @param array $page the page being processed * @param array $responses the responses to be processed * @param bool $goprevious whether we want to jump to previous page * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and launch information * @since Moodle 3.3 */ public static function process_page($feedbackid, $page, $responses = [], $goprevious = false, $courseid = 0) { global $USER; $params = array('feedbackid' => $feedbackid, 'page' => $page, 'responses' => $responses, 'goprevious' => $goprevious, 'courseid' => $courseid); $params = self::validate_parameters(self::process_page_parameters(), $params); $warnings = array(); $siteaftersubmit = $completionpagecontents = ''; list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); // Check we can do a new submission (or continue an existing). $feedbackcompletion = self::validate_feedback_access($feedback, $completioncourse, $cm, $context, true); // Create the $_POST object required by the feedback question engine. $_POST = array(); foreach ($responses as $response) { // First check if we are handling array parameters. if (preg_match('/(.+)\[(.+)\]$/', $response['name'], $matches)) { $_POST[$matches[1]][$matches[2]] = $response['value']; } else { $_POST[$response['name']] = $response['value']; } } // Force fields. $_POST['id'] = $cm->id; $_POST['courseid'] = $courseid; $_POST['gopage'] = $params['page']; $_POST['_qf__mod_feedback_complete_form'] = 1; // Determine where to go, backwards or forward. if (!$params['goprevious']) { $_POST['gonextpage'] = 1; // Even if we are saving values we need this set. if ($feedbackcompletion->get_next_page($params['page'], false) === null) { $_POST['savevalues'] = 1; // If there is no next page, it means we are finishing the feedback. } } // Ignore sesskey (deep in some APIs), the request is already validated. $USER->ignoresesskey = true; $feedbackcompletion->process_page($params['page'], $params['goprevious']); $completed = $feedbackcompletion->just_completed(); if ($completed) { $jumpto = 0; if ($feedback->page_after_submit) { $completionpagecontents = $feedbackcompletion->page_after_submit(); } if ($feedback->site_after_submit) { $siteaftersubmit = feedback_encode_target_url($feedback->site_after_submit); } } else { $jumpto = $feedbackcompletion->get_jumpto(); } $result = array( 'jumpto' => $jumpto, 'completed' => $completed, 'completionpagecontents' => $completionpagecontents, 'siteaftersubmit' => $siteaftersubmit, 'warnings' => $warnings ); return $result; } /** * Describes the process_page return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function process_page_returns() { return new external_single_structure( array( 'jumpto' => new external_value(PARAM_INT, 'The page to jump to.'), 'completed' => new external_value(PARAM_BOOL, 'If the user completed the feedback.'), 'completionpagecontents' => new external_value(PARAM_RAW, 'The completion page contents.'), 'siteaftersubmit' => new external_value(PARAM_RAW, 'The link (could be relative) to show after submit.'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_analysis. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_analysis_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group', VALUE_DEFAULT, 0), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Retrieves the feedback analysis. * * @param array $feedbackid feedback instance id * @param int $groupid group id, 0 means that the function will determine the user group * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and launch information * @since Moodle 3.3 */ public static function get_analysis($feedbackid, $groupid = 0, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'courseid' => $courseid); $params = self::validate_parameters(self::get_analysis_parameters(), $params); $warnings = $itemsdata = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); // Check permissions. $feedbackstructure = new mod_feedback_structure($feedback, $cm, $completioncourse->id); if (!$feedbackstructure->can_view_analysis()) { throw new required_capability_exception($context, 'mod/feedback:viewanalysepage', 'nopermission', ''); } if (!empty($params['groupid'])) { $groupid = $params['groupid']; // Determine is the group is visible to user. if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { // Check to see if groups are being used here. if ($groupmode = groups_get_activity_groupmode($cm)) { $groupid = groups_get_activity_group($cm); // Determine is the group is visible to user (this is particullary for the group 0 -> all groups). if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { $groupid = 0; } } // Summary data. $summary = new mod_feedback\output\summary($feedbackstructure, $groupid); $summarydata = $summary->export_for_template($PAGE->get_renderer('core')); $checkanonymously = true; if ($groupid > 0 AND $feedback->anonymous == FEEDBACK_ANONYMOUS_YES) { $completedcount = $feedbackstructure->count_completed_responses($groupid); if ($completedcount < FEEDBACK_MIN_ANONYMOUS_COUNT_IN_GROUP) { $checkanonymously = false; } } if ($checkanonymously) { // Get the items of the feedback. $items = $feedbackstructure->get_items(true); foreach ($items as $item) { $itemobj = feedback_get_item_class($item->typ); $itemnumber = empty($item->itemnr) ? null : $item->itemnr; unset($item->itemnr); // Added by the function, not part of the record. $exporter = new feedback_item_exporter($item, array('context' => $context, 'itemnumber' => $itemnumber)); $itemsdata[] = array( 'item' => $exporter->export($PAGE->get_renderer('core')), 'data' => $itemobj->get_analysed_for_external($item, $groupid), ); } } else { $warnings[] = array( 'item' => 'feedback', 'itemid' => $feedback->id, 'warningcode' => 'insufficientresponsesforthisgroup', 'message' => s(get_string('insufficient_responses_for_this_group', 'feedback')) ); } $result = array( 'completedcount' => $summarydata->completedcount, 'itemscount' => $summarydata->itemscount, 'itemsdata' => $itemsdata, 'warnings' => $warnings ); return $result; } /** * Describes the get_analysis return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_analysis_returns() { return new external_single_structure( array( 'completedcount' => new external_value(PARAM_INT, 'Number of completed submissions.'), 'itemscount' => new external_value(PARAM_INT, 'Number of items (questions).'), 'itemsdata' => new external_multiple_structure( new external_single_structure( array( 'item' => feedback_item_exporter::get_read_structure(), 'data' => new external_multiple_structure( new external_value(PARAM_RAW, 'The analysis data (can be json encoded)') ), ) ) ), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_unfinished_responses. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_unfinished_responses_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Retrieves responses from the current unfinished attempt. * * @param array $feedbackid feedback instance id * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and launch information * @since Moodle 3.3 */ public static function get_unfinished_responses($feedbackid, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid); $params = self::validate_parameters(self::get_unfinished_responses_parameters(), $params); $warnings = $itemsdata = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); $responses = array(); $unfinished = $feedbackcompletion->get_unfinished_responses(); foreach ($unfinished as $u) { $exporter = new feedback_valuetmp_exporter($u); $responses[] = $exporter->export($PAGE->get_renderer('core')); } $result = array( 'responses' => $responses, 'warnings' => $warnings ); return $result; } /** * Describes the get_unfinished_responses return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_unfinished_responses_returns() { return new external_single_structure( array( 'responses' => new external_multiple_structure( feedback_valuetmp_exporter::get_read_structure() ), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_finished_responses. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_finished_responses_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Retrieves responses from the last finished attempt. * * @param array $feedbackid feedback instance id * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and the responses * @since Moodle 3.3 */ public static function get_finished_responses($feedbackid, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid); $params = self::validate_parameters(self::get_finished_responses_parameters(), $params); $warnings = $itemsdata = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); $responses = array(); // Load and get the responses from the last completed feedback. $feedbackcompletion->find_last_completed(); $unfinished = $feedbackcompletion->get_finished_responses(); foreach ($unfinished as $u) { $exporter = new feedback_value_exporter($u); $responses[] = $exporter->export($PAGE->get_renderer('core')); } $result = array( 'responses' => $responses, 'warnings' => $warnings ); return $result; } /** * Describes the get_finished_responses return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_finished_responses_returns() { return new external_single_structure( array( 'responses' => new external_multiple_structure( feedback_value_exporter::get_read_structure() ), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_non_respondents. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_non_respondents_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group.', VALUE_DEFAULT, 0), 'sort' => new external_value(PARAM_ALPHA, 'Sort param, must be firstname, lastname or lastaccess (default).', VALUE_DEFAULT, 'lastaccess'), 'page' => new external_value(PARAM_INT, 'The page of records to return.', VALUE_DEFAULT, 0), 'perpage' => new external_value(PARAM_INT, 'The number of records to return per page.', VALUE_DEFAULT, 0), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Retrieves a list of students who didn't submit the feedback. * * @param int $feedbackid feedback instance id * @param int $groupid Group id, 0 means that the function will determine the user group' * @param str $sort sort param, must be firstname, lastname or lastaccess (default) * @param int $page the page of records to return * @param int $perpage the number of records to return per page * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and users ids * @since Moodle 3.3 */ public static function get_non_respondents($feedbackid, $groupid = 0, $sort = 'lastaccess', $page = 0, $perpage = 0, $courseid = 0) { global $CFG; require_once($CFG->dirroot . '/mod/feedback/lib.php'); $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'sort' => $sort, 'page' => $page, 'perpage' => $perpage, 'courseid' => $courseid); $params = self::validate_parameters(self::get_non_respondents_parameters(), $params); $warnings = $nonrespondents = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); $completioncourseid = $feedbackcompletion->get_courseid(); if ($feedback->anonymous != FEEDBACK_ANONYMOUS_NO || $feedback->course == SITEID) { throw new moodle_exception('anonymous', 'feedback'); } // Check permissions. require_capability('mod/feedback:viewreports', $context); if (!empty($params['groupid'])) { $groupid = $params['groupid']; // Determine is the group is visible to user. if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { // Check to see if groups are being used here. if ($groupmode = groups_get_activity_groupmode($cm)) { $groupid = groups_get_activity_group($cm); // Determine is the group is visible to user (this is particullary for the group 0 -> all groups). if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { $groupid = 0; } } if ($params['sort'] !== 'firstname' && $params['sort'] !== 'lastname' && $params['sort'] !== 'lastaccess') { throw new invalid_parameter_exception('Invalid sort param, must be firstname, lastname or lastaccess.'); } // Check if we are page filtering. if ($params['perpage'] == 0) { $page = $params['page']; $perpage = FEEDBACK_DEFAULT_PAGE_COUNT; } else { $perpage = $params['perpage']; $page = $perpage * $params['page']; } $users = feedback_get_incomplete_users($cm, $groupid, $params['sort'], $page, $perpage, true); foreach ($users as $user) { $nonrespondents[] = [ 'courseid' => $completioncourseid, 'userid' => $user->id, 'fullname' => fullname($user), 'started' => $user->feedbackstarted ]; } $result = array( 'users' => $nonrespondents, 'total' => feedback_count_incomplete_users($cm, $groupid), 'warnings' => $warnings ); return $result; } /** * Describes the get_non_respondents return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_non_respondents_returns() { return new external_single_structure( array( 'users' => new external_multiple_structure( new external_single_structure( array( 'courseid' => new external_value(PARAM_INT, 'Course id'), 'userid' => new external_value(PARAM_INT, 'The user id'), 'fullname' => new external_value(PARAM_TEXT, 'User full name'), 'started' => new external_value(PARAM_BOOL, 'If the user has started the attempt'), ) ) ), 'total' => new external_value(PARAM_INT, 'Total number of non respondents'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_responses_analysis. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_responses_analysis_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group', VALUE_DEFAULT, 0), 'page' => new external_value(PARAM_INT, 'The page of records to return.', VALUE_DEFAULT, 0), 'perpage' => new external_value(PARAM_INT, 'The number of records to return per page', VALUE_DEFAULT, 0), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Return the feedback user responses. * * @param int $feedbackid feedback instance id * @param int $groupid Group id, 0 means that the function will determine the user group * @param int $page the page of records to return * @param int $perpage the number of records to return per page * @param int $courseid course where user completes the feedback (for site feedbacks only) * @return array of warnings and users attemps and responses * @throws moodle_exception * @since Moodle 3.3 */ public static function get_responses_analysis($feedbackid, $groupid = 0, $page = 0, $perpage = 0, $courseid = 0) { $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'page' => $page, 'perpage' => $perpage, 'courseid' => $courseid); $params = self::validate_parameters(self::get_responses_analysis_parameters(), $params); $warnings = $itemsdata = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); // Check permissions. require_capability('mod/feedback:viewreports', $context); if (!empty($params['groupid'])) { $groupid = $params['groupid']; // Determine is the group is visible to user. if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { // Check to see if groups are being used here. if ($groupmode = groups_get_activity_groupmode($cm)) { $groupid = groups_get_activity_group($cm); // Determine is the group is visible to user (this is particullary for the group 0 -> all groups). if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { $groupid = 0; } } $feedbackstructure = new mod_feedback_structure($feedback, $cm, $completioncourse->id); $responsestable = new mod_feedback_responses_table($feedbackstructure, $groupid); // Ensure responses number is correct prior returning them. $feedbackstructure->shuffle_anonym_responses(); $anonresponsestable = new mod_feedback_responses_anon_table($feedbackstructure, $groupid); $result = array( 'attempts' => $responsestable->export_external_structure($params['page'], $params['perpage']), 'totalattempts' => $responsestable->get_total_responses_count(), 'anonattempts' => $anonresponsestable->export_external_structure($params['page'], $params['perpage']), 'totalanonattempts' => $anonresponsestable->get_total_responses_count(), 'warnings' => $warnings ); return $result; } /** * Describes the get_responses_analysis return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_responses_analysis_returns() { $responsestructure = new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Response id'), 'name' => new external_value(PARAM_RAW, 'Response name'), 'printval' => new external_value(PARAM_RAW, 'Response ready for output'), 'rawval' => new external_value(PARAM_RAW, 'Response raw value'), ) ) ); return new external_single_structure( array( 'attempts' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Completed id'), 'courseid' => new external_value(PARAM_INT, 'Course id'), 'userid' => new external_value(PARAM_INT, 'User who responded'), 'timemodified' => new external_value(PARAM_INT, 'Time modified for the response'), 'fullname' => new external_value(PARAM_TEXT, 'User full name'), 'responses' => $responsestructure ) ) ), 'totalattempts' => new external_value(PARAM_INT, 'Total responses count.'), 'anonattempts' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Completed id'), 'courseid' => new external_value(PARAM_INT, 'Course id'), 'number' => new external_value(PARAM_INT, 'Response number'), 'responses' => $responsestructure ) ) ), 'totalanonattempts' => new external_value(PARAM_INT, 'Total anonymous responses count.'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_last_completed. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_last_completed_parameters() { return new external_function_parameters ( array( 'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'), 'courseid' => new external_value(PARAM_INT, 'Course where user completes the feedback (for site feedbacks only).', VALUE_DEFAULT, 0), ) ); } /** * Retrieves the last completion record for the current user. * * @param int $feedbackid feedback instance id * @return array of warnings and the last completed record * @since Moodle 3.3 * @throws moodle_exception */ public static function get_last_completed($feedbackid, $courseid = 0) { global $PAGE; $params = array('feedbackid' => $feedbackid, 'courseid' => $courseid); $params = self::validate_parameters(self::get_last_completed_parameters(), $params); $warnings = array(); list($feedback, $course, $cm, $context, $completioncourse) = self::validate_feedback($params['feedbackid'], $params['courseid']); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $completioncourse->id); if ($feedbackcompletion->is_anonymous()) { throw new moodle_exception('anonymous', 'feedback'); } if ($completed = $feedbackcompletion->find_last_completed()) { $exporter = new feedback_completed_exporter($completed); return array( 'completed' => $exporter->export($PAGE->get_renderer('core')), 'warnings' => $warnings, ); } throw new moodle_exception('not_completed_yet', 'feedback'); } /** * Describes the get_last_completed return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function get_last_completed_returns() { return new external_single_structure( array( 'completed' => feedback_completed_exporter::get_read_structure(), 'warnings' => new external_warnings(), ) ); } } PKB\lGG%classes/form/create_template_form.phpnu[. namespace mod_feedback\form; use core_form\dynamic_form; use moodle_url; use context; use context_module; use context_system; /** * Prints the create new template form * * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ class create_template_form extends dynamic_form { /** * Define the form */ public function definition() { $mform =& $this->_form; $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('text', 'templatename', get_string('name', 'feedback'), ['maxlength' => '200', 'size' => '50']); $mform->setType('templatename', PARAM_TEXT); $mform->addRule('templatename', null, 'required', null, 'client'); if (has_capability('mod/feedback:createpublictemplate', context_system::instance())) { $mform->addElement('checkbox', 'ispublic', '', get_string('availableforallcourses', 'feedback')); } } /** * Returns context where this form is used * * @return context */ protected function get_context_for_dynamic_submission(): context { $id = $this->optional_param('id', null, PARAM_INT); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); return context_module::instance($cm->id); } /** * Checks if current user has access to this form, otherwise throws exception * * @throws \moodle_exception User does not have capability to access the form */ protected function check_access_for_dynamic_submission(): void { $context = $this->get_context_for_dynamic_submission(); if (!has_capability('mod/feedback:edititems', $context) || !(has_capability('mod/feedback:createprivatetemplate', $context) || has_capability('mod/feedback:createpublictemplate', $context))) { throw new \moodle_exception('nocapabilitytousethisservice'); } } /** * Process the form submission, used if form was submitted via AJAX * * @return array Returns whether a new template was created. */ public function process_dynamic_submission(): array { global $PAGE; $formdata = $this->get_data(); $ispublic = !empty($formdata->ispublic) ? 1 : 0; $result = feedback_save_as_template($PAGE->activityrecord, $formdata->templatename, $ispublic); return [ 'result' => $result, ]; } /** * Load in existing data as form defaults */ public function set_data_for_dynamic_submission(): void { $this->set_data((object)[ 'id' => $this->optional_param('id', null, PARAM_INT), ]); } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { $params = [ 'id' => $this->optional_param('id', null, PARAM_INT), ]; return new moodle_url('/mod/feedback/edit.php', $params); } } PKB\Bk=="classes/form/use_template_form.phpnu[. namespace mod_feedback\form; use core_form\dynamic_form; use moodle_url; use context; use context_module; /** * Prints the confirm use template form * * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ class use_template_form extends dynamic_form { /** * Define the form */ public function definition() { $mform =& $this->_form; $mform->addElement('static', 'generalheader', '', get_string("whatfor", 'feedback')); $mform->addElement('radio', 'deleteolditems', '', get_string('delete_old_items', 'feedback'), 1); $mform->addElement('radio', 'deleteolditems', '', get_string('append_new_items', 'feedback'), 0); $mform->setType('deleteolditems', PARAM_INT); $mform->setDefault('deleteolditems', 1); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'templateid'); $mform->setType('templateid', PARAM_INT); } /** * Returns context where this form is used * * @return context */ protected function get_context_for_dynamic_submission(): context { $id = $this->optional_param('id', null, PARAM_INT); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); return context_module::instance($cm->id); } /** * Checks if current user has access to this form, otherwise throws exception * * @throws \moodle_exception User does not have capability to access the form */ protected function check_access_for_dynamic_submission(): void { if (!has_capability('mod/feedback:edititems', $this->get_context_for_dynamic_submission())) { throw new \moodle_exception('nocapabilitytousethisservice'); } } /** * Process the form submission, used if form was submitted via AJAX * * @return array Returns the following information * - the template was successfully created/updated from the provided template * - the redirect url. */ public function process_dynamic_submission(): array { global $PAGE; $formdata = $this->get_data(); $templateid = $this->optional_param('templateid', null, PARAM_INT); $id = $this->optional_param('id', null, PARAM_INT); $response = feedback_items_from_template($PAGE->activityrecord, $templateid, $formdata->deleteolditems); $url = new moodle_url('/mod/feedback/edit.php', ['id' => $id]); if ($response !== false) { // Provide a notification on success as the user will be redirected. \core\notification::add(get_string('feedbackupdated', 'feedback'), \core\notification::SUCCESS); } return [ 'result' => $response !== false, 'url' => $url->out() ]; } /** * Load in existing data as form defaults */ public function set_data_for_dynamic_submission(): void { $this->set_data((object)[ 'id' => $this->optional_param('id', null, PARAM_INT), 'templateid' => $this->optional_param('templateid', null, PARAM_INT) ]); } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { $params = [ 'id' => $this->optional_param('id', null, PARAM_INT), 'templateid' => $this->optional_param('templateid', null, PARAM_INT) ]; return new moodle_url('/mod/feedback/use_templ.php', $params); } } PKB\.BB.classes/external/feedback_summary_exporter.phpnu[. namespace mod_feedback\external; use core\external\exporter; use renderer_base; use core_external\util as external_util; use core_external\external_files; /** * Class for exporting partial feedback data (some fields are only viewable by admins). * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_feedback */ class feedback_summary_exporter extends exporter { protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'The primary key of the record.', ), 'course' => array( 'type' => PARAM_INT, 'description' => 'Course id this feedback is part of.', ), 'name' => array( 'type' => PARAM_TEXT, 'description' => 'Feedback name.', ), 'intro' => array( 'default' => '', 'type' => PARAM_RAW, 'description' => 'Feedback introduction text.', ), 'introformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_MOODLE, 'description' => 'Feedback intro text format.', ), 'lang' => array( 'type' => PARAM_LANG, 'description' => 'Forced activity language', 'null' => NULL_ALLOWED, ), 'anonymous' => array( 'type' => PARAM_INT, 'description' => 'Whether the feedback is anonymous.', ), 'email_notification' => array( 'type' => PARAM_BOOL, 'optional' => true, 'description' => 'Whether email notifications will be sent to teachers.', ), 'multiple_submit' => array( 'default' => 1, 'type' => PARAM_BOOL, 'description' => 'Whether multiple submissions are allowed.', ), 'autonumbering' => array( 'default' => 1, 'type' => PARAM_BOOL, 'description' => 'Whether questions should be auto-numbered.', ), 'site_after_submit' => array( 'type' => PARAM_TEXT, 'optional' => true, 'description' => 'Link to next page after submission.', ), 'page_after_submit' => array( 'type' => PARAM_RAW, 'optional' => true, 'description' => 'Text to display after submission.', ), 'page_after_submitformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_MOODLE, 'description' => 'Text to display after submission format.', ), 'publish_stats' => array( 'default' => 0, 'type' => PARAM_BOOL, 'description' => 'Whether stats should be published.', ), 'timeopen' => array( 'type' => PARAM_INT, 'optional' => true, 'description' => 'Allow answers from this time.', ), 'timeclose' => array( 'type' => PARAM_INT, 'optional' => true, 'description' => 'Allow answers until this time.', ), 'timemodified' => array( 'type' => PARAM_INT, 'optional' => true, 'description' => 'The time this record was modified.', ), 'completionsubmit' => array( 'default' => 0, 'type' => PARAM_BOOL, 'description' => 'If this field is set to 1, then the activity will be automatically marked as complete on submission.', ), ); } protected static function define_related() { return array( 'context' => 'context' ); } protected static function define_other_properties() { return array( 'coursemodule' => array( 'type' => PARAM_INT ), 'introfiles' => array( 'type' => external_files::get_properties_for_exporter(), 'multiple' => true ), 'pageaftersubmitfiles' => array( 'type' => external_files::get_properties_for_exporter(), 'multiple' => true, 'optional' => true ), ); } protected function get_other_values(renderer_base $output) { $context = $this->related['context']; $values = array( 'coursemodule' => $context->instanceid, ); $values['introfiles'] = external_util::get_area_files($context->id, 'mod_feedback', 'intro', false, false); if (!empty($this->data->page_after_submit)) { $values['pageaftersubmitfiles'] = external_util::get_area_files($context->id, 'mod_feedback', 'page_after_submit'); } return $values; } /** * Get the formatting parameters for the intro. * * @return array */ protected function get_format_parameters_for_intro() { return [ 'component' => 'mod_feedback', 'filearea' => 'intro', 'options' => array('noclean' => true), ]; } /** * Get the formatting parameters for the page_after_submit. * * @return array */ protected function get_format_parameters_for_page_after_submit() { return [ 'component' => 'mod_feedback', 'filearea' => 'page_after_submit', 'itemid' => 0 ]; } } PKB\>G?e e &classes/external/questions/reorder.phpnu[. declare(strict_types=1); namespace mod_feedback\external\questions; use core_external\external_api; use core_external\external_value; use core_external\external_function_parameters; use context_module; /** * External method for reordering feedback questions. * * @package mod_feedback * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class reorder extends external_api { /** * Describes the parameters for reorder. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'cmid' => new external_value(PARAM_INT, 'Feedback course module id'), 'itemorder' => new external_value(PARAM_SEQUENCE, 'Feedback order by sequence of question item ids'), ] ); } /** * External function to reorder feedback questions. * * @param int $cmid * @param string $itemorder * @return bool */ public static function execute(int $cmid, string $itemorder): bool { global $DB; [ 'cmid' => $cmid, 'itemorder' => $itemorder, ] = self::validate_parameters(self::execute_parameters(), [ 'cmid' => $cmid, 'itemorder' => $itemorder, ]); $cm = get_coursemodule_from_id('feedback', $cmid, 0, false, MUST_EXIST); $feedback = $DB->get_record('feedback', ['id' => $cm->instance], '*', MUST_EXIST); $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/feedback:edititems', $context); $itemlist = explode(',', trim($itemorder, ',')) ?: []; if (count($itemlist) > 0) { return feedback_ajax_saveitemorder($itemlist, $feedback); } return false; } /** * Describes the data returned from the external function. * * @return external_value */ public static function execute_returns(): external_value { return new external_value(PARAM_BOOL, '', VALUE_REQUIRED); } } PKB\. /** * Class for exporting a feedback response. * * @package mod_feedback * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class for exporting a feedback response. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class feedback_value_exporter extends exporter { /** * Return the list of properties. * * @return array list of properties */ protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'The record id.', ), 'course_id' => array( 'type' => PARAM_INT, 'description' => 'The course id this record belongs to.', ), 'item' => array( 'type' => PARAM_INT, 'description' => 'The item id that was responded.', ), 'completed' => array( 'type' => PARAM_INT, 'description' => 'Reference to the feedback_completed table.', ), 'tmp_completed' => array( 'type' => PARAM_INT, 'description' => 'Old field - not used anymore.', ), 'value' => array( 'type' => PARAM_RAW, 'description' => 'The response value.', ), ); } } PKB\eO))+classes/external/feedback_item_exporter.phpnu[. /** * Class for exporting a feedback item (question). * * @package mod_feedback * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; use renderer_base; use core_files\external\stored_file_exporter; /** * Class for exporting a feedback item (question). * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class feedback_item_exporter extends exporter { protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'The record id.', ), 'feedback' => array( 'type' => PARAM_INT, 'description' => 'The feedback instance id this records belongs to.', 'default' => 0, ), 'template' => array( 'type' => PARAM_INT, 'description' => 'If it belogns to a template, the template id.', 'default' => 0, ), 'name' => array( 'type' => PARAM_RAW, 'description' => 'The item name.', ), 'nameformat' => [ 'choices' => [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN], 'type' => PARAM_INT, 'description' => 'The format of the item name.', 'default' => FORMAT_HTML, ], 'label' => array( 'type' => PARAM_NOTAGS, 'description' => 'The item label.', ), 'presentation' => array( 'type' => PARAM_RAW, 'description' => 'The text describing the item or the available possible answers.', ), 'presentationformat' => [ 'choices' => [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN], 'type' => PARAM_INT, 'description' => 'The format of the text describing the item or the available possible answers.', 'default' => FORMAT_HTML, ], 'typ' => array( 'type' => PARAM_ALPHA, 'description' => 'The type of the item.', ), 'hasvalue' => array( 'type' => PARAM_INT, 'description' => 'Whether it has a value or not.', 'default' => 0, ), 'position' => array( 'type' => PARAM_INT, 'description' => 'The position in the list of questions.', 'default' => 0, ), 'required' => array( 'type' => PARAM_BOOL, 'description' => 'Whether is a item (question) required or not.', 'default' => 0, ), 'dependitem' => array( 'type' => PARAM_INT, 'description' => 'The item id this item depend on.', 'default' => 0, ), 'dependvalue' => array( 'type' => PARAM_RAW, 'description' => 'The depend value.', ), 'options' => array( 'type' => PARAM_ALPHA, 'description' => 'Different additional settings for the item (question).', ), ); } protected static function define_related() { return array( 'context' => 'context', 'itemnumber' => 'int?' ); } protected static function define_other_properties() { return array( 'itemfiles' => array( 'type' => stored_file_exporter::read_properties_definition(), 'multiple' => true ), 'itemnumber' => array( 'type' => PARAM_INT, 'description' => 'The item position number', 'null' => NULL_ALLOWED ), 'otherdata' => array( 'type' => PARAM_RAW, 'description' => 'Additional data that may be required by external functions', 'null' => NULL_ALLOWED ), ); } protected function get_other_values(renderer_base $output) { $context = $this->related['context']; $itemobj = feedback_get_item_class($this->data->typ); $values = array( 'itemfiles' => array(), 'itemnumber' => $this->related['itemnumber'], 'otherdata' => $itemobj->get_data_for_external($this->data), ); $fs = get_file_storage(); $files = array(); $itemfiles = $fs->get_area_files($context->id, 'mod_feedback', 'item', $this->data->id, 'filename', false); if (!empty($itemfiles)) { foreach ($itemfiles as $storedfile) { $fileexporter = new stored_file_exporter($storedfile, array('context' => $context)); $files[] = $fileexporter->export($output); } $values['itemfiles'] = $files; } return $values; } /** * Get the formatting parameters for the name. * * @return array */ protected function get_format_parameters_for_name() { return [ 'component' => 'mod_feedback', 'filearea' => 'item', 'itemid' => $this->data->id, 'options' => ['noclean' => true, 'para' => false], ]; } /** * Get the formatting parameters for the presentation. * * @return array */ protected function get_format_parameters_for_presentation() { return [ 'component' => 'mod_feedback', 'filearea' => 'item', 'itemid' => $this->data->id, 'options' => ['noclean' => true, 'para' => false], ]; } } PKB\w( 3classes/external/feedback_completedtmp_exporter.phpnu[. /** * Class for exporting a feedback temporary completion record. * * @package mod_feedback * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class for exporting a feedback temporary completion record. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class feedback_completedtmp_exporter extends exporter { /** * Return the list of properties. * * @return array list of properties */ protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'The record id.', ), 'feedback' => array( 'type' => PARAM_INT, 'description' => 'The feedback instance id this records belongs to.', ), 'userid' => array( 'type' => PARAM_INT, 'description' => 'The user who completed the feedback (0 for anonymous).', ), 'guestid' => array( 'type' => PARAM_RAW, 'description' => 'For guests, this is the session key.', ), 'timemodified' => array( 'type' => PARAM_INT, 'description' => 'The last time the feedback was completed.', ), 'random_response' => array( 'type' => PARAM_INT, 'description' => 'The response number (used when shuffling anonymous responses).', ), 'anonymous_response' => array( 'type' => PARAM_INT, 'description' => 'Whether is an anonymous response.', ), 'courseid' => array( 'type' => PARAM_INT, 'description' => 'The course id where the feedback was completed.', ), ); } } PKB\+ + 0classes/external/feedback_completed_exporter.phpnu[. /** * Class for exporting a feedback completion record. * * @package mod_feedback * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class for exporting a feedback completion record. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class feedback_completed_exporter extends exporter { /** * Return the list of properties. * * @return array list of properties */ protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'The record id.', ), 'feedback' => array( 'type' => PARAM_INT, 'description' => 'The feedback instance id this records belongs to.', ), 'userid' => array( 'type' => PARAM_INT, 'description' => 'The user who completed the feedback (0 for anonymous).', ), 'timemodified' => array( 'type' => PARAM_INT, 'description' => 'The last time the feedback was completed.', ), 'random_response' => array( 'type' => PARAM_INT, 'description' => 'The response number (used when shuffling anonymous responses).', ), 'anonymous_response' => array( 'type' => PARAM_INT, 'description' => 'Whether is an anonymous response.', ), 'courseid' => array( 'type' => PARAM_INT, 'description' => 'The course id where the feedback was completed.', ), ); } } PKB\  /classes/external/feedback_valuetmp_exporter.phpnu[. /** * Class for exporting a feedback tmp response. * * @package mod_feedback * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class for exporting a feedback tmp response. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class feedback_valuetmp_exporter extends exporter { /** * Return the list of properties. * * @return array list of properties */ protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'The record id.', ), 'course_id' => array( 'type' => PARAM_INT, 'description' => 'The course id this record belongs to.', ), 'item' => array( 'type' => PARAM_INT, 'description' => 'The item id that was responded.', ), 'completed' => array( 'type' => PARAM_INT, 'description' => 'Reference to the feedback_completedtmp table.', ), 'tmp_completed' => array( 'type' => PARAM_INT, 'description' => 'Old field - not used anymore.', ), 'value' => array( 'type' => PARAM_RAW, 'description' => 'The response value.', ), ); } } PKB\J9classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for block_feedback. * * @package block_feedback * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace block_feedback\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for block_feedback implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PKB\r  classes/dates.phpnu[. /** * Contains the class for fetching the important dates in mod_feedback for a given module instance and a user. * * @package mod_feedback * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_feedback; use core\activity_dates; /** * Class for fetching the important dates in mod_feedback for a given module instance and a user. * * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dates extends activity_dates { /** * Returns a list of important dates in mod_feedback * * @return array */ protected function get_dates(): array { $timeopen = $this->cm->customdata['timeopen'] ?? null; $timeclose = $this->cm->customdata['timeclose'] ?? null; $now = time(); $dates = []; if ($timeopen) { $openlabelid = $timeopen > $now ? 'activitydate:opens' : 'activitydate:opened'; $dates[] = [ 'dataid' => 'timeopen', 'label' => get_string($openlabelid, 'course'), 'timestamp' => (int) $timeopen, ]; } if ($timeclose) { $closelabelid = $timeclose > $now ? 'activitydate:closes' : 'activitydate:closed'; $dates[] = [ 'dataid' => 'timeclose', 'label' => get_string($closelabelid, 'course'), 'timestamp' => (int) $timeclose, ]; } return $dates; } } PKB\RCC&classes/navigation/views/secondary.phpnu[. namespace mod_feedback\navigation\views; use core\navigation\views\secondary as core_secondary; use settings_navigation; use navigation_node; /** * Custom secondary navigation class * * A custom construct of secondary nav for feedback. This rearranges the nodes for the secondary * * @package mod_feedback * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class secondary extends core_secondary { protected function get_default_module_mapping(): array { $basenodes = parent::get_default_module_mapping(); $basenodes[self::TYPE_CUSTOM] += [ 'questionnode' => 2, 'templatenode' => 3, 'responses' => 4, 'nonrespondents' => 4.1, 'feedbackanalysis' => 5, 'mapcourse' => 12, ]; return $basenodes; } } PKB\ rmrmclasses/completion.phpnu[. /** * Collects information and methods about feedback completion (either complete.php or show_entries.php) * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_completion extends mod_feedback_structure { /** @var stdClass */ protected $completed; /** @var stdClass */ protected $completedtmp = null; /** @var stdClass[] */ protected $valuestmp = null; /** @var stdClass[] */ protected $values = null; /** @var bool */ protected $iscompleted = false; /** @var mod_feedback_complete_form the form used for completing the feedback */ protected $form = null; /** @var bool true when the feedback has been completed during the request */ protected $justcompleted = false; /** @var int the next page the user should jump after processing the form */ protected $jumpto = null; /** * Constructor * * @param stdClass $feedback feedback object * @param cm_info $cm course module object corresponding to the $feedback * (at least one of $feedback or $cm is required) * @param int $courseid current course (for site feedbacks only) * @param bool $iscompleted has feedback been already completed? If yes either completedid or userid must be specified. * @param int $completedid id in the table feedback_completed, may be omitted if userid is specified * but it is highly recommended because the same user may have multiple responses to the same feedback * for different courses * @param int $nonanonymouseuserid - Return only anonymous results or specified user's results. * If null only anonymous replies will be returned and the $completedid is mandatory. * If specified only non-anonymous replies of $nonanonymouseuserid will be returned. * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). */ public function __construct($feedback, $cm, $courseid, $iscompleted = false, $completedid = null, $nonanonymouseuserid = null, $userid = 0) { global $DB; parent::__construct($feedback, $cm, $courseid, 0, $userid); // Make sure courseid is always set for site feedback. if ($this->feedback->course == SITEID && !$this->courseid) { $this->courseid = SITEID; } if ($iscompleted) { // Retrieve information about the completion. $this->iscompleted = true; $params = array('feedback' => $this->feedback->id); if (!$nonanonymouseuserid && !$completedid) { throw new coding_exception('Either $completedid or $nonanonymouseuserid must be specified for completed feedbacks'); } if ($completedid) { $params['id'] = $completedid; } if ($nonanonymouseuserid) { // We must respect the anonymousity of the reply that the user saw when they were completing the feedback, // not the current state that may have been changed later by the teacher. $params['anonymous_response'] = FEEDBACK_ANONYMOUS_NO; $params['userid'] = $nonanonymouseuserid; } $this->completed = $DB->get_record('feedback_completed', $params, '*', MUST_EXIST); if (!groups_user_groups_visible($this->feedback->course, $this->completed->userid, $cm)) { throw new moodle_exception('nopermissiontoshow'); } $this->courseid = $this->completed->courseid; } } /** * Returns a record from 'feedback_completed' table * @return stdClass */ public function get_completed() { return $this->completed; } /** * Check if the feedback was just completed. * * @return bool true if the feedback was just completed. * @since Moodle 3.3 */ public function just_completed() { return $this->justcompleted; } /** * Return the jumpto property. * * @return int the next page to jump. * @since Moodle 3.3 */ public function get_jumpto() { return $this->jumpto; } /** * Returns the temporary completion record for the current user or guest session * * @return stdClass|false record from feedback_completedtmp or false if not found */ public function get_current_completed_tmp() { global $DB, $USER; if ($this->completedtmp === null) { $params = array('feedback' => $this->get_feedback()->id); if ($courseid = $this->get_courseid()) { $params['courseid'] = $courseid; } if ((isloggedin() || $USER->id != $this->userid) && !isguestuser($this->userid)) { $params['userid'] = $this->userid; } else { $params['guestid'] = sesskey(); } $this->completedtmp = $DB->get_record('feedback_completedtmp', $params); } return $this->completedtmp; } /** * Can the current user see the item, if dependency is met? * * @param stdClass $item * @return bool whether user can see item or not, * true if there is no dependency or dependency is met, * false if dependent question is visible or broken * and further it is either not answered or the dependency is not met, * null if dependency is broken. */ protected function can_see_item($item) { if (empty($item->dependitem)) { return true; } if ($this->dependency_has_error($item)) { return null; } $allitems = $this->get_items(); $ditem = $allitems[$item->dependitem]; $itemobj = feedback_get_item_class($ditem->typ); if ($this->iscompleted) { $value = $this->get_values($ditem); } else { $value = $this->get_values_tmp($ditem); } if ($value === null) { // Cyclic dependencies are no problem here, since they will throw an dependency error above. if ($this->can_see_item($ditem) === false) { return false; } return null; } $check = $itemobj->compare_value($ditem, $value, $item->dependvalue) ? true : false; if ($check) { return $this->can_see_item($ditem); } return false; } /** * Dependency condition has an error * @param stdClass $item * @return bool */ protected function dependency_has_error($item) { if (empty($item->dependitem)) { // No dependency - no error. return false; } $allitems = $this->get_items(); if (!array_key_exists($item->dependitem, $allitems)) { // Looks like dependent item has been removed. return true; } $itemids = array_keys($allitems); $index1 = array_search($item->dependitem, $itemids); $index2 = array_search($item->id, $itemids); if ($index1 >= $index2) { // Dependent item is after the current item in the feedback. return true; } for ($i = $index1 + 1; $i < $index2; $i++) { if ($allitems[$itemids[$i]]->typ === 'pagebreak') { return false; } } // There are no page breaks between dependent items. return true; } /** * Returns a value stored for this item in the feedback (temporary or not, depending on the mode) * @param stdClass $item * @return string */ public function get_item_value($item) { if ($this->iscompleted) { return $this->get_values($item); } else { return $this->get_values_tmp($item); } } /** * Retrieves responses from an unfinished attempt. * * @return array the responses (from the feedback_valuetmp table) * @since Moodle 3.3 */ public function get_unfinished_responses() { global $DB; $responses = array(); $completedtmp = $this->get_current_completed_tmp(); if ($completedtmp) { $responses = $DB->get_records('feedback_valuetmp', ['completed' => $completedtmp->id]); } return $responses; } /** * Returns all temporary values for this feedback or just a value for an item * @param stdClass $item * @return array */ protected function get_values_tmp($item = null) { global $DB; if ($this->valuestmp === null) { $this->valuestmp = array(); $responses = $this->get_unfinished_responses(); foreach ($responses as $r) { $this->valuestmp[$r->item] = $r->value; } } if ($item) { return array_key_exists($item->id, $this->valuestmp) ? $this->valuestmp[$item->id] : null; } return $this->valuestmp; } /** * Retrieves responses from an finished attempt. * * @return array the responses (from the feedback_value table) * @since Moodle 3.3 */ public function get_finished_responses() { global $DB; $responses = array(); if ($this->completed) { $responses = $DB->get_records('feedback_value', ['completed' => $this->completed->id]); } return $responses; } /** * Returns all completed values for this feedback or just a value for an item * @param stdClass $item * @return array */ protected function get_values($item = null) { global $DB; if ($this->values === null) { $this->values = array(); $responses = $this->get_finished_responses(); foreach ($responses as $r) { $this->values[$r->item] = $r->value; } } if ($item) { return array_key_exists($item->id, $this->values) ? $this->values[$item->id] : null; } return $this->values; } /** * Splits the feedback items into pages * * Items that we definitely know at this stage as not applicable are excluded. * Items that are dependent on something that has not yet been answered are * still present, as well as items with broken dependencies. * * @return array array of arrays of items */ public function get_pages() { $pages = [[]]; // The first page always exists. $items = $this->get_items(); foreach ($items as $item) { if ($item->typ === 'pagebreak') { $pages[] = []; } else if ($this->can_see_item($item) !== false) { $pages[count($pages) - 1][] = $item; } } return $pages; } /** * Returns the last page that has items with the value (i.e. not label) which have been answered * as well as the first page that has items with the values that have not been answered. * * Either of the two return values may be null if there are no answered page or there are no * unanswered pages left respectively. * * Two pages may not be directly following each other because there may be empty pages * or pages with information texts only between them * * @return array array of two elements [$lastcompleted, $firstincompleted] */ protected function get_last_completed_page() { $completed = []; $incompleted = []; $pages = $this->get_pages(); foreach ($pages as $pageidx => $pageitems) { foreach ($pageitems as $item) { if ($item->hasvalue) { if ($this->get_values_tmp($item) !== null) { $completed[$pageidx] = true; } else { $incompleted[$pageidx] = true; } } } } $completed = array_keys($completed); $incompleted = array_keys($incompleted); // If some page has both completed and incompleted items it is considered incompleted. $completed = array_diff($completed, $incompleted); // If the completed page follows an incompleted page, it does not count. $firstincompleted = $incompleted ? min($incompleted) : null; if ($firstincompleted !== null) { $completed = array_filter($completed, function($a) use ($firstincompleted) { return $a < $firstincompleted; }); } $lastcompleted = $completed ? max($completed) : null; return [$lastcompleted, $firstincompleted]; } /** * Get the next page for the feedback * * This is normally $gopage+1 but may be bigger if there are empty pages or * pages without visible questions. * * This method can only be called when questions on the current page are * already answered, otherwise it may be inaccurate. * * @param int $gopage current page * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions * @return int|null the index of the next page or null if this is the last page */ public function get_next_page($gopage, $strictcheck = true) { if ($strictcheck) { list($lastcompleted, $firstincompleted) = $this->get_last_completed_page(); if ($firstincompleted !== null && $firstincompleted <= $gopage) { return $firstincompleted; } } $pages = $this->get_pages(); for ($pageidx = $gopage + 1; $pageidx < count($pages); $pageidx++) { if (!empty($pages[$pageidx])) { return $pageidx; } } // No further pages in the feedback have any visible items. return null; } /** * Get the previous page for the feedback * * This is normally $gopage-1 but may be smaller if there are empty pages or * pages without visible questions. * * @param int $gopage current page * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions * @return int|null the index of the next page or null if this is the first page with items */ public function get_previous_page($gopage, $strictcheck = true) { if (!$gopage) { // If we are already on the first (0) page, there is definitely no previous page. return null; } $pages = $this->get_pages(); $rv = null; // Iterate through previous pages and find the closest one that has any items on it. for ($pageidx = $gopage - 1; $pageidx >= 0; $pageidx--) { if (!empty($pages[$pageidx])) { $rv = $pageidx; break; } } if ($rv === null) { // We are on the very first page that has items. return null; } if ($rv > 0 && $strictcheck) { // Check if this page is actually not past than first incompleted page. list($lastcompleted, $firstincompleted) = $this->get_last_completed_page(); if ($firstincompleted !== null && $firstincompleted < $rv) { return $firstincompleted; } } return $rv; } /** * Page index to resume the feedback * * When user abandones answering feedback and then comes back to it we should send him * to the first page after the last page he fully completed. * @return int */ public function get_resume_page() { list($lastcompleted, $firstincompleted) = $this->get_last_completed_page(); return $lastcompleted === null ? 0 : $this->get_next_page($lastcompleted, false); } /** * Creates a new record in the 'feedback_completedtmp' table for the current user/guest session * * @return stdClass record from feedback_completedtmp or false if not found */ protected function create_current_completed_tmp() { global $DB, $USER; $record = (object)['feedback' => $this->feedback->id]; if ($this->get_courseid()) { $record->courseid = $this->get_courseid(); } if ((isloggedin() || $USER->id != $this->userid) && !isguestuser($this->userid)) { $record->userid = $this->userid; } else { $record->guestid = sesskey(); } $record->timemodified = time(); $record->anonymous_response = $this->feedback->anonymous; $id = $DB->insert_record('feedback_completedtmp', $record); $this->completedtmp = $DB->get_record('feedback_completedtmp', ['id' => $id]); $this->valuestmp = null; return $this->completedtmp; } /** * If user has already completed the feedback, create the temproray values from last completed attempt * * @return stdClass record from feedback_completedtmp or false if not found */ public function create_completed_tmp_from_last_completed() { if (!$this->get_current_completed_tmp()) { $lastcompleted = $this->find_last_completed(); if ($lastcompleted) { $this->completedtmp = feedback_set_tmp_values($lastcompleted); } } return $this->completedtmp; } /** * Saves unfinished response to the temporary table * * This is called when user proceeds to the next/previous page in the complete form * and also right after the form submit. * After the form submit the {@link save_response()} is called to * move response from temporary table to completion table. * * @param stdClass $data data from the form mod_feedback_complete_form */ public function save_response_tmp($data) { global $DB; if (!$completedtmp = $this->get_current_completed_tmp()) { $completedtmp = $this->create_current_completed_tmp(); } else { $currentime = time(); $DB->update_record('feedback_completedtmp', ['id' => $completedtmp->id, 'timemodified' => $currentime]); $completedtmp->timemodified = $currentime; } // Find all existing values. $existingvalues = $DB->get_records_menu('feedback_valuetmp', ['completed' => $completedtmp->id], '', 'item, id'); // Loop through all feedback items and save the ones that are present in $data. $allitems = $this->get_items(); foreach ($allitems as $item) { if (!$item->hasvalue) { continue; } $keyname = $item->typ . '_' . $item->id; if (!isset($data->$keyname)) { // This item is either on another page or dependency was not met - nothing to save. continue; } $newvalue = ['item' => $item->id, 'completed' => $completedtmp->id, 'course_id' => $completedtmp->courseid]; // Convert the value to string that can be stored in 'feedback_valuetmp' or 'feedback_value'. $itemobj = feedback_get_item_class($item->typ); $newvalue['value'] = $itemobj->create_value($data->$keyname); // Update or insert the value in the 'feedback_valuetmp' table. if (array_key_exists($item->id, $existingvalues)) { $newvalue['id'] = $existingvalues[$item->id]; $DB->update_record('feedback_valuetmp', $newvalue); } else { $DB->insert_record('feedback_valuetmp', $newvalue); } } // Reset valuestmp cache. $this->valuestmp = null; } /** * Saves the response * * The form data has already been stored in the temporary table in * {@link save_response_tmp()}. This function copies the values * from the temporary table to the completion table. * It is also responsible for sending email notifications when applicable. */ public function save_response() { global $DB, $USER; $feedbackcompleted = $this->find_last_completed(); // If no record is found, change false to null for safe use in feedback_save_tmp_values. $feedbackcompleted = !$feedbackcompleted ? null : $feedbackcompleted; $feedbackcompletedtmp = $this->get_current_completed_tmp(); // Save values. $completedid = feedback_save_tmp_values($feedbackcompletedtmp, $feedbackcompleted); $this->completed = $DB->get_record('feedback_completed', array('id' => $completedid)); // Send email. if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO) { feedback_send_email($this->cm, $this->feedback, $this->cm->get_course(), $this->userid, $this->completed); } else { feedback_send_email_anonym($this->cm, $this->feedback, $this->cm->get_course()); } // Update completion state. $completion = new completion_info($this->cm->get_course()); if ((isloggedin() || $USER->id != $this->userid) && $completion->is_enabled($this->cm) && $this->cm->completion == COMPLETION_TRACKING_AUTOMATIC && $this->feedback->completionsubmit) { $completion->update_state($this->cm, COMPLETION_COMPLETE, $this->userid); } } /** * Retrieves the last completion record for the current user * * @return stdClass record from feedback_completed or false if not found */ public function find_last_completed() { global $DB, $USER; if ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid)) { // Not possible to retrieve completed feedback for guests. return false; } if ($this->is_anonymous()) { // Not possible to retrieve completed anonymous feedback. return false; } $params = array('feedback' => $this->feedback->id, 'userid' => $this->userid, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO ); if ($this->get_courseid()) { $params['courseid'] = $this->get_courseid(); } $this->completed = $DB->get_record('feedback_completed', $params); return $this->completed; } /** * Checks if user has capability to submit the feedback * * There is an exception for fully anonymous feedbacks when guests can complete * feedback without the proper capability. * * This should be followed by checking {@link can_submit()} because even if * user has capablity to complete, they may have already submitted feedback * and can not re-submit * * @return bool */ public function can_complete() { global $CFG, $USER; $context = context_module::instance($this->cm->id); if (has_capability('mod/feedback:complete', $context, $this->userid, false)) { return true; } if (!empty($CFG->feedback_allowfullanonymous) AND $this->feedback->course == SITEID AND $this->feedback->anonymous == FEEDBACK_ANONYMOUS_YES AND ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid))) { // Guests are allowed to complete fully anonymous feedback without having 'mod/feedback:complete' capability. return true; } return false; } /** * Checks if user is prevented from re-submission. * * This must be called after {@link can_complete()} * * @return bool */ public function can_submit() { if ($this->get_feedback()->multiple_submit == 0 ) { if ($this->is_already_submitted()) { return false; } } return true; } /** * Trigger module viewed event. * * @since Moodle 3.3 */ public function trigger_module_viewed() { $event = \mod_feedback\event\course_module_viewed::create_from_record($this->feedback, $this->cm, $this->cm->get_course()); $event->trigger(); } /** * Mark activity viewed for completion-tracking. * * @since Moodle 3.3 */ public function set_module_viewed() { global $CFG; require_once($CFG->libdir . '/completionlib.php'); $completion = new completion_info($this->cm->get_course()); $completion->set_module_viewed($this->cm, $this->userid); } /** * Process a page jump via the mod_feedback_complete_form. * * This function initializes the form and process the submission. * * @param int $gopage the current page * @param int $gopreviouspage if the user chose to go to the previous page * @return string the url to redirect the user (if any) * @since Moodle 3.3 */ public function process_page($gopage, $gopreviouspage = false) { global $PAGE; $urltogo = null; // Save the form for later during the request. $this->create_completed_tmp_from_last_completed(); $this->form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_COMPLETE, $this, 'feedback_complete_form', array('gopage' => $gopage)); if ($this->form->is_cancelled()) { // Form was cancelled - return to the course page. $urltogo = new moodle_url('/mod/feedback/view.php', ['id' => $this->get_cm()->id]); } else if ($this->form->is_submitted() && ($this->form->is_validated() || $gopreviouspage)) { // Form was submitted (skip validation for "Previous page" button). $data = $this->form->get_submitted_data(); $this->save_response_tmp($data); if (!empty($data->savevalues) || !empty($data->gonextpage)) { if (($nextpage = $this->get_next_page($gopage)) !== null) { if ($PAGE->has_set_url()) { $urltogo = new moodle_url($PAGE->url, array('gopage' => $nextpage)); } $this->jumpto = $nextpage; } else { $this->save_response(); if (!$this->get_feedback()->page_after_submit) { \core\notification::success(get_string('entries_saved', 'feedback')); } $this->justcompleted = true; } } else if (!empty($gopreviouspage)) { $prevpage = intval($this->get_previous_page($gopage)); if ($PAGE->has_set_url()) { $urltogo = new moodle_url($PAGE->url, array('gopage' => $prevpage)); } $this->jumpto = $prevpage; } } return $urltogo; } /** * Render the form with the questions. * * @return string the form rendered * @since Moodle 3.3 */ public function render_items() { return $this->form->render(); } } PKB\-)6  classes/course_map_form.phpnu[. /** * Contains class mod_feedback_course_map_form * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Form for mapping courses to the feedback * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_course_map_form extends moodleform { /** * Definition of the form */ public function definition() { $mform = $this->_form; $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $options = array('multiple' => true, 'includefrontpage' => true); $mform->addElement('course', 'mappedcourses', get_string('courses'), $options); $this->add_action_buttons(); } } PKB\P\WWclasses/complete_form.phpnu[. /** * Contains class mod_feedback_complete_form * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Class mod_feedback_complete_form * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_complete_form extends moodleform { /** @var int */ const MODE_COMPLETE = 1; /** @var int */ const MODE_PRINT = 2; /** @var int */ const MODE_EDIT = 3; /** @var int */ const MODE_VIEW_RESPONSE = 4; /** @var int */ const MODE_VIEW_TEMPLATE = 5; /** @var int */ protected $mode; /** @var mod_feedback_structure|mod_feedback_completion */ protected $structure; /** @var mod_feedback_completion */ protected $completion; /** @var int */ protected $gopage; /** @var bool */ protected $hasrequired = false; /** * Constructor * * @param int $mode * @param mod_feedback_structure $structure * @param string $formid CSS id attribute of the form * @param array $customdata */ public function __construct($mode, mod_feedback_structure $structure, $formid, $customdata = null) { $this->mode = $mode; $this->structure = $structure; $this->gopage = isset($customdata['gopage']) ? $customdata['gopage'] : 0; $isanonymous = $this->structure->is_anonymous() ? ' ianonymous' : ''; parent::__construct( customdata: $customdata, attributes: ['id' => $formid, 'class' => 'feedback_form' . $isanonymous], ); $this->set_display_vertical(); } /** * Form definition */ public function definition() { $mform = $this->_form; $mform->addElement('hidden', 'id', $this->get_cm()->id); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'courseid', $this->get_current_course_id()); $mform->setType('courseid', PARAM_INT); $mform->addElement('hidden', 'gopage'); $mform->setType('gopage', PARAM_INT); $mform->addElement('hidden', 'lastpage'); $mform->setType('lastpage', PARAM_INT); $mform->addElement('hidden', 'startitempos'); $mform->setType('startitempos', PARAM_INT); $mform->addElement('hidden', 'lastitempos'); $mform->setType('lastitempos', PARAM_INT); if (isloggedin() && !isguestuser() && $this->mode != self::MODE_EDIT && $this->mode != self::MODE_VIEW_TEMPLATE && $this->mode != self::MODE_VIEW_RESPONSE) { // Output information about the current mode (anonymous or not) in some modes. if ($this->structure->is_anonymous()) { $anonymousmodeinfo = get_string('anonymous', 'feedback'); } else { $anonymousmodeinfo = get_string('non_anonymous', 'feedback'); } $element = $mform->addElement('static', 'anonymousmode', '', get_string('mode', 'feedback') . ': ' . $anonymousmodeinfo); $element->setAttributes($element->getAttributes() + ['class' => 'feedback_mode']); } // Add buttons to go to previous/next pages and submit the feedback. if ($this->mode == self::MODE_COMPLETE) { $buttonarray = array(); $buttonarray[] = &$mform->createElement('submit', 'gopreviouspage', get_string('previous_page', 'feedback')); $buttonarray[] = &$mform->createElement('submit', 'gonextpage', get_string('next_page', 'feedback'), array('class' => 'form-submit')); $buttonarray[] = &$mform->createElement('submit', 'savevalues', get_string('save_entries', 'feedback'), array('class' => 'form-submit')); $buttonarray[] = &$mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); $mform->closeHeaderBefore('buttonar'); } if ($this->mode == self::MODE_COMPLETE) { $this->definition_complete(); } else { $this->definition_preview(); } // Set data. $this->set_data(array('gopage' => $this->gopage)); } /** * Called from definition_after_data() in the completion mode * * This will add only items from a current page to the feedback and adjust the buttons */ protected function definition_complete() { if (!$this->structure instanceof mod_feedback_completion) { // We should not really be here but just in case. return; } $pages = $this->structure->get_pages(); $gopage = $this->gopage; $pageitems = $pages[$gopage]; $hasnextpage = $gopage < count($pages) - 1; // Until we complete this page we can not trust get_next_page(). $hasprevpage = $gopage && ($this->structure->get_previous_page($gopage, false) !== null); // Add elements. foreach ($pageitems as $item) { $itemobj = feedback_get_item_class($item->typ); $itemobj->complete_form_element($item, $this); } // Remove invalid buttons (for example, no "previous page" if we are on the first page). if (!$hasprevpage) { $this->remove_button('gopreviouspage'); } if (!$hasnextpage) { $this->remove_button('gonextpage'); } if ($hasnextpage) { $this->remove_button('savevalues'); } } /** * Called from definition_after_data() in all modes except for completion * * This will add all items to the form, including pagebreaks as horizontal rules. */ protected function definition_preview() { $this->_form->addElement('html', html_writer::start_div('', ['data-region' => 'questions-sortable-list'])); foreach ($this->structure->get_items() as $feedbackitem) { $itemobj = feedback_get_item_class($feedbackitem->typ); $itemobj->complete_form_element($feedbackitem, $this); } $this->_form->addElement('html', html_writer::end_div()); } /** * Removes the button that is not applicable for the current page * * @param string $buttonname */ private function remove_button($buttonname) { $el = $this->_form->getElement('buttonar'); foreach ($el->_elements as $idx => $button) { if ($button instanceof MoodleQuickForm_submit && $button->getName() === $buttonname) { unset($el->_elements[$idx]); return; } } } /** * Returns value for this element that is already stored in temporary or permanent table, * usually only available when user clicked "Previous page". Null means no value is stored. * * @param stdClass $item * @return string */ public function get_item_value($item) { if ($this->structure instanceof mod_feedback_completion) { return $this->structure->get_item_value($item); } return null; } /** * Can be used by the items to get the course id for which feedback is taken * * This function returns 0 for feedbacks that are located inside the courses. * $this->get_feedback()->course will return the course where feedback is located. * $this->get_current_course_id() will return the course where user was before taking the feedback * * @return int */ public function get_course_id() { return $this->structure->get_courseid(); } /** * Record from 'feedback' table corresponding to the current feedback * @return stdClass */ public function get_feedback() { return $this->structure->get_feedback(); } /** * Current feedback mode, see constants on the top of this class * @return int */ public function get_mode() { return $this->mode; } /** * Returns whether the form is frozen, some items may prefer to change the element * type in case of frozen form. For example, text or textarea element does not look * nice when frozen * * @return bool */ public function is_frozen() { return $this->mode == self::MODE_VIEW_RESPONSE; } /** * Returns whether the form is considered read-only (e.g. when previewing it) * * @return bool */ private function is_readonly(): bool { return $this->mode === self::MODE_PRINT; } /** * Returns the current course module * @return cm_info */ public function get_cm() { return $this->structure->get_cm(); } /** * Returns the course where user was before taking the feedback. * * For feedbacks inside the course it will be the same as $this->get_feedback()->course. * For feedbacks on the frontpage it will be the same as $this->get_course_id() * * @return int */ public function get_current_course_id() { return $this->structure->get_courseid() ?: $this->get_feedback()->course; } /** * CSS class for the item * @param stdClass $item * @return string */ protected function get_suggested_class($item) { $class = "feedback_itemlist feedback-item-{$item->typ}"; if ($item->typ !== 'pagebreak') { $itemobj = feedback_get_item_class($item->typ); if ($itemobj->get_hasvalue()) { $class .= " feedback_hasvalue"; } } return $class; } /** * Adds an element to this form - to be used by items in their complete_form_element() method * * @param stdClass $item * @param HTML_QuickForm_element|array $element either completed form element or an array that * can be passed as arguments to $this->_form->createElement() function * @param bool $addrequiredrule automatically add 'required' rule * @param bool $setdefaultvalue automatically set default value for element * @return HTML_QuickForm_element */ public function add_form_element($item, $element, $addrequiredrule = true, $setdefaultvalue = true) { global $OUTPUT; if (is_array($element) && $element[0] == 'group') { // For groups, use the mforms addGroup API. // $element looks like: ['group', $groupinputname, $name, $objects, $separator, $appendname], $element = $this->_form->addGroup($element[3], $element[1], $element[2], $element[4], $element[5]); } else { // Add non-group element to the form. if (is_array($element)) { $element = call_user_func_array(array($this->_form, 'createElement'), $element); } $element = $this->_form->addElement($element); } // Prepend standard CSS classes to the element classes. $attributes = $element->getAttributes(); $class = !empty($attributes['class']) ? ' ' . $attributes['class'] : ''; $attributes['class'] = $this->get_suggested_class($item) . $class; $element->setAttributes($attributes); // Add required rule. if ($item->required && $addrequiredrule) { $this->_form->addRule($element->getName(), get_string('required'), 'required', null, 'client'); } // Set default value. if ($setdefaultvalue && ($tmpvalue = $this->get_item_value($item))) { $this->_form->setDefault($element->getName(), htmlspecialchars_decode($tmpvalue, ENT_QUOTES)); } // Freeze if needed. if ($this->is_frozen()) { $element->freeze(); } // For read-only forms, just disable each added element. if ($this->is_readonly()) { $this->_form->disabledIf($element->getName(), 'id'); } // Add red asterisks on required fields. if ($item->required) { $requiredlabel = get_string('requiredelement', 'form'); $pixparams = [ 'class' => 'ms-2', 'title' => $requiredlabel, ]; $required = $OUTPUT->pix_icon('req', '', 'moodle', $pixparams) . \core\output\html_writer::span("($requiredlabel)", 'visually-hidden'); $element->setLabel($element->getLabel() . $required); $this->hasrequired = true; } // Add different useful stuff to the question name. $this->add_item_label($item, $element); $this->add_item_dependencies($item, $element); $this->add_item_number($item, $element); if ($this->mode == self::MODE_EDIT) { $this->enhance_name_for_edit($item, $element); } return $element; } /** * Adds a group element to this form - to be used by items in their complete_form_element() method * * @param stdClass $item * @param string $groupinputname name for the form element * @param string $name question text * @param array $elements array of arrays that can be passed to $this->_form->createElement() * @param string $separator separator between group elements * @param string $class additional CSS classes for the form element * @return HTML_QuickForm_element */ public function add_form_group_element($item, $groupinputname, $name, $elements, $separator, $class = '') { $objects = array(); foreach ($elements as $element) { $object = call_user_func_array(array($this->_form, 'createElement'), $element); $objects[] = $object; } $element = $this->add_form_element($item, ['group', $groupinputname, $name, $objects, $separator, false], false, false); if ($class !== '') { $attributes = $element->getAttributes(); $attributes['class'] .= ' ' . $class; $element->setAttributes($attributes); } return $element; } /** * Adds an item number to the question name (if feedback autonumbering is on) * @param stdClass $item * @param HTML_QuickForm_element $element */ protected function add_item_number($item, $element) { if ($this->get_feedback()->autonumbering && !empty($item->itemnr)) { $name = $element->getLabel(); $element->setLabel(html_writer::span($item->itemnr. '.', 'itemnr') . ' ' . $name); } } /** * Adds an item label to the question name * @param stdClass $item * @param HTML_QuickForm_element $element */ protected function add_item_label($item, $element) { if (strlen($item->label) && ($this->mode == self::MODE_EDIT || $this->mode == self::MODE_VIEW_TEMPLATE)) { $name = get_string('nameandlabelformat', 'mod_feedback', (object)['label' => format_string($item->label), 'name' => $element->getLabel()]); $element->setLabel($name); } } /** * Adds a dependency description to the question name * @param stdClass $item * @param HTML_QuickForm_element $element */ protected function add_item_dependencies($item, $element) { $allitems = $this->structure->get_items(); if ($item->dependitem && ($this->mode == self::MODE_EDIT || $this->mode == self::MODE_VIEW_TEMPLATE)) { if (isset($allitems[$item->dependitem])) { $dependitem = $allitems[$item->dependitem]; $name = $element->getLabel(); $name .= html_writer::span(' ('.format_string($dependitem->label).'->'.$item->dependvalue.')', 'feedback_depend'); $element->setLabel($name); } } } /** * Returns the CSS id attribute that will be assigned by moodleform later to this element * @param stdClass $item * @param HTML_QuickForm_element $element */ protected function guess_element_id($item, $element) { if (!$id = $element->getAttribute('id')) { $attributes = $element->getAttributes(); $id = $attributes['id'] = 'feedback_item_' . $item->id; $element->setAttributes($attributes); } if ($element->getType() === 'group') { return 'fgroup_' . $id; } return 'fitem_' . $id; } /** * Adds editing actions to the question name in the edit mode * @param stdClass $item * @param HTML_QuickForm_element $element */ protected function enhance_name_for_edit($item, $element) { global $OUTPUT; $menu = new action_menu(); $menu->set_owner_selector('#' . $this->guess_element_id($item, $element)); $menu->set_kebab_trigger(get_string('edit')); $menu->prioritise = true; $itemobj = feedback_get_item_class($item->typ); $actions = $itemobj->edit_actions($item, $this->get_feedback(), $this->get_cm()); foreach ($actions as $action) { $menu->add($action); } $menudata = $menu->export_for_template($OUTPUT); $element->setLabel(html_writer::span($element->getLabel(), '', [ 'data-item-actions-menu' => json_encode($menudata), ])); } /** * Sets the default value for form element - alias to $this->_form->setDefault() * @param HTML_QuickForm_element|string $element * @param mixed $defaultvalue */ public function set_element_default($element, $defaultvalue) { if ($element instanceof HTML_QuickForm_element) { $element = $element->getName(); } $this->_form->setDefault($element, $defaultvalue); } /** * Sets the default value for form element - wrapper to $this->_form->setType() * @param HTML_QuickForm_element|string $element * @param int $type */ public function set_element_type($element, $type) { if ($element instanceof HTML_QuickForm_element) { $element = $element->getName(); } $this->_form->setType($element, $type); } /** * Adds a validation rule for the given field - wrapper for $this->_form->addRule() * * Do not use for 'required' rule! * Required * will be added automatically, if additional validation is needed * use method {@link self::add_validation_rule()} * * @param string $element Form element name * @param string $message Message to display for invalid data * @param string $type Rule type, use getRegisteredRules() to get types * @param string $format (optional)Required for extra rule data * @param string $validation (optional)Where to perform validation: "server", "client" * @param bool $reset Client-side validation: reset the form element to its original value if there is an error? * @param bool $force Force the rule to be applied, even if the target form element does not exist */ public function add_element_rule($element, $message, $type, $format = null, $validation = 'server', $reset = false, $force = false) { if ($element instanceof HTML_QuickForm_element) { $element = $element->getName(); } $this->_form->addRule($element, $message, $type, $format, $validation, $reset, $force); } /** * Adds a validation rule to the form * * @param callable $callback with arguments ($values, $files) */ public function add_validation_rule(callable $callback) { if ($this->mode == self::MODE_COMPLETE) { $this->_form->addFormRule($callback); } } /** * Returns a reference to the element - wrapper for function $this->_form->getElement() * * @param string $elementname Element name * @return HTML_QuickForm_element reference to element */ public function get_form_element($elementname) { return $this->_form->getElement($elementname); } /** * Displays the form */ public function display() { global $OUTPUT, $PAGE; // Finalize the form definition if not yet done. if (!$this->_definition_finalized) { $this->_definition_finalized = true; $this->definition_after_data(); } $mform = $this->_form; // Add "This form has required fields" text in the bottom of the form. if (($mform->_required || $this->hasrequired) && ($this->mode == self::MODE_COMPLETE || $this->mode == self::MODE_PRINT || $this->mode == self::MODE_VIEW_TEMPLATE)) { $element = $mform->addElement('static', 'requiredfields', '', get_string('somefieldsrequired', 'form', $OUTPUT->pix_icon('req', get_string('requiredelement', 'form')))); $element->setAttributes($element->getAttributes() + ['class' => 'requirednote']); } // Reset _required array so the default red * are not displayed. $mform->_required = array(); // Move buttons to the end of the form. if ($this->mode == self::MODE_COMPLETE) { $mform->addElement('hidden', '__dummyelement'); $buttons = $mform->removeElement('buttonar', false); $mform->insertElementBefore($buttons, '__dummyelement'); $mform->removeElement('__dummyelement'); } $this->_form->display(); } } PKB\0Tffclasses/manager.phpnu[. namespace mod_feedback; use cm_info; use stdClass; /** * Class manager for feedback * * @package mod_feedback * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** * Get the template record from the template id * * @param int $templateid * @return stdClass */ public static function get_template_record(int $templateid): stdClass { global $DB; return $DB->get_record('feedback_template', ['id' => $templateid], '*', MUST_EXIST); } /** * Check if the current user can see other users if in groups * * @param cm_info $cm * @return bool */ public static function can_see_others_in_groups(cm_info $cm): bool { $canaccessallgroups = has_capability('moodle/site:accessallgroups', $cm->context); if ($canaccessallgroups) { return true; } $course = $cm->get_course(); $groupmode = groups_get_activity_groupmode($cm, $course); $usergroups = groups_get_user_groups($course->id); return ($groupmode != SEPARATEGROUPS || !empty($usergroups['0'])); } } PKB\{classes/observer.phpnu[. /** * Event observers supported by this module * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Event observers supported by this module * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_observer { /** * Observer for the even course_content_deleted - delete all course templates. * * @param \core\event\course_content_deleted $event */ public static function course_content_deleted(\core\event\course_content_deleted $event) { global $DB; // Delete all templates of given course. $DB->delete_records('feedback_template', array('course' => $event->objectid)); } } PKB\"classes/templates_table.phpnu[. /** * Contains class mod_feedback_templates_table * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use core\output\notification; use core\output\action_menu; use core\output\action_link; use core\output\action_menu\link_secondary; use core\output\actions\confirm_action; global $CFG; require_once($CFG->libdir . '/tablelib.php'); /** * Class mod_feedback_templates_table * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_templates_table extends core_table\flexible_table { /** @var int|null The module id. */ private $cmid; /** * Constructor * @param int $uniqueid all tables have to have a unique id, this is used * as a key when storing table properties like sort order in the session. * @param moodle_url $baseurl * @param string|null $mode This parameter has been deprecated since 4.5 and should not be used anymore. */ public function __construct($uniqueid, $baseurl, ?string $mode = null) { if ($mode !== null) { debugging( 'The age argument has been deprecated. Please remove it from your method calls.', DEBUG_DEVELOPER, ); } parent::__construct($uniqueid); $this->cmid = $baseurl->param('id'); $tablecolumns = [ 'template' => get_string('template', 'feedback'), 'actions' => html_writer::span(get_string('actions'), 'visually-hidden'), ]; $this->set_attribute('class', 'templateslist'); $this->define_columns(array_keys($tablecolumns)); $this->define_headers(array_values($tablecolumns)); $this->define_baseurl($baseurl); $this->column_class('template', 'template'); $this->sortable(false); } /** * Displays the table with the given set of templates * @param array $templates */ public function display($templates) { global $OUTPUT; if (empty($templates)) { echo $OUTPUT->notification( get_string('no_templates_available_yet', 'feedback'), notification::NOTIFY_INFO, false, ); return; } $this->setup(); foreach ($templates as $template) { $showactions = has_any_capability( ['mod/feedback:deletetemplate', 'mod/feedback:edititems', 'mod/feedback:createpublictemplate'], $this->get_context() ); $data = [ format_string($template->name), $showactions ? $OUTPUT->render($this->get_row_actions($template)) : '', ]; $this->add_data($data); } $this->finish_output(); } /** * Get the row actions for the given template * * @param stdClass $template * @return action_menu */ private function get_row_actions(stdClass $template): action_menu { global $PAGE, $OUTPUT; $url = new moodle_url($this->baseurl, ['templateid' => $template->id, 'sesskey' => sesskey()]); $actions = new action_menu(); $label = get_string('actions'); $triggercontent = $OUTPUT->pix_icon('a/setting', '') . \core\output\html_writer::span($label, 'visually-hidden d-inline-block'); $actions->set_menu_trigger($triggercontent, 'btn btn-link btn-sm'); $actions->triggerattributes['title'] = $label; // Preview. $actions->add(new link_secondary( new moodle_url($this->baseurl, ['templateid' => $template->id, 'sesskey' => sesskey()]), new pix_icon('t/preview', ''), get_string('preview'), )); // Use template. if (has_capability('mod/feedback:edititems', context_module::instance($this->cmid))) { $PAGE->requires->js_call_amd('mod_feedback/usetemplate', 'init'); $actions->add(new link_secondary( new moodle_url('#'), new pix_icon('i/files', ''), get_string('use_this_template', 'mod_feedback'), ['data-action' => 'usetemplate', 'data-dataid' => $this->cmid, 'data-templateid' => $template->id], )); } // Delete. $showdelete = has_capability('mod/feedback:deletetemplate', context_module::instance($this->cmid)); if ($template->ispublic) { $showdelete = has_all_capabilities( ['mod/feedback:createpublictemplate', 'mod/feedback:deletetemplate'], context_system::instance() ); } if ($showdelete) { $exporturl = new moodle_url( '/mod/feedback/manage_templates.php', $url->params() + ['deletetemplate' => $template->id] ); $deleteaction = new action_link( $exporturl, get_string('delete'), new confirm_action(get_string('confirmdeletetemplate', 'feedback')), ['class' => 'text-danger'], new pix_icon('t/delete', ''), ); $actions->add_secondary_action($deleteaction); } return $actions; } } PKB\Mclasses/search/activity.phpnu[. /** * Search area for mod_feedback activities. * * @package mod_feedback * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\search; defined('MOODLE_INTERNAL') || die(); /** * Search area for mod_feedback activities. * * @package mod_feedback * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } } PKB\۩19  classes/output/renderer.phpnu[. namespace mod_feedback\output; use plugin_renderer_base; /** * Class renderer * * @package mod_feedback * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * Generate the tertiary nav * * @param base_action_bar $actionmenu * @return bool|string */ public function main_action_bar(base_action_bar $actionmenu) { $context = $actionmenu->export_for_template($this); return $this->render_from_template('mod_feedback/main_action_menu', $context); } /** * Render the create template form * * @param int $id * @return bool|string * @deprecated since 4.5 * @todo MDL-82164 This will be deleted in Moodle 6.0. */ #[\core\attribute\deprecated(replacement: null, since: '4.5')] public function create_template_form(int $id) { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); return $this->render_from_template('mod_feedback/create_template', ['id' => $id]); } /** * Builds the feedback page title. * * @param array $titleparts the different parts to add to the title. * @param string $pagetitle the page title to add to the rest of the parts. Empty by default. */ public function set_title(array $titleparts, string $pagetitle = '') { $title = implode(\moodle_page::TITLE_SEPARATOR, $titleparts); if (!empty($pagetitle)) { $title = $pagetitle . ': ' . $title; } $this->page->set_title($title); } } PKB\{Jyyclasses/output/summary.phpnu[. /** * Contains class mod_feedback\output\summary * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\output; use renderable; use templatable; use renderer_base; use stdClass; use moodle_url; use mod_feedback_structure; /** * Class to help display feedback summary * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class summary implements renderable, templatable { /** @var mod_feedback_structure */ protected $feedbackstructure; /** @var int */ protected $mygroupid; /** * Constructor. * * @param mod_feedback_structure $feedbackstructure * @param int $mygroupid currently selected group */ public function __construct($feedbackstructure, $mygroupid = false) { $this->feedbackstructure = $feedbackstructure; $this->mygroupid = $mygroupid; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { $r = new stdClass(); $r->completedcount = $this->feedbackstructure->count_completed_responses($this->mygroupid); $r->itemscount = count($this->feedbackstructure->get_items(true)); return $r; } } PKB\xt+classes/output/edit_template_action_bar.phpnu[. namespace mod_feedback\output; use confirm_action; use context_system; use moodle_url; use pix_icon; use core\output\action_menu; use core\output\action_link; use core\output\action_menu\link as action_menu_link; use mod_feedback\manager; /** * Class actionbar - Display the action bar * * @package mod_feedback * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class edit_template_action_bar extends base_action_bar { /** @var int $templateid The template that is being edited/used */ private $templateid; /** * edit_template_action_bar constructor. * @param int $cmid * @param int $templateid * @param string|null $mode This parameter has been deprecated since 4.5 and should not be used anymore. */ public function __construct(int $cmid, int $templateid, ?string $mode = null) { if ($mode !== null) { debugging( 'The age argument has been deprecated. Please remove it from your method calls.', DEBUG_DEVELOPER, ); } parent::__construct($cmid); $this->templateid = $templateid; } /** * Return the items to be used in the tertiary nav * * @return array */ public function get_items(): array { global $PAGE; $templateurl = new moodle_url('/mod/feedback/manage_templates.php', $this->urlparams); $template = manager::get_template_record($this->templateid); // Back button. $items['left'][]['actionlink'] = new action_link($templateurl, get_string('back'), null, ['class' => 'btn btn-secondary']); // Actions. if (has_capability('mod/feedback:edititems', $this->context)) { $actionsselect = new action_menu(); $actionsselect->set_menu_trigger(get_string('actions'), 'btn btn-outline-primary'); $PAGE->requires->js_call_amd('mod_feedback/usetemplate', 'init'); // Use template. $actionsselect->add(new action_menu_link( new moodle_url('#'), new pix_icon('i/files', get_string('preview')), get_string('use_this_template', 'mod_feedback'), false, ['data-action' => 'usetemplate', 'data-dataid' => $this->cmid, 'data-templateid' => $this->templateid], )); } // Delete. $showdelete = has_capability('mod/feedback:deletetemplate', $this->context); if ($template->ispublic) { $showdelete = has_all_capabilities( ['mod/feedback:createpublictemplate', 'mod/feedback:deletetemplate'], context_system::instance() ); } if ($showdelete) { $params = $this->urlparams + [ 'deletetemplate' => $this->templateid, 'sesskey' => sesskey() ]; $deleteurl = new moodle_url('/mod/feedback/manage_templates.php', $params); $deleteaction = new action_link( $deleteurl, get_string('delete'), new confirm_action(get_string('confirmdeletetemplate', 'feedback')), ['class' => 'text-danger'], new pix_icon('t/delete', get_string('delete_template', 'feedback')), ); $actionsselect->add_secondary_action($deleteaction); } $items['actionsselect'] = count($actionsselect->get_primary_actions()) > 0 ? $actionsselect : null; return $items; } } PKB\yy&classes/output/standard_action_bar.phpnu[. namespace mod_feedback\output; use action_link; use moodle_url; /** * Class standard_action_bar * * The default tertiary nav on the module landing page * * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ class standard_action_bar extends base_action_bar { /** @var int $startpage The page to resume with. */ private $startpage; /** @var int $viewcompletion Whether or not the user can finish the feedback */ private $viewcompletion; /** * standard_action_bar constructor. * * @param int $cmid * @param bool $viewcompletion Whether or not the user can finish the feedback * @param int|null $startpage The page to resume with. * @param int|null $courseid The course that the feedback is being accessed from. If null, courseid will be * set via the $cmid relationship */ public function __construct(int $cmid, bool $viewcompletion, ?int $startpage = null, ?int $courseid = null) { parent::__construct($cmid); $this->startpage = $startpage; $this->viewcompletion = $viewcompletion; if ($courseid && $courseid != $this->course->id) { $this->course = get_course($courseid); } $this->urlparams['courseid'] = $this->course->id; } /** * Return the items to be used in the tertiary nav * * @return array */ public function get_items(): array { $items = []; if (has_capability('mod/feedback:edititems', $this->context)) { $editurl = new moodle_url('/mod/feedback/edit.php', $this->urlparams); $items['left'][]['actionlink'] = new action_link($editurl, get_string('edit_items', 'feedback'), null, ['class' => 'btn btn-secondary']); } // The preview icon should be displayed only to users with capability to edit or view reports (to include // non-editing teachers too). $capabilities = [ 'mod/feedback:edititems', 'mod/feedback:viewreports', ]; if (has_any_capability($capabilities, $this->context)) { $previewlnk = new moodle_url('/mod/feedback/print.php', array('id' => $this->cmid)); if ($this->course->id) { $previewlnk->param('courseid', $this->course->id); } $items['left'][]['actionlink'] = new action_link($previewlnk, get_string('previewquestions', 'feedback'), null, ['class' => 'btn btn-secondary']); } if ($this->viewcompletion) { // Display a link to complete feedback or resume. $completeurl = new moodle_url('/mod/feedback/complete.php', ['id' => $this->cmid, 'courseid' => $this->course->id]); if ($this->startpage) { $completeurl->param('gopage', $this->startpage); $label = get_string('continue_the_form', 'feedback'); } else { $label = get_string('complete_the_form', 'feedback'); } $items['left'][]['actionlink'] = new action_link($completeurl, $label, null, ['class' => 'btn btn-primary']); } return $items; } } PKB\v"classes/output/edit_action_bar.phpnu[. namespace mod_feedback\output; use moodle_url; use action_menu; use action_menu_link; use pix_icon; /** * Class actionbar - Display the action bar * * @package mod_feedback * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class edit_action_bar extends base_action_bar { /** @var moodle_url $currenturl The current page url */ private $currenturl; /** @var int|null $lastposition The index of the last question type in the feedback module */ private $lastposition; /** * edit_action_bar constructor. * * @param int $cmid The course module id * @param moodle_url $pageurl The current page url * @param int|null $lastposition Index of the last question in the feedback */ public function __construct(int $cmid, moodle_url $pageurl, ?int $lastposition = null) { parent::__construct($cmid); $this->currenturl = $pageurl; $this->lastposition = $lastposition; } /** * Return the items to be used for the tertiary nav * * @return array */ public function get_items(): array { if (!has_capability('mod/feedback:edititems', $this->context)) { return []; } return [ 'addselect' => $this->get_add_question_menu(), 'actionsselect' => $this->get_edit_actions_menu(), ]; } /** * Return the add question menu * * @return action_menu */ private function get_add_question_menu(): action_menu { $addselect = new action_menu(); $addselect->set_menu_trigger(get_string('add_item', 'mod_feedback'), 'btn btn-primary'); $addselect->set_menu_left(); $addselectparams = ['cmid' => $this->cmid, 'position' => $this->lastposition, 'sesskey' => sesskey()]; foreach (feedback_load_feedback_items_options() as $key => $value) { $addselect->add(new action_menu_link( new moodle_url('/mod/feedback/edit_item.php', $addselectparams + ['typ' => $key]), null, $value, false, )); } return $addselect; } /** * Return the edit actions menu * * @return action_menu */ private function get_edit_actions_menu(): action_menu { global $DB, $PAGE; $actionsselect = new action_menu(); $actionsselect->set_menu_trigger(get_string('actions'), 'btn btn-outline-primary'); $hasitems = $DB->record_exists('feedback_item', ['feedback' => $this->feedback->id]); // Export. if ($hasitems) { $exporturl = new moodle_url('/mod/feedback/export.php', $this->urlparams + ['action' => 'exportfile']); $actionsselect->add(new action_menu_link( $exporturl, new pix_icon('i/file_export', get_string('export_questions', 'feedback')), get_string('export_questions', 'feedback'), false, )); } // Import. $importurl = new moodle_url('/mod/feedback/import.php', $this->urlparams); $actionsselect->add(new action_menu_link( $importurl, new pix_icon('i/file_import', get_string('import_questions', 'feedback')), get_string('import_questions', 'feedback'), false, )); // Save as template. $cancreatetemplates = has_any_capability([ 'mod/feedback:createprivatetemplate', 'mod/feedback:createpublictemplate'], \context_module::instance($this->cmid)); if ($cancreatetemplates && $hasitems) { $PAGE->requires->js_call_amd('mod_feedback/createtemplate', 'init'); $actionsselect->add(new action_menu_link( new moodle_url('#'), new pix_icon('i/file_plus', get_string('save_as_new_template', 'feedback')), get_string('save_as_new_template', 'feedback'), false, ['data-action' => 'createtemplate', 'data-dataid' => $this->cmid], )); } return $actionsselect; } } PKB\c 'classes/output/responses_action_bar.phpnu[. namespace mod_feedback\output; use moodle_url; use url_select; /** * Class responses_action_bar. The tertiary nav for the responses page * * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ class responses_action_bar extends base_action_bar { /** @var moodle_url $currenturl The current page url */ private $currenturl; /** * responses_action_bar constructor. * * @param int $cmid The cmid for the module we are operating on * @param moodle_url $pageurl The current page url */ public function __construct(int $cmid, moodle_url $pageurl) { parent::__construct($cmid); $this->currenturl = $pageurl; $this->urlparams['courseid'] = $this->course->id; } /** * Return the items to be used in the tertiary nav * * @return array */ public function get_items(): array { $items = []; if (has_capability('mod/feedback:viewreports', $this->context)) { $reporturl = new moodle_url('/mod/feedback/show_entries.php', $this->urlparams); $options[$reporturl->out(false)] = get_string('show_entries', 'feedback'); $selected = $this->currenturl->compare($reporturl, URL_MATCH_BASE) ? $reporturl : $this->currenturl; if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO && $this->course->id != SITEID) { $nonrespondenturl = new moodle_url('/mod/feedback/show_nonrespondents.php', $this->urlparams); $options[$nonrespondenturl->out(false)] = get_string('show_nonrespondents', 'feedback'); $selected = $this->currenturl->compare($nonrespondenturl, URL_MATCH_BASE) ? $nonrespondenturl : $this->currenturl;; } // Don't show the dropdown if it's only a single item. if (count($options) != 1) { $items['left'][]['urlselect'] = new url_select($options, $selected->out(false), null); } } return $items; } } PKB\ME "classes/output/base_action_bar.phpnu[. namespace mod_feedback\output; use context_module; use renderable; use renderer_base; use templatable; /** * Class base_action_bar * * Base class to be inherited by any other feedback action bar * * @package mod_feedback * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base_action_bar implements renderable, templatable { /** @var int $cmid The module id */ protected $cmid; /** @var object $context The context we are in */ protected $context; /** @var object $course The course we are in */ protected $course; /** @var array $urlparams The default params to be used when creating urls */ protected $urlparams; /** @var object $feedback The activity record that is being viewed */ protected $feedback; /** * base_action_bar constructor. * * @param int $cmid */ public function __construct(int $cmid) { global $PAGE; $this->cmid = $cmid; $this->context = context_module::instance($cmid); [$course, $cm] = get_course_and_cm_from_cmid($cmid); $this->course = $course; $this->urlparams = [ 'id' => $cmid ]; $this->feedback = $PAGE->activityrecord; } /** * Recursively iterates through to array of renderables and exports * * @param array $items Collection of renderables * @param renderer_base $output * @return array $items Data to be used in the mustache template */ private function export_items_for_template(array $items, renderer_base $output): array { $items = array_map(function($item) use ($output) { if (is_array($item)) { return $this->export_items_for_template($item, $output); } if (is_object($item) && method_exists($item, 'export_for_template')) { return $item->export_for_template($output); } return $item; }, $items); return $items; } /** * Export the data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return array */ public function export_for_template(renderer_base $output): array { $items = $this->export_items_for_template($this->get_items(), $output); return $items; } /** * Function to generate a list of renderables to be displayed * @return array */ abstract protected function get_items(): array; } PKB\Cad classes/responses_anon_table.phpnu[. /** * Contains class mod_feedback_responses_anon_table * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Class mod_feedback_responses_anon_table * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_responses_anon_table extends mod_feedback_responses_table { /** @var string */ protected $showallparamname = 'ashowall'; /** @var string */ protected $downloadparamname = 'adownload'; /** * Initialises table * @param int $group retrieve only users from this group (optional) */ public function init($group = 0) { $cm = $this->feedbackstructure->get_cm(); $this->uniqueid = 'feedback-showentry-anon-list-' . $cm->instance; // There potentially can be both tables with anonymouns and non-anonymous responses on // the same page (for example when feedback anonymity was changed after some people // already responded). In this case we need to distinguish tables' pagination parameters. $this->request[TABLE_VAR_PAGE] = 'apage'; $tablecolumns = ['random_response']; $tableheaders = [get_string('response_nr', 'feedback')]; if ($this->feedbackstructure->get_feedback()->course == SITEID && !$this->feedbackstructure->get_courseid()) { $tablecolumns[] = 'courseid'; $tableheaders[] = get_string('course'); } $this->define_columns($tablecolumns); $this->define_headers($tableheaders); $this->sortable(true, 'random_response'); $this->collapsible(true); $this->set_attribute('id', 'showentryanontable'); $params = ['instance' => $cm->instance, 'anon' => FEEDBACK_ANONYMOUS_YES, 'courseid' => $this->feedbackstructure->get_courseid()]; $fields = 'c.id, c.random_response, c.courseid'; $from = '{feedback_completed} c'; $where = 'c.anonymous_response = :anon AND c.feedback = :instance'; if ($this->feedbackstructure->get_courseid()) { $where .= ' AND c.courseid = :courseid'; } $group = (empty($group)) ? groups_get_activity_group($this->feedbackstructure->get_cm(), true) : $group; if ($group) { $where .= ' AND c.userid IN (SELECT g.userid FROM {groups_members} g WHERE g.groupid = :group)'; $params['group'] = $group; } $this->set_sql($fields, $from, $where, $params); $this->set_count_sql("SELECT COUNT(c.id) FROM $from WHERE $where", $params); } /** * Returns a link for viewing a single response * @param stdClass $row * @return \moodle_url */ protected function get_link_single_entry($row) { return new moodle_url($this->baseurl, ['showcompleted' => $row->id]); } /** * Prepares column reponse for display * @param stdClass $row * @return string */ public function col_random_response($row) { if ($this->is_downloading()) { return $row->random_response; } else { return html_writer::link($this->get_link_single_entry($row), get_string('response_nr', 'feedback').': '. $row->random_response); } } /** * Add data for the external structure that will be returned. * * @param stdClass $row a database query record row * @since Moodle 3.3 */ protected function add_data_for_external($row) { $this->dataforexternal[] = [ 'id' => $row->id, 'courseid' => $row->courseid, 'number' => $row->random_response, 'responses' => $this->get_responses_for_external($row) ]; } } PKB\nccclasses/responses_table.phpnu[. /** * Contains class mod_feedback_responses_table * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/tablelib.php'); /** * Class mod_feedback_responses_table * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_responses_table extends table_sql { /** * Maximum number of feedback questions to display in the "Show responses" table */ const PREVIEWCOLUMNSLIMIT = 10; /** * Maximum number of feedback questions answers to retrieve in one SQL query. * Mysql has a limit of 60, we leave 1 for joining with users table. */ const TABLEJOINLIMIT = 59; /** * When additional queries are needed to retrieve more than TABLEJOINLIMIT questions answers, do it in chunks every x rows. * Value too small will mean too many DB queries, value too big may cause memory overflow. */ const ROWCHUNKSIZE = 100; /** @var mod_feedback_structure */ protected $feedbackstructure; /** @var int */ protected $grandtotal = null; /** @var bool */ protected $showall = false; /** @var string */ protected $showallparamname = 'showall'; /** @var string */ protected $downloadparamname = 'download'; /** @var int number of columns that were not retrieved in the main SQL query * (no more than TABLEJOINLIMIT tables with values can be joined). */ protected $hasmorecolumns = 0; /** @var bool whether we are building this table for a external function */ protected $buildforexternal = false; /** @var array the data structure containing the table data for the external function */ protected $dataforexternal = []; /** @var bool true if elements per page > 0, otherwise false. */ protected $pageable; /** * Constructor * * @param mod_feedback_structure $feedbackstructure * @param int $group retrieve only users from this group (optional) */ public function __construct(mod_feedback_structure $feedbackstructure, $group = 0) { $this->feedbackstructure = $feedbackstructure; parent::__construct('feedback-showentry-list-' . $feedbackstructure->get_cm()->instance); $this->showall = optional_param($this->showallparamname, 0, PARAM_BOOL); $this->define_baseurl(new moodle_url('/mod/feedback/show_entries.php', ['id' => $this->feedbackstructure->get_cm()->id])); if ($courseid = $this->feedbackstructure->get_courseid()) { $this->baseurl->param('courseid', $courseid); } if ($this->showall) { $this->baseurl->param($this->showallparamname, $this->showall); } $name = format_string($feedbackstructure->get_feedback()->name); $this->is_downloadable(true); $this->is_downloading(optional_param($this->downloadparamname, 0, PARAM_ALPHA), $name, get_string('responses', 'feedback')); $this->useridfield = 'userid'; $this->init($group); } /** * Initialises table * @param int $group retrieve only users from this group (optional) */ protected function init($group = 0) { $tablecolumns = array('userpic', 'fullname', 'groups'); $tableheaders = array( get_string('userpic'), get_string('fullnameuser'), get_string('groups') ); // TODO Does not support custom user profile fields (MDL-70456). $userfieldsapi = \core_user\fields::for_identity($this->get_context(), false)->with_userpic(); $ufields = $userfieldsapi->get_sql('u', false, '', $this->useridfield, false)->selects; $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); $fields = 'c.id, c.timemodified as completed_timemodified, c.courseid, '.$ufields; $from = '{feedback_completed} c ' . 'JOIN {user} u ON u.id = c.userid AND u.deleted = :notdeleted'; $where = 'c.anonymous_response = :anon AND c.feedback = :instance'; if ($this->feedbackstructure->get_courseid()) { $where .= ' AND c.courseid = :courseid'; } if ($this->is_downloading()) { // When downloading data: // Remove 'userpic' from downloaded data. array_shift($tablecolumns); array_shift($tableheaders); // Add all identity fields as separate columns. foreach ($extrafields as $field) { $fields .= ", u.{$field}"; $tablecolumns[] = $field; $tableheaders[] = \core_user\fields::get_display_name($field); } } if ($this->feedbackstructure->get_feedback()->course == SITEID && !$this->feedbackstructure->get_courseid()) { $tablecolumns[] = 'courseid'; $tableheaders[] = get_string('course'); } $tablecolumns[] = 'completed_timemodified'; $tableheaders[] = get_string('date'); $this->define_columns($tablecolumns); $this->define_headers($tableheaders); $this->sortable(true, 'lastname', SORT_ASC); $this->no_sorting('groups'); $this->collapsible(true); $this->set_attribute('id', 'showentrytable'); $params = array(); $params['anon'] = FEEDBACK_ANONYMOUS_NO; $params['instance'] = $this->feedbackstructure->get_feedback()->id; $params['notdeleted'] = 0; $params['courseid'] = $this->feedbackstructure->get_courseid(); $group = (empty($group)) ? groups_get_activity_group($this->feedbackstructure->get_cm(), true) : $group; if ($group) { $where .= ' AND c.userid IN (SELECT g.userid FROM {groups_members} g WHERE g.groupid = :group)'; $params['group'] = $group; } $this->set_sql($fields, $from, $where, $params); $this->set_count_sql("SELECT COUNT(c.id) FROM $from WHERE $where", $params); } /** * Current context * @return context_module */ public function get_context(): context { return context_module::instance($this->feedbackstructure->get_cm()->id); } /** * Allows to set the display column value for all columns without "col_xxxxx" method. * @param string $column column name * @param stdClass $row current record result of SQL query */ public function other_cols($column, $row) { if (preg_match('/^val(\d+)$/', $column, $matches)) { $items = $this->feedbackstructure->get_items(); $itemobj = feedback_get_item_class($items[$matches[1]]->typ); $printval = $itemobj->get_printval($items[$matches[1]], (object) ['value' => $row->$column]); if ($this->is_downloading()) { $printval = s($printval); } return trim($printval); } return parent::other_cols($column, $row); } /** * Prepares column userpic for display * @param stdClass $row * @return string */ public function col_userpic($row) { global $OUTPUT; $user = user_picture::unalias($row, [], $this->useridfield); return $OUTPUT->user_picture($user, array('courseid' => $this->feedbackstructure->get_cm()->course)); } /** * Prepares column deleteentry for display * @param stdClass $row * @return string */ public function col_deleteentry($row) { global $OUTPUT; $deleteentryurl = new moodle_url($this->baseurl, ['delete' => $row->id, 'sesskey' => sesskey()]); $deleteaction = new confirm_action(get_string('confirmdeleteentry', 'feedback')); return $OUTPUT->action_icon($deleteentryurl, new pix_icon('t/delete', get_string('delete_entry', 'feedback')), $deleteaction); } /** * Returns a link for viewing a single response * @param stdClass $row * @return \moodle_url */ protected function get_link_single_entry($row) { return new moodle_url($this->baseurl, ['userid' => $row->{$this->useridfield}, 'showcompleted' => $row->id]); } /** * Prepares column completed_timemodified for display * @param stdClass $student * @return string */ public function col_completed_timemodified($student) { if ($this->is_downloading()) { return userdate($student->completed_timemodified); } else { return html_writer::link($this->get_link_single_entry($student), userdate($student->completed_timemodified)); } } /** * Prepares column courseid for display * @param array $row * @return string */ public function col_courseid($row) { $courses = $this->feedbackstructure->get_completed_courses(); $name = ''; if (isset($courses[$row->courseid])) { $name = $courses[$row->courseid]; if (!$this->is_downloading()) { $name = html_writer::link(course_get_url($row->courseid), $name); } } return $name; } /** * Prepares column groups for display * @param array $row * @return string */ public function col_groups($row) { $groups = ''; if ($usergrps = groups_get_all_groups($this->feedbackstructure->get_cm()->course, $row->userid, 0, 'name')) { foreach ($usergrps as $group) { $groups .= format_string($group->name). ' '; } } return trim($groups); } /** * Adds common values to the table that do not change the number or order of entries and * are only needed when outputting or downloading data. */ protected function add_all_values_to_output() { global $DB; $tablecolumns = array_keys($this->columns); $tableheaders = $this->headers; $items = $this->feedbackstructure->get_items(true); if (!$this->is_downloading() && !$this->buildforexternal) { // In preview mode do not show all columns or the page becomes unreadable. // The information message will be displayed to the teacher that the rest of the data can be viewed when downloading. $items = array_slice($items, 0, self::PREVIEWCOLUMNSLIMIT, true); } $columnscount = 0; $this->hasmorecolumns = max(0, count($items) - self::TABLEJOINLIMIT); $headernamepostfix = !$this->is_downloading(); // Add feedback response values. foreach ($items as $nr => $item) { if ($columnscount++ < self::TABLEJOINLIMIT) { // Mysql has a limit on the number of tables in the join, so we only add limited number of columns here, // the rest will be added in {@link self::build_table()} and {@link self::build_table_chunk()} functions. $this->sql->fields .= ", " . $DB->sql_cast_to_char("v{$nr}.value") . " AS val{$nr}"; $this->sql->from .= " LEFT OUTER JOIN {feedback_value} v{$nr} " . "ON v{$nr}.completed = c.id AND v{$nr}.item = :itemid{$nr}"; $this->sql->params["itemid{$nr}"] = $item->id; } $tablecolumns[] = "val{$nr}"; $itemobj = feedback_get_item_class($item->typ); $columnheader = $itemobj->get_display_name($item, $headernamepostfix); if (!$this->is_downloading()) { $columnheader = shorten_text($columnheader); } if (strval($item->label) !== '') { $columnheader = get_string('nameandlabelformat', 'mod_feedback', (object)['label' => format_string($item->label), 'name' => $columnheader]); } $tableheaders[] = $columnheader; } // Add 'Delete entry' column. if (!$this->is_downloading() && has_capability('mod/feedback:deletesubmissions', $this->get_context())) { $tablecolumns[] = 'deleteentry'; $tableheaders[] = ''; } $this->define_columns($tablecolumns); $this->define_headers($tableheaders); } /** * Query the db. Store results in the table object for use by build_table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. Bar * will only be used if there is a fullname column defined for the table. */ public function query_db($pagesize, $useinitialsbar=true) { global $DB; $this->totalrows = $grandtotal = $this->get_total_responses_count(); if (!$this->is_downloading()) { $this->initialbars($useinitialsbar); list($wsql, $wparams) = $this->get_sql_where(); if ($wsql) { $this->countsql .= ' AND '.$wsql; $this->countparams = array_merge($this->countparams, $wparams); $this->sql->where .= ' AND '.$wsql; $this->sql->params = array_merge($this->sql->params, $wparams); $this->totalrows = $DB->count_records_sql($this->countsql, $this->countparams); } if ($this->totalrows > $pagesize) { $this->pagesize($pagesize, $this->totalrows); } } if ($sort = $this->get_sql_sort()) { $sort = "ORDER BY $sort"; } $sql = "SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where} {$sort}"; if (!$this->is_downloading()) { $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); } else { $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params); } } /** * Returns total number of reponses (without any filters applied) * @return int */ public function get_total_responses_count() { global $DB; if ($this->grandtotal === null) { $this->grandtotal = $DB->count_records_sql($this->countsql, $this->countparams); } return $this->grandtotal; } /** * Defines columns * @param array $columns an array of identifying names for columns. If * columns are sorted then column names must correspond to a field in sql. */ public function define_columns($columns) { parent::define_columns($columns); foreach ($this->columns as $column => $column) { // Automatically assign classes to columns. $this->column_class[$column] = ' ' . $column; } } /** * Convenience method to call a number of methods for you to display the * table. * @param int $pagesize * @param bool $useinitialsbar * @param string $downloadhelpbutton */ public function out($pagesize, $useinitialsbar, $downloadhelpbutton='') { $this->add_all_values_to_output(); parent::out($pagesize, $useinitialsbar, $downloadhelpbutton); } /** * Displays the table */ public function display() { global $OUTPUT; groups_print_activity_menu($this->feedbackstructure->get_cm(), $this->baseurl->out()); $grandtotal = $this->get_total_responses_count(); if (!$grandtotal) { echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false); return; } if (count($this->feedbackstructure->get_items(true)) > self::PREVIEWCOLUMNSLIMIT) { echo $OUTPUT->notification(get_string('questionslimited', 'feedback', self::PREVIEWCOLUMNSLIMIT), 'info'); } $this->out($this->showall ? $grandtotal : FEEDBACK_DEFAULT_PAGE_COUNT, $grandtotal > FEEDBACK_DEFAULT_PAGE_COUNT); // Toggle 'Show all' link. if ($this->totalrows > FEEDBACK_DEFAULT_PAGE_COUNT) { if (!$this->use_pages) { echo html_writer::div(html_writer::link(new moodle_url($this->baseurl, [$this->showallparamname => 0]), get_string('showperpage', '', FEEDBACK_DEFAULT_PAGE_COUNT)), 'showall'); } else { echo html_writer::div(html_writer::link(new moodle_url($this->baseurl, [$this->showallparamname => 1]), get_string('showall', '', $this->totalrows)), 'showall'); } } } /** * Returns links to previous/next responses in the list * @param stdClass $record * @return array array of three elements [$prevresponseurl, $returnurl, $nextresponseurl] */ public function get_reponse_navigation_links($record) { $this->setup(); $grandtotal = $this->get_total_responses_count(); $this->query_db($grandtotal); $lastrow = $thisrow = $nextrow = null; $counter = 0; $page = 0; while ($this->rawdata->valid()) { $row = $this->rawdata->current(); if ($row->id == $record->id) { $page = $this->showall ? 0 : floor($counter / FEEDBACK_DEFAULT_PAGE_COUNT); $thisrow = $row; $this->rawdata->next(); $nextrow = $this->rawdata->valid() ? $this->rawdata->current() : null; break; } $lastrow = $row; $this->rawdata->next(); $counter++; } $this->rawdata->close(); if (!$thisrow) { $lastrow = null; } return [ $lastrow ? $this->get_link_single_entry($lastrow) : null, new moodle_url($this->baseurl, [$this->request[TABLE_VAR_PAGE] => $page]), $nextrow ? $this->get_link_single_entry($nextrow) : null, ]; } /** * Download the data. */ public function download() { \core\session\manager::write_close(); $this->out($this->get_total_responses_count(), false); exit; } /** * Take the data returned from the db_query and go through all the rows * processing each col using either col_{columnname} method or other_cols * method or if other_cols returns NULL then put the data straight into the * table. * * This overwrites the parent method because full SQL query may fail on Mysql * because of the limit in the number of tables in the join. Therefore we only * join 59 tables in the main query and add the rest here. * * @return void */ public function build_table() { if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) { return; } if (!$this->rawdata) { return; } $columnsgroups = []; if ($this->hasmorecolumns) { $items = $this->feedbackstructure->get_items(true); $notretrieveditems = array_slice($items, self::TABLEJOINLIMIT, $this->hasmorecolumns, true); $columnsgroups = array_chunk($notretrieveditems, self::TABLEJOINLIMIT, true); } $chunk = []; foreach ($this->rawdata as $row) { if ($this->hasmorecolumns) { $chunk[$row->id] = $row; if (count($chunk) >= self::ROWCHUNKSIZE) { $this->build_table_chunk($chunk, $columnsgroups); $chunk = []; } } else { if ($this->buildforexternal) { $this->add_data_for_external($row); } else { $this->add_data_keyed($this->format_row($row), $this->get_row_class($row)); } } } $this->build_table_chunk($chunk, $columnsgroups); } /** * Retrieve additional columns. Database engine may have a limit on number of joins. * * @param array $rows Array of rows with already retrieved data, new values will be added to this array * @param array $columnsgroups array of arrays of columns. Each element has up to self::TABLEJOINLIMIT items. This * is easy to calculate but because we can call this method many times we calculate it once and pass by * reference for performance reasons */ protected function build_table_chunk(&$rows, &$columnsgroups) { global $DB; if (!$rows) { return; } foreach ($columnsgroups as $columnsgroup) { $fields = 'c.id'; $from = '{feedback_completed} c'; $params = []; foreach ($columnsgroup as $nr => $item) { $fields .= ", " . $DB->sql_cast_to_char("v{$nr}.value") . " AS val{$nr}"; $from .= " LEFT OUTER JOIN {feedback_value} v{$nr} " . "ON v{$nr}.completed = c.id AND v{$nr}.item = :itemid{$nr}"; $params["itemid{$nr}"] = $item->id; } list($idsql, $idparams) = $DB->get_in_or_equal(array_keys($rows), SQL_PARAMS_NAMED); $sql = "SELECT $fields FROM $from WHERE c.id ".$idsql; $results = $DB->get_records_sql($sql, $params + $idparams); foreach ($results as $result) { foreach ($result as $key => $value) { $rows[$result->id]->{$key} = $value; } } } foreach ($rows as $row) { if ($this->buildforexternal) { $this->add_data_for_external($row); } else { $this->add_data_keyed($this->format_row($row), $this->get_row_class($row)); } } } /** * Returns html code for displaying "Download" button if applicable. */ public function download_buttons() { global $OUTPUT; if ($this->is_downloadable() && !$this->is_downloading()) { return $OUTPUT->download_dataformat_selector(get_string('downloadas', 'table'), $this->baseurl->out_omit_querystring(), $this->downloadparamname, $this->baseurl->params()); } else { return ''; } } /** * Return user responses data ready for the external function. * * @param stdClass $row the table row containing the responses * @return array returns the responses ready to be used by an external function * @since Moodle 3.3 */ protected function get_responses_for_external($row) { $responses = []; foreach ($row as $el => $val) { // Get id from column name. if (preg_match('/^val(\d+)$/', $el, $matches)) { $id = $matches[1]; $responses[] = [ 'id' => $id, 'name' => $this->headers[$this->columns[$el]], 'printval' => $this->other_cols($el, $row), 'rawval' => $val, ]; } } return $responses; } /** * Add data for the external structure that will be returned. * * @param stdClass $row a database query record row * @since Moodle 3.3 */ protected function add_data_for_external($row) { $this->dataforexternal[] = [ 'id' => $row->id, 'courseid' => $row->courseid, 'userid' => $row->userid, 'fullname' => fullname($row), 'timemodified' => $row->completed_timemodified, 'responses' => $this->get_responses_for_external($row), ]; } /** * Exports the table as an external structure handling pagination. * * @param int $page page number (for pagination) * @param int $perpage elements per page * @since Moodle 3.3 * @return array returns the table ready to be used by an external function */ public function export_external_structure($page = 0, $perpage = 0) { $this->buildforexternal = true; $this->add_all_values_to_output(); // Set-up. $this->setup(); // Override values, if needed. if ($perpage > 0) { $this->pageable = true; $this->currpage = $page; $this->pagesize = $perpage; } else { $this->pagesize = $this->get_total_responses_count(); } $this->query_db($this->pagesize, false); $this->build_table(); $this->close_recordset(); return $this->dataforexternal; } } PKB\=$classes/event/response_submitted.phpnu[. /** * The mod_feedback response submitted event. * * @package mod_feedback * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace mod_feedback\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_feedback response submitted event class. * * This event is triggered when a feedback response is submitted. * * @property-read array $other { * Extra information about event. * * - int anonymous: if feedback is anonymous. * - int cmid: course module id. * - int instanceid: id of instance. * } * * @package mod_feedback * @since Moodle 2.6 * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ class response_submitted extends \core\event\base { /** * Set basic properties for the event. */ protected function init() { global $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $this->data['objecttable'] = 'feedback_completed'; $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Creates an instance from the record from db table feedback_completed * * @param stdClass $completed * @param stdClass|cm_info $cm * @return self */ public static function create_from_record($completed, $cm) { $event = self::create(array( 'relateduserid' => $completed->userid, 'objectid' => $completed->id, 'context' => \context_module::instance($cm->id), 'anonymous' => ($completed->anonymous_response == FEEDBACK_ANONYMOUS_YES), 'other' => array( 'cmid' => $cm->id, 'instanceid' => $completed->feedback, 'anonymous' => $completed->anonymous_response // Deprecated. ) )); $event->add_record_snapshot('feedback_completed', $completed); return $event; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventresponsesubmitted', 'mod_feedback'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' submitted response for 'feedback' activity with " . "course module id '$this->contextinstanceid'."; } /** * Returns relevant URL based on the anonymous mode of the response. * @return \moodle_url */ public function get_url() { if ($this->anonymous) { return new \moodle_url('/mod/feedback/show_entries.php', array('id' => $this->other['cmid'], 'showcompleted' => $this->objectid)); } else { return new \moodle_url('/mod/feedback/show_entries.php' , array('id' => $this->other['cmid'], 'userid' => $this->userid, 'showcompleted' => $this->objectid)); } } /** * Define whether a user can view the event or not. Make sure no one except admin can see details of an anonymous response. * * @deprecated since 2.7 * * @param int|\stdClass $userorid ID of the user. * @return bool True if the user can view the event, false otherwise. */ public function can_view($userorid = null) { global $USER; debugging('can_view() method is deprecated, use anonymous flag instead if necessary.', DEBUG_DEVELOPER); if (empty($userorid)) { $userorid = $USER; } if ($this->anonymous) { return is_siteadmin($userorid); } else { return has_capability('mod/feedback:viewreports', $this->context, $userorid); } } /** * Custom validations. * * @throws \coding_exception in case of any problems. */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['anonymous'])) { throw new \coding_exception('The \'anonymous\' value must be set in other.'); } if (!isset($this->other['cmid'])) { throw new \coding_exception('The \'cmid\' value must be set in other.'); } if (!isset($this->other['instanceid'])) { throw new \coding_exception('The \'instanceid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'feedback_completed', 'restore' => 'feedback_completed'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['cmid'] = array('db' => 'course_modules', 'restore' => 'course_module'); $othermapped['instanceid'] = array('db' => 'feedback', 'restore' => 'feedback'); return $othermapped; } } PKB\&classes/event/course_module_viewed.phpnu[. /** * The mod_feedback course module viewed event. * * @package mod_feedback * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_feedback course module viewed event class. * * @property-read array $other { * Extra information about event. * * - int anonymous if feedback is anonymous. * } * * @package mod_feedback * @since Moodle 2.6 * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'feedback'; } /** * Creates an instance from feedback record * * @param stdClass $feedback * @param cm_info|stdClass $cm * @param stdClass $course * @return course_module_viewed */ public static function create_from_record($feedback, $cm, $course) { $event = self::create(array( 'objectid' => $feedback->id, 'context' => \context_module::instance($cm->id), 'anonymous' => ($feedback->anonymous == FEEDBACK_ANONYMOUS_YES), 'other' => array( 'anonymous' => $feedback->anonymous // Deprecated. ) )); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('feedback', $feedback); return $event; } /** * Define whether a user can view the event or not. Make sure no one except admin can see details of an anonymous response. * * @deprecated since 2.7 * * @param int|\stdClass $userorid ID of the user. * @return bool True if the user can view the event, false otherwise. */ public function can_view($userorid = null) { global $USER; debugging('can_view() method is deprecated, use anonymous flag instead if necessary.', DEBUG_DEVELOPER); if (empty($userorid)) { $userorid = $USER; } if ($this->anonymous) { return is_siteadmin($userorid); } else { return has_capability('mod/feedback:viewreports', $this->context, $userorid); } } /** * Custom validations. * * @throws \coding_exception in case of any problems. */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['anonymous'])) { throw new \coding_exception('The \'anonymous\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'feedback', 'restore' => 'feedback'); } public static function get_other_mapping() { // No need to map the 'anonymous' flag. return false; } } PKB\"classes/event/response_deleted.phpnu[. /** * The mod_feedback response deleted event. * * @package mod_feedback * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace mod_feedback\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_feedback response deleted event class. * * This event is triggered when a feedback response is deleted. * * @property-read array $other { * Extra information about event. * * - int anonymous: if feedback is anonymous. * - int cmid: course module id. * - int instanceid: id of instance. * } * * @package mod_feedback * @since Moodle 2.6 * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ class response_deleted extends \core\event\base { /** * Set basic properties for the event. */ protected function init() { $this->data['objecttable'] = 'feedback_completed'; $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Creates an instance from the record from db table feedback_completed * * @param stdClass $completed * @param stdClass|cm_info $cm * @param stdClass $feedback * @return self */ public static function create_from_record($completed, $cm, $feedback) { $event = self::create(array( 'relateduserid' => $completed->userid, 'objectid' => $completed->id, 'courseid' => $cm->course, 'context' => \context_module::instance($cm->id), 'anonymous' => ($completed->anonymous_response == FEEDBACK_ANONYMOUS_YES), 'other' => array( 'cmid' => $cm->id, 'instanceid' => $feedback->id, 'anonymous' => $completed->anonymous_response) // Deprecated. )); $event->add_record_snapshot('feedback_completed', $completed); $event->add_record_snapshot('feedback', $feedback); return $event; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventresponsedeleted', 'mod_feedback'); } /** * Returns non-localised event description with id's for admin use only. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the feedback for the user with id '$this->relateduserid' " . "for the feedback activity with course module id '$this->contextinstanceid'."; } /** * Define whether a user can view the event or not. Make sure no one except admin can see details of an anonymous response. * * @deprecated since 2.7 * * @param int|\stdClass $userorid ID of the user. * @return bool True if the user can view the event, false otherwise. */ public function can_view($userorid = null) { global $USER; debugging('can_view() method is deprecated, use anonymous flag instead if necessary.', DEBUG_DEVELOPER); if (empty($userorid)) { $userorid = $USER; } if ($this->anonymous) { return is_siteadmin($userorid); } else { return has_capability('mod/feedback:viewreports', $this->context, $userorid); } } /** * Custom validations * * @throws \coding_exception in case of any problems. */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['anonymous'])) { throw new \coding_exception('The \'anonymous\' value must be set in other.'); } if (!isset($this->other['cmid'])) { throw new \coding_exception('The \'cmid\' value must be set in other.'); } if (!isset($this->other['instanceid'])) { throw new \coding_exception('The \'instanceid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'feedback_completed', 'restore' => 'feedback_completed'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['cmid'] = array('db' => 'course_modules', 'restore' => 'course_module'); $othermapped['instanceid'] = array('db' => 'feedback', 'restore' => 'feedback'); return $othermapped; } } PKB\W4classes/event/course_module_instance_list_viewed.phpnu[. /** * The mod_feedback instance list viewed event. * * @package mod_feedback * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_feedback instance list viewed event class. * * @package mod_feedback * @since Moodle 2.7 * @copyright 2013 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { } PKB\ZbJ J (classes/completion/custom_completion.phpnu[. declare(strict_types=1); namespace mod_feedback\completion; use core_completion\activity_custom_completion; /** * Activity custom completion subclass for the feedback activity. * * Class for defining mod_feedback's custom completion rules and fetching the completion statuses * of the custom completion rules for a given feedback instance and a user. * * @package mod_feedback * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom_completion extends activity_custom_completion { /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ public function get_state(string $rule): int { global $DB; $this->validate_rule($rule); // Feedback only supports completionsubmit as a custom rule. $status = $DB->record_exists('feedback_completed', ['feedback' => $this->cm->instance, 'userid' => $this->userid]); return $status ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; } /** * Fetch the list of custom completion rules that this module defines. * * @return array */ public static function get_defined_custom_rules(): array { return ['completionsubmit']; } /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ public function get_custom_rule_descriptions(): array { return [ 'completionsubmit' => get_string('completiondetail:submit', 'feedback') ]; } /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ public function get_sort_order(): array { return [ 'completionview', 'completionsubmit', ]; } } PKB\ڈ/classes/analytics/indicator/cognitive_depth.phpnu[. /** * Cognitive depth indicator - feedback. * * @package mod_feedback * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Cognitive depth indicator - feedback. * * @package mod_feedback * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:cognitivedepth', 'mod_feedback'); } public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } public function get_cognitive_depth_level(\cm_info $cm) { $this->fill_instance_data($cm); if (!empty($this->instancedata[$cm->instance]->publish_stats)) { // If stats are published we count that the user viewed feedback. return self::COGNITIVE_LEVEL_3; } return self::COGNITIVE_LEVEL_2; } } PKB\K QQ-classes/analytics/indicator/activity_base.phpnu[. /** * Activity base class. * * @package mod_feedback * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Activity base class. * * @package mod_feedback * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { /** * Overwritten to mark as viewed if stats are published. * * @param \cm_info $cm * @param int $contextid * @param int $userid * @param int $after * @return bool */ protected function feedback_viewed(\cm_info $cm, $contextid, $userid, $after = null) { // If stats are published any write action counts as viewed feedback. if (!empty($this->instancedata[$cm->instance]->publish_stats)) { $user = (object)['id' => $userid]; return $this->any_write_log($contextid, $user); } return parent::feedback_viewed($cm, $contextid, $userid, $after); } /** * Returns the name of the field that controls activity availability. * * @return null|string */ protected function get_timeclose_field() { return 'timeclose'; } } PKB\FA%.classes/analytics/indicator/social_breadth.phpnu[. /** * Social breadth indicator - feedback. * * @package mod_feedback * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Social breadth indicator - feedback. * * @package mod_feedback * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:socialbreadth', 'mod_feedback'); } public function get_indicator_type() { return self::INDICATOR_SOCIAL; } public function get_social_breadth_level(\cm_info $cm) { $this->fill_instance_data($cm); return self::SOCIAL_LEVEL_2; } } PKB\IH  classes/course_select_form.phpnu[. /** * Contains class mod_feedback_course_map_form * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Form for mapping courses to the feedback * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_course_select_form extends moodleform { /** @var moodle_url */ protected $action; /** @var mod_feedback_structure $feedbackstructure */ protected $feedbackstructure; /** * Constructor * * @param string|moodle_url $action the action attribute for the form * @param mod_feedback_structure $feedbackstructure * @param bool $editable */ public function __construct($action, mod_feedback_structure $feedbackstructure, $editable = true) { $this->action = new moodle_url($action, ['courseid' => null]); $this->feedbackstructure = $feedbackstructure; parent::__construct($action, null, 'post', '', ['id' => 'feedback_course_filter'], $editable); } /** * Definition of the form */ public function definition() { $mform = $this->_form; $feedbackstructure = $this->feedbackstructure; $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); if (!$this->_form->_freezeAll && ($courses = $feedbackstructure->get_completed_courses()) && count($courses) > 1) { $elements = []; $elements[] = $mform->createElement('autocomplete', 'courseid', get_string('filter_by_course', 'feedback'), ['' => get_string('fulllistofcourses')] + $courses); $elements[] = $mform->createElement('submit', 'submitbutton', get_string('filter')); if ($feedbackstructure->get_courseid()) { $elements[] = $mform->createElement('static', 'showall', '', html_writer::link($this->action, get_string('show_all', 'feedback'))); } if (defined('BEHAT_SITE_RUNNING')) { // TODO MDL-53734 remove this - behat does not recognise autocomplete element inside a group. foreach ($elements as $element) { $mform->addElement($element); } } else { $mform->addGroup($elements, 'coursefilter', get_string('filter_by_course', 'feedback'), array(' '), false); } } $this->set_data(['courseid' => $feedbackstructure->get_courseid(), 'id' => $feedbackstructure->get_cm()->id]); } } PKB\55classes/structure.phpnu[. /** * Contains class mod_feedback_structure * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Stores and manipulates the structure of the feedback or template (items, pages, etc.) * * @package mod_feedback * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_structure { /** @var stdClass record from 'feedback' table. * Reliably has fields: id, course, timeopen, timeclose, anonymous, completionsubmit. * For full object or to access any other field use $this->get_feedback() */ protected $feedback; /** @var cm_info */ protected $cm; /** @var int course where the feedback is filled. For feedbacks that are NOT on the front page this is 0 */ protected $courseid = 0; /** @var int */ protected $templateid; /** @var array */ protected $allitems; /** @var array */ protected $allcourses; /** @var int */ protected $userid; /** * Constructor * * @param stdClass $feedback feedback object, in case of the template * this is the current feedback the template is accessed from * @param stdClass|cm_info $cm course module object corresponding to the $feedback * (at least one of $feedback or $cm is required) * @param int $courseid current course (for site feedbacks only) * @param int $templateid template id if this class represents the template structure * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). */ public function __construct($feedback, $cm, $courseid = 0, $templateid = null, $userid = 0) { global $USER; if ((empty($feedback->id) || empty($feedback->course)) && (empty($cm->instance) || empty($cm->course))) { throw new coding_exception('Either $feedback or $cm must be passed to constructor'); } $this->feedback = $feedback ?: (object)['id' => $cm->instance, 'course' => $cm->course]; $this->cm = ($cm && $cm instanceof cm_info) ? $cm : get_fast_modinfo($this->feedback->course)->instances['feedback'][$this->feedback->id]; $this->templateid = $templateid; $this->courseid = ($this->feedback->course == SITEID) ? $courseid : 0; if (empty($userid)) { $this->userid = $USER->id; } else { $this->userid = $userid; } if (!$feedback) { // If feedback object was not specified, populate object with fields required for the most of methods. // These fields were added to course module cache in feedback_get_coursemodule_info(). // Full instance record can be retrieved by calling mod_feedback_structure::get_feedback(). $customdata = ($this->cm->customdata ?: []) + ['timeopen' => 0, 'timeclose' => 0, 'anonymous' => 0]; $this->feedback->timeopen = $customdata['timeopen']; $this->feedback->timeclose = $customdata['timeclose']; $this->feedback->anonymous = $customdata['anonymous']; $this->feedback->completionsubmit = empty($this->cm->customdata['customcompletionrules']['completionsubmit']) ? 0 : 1; } } /** * Current feedback * @return stdClass */ public function get_feedback() { global $DB; if (!isset($this->feedback->publish_stats) || !isset($this->feedback->name)) { // Make sure the full object is retrieved. $this->feedback = $DB->get_record('feedback', ['id' => $this->feedback->id], '*', MUST_EXIST); } return $this->feedback; } /** * Current course module * @return stdClass */ public function get_cm() { return $this->cm; } /** * Id of the current course (for site feedbacks only) * @return stdClass */ public function get_courseid() { return $this->courseid; } /** * Template id * @return int */ public function get_templateid() { return $this->templateid; } /** * Is this feedback open (check timeopen and timeclose) * @return bool */ public function is_open() { $checktime = time(); return (!$this->feedback->timeopen || $this->feedback->timeopen <= $checktime) && (!$this->feedback->timeclose || $this->feedback->timeclose >= $checktime); } /** * Get all items in this feedback or this template * @param bool $hasvalueonly only count items with a value. * @return array of objects from feedback_item with an additional attribute 'itemnr' */ public function get_items($hasvalueonly = false) { global $DB; if ($this->allitems === null) { if ($this->templateid) { $this->allitems = $DB->get_records('feedback_item', ['template' => $this->templateid], 'position'); } else { $this->allitems = $DB->get_records('feedback_item', ['feedback' => $this->feedback->id], 'position'); } $idx = 1; foreach ($this->allitems as $id => $item) { $this->allitems[$id]->itemnr = $item->hasvalue ? ($idx++) : null; } } if ($hasvalueonly && $this->allitems) { return array_filter($this->allitems, function($item) { return $item->hasvalue; }); } return $this->allitems; } /** * Is the items list empty? * @return bool */ public function is_empty() { $items = $this->get_items(); $displayeditems = array_filter($items, function($item) { return $item->typ !== 'pagebreak'; }); return !$displayeditems; } /** * Is this feedback anonymous? * @return bool */ public function is_anonymous() { return $this->feedback->anonymous == FEEDBACK_ANONYMOUS_YES; } /** * Returns the formatted text of the page after submit or null if it is not set * * @return string|null */ public function page_after_submit() { global $CFG; require_once($CFG->libdir . '/filelib.php'); $pageaftersubmit = $this->get_feedback()->page_after_submit; if (empty($pageaftersubmit)) { return null; } $pageaftersubmitformat = $this->get_feedback()->page_after_submitformat; $context = context_module::instance($this->get_cm()->id); $output = file_rewrite_pluginfile_urls($pageaftersubmit, 'pluginfile.php', $context->id, 'mod_feedback', 'page_after_submit', 0); return format_text($output, $pageaftersubmitformat, array('overflowdiv' => true)); } /** * Checks if current user is able to view feedback on this course. * * @return bool */ public function can_view_analysis() { global $USER; $context = context_module::instance($this->cm->id); if (has_capability('mod/feedback:viewreports', $context, $this->userid)) { return true; } if (intval($this->get_feedback()->publish_stats) != 1 || !has_capability('mod/feedback:viewanalysepage', $context, $this->userid)) { return false; } if ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid)) { // There is no tracking for the guests, assume that they can view analysis if condition above is satisfied. return $this->feedback->course == SITEID; } return $this->is_already_submitted(true); } /** * check for multiple_submit = false. * if the feedback is global so the courseid must be given * * @param bool $anycourseid if true checks if this feedback was submitted in any course, otherwise checks $this->courseid . * Applicable to frontpage feedbacks only * @return bool true if the feedback already is submitted otherwise false */ public function is_already_submitted($anycourseid = false) { global $DB, $USER; if ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid)) { return false; } $params = array('userid' => $this->userid, 'feedback' => $this->feedback->id); if (!$anycourseid && $this->courseid) { $params['courseid'] = $this->courseid; } return $DB->record_exists('feedback_completed', $params); } /** * Check whether the feedback is mapped to the given courseid. */ public function check_course_is_mapped() { global $DB; if ($this->feedback->course != SITEID) { return true; } if ($DB->get_records('feedback_sitecourse_map', array('feedbackid' => $this->feedback->id))) { $params = array('feedbackid' => $this->feedback->id, 'courseid' => $this->courseid); if (!$DB->get_record('feedback_sitecourse_map', $params)) { return false; } } // No mapping means any course is mapped. return true; } /** * If there are any new responses to the anonymous feedback, re-shuffle all * responses and assign response number to each of them. */ public function shuffle_anonym_responses() { global $DB; $params = array('feedback' => $this->feedback->id, 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_YES); if ($DB->count_records('feedback_completed', $params, 'random_response')) { // Get all of the anonymous records, go through them and assign a response id. unset($params['random_response']); $feedbackcompleteds = $DB->get_records('feedback_completed', $params, 'id'); shuffle($feedbackcompleteds); $num = 1; foreach ($feedbackcompleteds as $compl) { $compl->random_response = $num++; $DB->update_record('feedback_completed', $compl); } } } /** * Counts records from {feedback_completed} table for a given feedback * * If $groupid or $this->courseid is set, the records are filtered by the group/course * * @param int $groupid * @return mixed array of found completeds otherwise false */ public function count_completed_responses($groupid = 0) { global $DB; if (intval($groupid) > 0) { $query = "SELECT COUNT(DISTINCT fbc.id) FROM {feedback_completed} fbc, {groups_members} gm WHERE fbc.feedback = :feedback AND gm.groupid = :groupid AND fbc.userid = gm.userid"; } else if ($this->courseid) { $query = "SELECT COUNT(fbc.id) FROM {feedback_completed} fbc WHERE fbc.feedback = :feedback AND fbc.courseid = :courseid"; } else { $query = "SELECT COUNT(fbc.id) FROM {feedback_completed} fbc WHERE fbc.feedback = :feedback"; } $params = ['feedback' => $this->feedback->id, 'groupid' => $groupid, 'courseid' => $this->courseid]; return $DB->get_field_sql($query, $params); } /** * For the frontpage feedback returns the list of courses with at least one completed feedback * * @return array id=>name pairs of courses */ public function get_completed_courses() { global $DB; if ($this->get_feedback()->course != SITEID) { return []; } if ($this->allcourses !== null) { return $this->allcourses; } $courseselect = "SELECT fbc.courseid FROM {feedback_completed} fbc WHERE fbc.feedback = :feedbackid"; $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $sql = 'SELECT c.id, c.shortname, c.fullname, c.idnumber, c.visible, '. $ctxselect. ' FROM {course} c JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextcourse WHERE c.id IN ('. $courseselect.') ORDER BY c.sortorder'; $list = $DB->get_records_sql($sql, ['contextcourse' => CONTEXT_COURSE, 'feedbackid' => $this->get_feedback()->id]); $this->allcourses = array(); foreach ($list as $course) { context_helper::preload_from_record($course); if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id), $this->userid)) { // Do not return courses that current user can not see. continue; } $label = get_course_display_name_for_list($course); $this->allcourses[$course->id] = $label; } return $this->allcourses; } }PKB\!classes/courseformat/overview.phpnu[. namespace mod_feedback\courseformat; use core_courseformat\local\overview\overviewitem; use core\output\action_link; use core\output\local\properties\button; use core\output\local\properties\text_align; use core\url; use core\output\pix_icon; /** * Class overview * * @package mod_feedback * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overview extends \core_courseformat\activityoverviewbase { #[\Override] public function get_extra_overview_items(): array { return [ 'submitted' => $this->get_extra_submitted_overview(), ]; } #[\Override] public function get_actions_overview(): ?overviewitem { global $CFG, $USER; if (!has_capability('mod/feedback:viewreports', $this->context)) { return null; } require_once($CFG->dirroot . '/mod/feedback/lib.php'); $submissions = feedback_get_completeds_group_count( $this->cm->get_instance_record() ); // Normalize the value. if (!$submissions) { $submissions = 0; } $total = $submissions + feedback_count_incomplete_users($this->cm); $content = new action_link( url: new url('/mod/feedback/show_entries.php', ['id' => $this->cm->id]), text: get_string( 'count_of_total', 'core', ['count' => $submissions, 'total' => $total] ), attributes: ['class' => button::SECONDARY_OUTLINE->classes()], ); return new overviewitem( name: get_string('responses', 'mod_feedback'), value: $submissions, content: $content, textalign: text_align::CENTER, ); } #[\Override] public function get_due_date_overview(): ?overviewitem { $duedate = null; if (isset($this->cm->customdata['timeclose'])) { $duedate = $this->cm->customdata['timeclose']; } if (empty($duedate)) { return new overviewitem( name: get_string('feedbackclose', 'mod_feedback'), value: null, content: '-', ); } return new overviewitem( name: get_string('feedbackclose', 'mod_feedback'), value: $duedate, content: userdate($duedate), ); } /** * Get the submitted status overview item. * * @return overviewitem|null The overview item (or null if the user cannot complete the feedback). */ private function get_extra_submitted_overview(): ?overviewitem { global $USER; if (!has_capability('mod/feedback:complete', $this->context, $USER, false)) { return null; } $structure = new \mod_feedback_structure( feedback: $this->cm->get_instance_record(), cm: $this->cm, courseid: $this->course->id, userid: $USER->id, ); $value = false; $content = '-'; if ($structure->is_already_submitted()) { $value = true; $content = new pix_icon( 'i/checkedcircle', alt: get_string('this_feedback_is_already_submitted', 'mod_feedback'), attributes: ['class' => 'text-success'], ); } return new overviewitem( name: get_string('responded', 'mod_feedback'), value: $value, content: $content, textalign: text_align::CENTER, ); } } PKB\$+templates/item_edit_enhanced_title.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_feedback/item_edit_enhanced_title The enhanced item title template when editing questions in the feedback activity. Context variables required for this template: * movetitle - The string for the title attribute of the move button. * label - The question text. * actionsmenu - Context data for the actions menu. Example context (json): { "movetitle": "Move item", "label": "Hello world!", "actionsmenu": {} } }}
{{> core/drag_handle}}
{{{label}}}
{{#actionsmenu}}
{{> core/action_menu}}
{{/actionsmenu}}
PKB\A=templates/summary.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_feedback/summary Displays feedback summary Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "completedcount" : 15, "itemscount" : 5 } }} PKB\0DD#templates/main_action_menu.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_feedback/main_action_menu Tertiary nav for the feedback module Classes required for JS: * none Data attributes required for JS: * data-region Context variables required for this template: * see mod/feedback/classes/output/edit_action_bar.php * see mod/feedback/classes/output/edit_template_action_bar.php * see mod/feedback/classes/output/responses_action_bar.php * see mod/feedback/classes/output/standard_action_bar.php Example context (json): { "left": { "actionlink": { "disabled": false, "url": "#", "id": "test-id", "classes": "btn btn-link", "attributes": [ { "name": "title", "value": "Action link's title" } ], "icon": { "attributes": [ {"name": "src", "value": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png"}, {"name": "alt", "value": "Action link's icon"} ] }, "text": "This is an action link" }, "urlselect": { "id": "url_select_test", "action": "https://example.com/post", "formid": "url_select_form", "sesskey": "sesskey", "label": "core/url_select", "helpicon": { "title": "Help with something", "text": "Help with something", "url": "http://example.org/help", "linktext": "", "icon":{ "extraclasses": "", "attributes": [ {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } }, "showbutton": "Go", "options": [{ "name": "Group 1", "isgroup": true, "options": [ {"name": "Item 1", "isgroup": false, "value": "1"}, {"name": "Item 2", "isgroup": false, "value": "2"} ]}, {"name": "Group 2", "isgroup": true, "options": [ {"name": "Item 3", "isgroup": false, "value": "3"}, {"name": "Item 4", "isgroup": false, "value": "4"} ]}], "disabled": false, "title": "Some cool title" }, "singleselect": { "name": "lang", "method": "get", "action": "http://localhost/stable_master/mod/scorm/player.php", "classes": "langmenu", "label": "Zombies are coming...", "disabled": false, "title": null, "formid": "randomid", "id": "single_select5833dd4f4b08d108", "params": [ { "name": "scoid", "value": "12" }, { "name": "cm", "value": "15" }, { "name": "mode", "value": "review" }, { "name": "currentorg", "value": "eXeMapADrive4823c6301cf72b22b72" } ], "options": [ { "value": "en", "name": "English ‎(en)‎", "selected": true, "optgroup": false }, { "value": "ar", "name": "Muhaaaa..", "selected": false, "optgroup": false } ], "labelattributes": [], "helpicon": false } } } }}
{{#left}} {{#actionlink}} {{/actionlink}} {{#urlselect}} {{/urlselect}} {{#singleselect}} {{/singleselect}} {{/left}} {{#addselect}} {{/addselect}} {{#actionsselect}} {{/actionsselect}}
PKB\}44"templates/create_template.mustachenu[{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_feedback/create_template Trigger button for the create template modal form. Classes required for JS: * none Data attributes required for JS: * All data attributes are required Context variables required for this template: * id - The feedback id. Example context (json): { "id": 1 } }} {{#js}} require(['mod_feedback/createtemplate'], function(createtemplate) { createtemplate.init(); }); {{/js}} PKB\> 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 /mod/feedback/* Information provided here is intended especially for developers. === 4.3 === * The `$extradetails` parameter of the `\mod_feedback\output\summary` class constructor has been removed === 4.2 === * The external methods get_items() and get_page_items() now only return the items when the user can access the feedback; if the user can't see them, a warning message will be returned, with the reasons. === 4.0 === * The following files and classes within them have been deprecated in favour of dynamic forms: * edit_form.php * use_templ_form.php * The page delete_template.php has been removed as it is now handled within manage_templates.php === 3.11 === The $extradetails parameter in the constructor of the mod_feedback\output\summary class has been deprecated and is not used anymore. === 3.8 === * The following functions have been finally deprecated and can not be used anymore: * feedback_scale_used() === 3.6 === * The following functions have been finally deprecated and can not be used anymore: * feedback_print_item_preview() * feedback_print_item_complete() * feedback_print_item_show_value() * feedback_check_values() * feedback_clean_input_value() * feedback_get_context() * feedback_create_item() * feedback_delete_completedtmp() * feedback_get_page_to_continue() * feedback_save_values() * feedback_save_guest_values() * feedback_create_values() * feedback_update_values() * feedback_get_current_completed() * feedback_is_course_in_sitecourse_map() * feedback_is_feedback_in_sitecourse_map() * feedback_clean_up_sitecourse_map() * feedback_print_numeric_option_list() * Web Service functions now support an optional courseid parameter (course where user completes the feedback). This new parameter was necessary to support site feedbacks. === 3.5 === * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-59159 for more info: - mod_feedback_upgrade_delete_duplicate_values() - mod_feedback_upgrade_courseid() === 3.3.2 === * feedback_refresh_events() Now takes two additional parameters to refine the update to a specific instance. This function now optionally takes the module instance object or ID, and the course module object or ID. Please try to send the full objects instead of the ids to save DB calls. === 3.3 === * Method get_current_completed_tmp in mod_feedback_completion class is now public. * Function feedback_set_tmp_values is not deprecated anymore. === 3.1 === * feedback_get_courses_from_sitecourse_map() now returns course id as 'id' attribute * Base class for feedback items - feedback_item_base - has been significantly changed, all item types now must implement method complete_form_element() instead of print_item_preview(), print_item_complete(), print_item_show_value(), check_values(), clean_input_value() and value_is_array(). Rendering the items on complete form, feedback preview, feedback edit and when viewing response is done using moodleform. * The following functions have been deprecated: * feedback_print_item_preview() * feedback_print_item_complete() * feedback_print_item_show_value * feedback_check_values() * feedback_clean_input_value() * feedback_get_context() * feedback_create_item() * feedback_set_tmp_values() * feedback_delete_completedtmp() * feedback_get_page_to_continue() * feedback_save_values() * feedback_save_guest_values() * feedback_create_values() * feedback_update_values() * feedback_get_current_completed() * feedback_is_course_in_sitecourse_map() * feedback_is_feedback_in_sitecourse_map() * feedback_clean_up_sitecourse_map() * feedback_print_numeric_option_list() === 2.7 === * The validation of the following events is now stricter (see MDL-45445): - \mod_feedback\event\response_deleted - \mod_feedback\event\response_submitted PKB\A9   analysis.phpnu[. /** * shows an analysed view of feedback * * @copyright Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id', PARAM_INT); // Course module id. $url = new moodle_url('/mod/feedback/analysis.php', array('id'=>$id)); $PAGE->set_url($url); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); require_course_login($course, true, $cm); $feedback = $PAGE->activityrecord; $feedbackstructure = new mod_feedback_structure($feedback, $cm); $context = context_module::instance($cm->id); if (!$feedbackstructure->can_view_analysis()) { throw new \moodle_exception('error'); } /// Print the page header $PAGE->set_heading($course->fullname); $renderer = $PAGE->get_renderer('mod_feedback'); $renderer->set_title( [format_string($feedback->name), format_string($course->fullname)], get_string('analysis', 'feedback') ); $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); $PAGE->add_body_class('limitedwidth'); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('analysis', 'mod_feedback'), 3); //get the groupid $mygroupid = groups_get_activity_group($cm, true); groups_print_activity_menu($cm, $url); // Button "Export to excel". if (has_capability('mod/feedback:viewreports', $context) && $feedbackstructure->get_items()) { echo $OUTPUT->container_start('form-buttons'); $aurl = new moodle_url('/mod/feedback/analysis_to_excel.php', ['sesskey' => sesskey(), 'id' => $id]); echo $OUTPUT->single_button($aurl, get_string('export_to_excel', 'feedback')); echo $OUTPUT->container_end(); } // Show the summary. $summary = new mod_feedback\output\summary($feedbackstructure, $mygroupid); echo $OUTPUT->render_from_template('mod_feedback/summary', $summary->export_for_template($OUTPUT)); // Get the items of the feedback. $items = $feedbackstructure->get_items(true); $check_anonymously = true; if ($mygroupid > 0 AND $feedback->anonymous == FEEDBACK_ANONYMOUS_YES) { $completedcount = $feedbackstructure->count_completed_responses($mygroupid); if ($completedcount < FEEDBACK_MIN_ANONYMOUS_COUNT_IN_GROUP) { $check_anonymously = false; } } echo '
'; if ($check_anonymously) { // Print the items in an analysed form. foreach ($items as $item) { $itemobj = feedback_get_item_class($item->typ); $printnr = ($feedback->autonumbering && $item->itemnr) ? ($item->itemnr . '.') : ''; $itemobj->print_analysed($item, $printnr, $mygroupid); } } else { echo $OUTPUT->heading_with_help(get_string('insufficient_responses_for_this_group', 'feedback'), 'insufficient_responses', 'feedback', '', '', 3); } echo '
'; echo $OUTPUT->footer(); PKB\8Z$. /** * prints the form so the user can fill out the feedback * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id', PARAM_INT); $courseid = optional_param('courseid', null, PARAM_INT); $gopage = optional_param('gopage', 0, PARAM_INT); $gopreviouspage = optional_param('gopreviouspage', null, PARAM_RAW); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); $feedback = $DB->get_record("feedback", array("id" => $cm->instance), '*', MUST_EXIST); $urlparams = array('id' => $cm->id, 'gopage' => $gopage, 'courseid' => $courseid); $PAGE->set_url('/mod/feedback/complete.php', $urlparams); require_course_login($course, true, $cm); $PAGE->set_activity_record($feedback); $context = context_module::instance($cm->id); $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $courseid); $courseid = $feedbackcompletion->get_courseid(); // Check whether the feedback is mapped to the given courseid. if (!has_capability('mod/feedback:edititems', $context) && !$feedbackcompletion->check_course_is_mapped()) { echo $OUTPUT->header(); echo $OUTPUT->notification(get_string('cannotaccess', 'mod_feedback')); echo $OUTPUT->footer(); exit; } //check whether the given courseid exists if ($courseid AND $courseid != SITEID) { require_course_login(get_course($courseid)); // This overwrites the object $COURSE . } if (!$feedbackcompletion->can_complete()) { throw new \moodle_exception('error'); } $PAGE->navbar->add(get_string('feedback:complete', 'feedback')); $PAGE->set_heading($course->fullname); $PAGE->set_title($feedback->name); $PAGE->set_pagelayout('incourse'); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->add_body_class('limitedwidth'); // Check if the feedback is open (timeopen, timeclose). if (!$feedbackcompletion->is_open()) { echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox boxaligncenter'); echo $OUTPUT->notification(get_string('feedback_is_not_open', 'feedback')); echo $OUTPUT->continue_button(course_get_url($courseid ?: $feedback->course)); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } // Mark activity viewed for completion-tracking. if (isloggedin() && !isguestuser()) { $feedbackcompletion->set_module_viewed(); } // Check if user is prevented from re-submission. $cansubmit = $feedbackcompletion->can_submit(); // Initialise the form processing feedback completion. if (!$feedbackcompletion->is_empty() && $cansubmit) { // Process the page via the form. $urltogo = $feedbackcompletion->process_page($gopage, $gopreviouspage); if ($urltogo !== null) { redirect($urltogo); } } // Print the page header. $strfeedbacks = get_string("modulenameplural", "feedback"); $strfeedback = get_string("modulename", "feedback"); echo $OUTPUT->header(); if ($feedbackcompletion->is_empty()) { \core\notification::error(get_string('no_items_available_yet', 'feedback')); } else if ($cansubmit) { if ($feedbackcompletion->just_completed()) { // Display information after the submit. if ($feedback->page_after_submit) { echo $OUTPUT->box($feedbackcompletion->page_after_submit(), 'generalbox boxaligncenter'); } if (!$PAGE->has_secondary_navigation() && $feedbackcompletion->can_view_analysis()) { echo '

'; $analysisurl = new moodle_url('/mod/feedback/analysis.php', array('id' => $cm->id, 'courseid' => $courseid)); echo html_writer::link($analysisurl, get_string('completed_feedbacks', 'feedback')); echo '

'; } if ($feedback->site_after_submit) { $url = feedback_encode_target_url($feedback->site_after_submit); } else { $url = course_get_url($courseid ?: $course->id); } echo $OUTPUT->continue_button($url); } else { // Display the form with the questions. echo $feedbackcompletion->render_items(); } } else { echo $OUTPUT->box_start('generalbox boxaligncenter'); echo $OUTPUT->notification( get_string('this_feedback_is_already_submitted', 'feedback'), \core\output\notification::NOTIFY_INFO, closebutton: false, ); echo $OUTPUT->continue_button(course_get_url($courseid ?: $course->id)); echo $OUTPUT->box_end(); } echo $OUTPUT->footer(); PKB\] N edit.phpnu[. /** * prints the form to edit the feedback items such moving, deleting and so on * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once('../../config.php'); require_once('lib.php'); feedback_init_feedback_session(); $id = required_param('id', PARAM_INT); if (($formdata = data_submitted()) AND !confirm_sesskey()) { throw new \moodle_exception('invalidsesskey'); } $switchitemrequired = optional_param('switchitemrequired', false, PARAM_INT); $deleteitem = optional_param('deleteitem', false, PARAM_INT); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); $context = context_module::instance($cm->id); require_login($course, false, $cm); require_capability('mod/feedback:edititems', $context); $feedback = $PAGE->activityrecord; $feedbackstructure = new mod_feedback_structure($feedback, $cm); $url = new moodle_url('/mod/feedback/edit.php', ['id' => $cm->id]); if ($switchitemrequired) { require_sesskey(); $items = $feedbackstructure->get_items(); if (isset($items[$switchitemrequired])) { feedback_switch_item_required($items[$switchitemrequired]); } redirect($url); } if ($deleteitem) { require_sesskey(); $items = $feedbackstructure->get_items(); if (isset($items[$deleteitem])) { feedback_delete_item($deleteitem); } redirect($url); } //Get the feedbackitems $lastposition = 0; $feedbackitems = $DB->get_records('feedback_item', ['feedback' => $feedback->id], 'position'); if (is_array($feedbackitems)) { $feedbackitems = array_values($feedbackitems); if (count($feedbackitems) > 0) { $lastitem = $feedbackitems[count($feedbackitems)-1]; $lastposition = $lastitem->position; } else { $lastposition = 0; } } $lastposition++; $PAGE->set_url($url); $PAGE->set_heading($course->fullname); /** @var \mod_feedback\output\renderer $renderer */ $renderer = $PAGE->get_renderer('mod_feedback'); $renderer->set_title( [format_string($feedback->name), format_string($course->fullname)], get_string('questions', 'feedback') ); $actionbar = new \mod_feedback\output\edit_action_bar($cm->id, $url, $lastposition); $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); $PAGE->add_body_class('limitedwidth'); $PAGE->requires->js_call_amd('mod_feedback/edit', 'init', [$cm->id]); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('edit_items', 'mod_feedback'), $PAGE->activityheader->get_heading_level()); echo $renderer->main_action_bar($actionbar); $form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_EDIT, $feedbackstructure, 'feedback_edit_form'); $form->display(); echo $OUTPUT->footer(); PKB\e{)) index.phpnu[. /** * prints the overview of all feedbacks included into the current course * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); $courseid = required_param('id', PARAM_INT); \core_courseformat\activityoverviewbase::redirect_to_overview_page($courseid, 'feedback'); PKB\fchoose_group_form.phpnu[. /** * prints the form to choose the group you want to analyse * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ //This file can be deleted PKB\P/! use_templ.phpnu[. /** * print the confirm dialog to use template and create new items from template * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id', PARAM_INT); $templateid = optional_param('templateid', false, PARAM_INT); if (!$templateid) { redirect('edit.php?id='.$id); } $template = $DB->get_record('feedback_template', ['id' => $templateid], '*', MUST_EXIST); $url = new moodle_url('/mod/feedback/use_templ.php', array('id'=>$id, 'templateid'=>$templateid)); $PAGE->set_url($url); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); $context = context_module::instance($cm->id); require_login($course, true, $cm); $feedback = $PAGE->activityrecord; $feedbackstructure = new mod_feedback_structure($feedback, $cm, 0, $templateid); require_capability('mod/feedback:edititems', $context); /// Print the page header $strfeedbacks = get_string("modulenameplural", "feedback"); $strfeedback = get_string("modulename", "feedback"); $params = ['id' => $id]; $activeurl = new moodle_url('/mod/feedback/manage_templates.php', $params); $PAGE->set_url($activeurl); $PAGE->set_heading($course->fullname); $PAGE->set_title($feedback->name); $PAGE->add_body_class('limitedwidth'); $PAGE->activityheader->set_attrs([ "hidecompletion" => true, "description" => '' ]); $actionbar = new \mod_feedback\output\edit_template_action_bar($cm->id, $templateid); /** @var \mod_feedback\output\renderer $renderer */ $renderer = $PAGE->get_renderer('mod_feedback'); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('previewtemplate', 'mod_feedback', $template->name), 3); echo $renderer->main_action_bar($actionbar); $form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_VIEW_TEMPLATE, $feedbackstructure, 'feedback_preview_form', ['templateid' => $templateid]); $form->display(); echo $OUTPUT->footer(); PKB\xAX%X% import.phpnu[. /** * prints the form to import items from xml-file * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); require_once('import_form.php'); // get parameters $id = required_param('id', PARAM_INT); $choosefile = optional_param('choosefile', false, PARAM_PATH); $action = optional_param('action', false, PARAM_ALPHA); $url = new moodle_url('/mod/feedback/import.php', ['id' => $id]); if ($choosefile !== false) { $url->param('choosefile', $choosefile); } if ($action !== false) { $url->param('action', $action); } $PAGE->set_url($url); navigation_node::override_active_url(new moodle_url('/mod/feedback/edit.php')); if (! $cm = get_coursemodule_from_id('feedback', $id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $course = $DB->get_record("course", ["id" => $cm->course])) { throw new \moodle_exception('coursemisconf'); } if (! $feedback = $DB->get_record("feedback", ["id" => $cm->instance])) { throw new \moodle_exception('invalidcoursemodule'); } $context = context_module::instance($cm->id); require_login($course, true, $cm); require_capability('mod/feedback:edititems', $context); $mform = new feedback_import_form(); $newformdata = [ 'id' => $id, 'deleteolditems' => '1', 'action' => 'choosefile', 'confirmadd' => '1', 'do_show' => 'templates', ]; $mform->set_data($newformdata); $formdata = $mform->get_data(); if ($mform->is_cancelled()) { redirect('edit.php?id='.$id.'&do_show=templates'); } // process if we are happy file is ok if ($choosefile) { $xmlcontent = $mform->get_file_content('choosefile'); if (!$xmldata = feedback_load_xml_data($xmlcontent)) { throw new \moodle_exception('cannotloadxml', 'feedback', 'edit.php?id='.$id); } $importerror = feedback_import_loaded_data($xmldata, $feedback->id); if ($importerror->stat == true) { $url = 'edit.php?id='.$id.'&do_show=templates'; redirect($url, get_string('import_successfully', 'feedback'), 3); exit; } } /// Print the page header $strfeedbacks = get_string("modulenameplural", "feedback"); $strfeedback = get_string("modulename", "feedback"); $PAGE->set_heading($course->fullname); $PAGE->set_title($feedback->name); $PAGE->activityheader->set_attrs([ "hidecompletion" => true, "description" => '' ]); $PAGE->add_body_class('limitedwidth'); echo $OUTPUT->header(); /** @var \mod_feedback\output\renderer $renderer */ $renderer = $PAGE->get_renderer('mod_feedback'); /// Print the main part of the page /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// echo $OUTPUT->heading(get_string('import_questions', 'feedback'), 3); if (isset($importerror->msg) AND is_array($importerror->msg)) { echo $OUTPUT->box_start('generalbox errorboxcontent boxaligncenter'); foreach ($importerror->msg as $msg) { echo $msg.'
'; } echo $OUTPUT->box_end(); } $mform->display(); echo $OUTPUT->footer(); function feedback_load_xml_data($xmlcontent) { global $CFG; require_once($CFG->dirroot.'/lib/xmlize.php'); if (!$xmlcontent = feedback_check_xml_utf8($xmlcontent)) { return false; } $data = xmlize($xmlcontent, 1, 'UTF-8'); if (intval($data['FEEDBACK']['@']['VERSION']) != 200701) { return false; } $data = $data['FEEDBACK']['#']['ITEMS'][0]['#']['ITEM']; return $data; } function feedback_import_loaded_data(&$data, $feedbackid) { global $CFG, $DB; feedback_load_feedback_items(); $deleteolditems = optional_param('deleteolditems', 0, PARAM_INT); $error = new stdClass(); $error->stat = true; $error->msg = []; if (!is_array($data)) { $error->msg[] = get_string('data_is_not_an_array', 'feedback'); $error->stat = false; return $error; } if ($deleteolditems) { feedback_delete_all_items($feedbackid); $position = 0; } else { //items will be add to the end of the existing items $position = $DB->count_records('feedback_item', ['feedback' => $feedbackid]); } // Depend items we are storing temporary in an mapping list [new id => dependitem]. // We also store a mapping of all items [oldid => newid]. $dependitemsmap = []; $itembackup = []; foreach ($data as $item) { $position++; //check the typ $typ = $item['@']['TYPE']; //check oldtypes first switch($typ) { case 'radio': $typ = 'multichoice'; $oldtyp = 'radio'; break; case 'dropdown': $typ = 'multichoice'; $oldtyp = 'dropdown'; break; case 'check': $typ = 'multichoice'; $oldtyp = 'check'; break; case 'radiorated': $typ = 'multichoicerated'; $oldtyp = 'radiorated'; break; case 'dropdownrated': $typ = 'multichoicerated'; $oldtyp = 'dropdownrated'; break; default: $oldtyp = $typ; } $itemclass = 'feedback_item_'.$typ; if ($typ != 'pagebreak' AND !class_exists($itemclass)) { $error->stat = false; $error->msg[] = 'type ('.$typ.') not found'; continue; } $itemobj = new $itemclass(); $newitem = new stdClass(); $newitem->feedback = $feedbackid; $newitem->template = 0; $newitem->typ = $typ; $newitem->name = trim($item['#']['ITEMTEXT'][0]['#']); $newitem->label = trim($item['#']['ITEMLABEL'][0]['#']); if ($typ === 'captcha' || $typ === 'label') { $newitem->label = ''; $newitem->name = ''; } $newitem->options = trim($item['#']['OPTIONS'][0]['#']); $newitem->presentation = trim($item['#']['PRESENTATION'][0]['#']); //check old types of radio, check, and so on switch($oldtyp) { case 'radio': $newitem->presentation = 'r>>>>>'.$newitem->presentation; break; case 'dropdown': $newitem->presentation = 'd>>>>>'.$newitem->presentation; break; case 'check': $newitem->presentation = 'c>>>>>'.$newitem->presentation; break; case 'radiorated': $newitem->presentation = 'r>>>>>'.$newitem->presentation; break; case 'dropdownrated': $newitem->presentation = 'd>>>>>'.$newitem->presentation; break; } if (isset($item['#']['DEPENDITEM'][0]['#'])) { $newitem->dependitem = intval($item['#']['DEPENDITEM'][0]['#']); } else { $newitem->dependitem = 0; } if (isset($item['#']['DEPENDVALUE'][0]['#'])) { $newitem->dependvalue = trim($item['#']['DEPENDVALUE'][0]['#']); } else { $newitem->dependvalue = ''; } $olditemid = intval($item['#']['ITEMID'][0]['#']); if ($typ != 'pagebreak') { $newitem->hasvalue = $itemobj->get_hasvalue(); } else { $newitem->hasvalue = 0; } $newitem->required = intval($item['@']['REQUIRED']); $newitem->position = $position; $newid = $DB->insert_record('feedback_item', $newitem); $itembackup[$olditemid] = $newid; if ($newitem->dependitem) { $dependitemsmap[$newid] = $newitem->dependitem; } } //remapping the dependency foreach ($dependitemsmap as $key => $dependitem) { $newitem = $DB->get_record('feedback_item', ['id' => $key]); $newitem->dependitem = $itembackup[$newitem->dependitem]; $DB->update_record('feedback_item', $newitem); } return $error; } function feedback_check_xml_utf8($text) { //find the encoding $searchpattern = '/^\<\?xml.+(encoding=\"([a-z0-9-]*)\").+\?\>/is'; if (!preg_match($searchpattern, $text, $match)) { return false; //no xml-file } //$match[0] = \<\? xml ... \?\> (without \) //$match[1] = encoding="...." //$match[2] = ISO-8859-1 or so on if (isset($match[0]) AND !isset($match[1])) { //no encoding given. we assume utf-8 return $text; } //encoding is given in $match[2] if (isset($match[0]) AND isset($match[1]) AND isset($match[2])) { $enc = $match[2]; return core_text::convert($text, $enc); } } PKB\LLamd/src/edit.jsnu[// 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 . /** * Edit items in feedback module * * @module mod_feedback/edit * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ "use strict"; import {addIconToContainerRemoveOnCompletion} from 'core/loadingicon'; import Notification from 'core/notification'; import Pending from 'core/pending'; import {prefetchStrings} from 'core/prefetch'; import SortableList from 'core/sortable_list'; import {getString, getStrings} from 'core/str'; import {add as addToast} from 'core/toast'; import {reorderQuestions} from 'mod_feedback/local/repository'; import Templates from 'core/templates'; const Selectors = { deleteQuestionButton: '[data-action="delete"]', sortableListRegion: '[data-region="questions-sortable-list"]', sortableElement: '[data-region="questions-sortable-list"] .feedback_itemlist[id]', sortableElementTitle: '[data-region="item-title"] span', questionLabel: '[data-region="questions-sortable-list"] .col-form-label', actionsMenuData: '[data-item-actions-menu]', }; /** * Returns the Feedback question item id from the DOM id of an item. * * @param {String} id The dom id, f.g.: feedback_item_22 * @return int */ const getItemId = (id) => { return Number(id.replace(/^.*feedback_item_/i, '')); }; /** * Returns the order of the items in the sortable list. * * @param {Element} element The element to get the order from. * @return string */ const getItemOrder = (element) => { const sortableList = element.closest(Selectors.sortableListRegion); let itemOrder = []; sortableList.querySelectorAll(Selectors.sortableElement).forEach((item) => { itemOrder.push(getItemId(item.id)); }); return itemOrder.toString(); }; let initialized = false; let moduleId = null; /** * Initialise editor and all it's modules * * @param {Integer} cmId */ export const init = async(cmId) => { moduleId = cmId; // Ensure we only add our listeners once (can be called multiple times). if (initialized) { return; } prefetchStrings('core', [ 'yes', 'no', ]); prefetchStrings('admin', [ 'confirmation', ]); prefetchStrings('mod_feedback', [ 'confirmdeleteitem', 'questionmoved', 'move_item', ]); await enhanceEditForm(); // Add event listeners. document.addEventListener('click', async event => { // Delete question. const deleteButton = event.target.closest(Selectors.deleteQuestionButton); if (deleteButton) { event.preventDefault(); const confirmationStrings = await getStrings([ {key: 'confirmation', component: 'admin'}, {key: 'confirmdeleteitem', component: 'mod_feedback'}, {key: 'yes', component: 'core'}, {key: 'no', component: 'core'}, ]); Notification.confirm(...confirmationStrings, () => { window.location = deleteButton.getAttribute('href'); }); return; } }); // Initialize sortable list to handle active conditions moving. const sortableList = new SortableList(document.querySelector(Selectors.sortableListRegion)); sortableList.getElementName = element => Promise.resolve(element[0].querySelector(Selectors.sortableElementTitle)?.textContent); document.addEventListener(SortableList.EVENTS.elementDrop, event => { if (!event.detail.positionChanged) { return; } const pendingPromise = new Pending('mod_feedback/questions:reorder'); const itemOrder = getItemOrder(event.detail.element[0]); addIconToContainerRemoveOnCompletion(event.detail.element[0], pendingPromise); reorderQuestions(moduleId, itemOrder) .then(() => getString('questionmoved', 'mod_feedback')) .then(addToast) .then(() => pendingPromise.resolve()) .catch(Notification.exception); }); initialized = true; }; /** * Enhance the edit form by adding a move item button and an action menu to each question. * * @returns {Promise} */ const enhanceEditForm = async() => { const questionLabels = document.querySelectorAll(Selectors.questionLabel); const movetitle = await getString('move_item', 'mod_feedback'); const updates = Array.from(questionLabels).map(async(container) => { const label = container.querySelector(Selectors.actionsMenuData); if (!label) { return; } try { const contextData = { movetitle, label: label.parentElement.outerHTML, actionsmenu: JSON.parse(label.dataset.itemActionsMenu || '{}'), }; container.innerHTML = await Templates.render('mod_feedback/item_edit_enhanced_title', contextData); } catch (error) { await Notification.exception(error); } }); await Promise.all(updates); }; PKB\nϺ= = amd/src/createtemplate.jsnu[// 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 . /** * Javascript module for saving a new template. * * @module mod_feedback/createtemplate * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ModalForm from 'core_form/modalform'; import Notification from 'core/notification'; import {getString} from 'core/str'; import {add as addToast} from 'core/toast'; const selectors = { modaltrigger: '[data-action="createtemplate"]', }; /** * Initialize module */ export const init = () => { const trigger = document.querySelector(selectors.modaltrigger); trigger.addEventListener('click', event => { event.preventDefault(); const ele = event.currentTarget; const modalForm = new ModalForm({ modalConfig: { title: getString('save_as_new_template', 'mod_feedback'), }, formClass: 'mod_feedback\\form\\create_template_form', args: { id: ele.dataset.dataid }, saveButtonText: getString('save', 'core') }); // Show a toast notification when the form is submitted. modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { if (event.detail.result) { getString('template_saved', 'feedback').then(addToast).catch(); } else { getString('saving_failed', 'feedback').then(string => { return Notification.addNotification({ type: 'error', message: string }); }).catch(); } }); modalForm.show(); }); }; PKB\"Y<((amd/src/local/repository.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Module to handle feedback AJAX requests * * @module mod_feedback/local/repository * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; /** * Reorder questions for a given feedback course module * * @param {Number} moduleId * @param {String} itemOrder * @return {Promise} */ export const reorderQuestions = (moduleId, itemOrder) => { const request = { methodname: 'mod_feedback_questions_reorder', args: {cmid: moduleId, itemorder: itemOrder} }; return Ajax.call([request])[0]; }; PKB\Ί  amd/src/usetemplate.jsnu[// 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 . /** * Javascript module for using an existing template * * @module mod_feedback/usetemplate * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ModalForm from 'core_form/modalform'; import Notification from 'core/notification'; import {getString} from 'core/str'; const selectors = { modaltrigger: '[data-action="usetemplate"]', }; let initialized = false; /** * Initialize module */ export const init = () => { if (initialized) { // We already added the event listeners (can be called multiple times). return; } document.addEventListener('click', event => { // Use the template. const trigger = event.target.closest(selectors.modaltrigger); if (trigger) { event.preventDefault(); const modalForm = new ModalForm({ modalConfig: { title: getString('use_this_template', 'mod_feedback'), }, formClass: 'mod_feedback\\form\\use_template_form', args: { id: trigger.getAttribute('data-dataid'), templateid: trigger.getAttribute('data-templateid') }, saveButtonText: getString('save', 'core') }); // Show a toast notification when the form is submitted. modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { if (event.detail.result) { window.location.assign(event.detail.url); } else { Notification.addNotification({ type: 'error', message: getString('saving_failed', 'mod_feedback') }); } }); modalForm.show(); } }); initialized = true; }; PKB\ȸamd/build/edit.min.jsnu[define("mod_feedback/edit",["exports","core/loadingicon","core/notification","core/pending","core/prefetch","core/sortable_list","core/str","core/toast","mod_feedback/local/repository","core/templates"],(function(_exports,_loadingicon,_notification,_pending,_prefetch,_sortable_list,_str,_toast,_repository,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates);const Selectors_deleteQuestionButton='[data-action="delete"]',Selectors_sortableListRegion='[data-region="questions-sortable-list"]',Selectors_sortableElement='[data-region="questions-sortable-list"] .feedback_itemlist[id]',Selectors_sortableElementTitle='[data-region="item-title"] span',Selectors_questionLabel='[data-region="questions-sortable-list"] .col-form-label',Selectors_actionsMenuData="[data-item-actions-menu]",getItemOrder=element=>{const sortableList=element.closest(Selectors_sortableListRegion);let itemOrder=[];return sortableList.querySelectorAll(Selectors_sortableElement).forEach((item=>{var id;itemOrder.push((id=item.id,Number(id.replace(/^.*feedback_item_/i,""))))})),itemOrder.toString()};let initialized=!1,moduleId=null;_exports.init=async cmId=>{if(moduleId=cmId,initialized)return;(0,_prefetch.prefetchStrings)("core",["yes","no"]),(0,_prefetch.prefetchStrings)("admin",["confirmation"]),(0,_prefetch.prefetchStrings)("mod_feedback",["confirmdeleteitem","questionmoved","move_item"]),await enhanceEditForm(),document.addEventListener("click",(async event=>{const deleteButton=event.target.closest(Selectors_deleteQuestionButton);if(deleteButton){event.preventDefault();const confirmationStrings=await(0,_str.getStrings)([{key:"confirmation",component:"admin"},{key:"confirmdeleteitem",component:"mod_feedback"},{key:"yes",component:"core"},{key:"no",component:"core"}]);_notification.default.confirm(...confirmationStrings,(()=>{window.location=deleteButton.getAttribute("href")}))}else;}));new _sortable_list.default(document.querySelector(Selectors_sortableListRegion)).getElementName=element=>{var _element$0$querySelec;return Promise.resolve(null===(_element$0$querySelec=element[0].querySelector(Selectors_sortableElementTitle))||void 0===_element$0$querySelec?void 0:_element$0$querySelec.textContent)},document.addEventListener(_sortable_list.default.EVENTS.elementDrop,(event=>{if(!event.detail.positionChanged)return;const pendingPromise=new _pending.default("mod_feedback/questions:reorder"),itemOrder=getItemOrder(event.detail.element[0]);(0,_loadingicon.addIconToContainerRemoveOnCompletion)(event.detail.element[0],pendingPromise),(0,_repository.reorderQuestions)(moduleId,itemOrder).then((()=>(0,_str.getString)("questionmoved","mod_feedback"))).then(_toast.add).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})),initialized=!0};const enhanceEditForm=async()=>{const questionLabels=document.querySelectorAll(Selectors_questionLabel),movetitle=await(0,_str.getString)("move_item","mod_feedback"),updates=Array.from(questionLabels).map((async container=>{const label=container.querySelector(Selectors_actionsMenuData);if(label)try{const contextData={movetitle:movetitle,label:label.parentElement.outerHTML,actionsmenu:JSON.parse(label.dataset.itemActionsMenu||"{}")};container.innerHTML=await _templates.default.render("mod_feedback/item_edit_enhanced_title",contextData)}catch(error){await _notification.default.exception(error)}}));await Promise.all(updates)}})); //# sourceMappingURL=edit.min.js.mapPKB\$  #amd/build/createtemplate.min.js.mapnu[{"version":3,"file":"createtemplate.min.js","sources":["../src/createtemplate.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for saving a new template.\n *\n * @module mod_feedback/createtemplate\n * @copyright 2021 Peter Dias\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport {getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\n\nconst selectors = {\n modaltrigger: '[data-action=\"createtemplate\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n const trigger = document.querySelector(selectors.modaltrigger);\n\n trigger.addEventListener('click', event => {\n event.preventDefault();\n const ele = event.currentTarget;\n\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('save_as_new_template', 'mod_feedback'),\n },\n formClass: 'mod_feedback\\\\form\\\\create_template_form',\n args: {\n id: ele.dataset.dataid\n },\n saveButtonText: getString('save', 'core')\n });\n\n // Show a toast notification when the form is submitted.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n if (event.detail.result) {\n getString('template_saved', 'feedback').then(addToast).catch();\n } else {\n getString('saving_failed', 'feedback').then(string => {\n return Notification.addNotification({\n type: 'error',\n message: string\n });\n }).catch();\n }\n });\n\n modalForm.show();\n });\n};\n"],"names":["selectors","document","querySelector","addEventListener","event","preventDefault","ele","currentTarget","modalForm","ModalForm","modalConfig","title","formClass","args","id","dataset","dataid","saveButtonText","events","FORM_SUBMITTED","detail","result","then","addToast","catch","string","Notification","addNotification","type","message","show"],"mappings":";;;;;;;0LA4BMA,uBACY,+CAME,KACAC,SAASC,cAAcF,wBAE/BG,iBAAiB,SAASC,QAC9BA,MAAMC,uBACAC,IAAMF,MAAMG,cAEZC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,kBAAU,uBAAwB,iBAE7CC,UAAW,2CACXC,KAAM,CACFC,GAAIR,IAAIS,QAAQC,QAEpBC,gBAAgB,kBAAU,OAAQ,UAItCT,UAAUL,iBAAiBK,UAAUU,OAAOC,gBAAgBf,QACpDA,MAAMgB,OAAOC,0BACH,iBAAkB,YAAYC,KAAKC,YAAUC,2BAE7C,gBAAiB,YAAYF,MAAKG,QACjCC,sBAAaC,gBAAgB,CAChCC,KAAM,QACNC,QAASJ,WAEdD,WAIXhB,UAAUsB"}PKB\zY& & amd/build/edit.min.js.mapnu[{"version":3,"file":"edit.min.js","sources":["../src/edit.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Edit items in feedback module\n *\n * @module mod_feedback/edit\n * @copyright 2016 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport SortableList from 'core/sortable_list';\nimport {getString, getStrings} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport {reorderQuestions} from 'mod_feedback/local/repository';\nimport Templates from 'core/templates';\n\nconst Selectors = {\n deleteQuestionButton: '[data-action=\"delete\"]',\n sortableListRegion: '[data-region=\"questions-sortable-list\"]',\n sortableElement: '[data-region=\"questions-sortable-list\"] .feedback_itemlist[id]',\n sortableElementTitle: '[data-region=\"item-title\"] span',\n questionLabel: '[data-region=\"questions-sortable-list\"] .col-form-label',\n actionsMenuData: '[data-item-actions-menu]',\n};\n\n/**\n * Returns the Feedback question item id from the DOM id of an item.\n *\n * @param {String} id The dom id, f.g.: feedback_item_22\n * @return int\n */\nconst getItemId = (id) => {\n return Number(id.replace(/^.*feedback_item_/i, ''));\n};\n\n/**\n * Returns the order of the items in the sortable list.\n *\n * @param {Element} element The element to get the order from.\n * @return string\n */\nconst getItemOrder = (element) => {\n const sortableList = element.closest(Selectors.sortableListRegion);\n let itemOrder = [];\n sortableList.querySelectorAll(Selectors.sortableElement).forEach((item) => {\n itemOrder.push(getItemId(item.id));\n });\n return itemOrder.toString();\n};\n\nlet initialized = false;\nlet moduleId = null;\n\n/**\n * Initialise editor and all it's modules\n *\n * @param {Integer} cmId\n */\nexport const init = async(cmId) => {\n\n moduleId = cmId;\n\n // Ensure we only add our listeners once (can be called multiple times).\n if (initialized) {\n return;\n }\n\n prefetchStrings('core', [\n 'yes',\n 'no',\n ]);\n prefetchStrings('admin', [\n 'confirmation',\n ]);\n prefetchStrings('mod_feedback', [\n 'confirmdeleteitem',\n 'questionmoved',\n 'move_item',\n ]);\n\n await enhanceEditForm();\n\n // Add event listeners.\n document.addEventListener('click', async event => {\n\n // Delete question.\n const deleteButton = event.target.closest(Selectors.deleteQuestionButton);\n if (deleteButton) {\n event.preventDefault();\n const confirmationStrings = await getStrings([\n {key: 'confirmation', component: 'admin'},\n {key: 'confirmdeleteitem', component: 'mod_feedback'},\n {key: 'yes', component: 'core'},\n {key: 'no', component: 'core'},\n ]);\n Notification.confirm(...confirmationStrings, () => {\n window.location = deleteButton.getAttribute('href');\n });\n return;\n }\n });\n\n // Initialize sortable list to handle active conditions moving.\n const sortableList = new SortableList(document.querySelector(Selectors.sortableListRegion));\n sortableList.getElementName = element => Promise.resolve(element[0].querySelector(Selectors.sortableElementTitle)?.textContent);\n\n document.addEventListener(SortableList.EVENTS.elementDrop, event => {\n if (!event.detail.positionChanged) {\n return;\n }\n const pendingPromise = new Pending('mod_feedback/questions:reorder');\n const itemOrder = getItemOrder(event.detail.element[0]);\n addIconToContainerRemoveOnCompletion(event.detail.element[0], pendingPromise);\n reorderQuestions(moduleId, itemOrder)\n .then(() => getString('questionmoved', 'mod_feedback'))\n .then(addToast)\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n });\n\n initialized = true;\n};\n\n/**\n * Enhance the edit form by adding a move item button and an action menu to each question.\n *\n * @returns {Promise}\n */\nconst enhanceEditForm = async() => {\n const questionLabels = document.querySelectorAll(Selectors.questionLabel);\n const movetitle = await getString('move_item', 'mod_feedback');\n\n const updates = Array.from(questionLabels).map(async(container) => {\n const label = container.querySelector(Selectors.actionsMenuData);\n if (!label) {\n return;\n }\n\n try {\n const contextData = {\n movetitle,\n label: label.parentElement.outerHTML,\n actionsmenu: JSON.parse(label.dataset.itemActionsMenu || '{}'),\n };\n container.innerHTML = await Templates.render('mod_feedback/item_edit_enhanced_title', contextData);\n } catch (error) {\n await Notification.exception(error);\n }\n });\n await Promise.all(updates);\n};\n"],"names":["Selectors","getItemOrder","element","sortableList","closest","itemOrder","querySelectorAll","forEach","item","id","push","Number","replace","toString","initialized","moduleId","async","cmId","enhanceEditForm","document","addEventListener","deleteButton","event","target","preventDefault","confirmationStrings","key","component","confirm","window","location","getAttribute","SortableList","querySelector","getElementName","Promise","resolve","_element$0$querySelec","textContent","EVENTS","elementDrop","detail","positionChanged","pendingPromise","Pending","then","addToast","catch","Notification","exception","questionLabels","movetitle","updates","Array","from","map","label","container","contextData","parentElement","outerHTML","actionsmenu","JSON","parse","dataset","itemActionsMenu","innerHTML","Templates","render","error","all"],"mappings":"sqBAmCMA,+BACoB,yBADpBA,6BAEkB,0CAFlBA,0BAGe,iEAHfA,+BAIoB,kCAJpBA,wBAKa,0DALbA,0BAMe,2BAmBfC,aAAgBC,gBACZC,aAAeD,QAAQE,QAAQJ,kCACjCK,UAAY,UAChBF,aAAaG,iBAAiBN,2BAA2BO,SAASC,OAbnDC,IAAAA,GAcXJ,UAAUK,MAdCD,GAccD,KAAKC,GAb3BE,OAAOF,GAAGG,QAAQ,qBAAsB,UAexCP,UAAUQ,gBAGjBC,aAAc,EACdC,SAAW,mBAOKC,MAAAA,UAEhBD,SAAWE,KAGPH,iDAIY,OAAQ,CACpB,MACA,qCAEY,QAAS,CACrB,+CAEY,eAAgB,CAC5B,oBACA,gBACA,oBAGEI,kBAGNC,SAASC,iBAAiB,SAASJ,MAAAA,cAGzBK,aAAeC,MAAMC,OAAOnB,QAAQJ,mCACtCqB,cACAC,MAAME,uBACAC,0BAA4B,mBAAW,CACzC,CAACC,IAAK,eAAgBC,UAAW,SACjC,CAACD,IAAK,oBAAqBC,UAAW,gBACtC,CAACD,IAAK,MAAOC,UAAW,QACxB,CAACD,IAAK,KAAMC,UAAW,gCAEdC,WAAWH,qBAAqB,KACzCI,OAAOC,SAAWT,aAAaU,aAAa,oBAOnC,IAAIC,uBAAab,SAASc,cAAcjC,+BAChDkC,eAAiBhC,2CAAWiC,QAAQC,sCAAQlC,QAAQ,GAAG+B,cAAcjC,wEAAzBqC,sBAA0DC,cAEnHnB,SAASC,iBAAiBY,uBAAaO,OAAOC,aAAalB,YAClDA,MAAMmB,OAAOC,6BAGZC,eAAiB,IAAIC,iBAAQ,kCAC7BvC,UAAYJ,aAAaqB,MAAMmB,OAAOvC,QAAQ,0DACfoB,MAAMmB,OAAOvC,QAAQ,GAAIyC,iDAC7C5B,SAAUV,WACtBwC,MAAK,KAAM,kBAAU,gBAAiB,kBACtCA,KAAKC,YACLD,MAAK,IAAMF,eAAeP,YAC1BW,MAAMC,sBAAaC,cAG5BnC,aAAc,SAQZI,gBAAkBF,gBACdkC,eAAiB/B,SAASb,iBAAiBN,yBAC3CmD,gBAAkB,kBAAU,YAAa,gBAEzCC,QAAUC,MAAMC,KAAKJ,gBAAgBK,KAAIvC,MAAAA,kBACrCwC,MAAQC,UAAUxB,cAAcjC,8BACjCwD,gBAKKE,YAAc,CAChBP,UAAAA,UACAK,MAAOA,MAAMG,cAAcC,UAC3BC,YAAaC,KAAKC,MAAMP,MAAMQ,QAAQC,iBAAmB,OAE7DR,UAAUS,gBAAkBC,mBAAUC,OAAO,wCAAyCV,aACxF,MAAOW,aACCrB,sBAAaC,UAAUoB,iBAG/BlC,QAAQmC,IAAIlB"}PKB\d=amd/build/usetemplate.min.jsnu[define("mod_feedback/usetemplate",["exports","core_form/modalform","core/notification","core/str"],(function(_exports,_modalform,_notification,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for using an existing template * * @module mod_feedback/usetemplate * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification);const selectors_modaltrigger='[data-action="usetemplate"]';let initialized=!1;_exports.init=()=>{initialized||(document.addEventListener("click",(event=>{const trigger=event.target.closest(selectors_modaltrigger);if(trigger){event.preventDefault();const modalForm=new _modalform.default({modalConfig:{title:(0,_str.getString)("use_this_template","mod_feedback")},formClass:"mod_feedback\\form\\use_template_form",args:{id:trigger.getAttribute("data-dataid"),templateid:trigger.getAttribute("data-templateid")},saveButtonText:(0,_str.getString)("save","core")});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{event.detail.result?window.location.assign(event.detail.url):_notification.default.addNotification({type:"error",message:(0,_str.getString)("saving_failed","mod_feedback")})})),modalForm.show()}})),initialized=!0)}})); //# sourceMappingURL=usetemplate.min.js.mapPKB\a);!amd/build/local/repository.min.jsnu[define("mod_feedback/local/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; /** * Module to handle feedback AJAX requests * * @module mod_feedback/local/repository * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.reorderQuestions=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.reorderQuestions=(moduleId,itemOrder)=>{const request={methodname:"mod_feedback_questions_reorder",args:{cmid:moduleId,itemorder:itemOrder}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=repository.min.js.mapPKB\v̜%amd/build/local/repository.min.js.mapnu[{"version":3,"file":"repository.min.js","sources":["../../src/local/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to handle feedback AJAX requests\n *\n * @module mod_feedback/local/repository\n * @copyright 2024 Mikel Martín \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Reorder questions for a given feedback course module\n *\n * @param {Number} moduleId\n * @param {String} itemOrder\n * @return {Promise}\n */\nexport const reorderQuestions = (moduleId, itemOrder) => {\n const request = {\n methodname: 'mod_feedback_questions_reorder',\n args: {cmid: moduleId, itemorder: itemOrder}\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["moduleId","itemOrder","request","methodname","args","cmid","itemorder","Ajax","call"],"mappings":";;;;;;;4KAgCgC,CAACA,SAAUC,mBACjCC,QAAU,CACZC,WAAY,iCACZC,KAAM,CAACC,KAAML,SAAUM,UAAWL,mBAG/BM,cAAKC,KAAK,CAACN,UAAU"}PKB\+* amd/build/usetemplate.min.js.mapnu[{"version":3,"file":"usetemplate.min.js","sources":["../src/usetemplate.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for using an existing template\n *\n * @module mod_feedback/usetemplate\n * @copyright 2021 Peter Dias\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport {getString} from 'core/str';\n\nconst selectors = {\n modaltrigger: '[data-action=\"usetemplate\"]',\n};\n\nlet initialized = false;\n\n/**\n * Initialize module\n */\nexport const init = () => {\n\n if (initialized) {\n // We already added the event listeners (can be called multiple times).\n return;\n }\n\n document.addEventListener('click', event => {\n // Use the template.\n const trigger = event.target.closest(selectors.modaltrigger);\n if (trigger) {\n event.preventDefault();\n\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('use_this_template', 'mod_feedback'),\n },\n formClass: 'mod_feedback\\\\form\\\\use_template_form',\n args: {\n id: trigger.getAttribute('data-dataid'),\n templateid: trigger.getAttribute('data-templateid')\n },\n saveButtonText: getString('save', 'core')\n });\n\n // Show a toast notification when the form is submitted.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n if (event.detail.result) {\n window.location.assign(event.detail.url);\n } else {\n Notification.addNotification({\n type: 'error',\n message: getString('saving_failed', 'mod_feedback')\n });\n }\n });\n\n modalForm.show();\n }\n });\n\n initialized = true;\n};\n"],"names":["selectors","initialized","document","addEventListener","event","trigger","target","closest","preventDefault","modalForm","ModalForm","modalConfig","title","formClass","args","id","getAttribute","templateid","saveButtonText","events","FORM_SUBMITTED","detail","result","window","location","assign","url","addNotification","type","message","show"],"mappings":";;;;;;;0LA2BMA,uBACY,kCAGdC,aAAc,gBAKE,KAEZA,cAKJC,SAASC,iBAAiB,SAASC,cAEzBC,QAAUD,MAAME,OAAOC,QAAQP,2BACjCK,QAAS,CACTD,MAAMI,uBAEAC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,kBAAU,oBAAqB,iBAE1CC,UAAW,wCACXC,KAAM,CACFC,GAAIV,QAAQW,aAAa,eACzBC,WAAYZ,QAAQW,aAAa,oBAErCE,gBAAgB,kBAAU,OAAQ,UAItCT,UAAUN,iBAAiBM,UAAUU,OAAOC,gBAAgBhB,QACpDA,MAAMiB,OAAOC,OACbC,OAAOC,SAASC,OAAOrB,MAAMiB,OAAOK,2BAEvBC,gBAAgB,CACzBC,KAAM,QACNC,SAAU,kBAAU,gBAAiB,qBAKjDpB,UAAUqB,WAIlB7B,aAAc"}PKB\:oamd/build/createtemplate.min.jsnu[define("mod_feedback/createtemplate",["exports","core_form/modalform","core/notification","core/str","core/toast"],(function(_exports,_modalform,_notification,_str,_toast){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for saving a new template. * * @module mod_feedback/createtemplate * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification);const selectors_modaltrigger='[data-action="createtemplate"]';_exports.init=()=>{document.querySelector(selectors_modaltrigger).addEventListener("click",(event=>{event.preventDefault();const ele=event.currentTarget,modalForm=new _modalform.default({modalConfig:{title:(0,_str.getString)("save_as_new_template","mod_feedback")},formClass:"mod_feedback\\form\\create_template_form",args:{id:ele.dataset.dataid},saveButtonText:(0,_str.getString)("save","core")});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{event.detail.result?(0,_str.getString)("template_saved","feedback").then(_toast.add).catch():(0,_str.getString)("saving_failed","feedback").then((string=>_notification.default.addNotification({type:"error",message:string}))).catch()})),modalForm.show()}))}})); //# sourceMappingURL=createtemplate.min.js.mapPKB\', UPGRADING.mdnu[# mod_feedback Upgrade notes ## 5.0 ### Added - Added new `mod_feedback_questions_reorder` external function For more information see [MDL-81745](https://tracker.moodle.org/browse/MDL-81745) ### Deprecated - The 'mode' parameter has been deprecated from 'edit_template_action_bar' and 'templates_table' contructors. For more information see [MDL-81744](https://tracker.moodle.org/browse/MDL-81744) ### Removed - The 'use_template' template has been removed as it is not needed anymore. For more information see [MDL-81744](https://tracker.moodle.org/browse/MDL-81744) ## 4.5 ### Deprecated - The `\feedback_check_is_switchrole()` function has been deprecated as it didn't work. For more information see [MDL-72424](https://tracker.moodle.org/browse/MDL-72424) - The method `\mod_feedback\output\renderer::create_template_form()` has been deprecated. It is not used anymore. For more information see [MDL-81742](https://tracker.moodle.org/browse/MDL-81742) PKB\HHtests/fixtures/testexport.xmlnu[ >>>>option a|option b|option c]]> >>>>option d|option e|option f]]> >>>>option g|option h|option i]]> >>>>0####option k|1####option l|5####option m]]> PKB\qp<"tests/backup/restore_date_test.phpnu[. namespace mod_feedback\backup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); /** * Restore date tests. * * @package mod_feedback * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_date_test extends \restore_date_testcase { public function test_restore_dates(): void { global $DB, $USER; $time = 10000; list($course, $feedback) = $this->create_course_and_module('feedback', ['timeopen' => $time, 'timeclose' => $time]); // Create response. $response = new \stdClass(); $response->feedback = $feedback->id; $response->userid = $USER->id; $response->anonymous_response = FEEDBACK_ANONYMOUS_NO; $response->timemodified = $time; $completedid = $DB->insert_record('feedback_completed', $response); $response = $DB->get_record('feedback_completed', array('id' => $completedid), '*', MUST_EXIST); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); $newfeedback = $DB->get_record('feedback', ['course' => $newcourseid]); $newresponse = $DB->get_record('feedback_completed', ['feedback' => $newfeedback->id]); $this->assertFieldsNotRolledForward($feedback, $newfeedback, ['timemodified']); $props = ['timeopen', 'timeclose']; $this->assertFieldsRolledForward($feedback, $newfeedback, $props); $this->assertEquals($response->timemodified, $newresponse->timemodified); } /** * Test that dependency for items is restored correctly. */ public function test_restore_item_dependency(): void { global $DB; // Create a course and a feedback activity. $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); // Create a couple of items which depend on each other. $item1 = $feedbackgenerator->create_item_numeric($feedback); $item2 = $feedbackgenerator->create_item_numeric($feedback, array('dependitem' => $item1->id)); $DB->set_field('feedback_item', 'dependitem', $item2->id, ['id' => $item1->id]); // Create one more item with fake/broken dependitem. $item3 = $feedbackgenerator->create_item_numeric($feedback, array('dependitem' => 123456)); // Backup and restore the course. $restoredcourseid = $this->backup_and_restore($course); $restoredfeedback = $DB->get_record('feedback', ['course' => $restoredcourseid]); // Restored item1 and item2 are expected to be dependent the same way as the original ones. $restoreditem1 = $DB->get_record('feedback_item', ['feedback' => $restoredfeedback->id, 'name' => $item1->name]); $restoreditem2 = $DB->get_record('feedback_item', ['feedback' => $restoredfeedback->id, 'name' => $item2->name]); $this->assertEquals($restoreditem2->id, $restoreditem1->dependitem); $this->assertEquals($restoreditem1->id, $restoreditem2->dependitem); // Restored item3 is expected to have an empty dependitem. $restoreditem3 = $DB->get_record('feedback_item', ['feedback' => $restoredfeedback->id, 'name' => $item3->name]); $this->assertEquals(0, $restoreditem3->dependitem); } } PKB\ϘKKtests/generator/lib.phpnu[. /** * mod_feedback data generator. * * @package mod_feedback * @category test * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * mod_feedback data generator class. * * @package mod_feedback * @category test * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_feedback_generator extends testing_module_generator { public function create_instance($record = null, ?array $options = null) { global $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $record = (object)(array)$record; if (!isset($record->anonymous)) { $record->anonymous = FEEDBACK_ANONYMOUS_YES; } if (!isset($record->email_notification)) { $record->email_notification = 0; } if (!isset($record->multiple_submit)) { $record->multiple_submit = 0; } if (!isset($record->autonumbering)) { $record->autonumbering = 0; } if (!isset($record->site_after_submit)) { $record->site_after_submit = ''; } if (!isset($record->page_after_submit)) { $record->page_after_submit = 'This is page after submit'; } if (!isset($record->page_after_submitformat)) { $record->page_after_submitformat = FORMAT_MOODLE; } if (!isset($record->publish_stats)) { $record->publish_stats = 0; } if (!isset($record->timeopen)) { $record->timeopen = 0; } if (!isset($record->timeclose)) { $record->timeclose = 0; } if (!isset($record->timemodified)) { $record->timemodified = time(); } if (!isset($record->completionsubmit)) { $record->completionsubmit = 0; } // Hack to bypass draft processing of feedback_add_instance. $record->page_after_submit_editor['itemid'] = false; return parent::create_instance($record, (array)$options); } /** * Create question. * * @param array $data Question data * @return mixed Question instance */ public function create_question(array $data) { global $DB; $questiontype = $data['questiontype'] ?? 'textfield'; $cm = get_coursemodule_from_id('feedback', $data['cmid']); $feedback = $DB->get_record('feedback', ['id' => $cm->instance]); unset($data['questiontype']); unset($data['cmid']); if (isset($data['values'])) { $data['values'] = $this->format_item_values($questiontype, $data['values']); } if (isset($data['dependitem'])) { $select = 'label = ? OR name = ?'; $params = [ $data['dependitem'], $data['dependitem'], ]; $dependid = $DB->get_field_select('feedback_item', 'id', $select, $params, MUST_EXIST); $data['dependitem'] = $dependid; } else { // Make sure `dependvalue` is not set if we are not creating a dependent item. unset($data['dependvalue']); } return call_user_func([$this, "create_item_{$questiontype}"], $feedback, $data); } /** * Create response. * * @param array $data Response data. * @return stdClass feedback_completed response instance. */ public function create_response(array $data): stdClass { global $DB; $userid = $data['userid']; $responsenumber = null; $cm = get_coursemodule_from_id('feedback', $data['cmid']); $feedback = $DB->get_record('feedback', ['id' => $cm->instance]); $answers = []; if (isset($data['responsenumber']) && trim($data['responsenumber']) !== '') { $responsenumber = $data['responsenumber']; } if (isset($data['anonymous']) && trim($data['anonymous']) !== '') { $anonymous = filter_var(trim($data['anonymous']), FILTER_VALIDATE_BOOLEAN); $feedback->anonymous = $anonymous ? FEEDBACK_ANONYMOUS_YES : FEEDBACK_ANONYMOUS_NO; } unset($data['cmid']); unset($data['userid']); unset($data['anonymous']); unset($data['responsenumber']); foreach ($data as $question => $response) { $item = $DB->get_record('feedback_item', ['name' => trim($question)], '*', MUST_EXIST); $answers["{$item->typ}_{$item->id}"] = $this->get_item_response_value($item, $response); } $feedbackcompletion = new mod_feedback_completion( $feedback, $cm, $cm->course, false, null, $feedback->anonymous === FEEDBACK_ANONYMOUS_YES ? null : $userid, $userid ); if (!$feedbackcompletion->can_complete()) { throw new coding_exception("User {$userid} cannot complete this feedback activity."); } if (!$feedbackcompletion->is_open()) { throw new coding_exception("This activity is not open."); } $feedbackcompletion->set_module_viewed(); $feedbackcompletion->save_response_tmp((object) $answers); $feedbackcompletion->save_response(); $completed = $feedbackcompletion->get_completed(); if (!is_null($responsenumber)) { $DB->update_record('feedback_completed', [ 'id' => $completed->id, 'random_response' => $responsenumber, ]); } return $completed; } /** * Create info question item. * * @param object $feedback feedback record * @param array $record (optional) to override default values * @return stdClass */ public function create_item_info($feedback, $record = array()) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $itemobj = feedback_get_item_class('info'); $position = $DB->count_records('feedback_item', array('feedback' => $feedback->id)) + 1; $record = (array)$record + array( 'id' => 0, 'feedback' => $feedback->id, 'template' => 0, 'name' => 'Feedback question item ' . $position, 'label' => 'Feedback label ' . $position, 'presentation' => \feedback_item_info::MODE_COURSE, 'typ' => 'info', 'hasvalue' => 0, 'position' => $position, 'required' => 0, 'dependitem' => 0, 'dependvalue' => '', 'options' => '', ); $itemobj->set_data((object) $record); return $itemobj->save_item(); } /** * Create label question item. * * @param object $feedback feedback record * @param array $record (optional) to override default values * @return stdClass */ public function create_item_label($feedback, $record = array()) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $itemobj = feedback_get_item_class('label'); $position = $DB->count_records('feedback_item', array('feedback' => $feedback->id)) + 1; $record = (array)$record + array( 'id' => 0, 'feedback' => $feedback->id, 'template' => 0, 'name' => 'label', 'label' => '', 'presentation' => '', 'typ' => 'label', 'hasvalue' => 0, 'position' => $position, 'required' => 0, 'dependitem' => 0, 'dependvalue' => '', 'options' => '', ); if (!isset($record['presentation_editor'])) { $record['presentation_editor'] = array( 'text' => "The label $position text goes here", 'format' => FORMAT_HTML, 'itemid' => 0 ); } $itemobj->set_data((object) $record); return $itemobj->save_item(); } /** * Create multichoice question item. * * @param object $feedback feedback record * @param array $record (optional) to override default values * @return stdClass */ public function create_item_multichoice($feedback, $record = array()) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $itemobj = feedback_get_item_class('multichoice'); $position = $DB->count_records('feedback_item', array('feedback' => $feedback->id)) + 1; $record = (array)$record + array( 'id' => 0, 'feedback' => $feedback->id, 'template' => 0, 'name' => 'Feedback question item ' . $position, 'label' => 'Feedback label ' . $position, 'presentation' => '', 'typ' => 'multichoice', 'hasvalue' => 0, 'position' => $position, 'required' => 0, 'dependitem' => 0, 'dependvalue' => '', 'options' => '', 'subtype' => 'r', 'horizontal' => 0, 'hidenoselect' => 1, 'ignoreempty' => 0, 'values' => "a\nb\nc\nd\ne" ); $presentation = str_replace("\n", FEEDBACK_MULTICHOICE_LINE_SEP, trim($record['values'])); if ($record['horizontal'] == 1 AND $record['subtype'] != 'd') { $presentation .= FEEDBACK_MULTICHOICE_ADJUST_SEP.'1'; } $record['presentation'] = $record['subtype'].FEEDBACK_MULTICHOICE_TYPE_SEP.$presentation; $itemobj->set_data((object) $record); return $itemobj->save_item(); } /** * Create multichoicerated question item. * * @param object $feedback feedback record * @param array $record (optional) to override default values * @return stdClass */ public function create_item_multichoicerated($feedback, $record = array()) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $itemobj = feedback_get_item_class('multichoicerated'); $position = $DB->count_records('feedback_item', array('feedback' => $feedback->id)) + 1; $record = (array)$record + array( 'id' => 0, 'feedback' => $feedback->id, 'template' => 0, 'name' => 'Feedback question item ' . $position, 'label' => 'Feedback label ' . $position, 'presentation' => '', 'typ' => 'multichoicerated', 'hasvalue' => 0, 'position' => $position, 'required' => 0, 'dependitem' => 0, 'dependvalue' => '', 'options' => '', 'subtype' => 'r', 'horizontal' => 0, 'hidenoselect' => 1, 'ignoreempty' => 0, 'values' => "0/a\n1/b\n2/c\n3/d\n4/e" ); $itemobj = new feedback_item_multichoicerated(); $presentation = $itemobj->prepare_presentation_values_save(trim($record['values']), FEEDBACK_MULTICHOICERATED_VALUE_SEP2, FEEDBACK_MULTICHOICERATED_VALUE_SEP); if ($record['horizontal'] == 1 AND $record['subtype'] != 'd') { $presentation .= FEEDBACK_MULTICHOICERATED_ADJUST_SEP.'1'; } $record['presentation'] = $record['subtype'].FEEDBACK_MULTICHOICERATED_TYPE_SEP.$presentation; $itemobj->set_data((object) $record); return $itemobj->save_item(); } /** * Create numeric question item. * * @param object $feedback feedback record * @param array $record (optional) to override default values * @return stdClass */ public function create_item_numeric($feedback, $record = array()) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $itemobj = feedback_get_item_class('numeric'); $position = $DB->count_records('feedback_item', array('feedback' => $feedback->id)) + 1; $record = (array)$record + array( 'id' => 0, 'feedback' => $feedback->id, 'template' => 0, 'name' => 'Feedback question item ' . $position, 'label' => 'Feedback label ' . $position, 'presentation' => '', 'typ' => 'numeric', 'hasvalue' => 0, 'position' => $position, 'required' => 0, 'dependitem' => 0, 'dependvalue' => '', 'options' => '', 'rangefrom' => '-', 'rangeto' => '-', ); if ($record['rangefrom'] === '-' OR $record['rangeto'] === '-') { $record['presentation'] = $record['rangefrom'] . '|'. $record['rangeto']; } else if ($record['rangefrom'] > $record['rangeto']) { $record['presentation'] = $record['rangeto'] . '|'. $record['rangefrom']; } else { $record['presentation'] = $record['rangefrom'] . '|'. $record['rangeto']; } $itemobj->set_data((object) $record); return $itemobj->save_item(); } /** * Create textarea question item. * * @param object $feedback feedback record * @param array $record (optional) to override default values * @return stdClass */ public function create_item_textarea($feedback, $record = array()) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $itemobj = feedback_get_item_class('textarea'); $position = $DB->count_records('feedback_item', array('feedback' => $feedback->id)) + 1; $record = (array)$record + array( 'id' => 0, 'feedback' => $feedback->id, 'template' => 0, 'name' => 'Feedback question item ' . $position, 'label' => 'Feedback label ' . $position, 'presentation' => '', 'typ' => 'textarea', 'hasvalue' => 0, 'position' => $position, 'required' => 0, 'dependitem' => 0, 'dependvalue' => '', 'options' => '', 'itemwidth' => '40', 'itemheight' => '20', ); $record['presentation'] = $record['itemwidth'] . '|'. $record['itemheight']; $itemobj->set_data((object) $record); return $itemobj->save_item(); } /** * Create textfield question item. * * @param object $feedback feedback record * @param array $record (optional) to override default values * @return stdClass */ public function create_item_textfield($feedback, $record = array()) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); $itemobj = feedback_get_item_class('textfield'); $position = $DB->count_records('feedback_item', array('feedback' => $feedback->id)) + 1; $record = (array)$record + array( 'id' => 0, 'feedback' => $feedback->id, 'template' => 0, 'name' => 'Feedback question item ' . $position, 'label' => 'Feedback label ' . $position, 'presentation' => '', 'typ' => 'textfield', 'hasvalue' => 0, 'position' => $position, 'required' => 0, 'dependitem' => 0, 'dependvalue' => '', 'options' => '', 'itemsize' => '20', 'itemmaxlength' => '30', ); $record['presentation'] = $record['itemsize'] . '|'. $record['itemmaxlength']; $itemobj->set_data((object) $record); return $itemobj->save_item(); } /** * Create pagebreak. * * @param object $feedback feedback record * @return int|false false if there already is a pagebreak on last position or the id of the pagebreak-item */ public function create_item_pagebreak($feedback) { global $CFG; require_once($CFG->dirroot.'/mod/feedback/lib.php'); return feedback_create_pagebreak($feedback->id); } /** * Format feedback item values. * * This method will replace newline characters with the proper line separator for each question type. * * @param string $questiontype Question types * @param string $values Values * @return string Formatted values */ protected function format_item_values(string $questiontype, string $values): string { global $CFG; if (!file_exists($CFG->dirroot.'/mod/feedback/item/'.$questiontype.'/lib.php')) { throw new coding_exception("Question type '$questiontype' not found"); } require_once($CFG->dirroot.'/mod/feedback/item/'.$questiontype.'/lib.php'); $questiontype = strtoupper($questiontype); if (defined("FEEDBACK_{$questiontype}_LINE_SEP")) { return implode(constant("FEEDBACK_{$questiontype}_LINE_SEP"), explode('\n', $values)); } return $values; } /** * Given a response to a feedback item, return its corresponding value. * * @param mixed $record Item record * @param string $response Response name * @return int|string Response value */ protected function get_item_response_value($record, string $response) { if (strpos($record->typ, 'multichoice') === 0) { $item = feedback_get_item_class($record->typ); return $this->get_choice_item_response_value($item, $record, $response); } return $response; } /** * Given a response to a feedback choice item, return its corresponding value. * * @param feedback_item_base $item Feedback item * @param mixed $record Item record * @param string $response Response * @param int $offset Choice to start looking from * @return int Response choice index */ protected function get_choice_item_response_value(feedback_item_base $item, $record, string $response, int $offset = 1): int { $printval = $item->get_printval($record, (object) ['value' => $offset]); if (empty($printval)) { throw new coding_exception("Value '$offset' not found"); } if ($printval === $response) { return $offset; } return $this->get_choice_item_response_value($item, $record, $response, $offset + 1); } } PKB\j0tests/generator/behat_mod_feedback_generator.phpnu[. /** * Behat data generator for mod_feedback. * * @package mod_feedback * @category test * @copyright 2022 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_feedback_generator extends behat_generator_base { /** * Get a list of the entities that Behat can create using the generator step. * * @return array */ protected function get_creatable_entities(): array { return [ 'questions' => [ 'singular' => 'question', 'datagenerator' => 'question', 'required' => ['activity'], 'switchids' => ['activity' => 'cmid'], ], 'responses' => [ 'singular' => 'response', 'datagenerator' => 'response', 'required' => ['activity', 'user'], 'switchids' => ['activity' => 'cmid', 'user' => 'userid'], ], ]; } } PKB\?8%tests/form/use_template_form_test.phpnu[. namespace mod_feedback\form; /** * Tests the confirm use template form * * @author Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ final class use_template_form_test extends \advanced_testcase { /** * Run the basic setup for the test */ public function setup_instance(): array { global $DB, $PAGE; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $cm = get_coursemodule_from_instance('feedback', $feedback->id, $course->id); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); // Create at least one page. $feedbackgenerator->create_item_multichoice($feedback, ['values' => "y\nn"]); feedback_save_as_template($feedback, 'my template', 0); $feedbackgenerator->create_item_multichoice($feedback, ['values' => "0\n1"]); feedback_save_as_template($feedback, 'mytemplate2', 1); $records = array_keys($DB->get_records('feedback_template', null, 'id ASC')); $feedbackparams = [ 'id' => $cm->id, 'privatetemplate' => $records[0], 'publictemplate' => $records[1], ]; $PAGE->set_cm($cm); $PAGE->set_activity_record($feedback); return [$user, $feedbackparams]; } /** * Test the form * * @param string $loginas Which user to log in as * @param bool $private Whether we are creating a private template * @param bool $expected Whether or not the form should be validated * @dataProvider usetemplate_form_provider */ public function test_usetemplate_form(string $loginas, bool $private, bool $expected): void { [$user, $feedback] = $this->setup_instance(); switch($loginas) { case 'admin': $this->setAdminUser(); break; case 'student': $this->setUser($user); break; } $data = [ 'id' => $feedback['id'], 'templateid' => $private ? $feedback['privatetemplate'] : $feedback['publictemplate'], ]; $submitdata = use_template_form::mock_ajax_submit($data); if (!$expected) { $this->expectException(\moodle_exception::class); } $form = new use_template_form(null, null, 'post', '', null, true, $submitdata, true); $form->set_data_for_dynamic_submission(); if ($expected) { $this->assertTrue($form->is_validated()); } $form->process_dynamic_submission(); } /** * Provider for the test_usetemplate_form test * * @return array */ public static function usetemplate_form_provider(): array { return [ 'Test submission with a private template as an admin' => [ 'admin', true, true ], 'Test submission with a public template as an admin' => [ 'admin', false, true ], 'Test submission with a public template as a student' => [ 'student', false, false ], 'Test submission with a private template as a student' => [ 'student', true, false ], ]; } } PKB\B,(tests/form/create_template_form_test.phpnu[. namespace mod_feedback\form; /** * Tests the confirm use template form * * @author Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ final class create_template_form_test extends \advanced_testcase { /** * Run the basic setup for the test */ public function setup_instance(): array { global $DB, $PAGE; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $cm = get_coursemodule_from_instance('feedback', $feedback->id, $course->id); $user = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $manager = $this->getDataGenerator()->create_user(); // Enrol a student and teacher. $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); // Setup the site wide manager role. $managerrole = $DB->get_record('role', ['shortname' => 'manager']); role_assign($managerrole->id, $manager->id, SYSCONTEXTID); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); // Create at least one page. $feedbackgenerator->create_item_multichoice($feedback, ['values' => "y\nn"]); $feedbackgenerator->create_item_multichoice($feedback, ['values' => "0\n1"]); $feedbackparams = [ 'id' => $cm->id, ]; $PAGE->set_cm($cm); $PAGE->set_activity_record($feedback); return [$manager, $teacher, $user, $managerrole, $feedbackparams]; } /** * Test the create template for when capabilities have been modified * * @param array $unassignedroles * @param bool $accessallowed * @param bool $public * @param bool $expectedispublicvalue * @dataProvider createtemplate_form_with_modified_capabilities_provider */ public function test_createtemplate_form_with_modified_capabilities(array $unassignedroles, bool $accessallowed, bool $public = false, bool $expectedispublicvalue = false): void { global $DB; [$manager, $teacher, $user, $managerrole, $feedback] = $this->setup_instance(); $this->setAdminUser(); foreach ($unassignedroles as $role) { unassign_capability($role, $managerrole->id); } $data = [ 'id' => $feedback['id'], 'templatename' => 'mytemplate', 'ispublic' => $public ]; $this->setUser($manager); $submitdata = create_template_form::mock_ajax_submit($data); if (!$accessallowed) { $this->expectException(\moodle_exception::class); } $form = new create_template_form(null, null, 'post', '', null, true, $submitdata, true); $form->set_data_for_dynamic_submission(); $this->assertTrue($form->is_validated()); $form->process_dynamic_submission(); $records = array_values($DB->get_records('feedback_template', null, 'id ASC')); $this->assertEquals($expectedispublicvalue, (bool) $records[0]->ispublic); } /** * Provider for the test_createtemplate_form_with_modified_capabilities * * @return array */ public static function createtemplate_form_with_modified_capabilities_provider(): array { return [ "Manager without edititems permission cannot create any templates" => [ ['mod/feedback:edititems'], false ], "Manager without createprivatetemplate permission creating public template" => [ ['mod/feedback:createprivatetemplate'], true, true, true ], "Manager without createprivatetemplate permission creating private template" => [ ['mod/feedback:createprivatetemplate'], true ], "Manager without createpublictemplate permission creating private template" => [ ['mod/feedback:createpublictemplate'], true ], "Manager without createpublictemplate permission creating public template" => [ ['mod/feedback:createpublictemplate'], true, true ], "Manager without createprivatetemplate,createpublictemplate permission cannot create templates" => [ ['mod/feedback:createpublictemplate', 'mod/feedback:createprivatetemplate'], false ] ]; } /** * Test the form * * @param string $loginas * @param bool $public * @param bool $accessallowed * @dataProvider createtemplate_form_provider */ public function test_createtemplate_form(string $loginas, bool $public, bool $accessallowed = true): void { global $DB; [$manager, $teacher, $user, $managerrole, $feedback] = $this->setup_instance(); switch($loginas) { case 'admin': $this->setAdminUser(); break; case 'student': $this->setUser($user); break; case 'teacher': $this->setUser($teacher); break; case 'manager': $this->setUser($manager); break; } $data = [ 'id' => $feedback['id'], 'templatename' => 'mytemplate', 'ispublic' => $public ]; $submitdata = create_template_form::mock_ajax_submit($data); if (!$accessallowed) { $this->expectException(\moodle_exception::class); } $form = new create_template_form(null, null, 'post', '', null, true, $submitdata, true); $form->set_data_for_dynamic_submission(); $this->assertTrue($form->is_validated()); $form->process_dynamic_submission(); // A teacher can access the form but cannot create public templates. if ($loginas == 'teacher' && $public) { $records = array_values($DB->get_records('feedback_template', null, 'id ASC')); $this->assertFalse((bool) $records[0]->ispublic); } } /** * Provider for the test_createtemplate_form * * @return array */ public static function createtemplate_form_provider(): array { return [ 'Create a private template as an admin' => [ 'admin', false ], 'Create a public template as an admin' => [ 'admin', true ], 'Create a private template as a manager' => [ 'manager', false ], 'Create a public template as a manager' => [ 'manager', true ], 'Create a private template as a teacher' => [ 'teacher', false ], 'Create a public template as a teacher' => [ 'teacher', true ], 'Create a public template as a student' => [ 'student', true, false ], 'Create a private template as a student' => [ 'student', false, false ], ]; } } PKB\RpH> > )tests/external/questions/reorder_test.phpnu[. namespace mod_feedback\external\questions; use core_external\external_api; /** * Unit tests of external class for re-ordering feedback question items * * @package mod_feedback * @covers \mod_feedback\external\questions\reorder * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class reorder_test extends \advanced_testcase { /** * Text execute method */ public function test_execute(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course with a feedback activity and some questions. $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_label($feedback); $item2 = $feedbackgenerator->create_item_info($feedback); $item3 = $feedbackgenerator->create_item_numeric($feedback); // Check initial items order. $this->assertEquals([$item1->id, $item2->id, $item3->id], $this->get_feedback_item_order($feedback)); // Call the execute method to invert the items order. $result = reorder::execute($cm->id, "$item3->id,$item2->id,$item1->id"); $result = external_api::clean_returnvalue(reorder::execute_returns(), $result); $this->assertTrue($result); // Check items order is inverted. $this->assertEquals([$item3->id, $item2->id, $item1->id], $this->get_feedback_item_order($feedback)); } /** * Get the order of the feedback items. * * @param object $feedback The feedback activity. * @return array */ private function get_feedback_item_order($feedback) { global $DB; return $DB->get_fieldset_select( 'feedback_item', 'id', 'feedback = :feedbackid ORDER BY position ASC', ['feedbackid' => $feedback->id] ); } } PKB\i tests/external/external_test.phpnu[. namespace mod_feedback\external; use core_external\external_api; use externallib_advanced_testcase; use feedback_item_multichoice; use mod_feedback_external; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/mod/feedback/lib.php'); /** * Feedback module external functions tests * * @package mod_feedback * @category external * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.3 * @covers \mod_feedback_external */ final class external_test extends externallib_advanced_testcase { // TODO These should be removed. // Testcase classes should not have any properties or store state. protected $course; protected $feedback; protected $context; protected $cm; protected $student; protected $teacher; protected $studentrole; protected $teacherrole; /** * Set up for every test */ public function setUp(): void { global $DB; parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $this->course = $this->getDataGenerator()->create_course(); $this->feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $this->course->id, 'email_notification' => 1)); $this->context = \context_module::instance($this->feedback->cmid); $this->cm = get_coursemodule_from_instance('feedback', $this->feedback->id); // Create users. $this->student = self::getDataGenerator()->create_user(); $this->teacher = self::getDataGenerator()->create_user(); // Users enrolments. $this->studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual'); } /** * Helper method to add items to an existing feedback. * * @param \stdClass $feedback feedback instance * @param integer $pagescount the number of pages we want in the feedback * @return array list of items created */ public function populate_feedback($feedback, $pagescount = 1) { $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $itemscreated = []; // Create at least one page. $itemscreated[] = $feedbackgenerator->create_item_label($feedback); $itemscreated[] = $feedbackgenerator->create_item_info($feedback); $itemscreated[] = $feedbackgenerator->create_item_numeric($feedback); // Check if we want more pages. for ($i = 1; $i < $pagescount; $i++) { $itemscreated[] = $feedbackgenerator->create_item_pagebreak($feedback); $itemscreated[] = $feedbackgenerator->create_item_multichoice($feedback); $itemscreated[] = $feedbackgenerator->create_item_multichoicerated($feedback); $itemscreated[] = $feedbackgenerator->create_item_textarea($feedback); $itemscreated[] = $feedbackgenerator->create_item_textfield($feedback); $itemscreated[] = $feedbackgenerator->create_item_numeric($feedback); } return $itemscreated; } /** * Test test_mod_feedback_get_feedbacks_by_courses */ public function test_mod_feedback_get_feedbacks_by_courses(): void { // Create additional course. $course2 = self::getDataGenerator()->create_course(); // Second feedback. $record = new \stdClass(); $record->course = $course2->id; $feedback2 = self::getDataGenerator()->create_module('feedback', $record); // Execute real Moodle enrolment as we'll call unenrol() method on the instance later. $enrol = enrol_get_plugin('manual'); $enrolinstances = enrol_get_instances($course2->id, true); $instance2 = (object) []; foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { $instance2 = $courseenrolinstance; break; } } $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id); self::setUser($this->student); $returndescription = mod_feedback_external::get_feedbacks_by_courses_returns(); // Create what we expect to be returned when querying the two courses. // First for the student user. $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'anonymous', 'multiple_submit', 'autonumbering', 'page_after_submitformat', 'publish_stats', 'completionsubmit'); $properties = feedback_summary_exporter::read_properties_definition(); // Add expected coursemodule and data. $feedback1 = $this->feedback; $feedback1->coursemodule = $feedback1->cmid; $feedback1->introformat = 1; $feedback1->introfiles = []; $feedback1->lang = ''; $feedback2->coursemodule = $feedback2->cmid; $feedback2->introformat = 1; $feedback2->introfiles = []; $feedback2->lang = ''; foreach ($expectedfields as $field) { if (!empty($properties[$field]) && $properties[$field]['type'] == PARAM_BOOL) { $feedback1->{$field} = (bool) $feedback1->{$field}; $feedback2->{$field} = (bool) $feedback2->{$field}; } $expected1[$field] = $feedback1->{$field}; $expected2[$field] = $feedback2->{$field}; } $expectedfeedbacks = array($expected2, $expected1); // Call the external function passing course ids. $result = mod_feedback_external::get_feedbacks_by_courses(array($course2->id, $this->course->id)); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedfeedbacks, $result['feedbacks']); $this->assertCount(0, $result['warnings']); // Call the external function without passing course id. $result = mod_feedback_external::get_feedbacks_by_courses(); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedfeedbacks, $result['feedbacks']); $this->assertCount(0, $result['warnings']); // Unenrol user from second course and alter expected feedbacks. $enrol->unenrol_user($instance2, $this->student->id); array_shift($expectedfeedbacks); // Call the external function without passing course id. $result = mod_feedback_external::get_feedbacks_by_courses(); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedfeedbacks, $result['feedbacks']); // Call for the second course we unenrolled the user from, expected warning. $result = mod_feedback_external::get_feedbacks_by_courses(array($course2->id)); $this->assertCount(1, $result['warnings']); $this->assertEquals('1', $result['warnings'][0]['warningcode']); $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); // Now, try as a teacher for getting all the additional fields. self::setUser($this->teacher); $additionalfields = array('email_notification', 'site_after_submit', 'page_after_submit', 'timeopen', 'timeclose', 'timemodified', 'pageaftersubmitfiles'); $feedback1->pageaftersubmitfiles = []; foreach ($additionalfields as $field) { if (!empty($properties[$field]) && $properties[$field]['type'] == PARAM_BOOL) { $feedback1->{$field} = (bool) $feedback1->{$field}; } $expectedfeedbacks[0][$field] = $feedback1->{$field}; } $expectedfeedbacks[0]['page_after_submitformat'] = 1; $result = mod_feedback_external::get_feedbacks_by_courses(); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedfeedbacks, $result['feedbacks']); // Admin also should get all the information. self::setAdminUser(); $result = mod_feedback_external::get_feedbacks_by_courses(array($this->course->id)); $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedfeedbacks, $result['feedbacks']); } /** * Test get_feedback_access_information function with basic defaults for student. */ public function test_get_feedback_access_information_student(): void { self::setUser($this->student); $result = mod_feedback_external::get_feedback_access_information($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result); $this->assertFalse($result['canviewanalysis']); $this->assertFalse($result['candeletesubmissions']); $this->assertFalse($result['canviewreports']); $this->assertFalse($result['canedititems']); $this->assertTrue($result['cancomplete']); $this->assertTrue($result['cansubmit']); $this->assertTrue($result['isempty']); $this->assertTrue($result['isopen']); $this->assertTrue($result['isanonymous']); $this->assertFalse($result['isalreadysubmitted']); } /** * Test get_feedback_access_information function with basic defaults for teacher. */ public function test_get_feedback_access_information_teacher(): void { self::setUser($this->teacher); $result = mod_feedback_external::get_feedback_access_information($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result); $this->assertTrue($result['canviewanalysis']); $this->assertTrue($result['canviewreports']); $this->assertTrue($result['canedititems']); $this->assertTrue($result['candeletesubmissions']); $this->assertFalse($result['cancomplete']); $this->assertTrue($result['cansubmit']); $this->assertTrue($result['isempty']); $this->assertTrue($result['isopen']); $this->assertTrue($result['isanonymous']); $this->assertFalse($result['isalreadysubmitted']); // Add some items to the feedback and check is not empty any more. self::populate_feedback($this->feedback); $result = mod_feedback_external::get_feedback_access_information($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result); $this->assertFalse($result['isempty']); } /** * Test view_feedback invalid id. */ public function test_view_feedback_invalid_id(): void { // Test invalid instance id. $this->expectException(moodle_exception::class); mod_feedback_external::view_feedback(0); } /** * Test view_feedback not enrolled user. */ public function test_view_feedback_not_enrolled_user(): void { $usernotenrolled = self::getDataGenerator()->create_user(); $this->setUser($usernotenrolled); $this->expectException(moodle_exception::class); mod_feedback_external::view_feedback(0); } /** * Test view_feedback no capabilities. */ public function test_view_feedback_no_capabilities(): void { // Test user with no capabilities. // We need a explicit prohibit since this capability is allowed for students by default. assign_capability('mod/feedback:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id); accesslib_clear_all_caches_for_unit_testing(); $this->expectException(moodle_exception::class); mod_feedback_external::view_feedback(0); } /** * Test view_feedback. */ public function test_view_feedback(): void { // Test user with full capabilities. $this->setUser($this->student); // Trigger and capture the event. $sink = $this->redirectEvents(); $result = mod_feedback_external::view_feedback($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::view_feedback_returns(), $result); $events = $sink->get_events(); $this->assertCount(1, $events); $event = array_shift($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_feedback\event\course_module_viewed', $event); $this->assertEquals($this->context, $event->get_context()); $moodledata = new \moodle_url('/mod/feedback/view.php', array('id' => $this->cm->id)); $this->assertEquals($moodledata, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test get_current_completed_tmp. */ public function test_get_current_completed_tmp(): void { global $DB; // Force non anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id)); // Add a completed_tmp record. $record = [ 'feedback' => $this->feedback->id, 'userid' => $this->student->id, 'guestid' => '', 'timemodified' => time() - DAYSECS, 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO, 'courseid' => $this->course->id, ]; $record['id'] = $DB->insert_record('feedback_completedtmp', (object) $record); // Test user with full capabilities. $this->setUser($this->student); $result = mod_feedback_external::get_current_completed_tmp($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_current_completed_tmp_returns(), $result); $this->assertEquals($record['id'], $result['feedback']['id']); } /** * Test get_items. */ public function test_get_items(): void { // Test user with full capabilities. $this->setUser($this->student); // Add questions to the feedback, we are adding 2 pages of questions. $itemscreated = self::populate_feedback($this->feedback, 2); $result = mod_feedback_external::get_items($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_items_returns(), $result); $this->assertCount(count($itemscreated), $result['items']); $index = 1; foreach ($result['items'] as $key => $item) { if (is_numeric($itemscreated[$key])) { continue; // Page break. } // Cannot compare directly the exporter and the db object (exporter have more fields). $this->assertEquals($itemscreated[$key]->id, $item['id']); $this->assertEquals($itemscreated[$key]->typ, $item['typ']); $this->assertEquals($itemscreated[$key]->name, $item['name']); $this->assertEquals($itemscreated[$key]->label, $item['label']); if ($item['hasvalue']) { $this->assertEquals($index, $item['itemnumber']); $index++; } } } /** * Test get_items, to confirm validation is done too. * * @dataProvider items_provider * @param string $role Whether the current user should be a student or a teacher. * @param array $info Settings to create the feedback. * @param string|null $warning The warning message to display or null if warnings result is empty. */ public function test_get_items_validation(string $role, array $info, ?string $warning): void { global $DB; // Test user with full capabilities. if ($role === 'teacher') { $this->setUser($this->teacher); } else { $this->setUser($this->student); } // Create the feedback. $data = ['course' => $this->course->id]; if (array_key_exists('closed', $info) && $info['closed']) { $data['timeopen'] = time() + DAYSECS; } $feedback = $this->getDataGenerator()->create_module('feedback', $data); $empty = true; if (!array_key_exists('empty', $info) || !$info['empty']) { $empty = false; /** @var \mod_feedback_generator $feedbackgenerator */ $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); // Add,at least, one item to the feedback. $feedbackgenerator->create_item_label($feedback); } if (array_key_exists('complete', $info) && !$info['complete']) { $studentrole = $DB->get_record('role', array('shortname' => 'student')); $coursecontext = \context_course::instance($this->course->id); assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $coursecontext->id); // Empty all the caches that may be affected by this change. accesslib_clear_all_caches_for_unit_testing(); \course_modinfo::clear_instance_cache(); } $result = mod_feedback_external::get_items($feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_items_returns(), $result); if ($warning) { $this->assertEmpty($result['items']); $this->assertCount(1, $result['warnings']); $resultwarning = reset($result['warnings']); if ($warning == 'required_capability_exception') { $this->assertStringContainsString('error/nopermission', $resultwarning['message']); } else { $this->assertEquals($warning, $resultwarning['message']); } } else { if ($empty) { $this->assertEmpty($result['items']); } else { $this->assertCount(1, $result['items']); } $this->assertEmpty($result['warnings']); } } /** * Data provider for test_get_items_validation() and test_get_page_items_validation(). * * @return array */ public static function items_provider(): array { return [ 'Valid feedback (as student)' => [ 'role' => 'student', 'info' => [], 'warning' => null, ], 'Closed feedback (as student)' => [ 'role' => 'student', 'info' => ['closed' => true], 'warning' => get_string('feedback_is_not_open', 'feedback'), ], 'Empty feedback (as student)' => [ 'role' => 'student', 'info' => ['empty' => true], 'warning' => get_string('no_items_available_yet', 'feedback'), ], 'Cannot complete feedback (as student)' => [ 'role' => 'student', 'info' => ['complete' => false], 'warning' => 'required_capability_exception', ], 'Valid feedback (as teacher)' => [ 'role' => 'teacher', 'info' => [], 'warning' => null, ], 'Closed feedback (as teacher)' => [ 'role' => 'teacher', 'info' => ['closed' => true], 'warning' => null, ], 'Empty feedback (as teacher)' => [ 'role' => 'teacher', 'info' => ['empty' => true], 'warning' => null, ], 'Cannot complete feedback (as teacher)' => [ 'role' => 'teacher', 'info' => ['complete' => false], 'warning' => null, ], ]; } /** * Test launch_feedback. */ public function test_launch_feedback(): void { global $DB; // Test user with full capabilities. $this->setUser($this->student); // Add questions to the feedback, we are adding 2 pages of questions. $itemscreated = self::populate_feedback($this->feedback, 2); // First try a feedback we didn't attempt. $result = mod_feedback_external::launch_feedback($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::launch_feedback_returns(), $result); $this->assertEquals(0, $result['gopage']); // Now, try a feedback that we attempted. // Force non anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id)); // Add a completed_tmp record. $record = [ 'feedback' => $this->feedback->id, 'userid' => $this->student->id, 'guestid' => '', 'timemodified' => time() - DAYSECS, 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO, 'courseid' => $this->course->id, ]; $record['id'] = $DB->insert_record('feedback_completedtmp', (object) $record); // Add a response to the feedback for each question type with possible values. $response = [ 'course_id' => $this->course->id, 'item' => $itemscreated[1]->id, // First item is the info question. 'completed' => $record['id'], 'tmp_completed' => $record['id'], 'value' => 'A', ]; $DB->insert_record('feedback_valuetmp', (object) $response); $response = [ 'course_id' => $this->course->id, 'item' => $itemscreated[2]->id, // Second item is the numeric question. 'completed' => $record['id'], 'tmp_completed' => $record['id'], 'value' => 5, ]; $DB->insert_record('feedback_valuetmp', (object) $response); $result = mod_feedback_external::launch_feedback($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::launch_feedback_returns(), $result); $this->assertEquals(1, $result['gopage']); } /** * Test get_page_items. */ public function test_get_page_items(): void { // Test user with full capabilities. $this->setUser($this->student); // Add questions to the feedback, we are adding 2 pages of questions. $itemscreated = self::populate_feedback($this->feedback, 2); // Retrieve first page. $result = mod_feedback_external::get_page_items($this->feedback->id, 0); $result = external_api::clean_returnvalue(mod_feedback_external::get_page_items_returns(), $result); $this->assertCount(3, $result['items']); // The first page has 3 items. $this->assertTrue($result['hasnextpage']); $this->assertFalse($result['hasprevpage']); // Retrieve second page. $result = mod_feedback_external::get_page_items($this->feedback->id, 1); $result = external_api::clean_returnvalue(mod_feedback_external::get_page_items_returns(), $result); $this->assertCount(5, $result['items']); // The second page has 5 items (page break doesn't count). $this->assertFalse($result['hasnextpage']); $this->assertTrue($result['hasprevpage']); } /** * Test get_page_items, to confirm validation is done too. * * @dataProvider items_provider * @param string $role Whether the current user should be a student or a teacher. * @param array $info Settings to create the feedback. * @param string|null $warning The warning message to display or null if warnings result is empty. */ public function test_get_page_items_validation(string $role, array $info, ?string $warning): void { global $DB; // Test user with full capabilities. if ($role === 'teacher') { $this->setUser($this->teacher); } else { $this->setUser($this->student); } // Create the feedback. $data = ['course' => $this->course->id]; if (array_key_exists('closed', $info) && $info['closed']) { $data['timeopen'] = time() + DAYSECS; } $feedback = $this->getDataGenerator()->create_module('feedback', $data); $empty = true; if (!array_key_exists('empty', $info) || !$info['empty']) { $empty = false; /** @var \mod_feedback_generator $feedbackgenerator */ $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); // Add,at least, one item to the feedback. $feedbackgenerator->create_item_label($feedback); } if (array_key_exists('complete', $info) && !$info['complete']) { $studentrole = $DB->get_record('role', array('shortname' => 'student')); $coursecontext = \context_course::instance($this->course->id); assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $coursecontext->id); // Empty all the caches that may be affected by this change. accesslib_clear_all_caches_for_unit_testing(); \course_modinfo::clear_instance_cache(); } $result = mod_feedback_external::get_page_items($feedback->id, 0); $result = external_api::clean_returnvalue(mod_feedback_external::get_items_returns(), $result); if ($warning) { $this->assertEmpty($result['items']); $this->assertCount(1, $result['warnings']); $resultwarning = reset($result['warnings']); if ($warning == 'required_capability_exception') { $this->assertStringContainsString('error/nopermission', $resultwarning['message']); } else { $this->assertEquals($warning, $resultwarning['message']); } } else { if ($empty) { $this->assertEmpty($result['items']); } else { $this->assertCount(1, $result['items']); } $this->assertEmpty($result['warnings']); } } /** * Test process_page. */ public function test_process_page(): void { global $DB; // Test user with full capabilities. $this->setUser($this->student); $pagecontents = 'You finished it!'; $DB->set_field('feedback', 'page_after_submit', $pagecontents, array('id' => $this->feedback->id)); // Add questions to the feedback, we are adding 2 pages of questions. $itemscreated = self::populate_feedback($this->feedback, 2); $data = []; foreach ($itemscreated as $item) { if (empty($item->hasvalue)) { continue; } switch ($item->typ) { case 'textarea': case 'textfield': $value = 'Lorem ipsum'; break; case 'numeric': $value = 5; break; case 'multichoice': $value = '1'; break; case 'multichoicerated': $value = '1'; break; case 'info': $value = format_string($this->course->shortname, true, array('context' => $this->context)); break; default: $value = ''; } $data[] = ['name' => $item->typ . '_' . $item->id, 'value' => $value]; } // Process first page. $firstpagedata = [$data[0], $data[1]]; $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertEquals(1, $result['jumpto']); $this->assertFalse($result['completed']); // Now, process the second page. But first we are going back to the first page. $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]]; $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata, true); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertFalse($result['completed']); $this->assertEquals(0, $result['jumpto']); // We jumped to the first page. // Check the values were correctly saved. $tmpitems = $DB->get_records('feedback_valuetmp'); $this->assertCount(7, $tmpitems); // 2 from the first page + 5 from the second page. // Go forward again (sending the same data). $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertEquals(1, $result['jumpto']); $this->assertFalse($result['completed']); $tmpitems = $DB->get_records('feedback_valuetmp'); $this->assertCount(7, $tmpitems); // 2 from the first page + 5 from the second page. // And finally, save everything! We are going to modify one previous recorded value. $messagessink = $this->redirectMessages(); $data[2]['value'] = 2; // 2 is value of the option 'b'. $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]]; $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); $this->assertTrue(strpos($result['completionpagecontents'], $pagecontents) !== false); // Check all the items were saved. $items = $DB->get_records('feedback_value'); $this->assertCount(7, $items); // Check if the one we modified was correctly saved. $itemid = $itemscreated[4]->id; $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid)); $mcitem = new feedback_item_multichoice(); $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]); $this->assertEquals('b', $itemval); // Check that the answers are saved for course 0. foreach ($items as $item) { $this->assertEquals(0, $item->course_id); } $completed = $DB->get_record('feedback_completed', []); $this->assertEquals(0, $completed->courseid); // Test notifications sent. $messages = $messagessink->get_messages(); $messagessink->close(); // Test customdata. $customdata = json_decode($messages[0]->customdata); $this->assertEquals($this->feedback->id, $customdata->instance); $this->assertEquals($this->feedback->cmid, $customdata->cmid); $this->assertObjectHasProperty('notificationiconurl', $customdata); } /** * Test process_page for a site feedback. */ public function test_process_page_site_feedback(): void { global $DB; $pagecontents = 'You finished it!'; $this->feedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID, 'page_after_submit' => $pagecontents)); // Test user with full capabilities. $this->setUser($this->student); // Add questions to the feedback, we are adding 2 pages of questions. $itemscreated = self::populate_feedback($this->feedback, 2); $data = []; foreach ($itemscreated as $item) { if (empty($item->hasvalue)) { continue; } switch ($item->typ) { case 'textarea': case 'textfield': $value = 'Lorem ipsum'; break; case 'numeric': $value = 5; break; case 'multichoice': $value = '1'; break; case 'multichoicerated': $value = '1'; break; case 'info': $value = format_string($this->course->shortname, true, array('context' => $this->context)); break; default: $value = ''; } $data[] = ['name' => $item->typ . '_' . $item->id, 'value' => $value]; } // Process first page. $firstpagedata = [$data[0], $data[1]]; $result = mod_feedback_external::process_page($this->feedback->id, 0, $firstpagedata, false, $this->course->id); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertEquals(1, $result['jumpto']); $this->assertFalse($result['completed']); // Process second page. $data[2]['value'] = 2; // 2 is value of the option 'b'; $secondpagedata = [$data[2], $data[3], $data[4], $data[5], $data[6]]; $result = mod_feedback_external::process_page($this->feedback->id, 1, $secondpagedata, false, $this->course->id); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); $this->assertTrue(strpos($result['completionpagecontents'], $pagecontents) !== false); // Check all the items were saved. $items = $DB->get_records('feedback_value'); $this->assertCount(7, $items); // Check if the one we modified was correctly saved. $itemid = $itemscreated[4]->id; $itemsaved = $DB->get_field('feedback_value', 'value', array('item' => $itemid)); $mcitem = new feedback_item_multichoice(); $itemval = $mcitem->get_printval($itemscreated[4], (object) ['value' => $itemsaved]); $this->assertEquals('b', $itemval); // Check that the answers are saved for the correct course. foreach ($items as $item) { $this->assertEquals($this->course->id, $item->course_id); } $completed = $DB->get_record('feedback_completed', []); $this->assertEquals($this->course->id, $completed->courseid); } /** * Test get_analysis. */ public function test_get_analysis(): void { // Test user with full capabilities. $this->setUser($this->student); // Create a very simple feedback. $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $numericitem = $feedbackgenerator->create_item_numeric($this->feedback); $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback); $pagedata = [ ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5], ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'], ]; // Process the feedback, there is only one page so the feedback will be completed. $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); // Retrieve analysis. $this->setUser($this->teacher); $result = mod_feedback_external::get_analysis($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_analysis_returns(), $result); $this->assertEquals(1, $result['completedcount']); // 1 feedback completed. $this->assertEquals(2, $result['itemscount']); // 2 items in the feedback. $this->assertCount(2, $result['itemsdata']); $this->assertCount(1, $result['itemsdata'][0]['data']); // There are 1 response per item. $this->assertCount(1, $result['itemsdata'][1]['data']); // Check we receive the info the students filled. foreach ($result['itemsdata'] as $data) { if ($data['item']['id'] == $numericitem->id) { $this->assertEquals(5, $data['data'][0]); } else { $this->assertEquals('abc', $data['data'][0]); } } // Create another user / response. $anotherstudent = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); $this->setUser($anotherstudent); // Process the feedback, there is only one page so the feedback will be completed. $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); // Retrieve analysis. $this->setUser($this->teacher); $result = mod_feedback_external::get_analysis($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_analysis_returns(), $result); $this->assertEquals(2, $result['completedcount']); // 2 feedback completed. $this->assertEquals(2, $result['itemscount']); $this->assertCount(2, $result['itemsdata'][0]['data']); // There are 2 responses per item. $this->assertCount(2, $result['itemsdata'][1]['data']); } /** * Test get_unfinished_responses. */ public function test_get_unfinished_responses(): void { // Test user with full capabilities. $this->setUser($this->student); // Create a very simple feedback. $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $numericitem = $feedbackgenerator->create_item_numeric($this->feedback); $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback); $feedbackgenerator->create_item_pagebreak($this->feedback); $labelitem = $feedbackgenerator->create_item_label($this->feedback); $numericitem2 = $feedbackgenerator->create_item_numeric($this->feedback); $pagedata = [ ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5], ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'], ]; // Process the feedback, there are two pages so the feedback will be unfinished yet. $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertFalse($result['completed']); // Retrieve the unfinished responses. $result = mod_feedback_external::get_unfinished_responses($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_unfinished_responses_returns(), $result); // Check that ids and responses match. foreach ($result['responses'] as $r) { if ($r['item'] == $numericitem->id) { $this->assertEquals(5, $r['value']); } else { $this->assertEquals($textfielditem->id, $r['item']); $this->assertEquals('abc', $r['value']); } } } /** * Test get_finished_responses. */ public function test_get_finished_responses(): void { // Test user with full capabilities. $this->setUser($this->student); // Create a very simple feedback. $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $numericitem = $feedbackgenerator->create_item_numeric($this->feedback); $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback); $pagedata = [ ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5], ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'], ]; // Process the feedback, there is only one page so the feedback will be completed. $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); // Retrieve the responses. $result = mod_feedback_external::get_finished_responses($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_finished_responses_returns(), $result); // Check that ids and responses match. foreach ($result['responses'] as $r) { if ($r['item'] == $numericitem->id) { $this->assertEquals(5, $r['value']); } else { $this->assertEquals($textfielditem->id, $r['item']); $this->assertEquals('abc', $r['value']); } } } /** * Test get_non_respondents (student trying to get this information). */ public function test_get_non_respondents_no_permissions(): void { $this->setUser($this->student); $this->expectException(moodle_exception::class); mod_feedback_external::get_non_respondents($this->feedback->id); } /** * Test get_non_respondents from an anonymous feedback. */ public function test_get_non_respondents_from_anonymous_feedback(): void { $this->setUser($this->student); $this->expectException(moodle_exception::class); $this->expectExceptionMessage(get_string('anonymous', 'feedback')); mod_feedback_external::get_non_respondents($this->feedback->id); } /** * Test get_non_respondents. */ public function test_get_non_respondents(): void { global $DB; // Force non anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id)); // Create another student. $anotherstudent = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual'); $this->setUser($anotherstudent); // Test user with full capabilities. $this->setUser($this->student); // Create a very simple feedback. $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $numericitem = $feedbackgenerator->create_item_numeric($this->feedback); $pagedata = [ ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5], ]; // Process the feedback, there is only one page so the feedback will be completed. $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); // Retrieve the non-respondent users. $this->setUser($this->teacher); $result = mod_feedback_external::get_non_respondents($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['users']); $this->assertEquals($anotherstudent->id, $result['users'][0]['userid']); // Create another student. $anotherstudent2 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($anotherstudent2->id, $this->course->id, $this->studentrole->id, 'manual'); $this->setUser($anotherstudent2); $this->setUser($this->teacher); $result = mod_feedback_external::get_non_respondents($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(2, $result['users']); // Test pagination. $result = mod_feedback_external::get_non_respondents($this->feedback->id, 0, 'lastaccess', 0, 1); $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['users']); } /** * Helper function that completes the feedback for two students. */ protected function complete_basic_feedback() { global $DB; $generator = $this->getDataGenerator(); // Create separated groups. $DB->set_field('course', 'groupmode', SEPARATEGROUPS); $DB->set_field('course', 'groupmodeforce', 1); assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $this->teacherrole->id, $this->context); accesslib_clear_all_caches_for_unit_testing(); $group1 = $generator->create_group(array('courseid' => $this->course->id)); $group2 = $generator->create_group(array('courseid' => $this->course->id)); // Create another students. $anotherstudent1 = self::getDataGenerator()->create_user(); $anotherstudent2 = self::getDataGenerator()->create_user(); $generator->enrol_user($anotherstudent1->id, $this->course->id, $this->studentrole->id, 'manual'); $generator->enrol_user($anotherstudent2->id, $this->course->id, $this->studentrole->id, 'manual'); $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $this->student->id)); $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $this->teacher->id)); $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $anotherstudent1->id)); $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $anotherstudent2->id)); // Test user with full capabilities. $this->setUser($this->student); // Create a very simple feedback. $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $numericitem = $feedbackgenerator->create_item_numeric($this->feedback); $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback); $pagedata = [ ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5], ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'], ]; // Process the feedback, there is only one page so the feedback will be completed. $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); $this->setUser($anotherstudent1); $pagedata = [ ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 10], ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'def'], ]; $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); $this->setUser($anotherstudent2); $pagedata = [ ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 10], ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'def'], ]; $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata); $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result); $this->assertTrue($result['completed']); } /** * Test get_responses_analysis for anonymous feedback. */ public function test_get_responses_analysis_anonymous(): void { self::complete_basic_feedback(); // Retrieve the responses analysis. $this->setUser($this->teacher); $result = mod_feedback_external::get_responses_analysis($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_responses_analysis_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertEquals(0, $result['totalattempts']); $this->assertEquals(2, $result['totalanonattempts']); // Only see my groups. foreach ($result['attempts'] as $attempt) { $this->assertEmpty($attempt['userid']); // Is anonymous. } } /** * Test get_responses_analysis for non-anonymous feedback. */ public function test_get_responses_analysis_non_anonymous(): void { global $DB; // Force non anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id)); self::complete_basic_feedback(); // Retrieve the responses analysis. $this->setUser($this->teacher); $result = mod_feedback_external::get_responses_analysis($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_responses_analysis_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertEquals(2, $result['totalattempts']); $this->assertEquals(0, $result['totalanonattempts']); // Only see my groups. foreach ($result['attempts'] as $attempt) { $this->assertNotEmpty($attempt['userid']); // Is not anonymous. } } /** * Test get_last_completed for feedback anonymous not completed. */ public function test_get_last_completed_anonymous_not_completed(): void { global $DB; // Force anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_YES, array('id' => $this->feedback->id)); // Test user with full capabilities that didn't complete the feedback. $this->setUser($this->student); $this->expectExceptionMessage(get_string('anonymous', 'feedback')); $this->expectException(moodle_exception::class); mod_feedback_external::get_last_completed($this->feedback->id); } /** * Test get_last_completed for feedback anonymous and completed. */ public function test_get_last_completed_anonymous_completed(): void { global $DB; // Force anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_YES, array('id' => $this->feedback->id)); // Add one completion record.. $record = [ 'feedback' => $this->feedback->id, 'userid' => $this->student->id, 'timemodified' => time() - DAYSECS, 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_YES, 'courseid' => $this->course->id, ]; $record['id'] = $DB->insert_record('feedback_completed', (object) $record); // Test user with full capabilities. $this->setUser($this->student); $this->expectExceptionMessage(get_string('anonymous', 'feedback')); $this->expectException(moodle_exception::class); mod_feedback_external::get_last_completed($this->feedback->id); } /** * Test get_last_completed for feedback not anonymous and completed. */ public function test_get_last_completed_not_anonymous_completed(): void { global $DB; // Force non anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id)); // Add one completion record.. $record = [ 'feedback' => $this->feedback->id, 'userid' => $this->student->id, 'timemodified' => time() - DAYSECS, 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO, 'courseid' => $this->course->id, ]; $record['id'] = $DB->insert_record('feedback_completed', (object) $record); // Test user with full capabilities. $this->setUser($this->student); $result = mod_feedback_external::get_last_completed($this->feedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_last_completed_returns(), $result); $this->assertEquals($record, $result['completed']); } /** * Test get_last_completed for feedback not anonymous and not completed. */ public function test_get_last_completed_not_anonymous_not_completed(): void { global $DB; // Force anonymous. $DB->set_field('feedback', 'anonymous', FEEDBACK_ANONYMOUS_NO, array('id' => $this->feedback->id)); // Test user with full capabilities that didn't complete the feedback. $this->setUser($this->student); $this->expectExceptionMessage(get_string('not_completed_yet', 'feedback')); $this->expectException(moodle_exception::class); mod_feedback_external::get_last_completed($this->feedback->id); } /** * Test get_feedback_access_information for site feedback. */ public function test_get_feedback_access_information_for_site_feedback(): void { $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID)); $this->setUser($this->student); // Access the site feedback via the site activity. $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result); $this->assertTrue($result['cancomplete']); $this->assertTrue($result['cansubmit']); // Access the site feedback via course where I'm enrolled. $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result); $this->assertTrue($result['cancomplete']); $this->assertTrue($result['cansubmit']); // Access the site feedback via course where I'm not enrolled. $othercourse = $this->getDataGenerator()->create_course(); $this->expectException(moodle_exception::class); mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id); } /** * Test get_feedback_access_information for site feedback mapped. */ public function test_get_feedback_access_information_for_site_feedback_mapped(): void { global $DB; $sitefeedback = $this->getDataGenerator()->create_module('feedback', array('course' => SITEID)); $this->setUser($this->student); $DB->insert_record('feedback_sitecourse_map', array('feedbackid' => $sitefeedback->id, 'courseid' => $this->course->id)); // Access the site feedback via course where I'm enrolled and mapped. $result = mod_feedback_external::get_feedback_access_information($sitefeedback->id, $this->course->id); $result = external_api::clean_returnvalue(mod_feedback_external::get_feedback_access_information_returns(), $result); $this->assertTrue($result['cancomplete']); $this->assertTrue($result['cansubmit']); // Access the site feedback via course where I'm enrolled but not mapped. $othercourse = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($this->student->id, $othercourse->id, $this->studentrole->id, 'manual'); $this->expectException(moodle_exception::class); $this->expectExceptionMessage(get_string('cannotaccess', 'mod_feedback')); mod_feedback_external::get_feedback_access_information($sitefeedback->id, $othercourse->id); } } PKB\$Lcctests/privacy/provider_test.phpnu[. /** * Data provider tests. * * @package mod_feedback * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_privacy\tests\provider_testcase; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use mod_feedback\privacy\provider; require_once($CFG->dirroot . '/mod/feedback/lib.php'); /** * Data provider testcase class. * * @package mod_feedback * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test getting the contexts for a user. */ public function test_get_contexts_for_userid(): void { global $DB; $dg = $this->getDataGenerator(); $fg = $dg->get_plugin_generator('mod_feedback'); $c1 = $dg->create_course(); $c2 = $dg->create_course(); $cm0a = $dg->create_module('feedback', ['course' => SITEID]); $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); $cm1b = $dg->create_module('feedback', ['course' => $c1]); $cm2a = $dg->create_module('feedback', ['course' => $c2]); $cm2b = $dg->create_module('feedback', ['course' => $c2]); $cm2c = $dg->create_module('feedback', ['course' => $c2]); $u1 = $dg->create_user(); $u2 = $dg->create_user(); foreach ([$cm0a, $cm1a, $cm1b, $cm2a] as $feedback) { $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; if ($feedback == $cm1b) { $this->create_submission_with_answers($feedback, $u2, $answers); } else { $this->create_submission_with_answers($feedback, $u1, $answers); } } // Unsaved submission for u1 in cm2b. $feedback = $cm2b; $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; $this->create_tmp_submission_with_answers($feedback, $u1, $answers); // Unsaved submission for u2 in cm2c. $feedback = $cm2c; $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; $this->create_tmp_submission_with_answers($feedback, $u2, $answers); $contextids = provider::get_contexts_for_userid($u1->id)->get_contextids(); $this->assertCount(4, $contextids); $this->assertTrue(in_array(\context_module::instance($cm0a->cmid)->id, $contextids)); $this->assertTrue(in_array(\context_module::instance($cm1a->cmid)->id, $contextids)); $this->assertTrue(in_array(\context_module::instance($cm2a->cmid)->id, $contextids)); $this->assertFalse(in_array(\context_module::instance($cm1b->cmid)->id, $contextids)); $this->assertTrue(in_array(\context_module::instance($cm2b->cmid)->id, $contextids)); $this->assertFalse(in_array(\context_module::instance($cm2c->cmid)->id, $contextids)); $contextids = provider::get_contexts_for_userid($u2->id)->get_contextids(); $this->assertCount(2, $contextids); $this->assertFalse(in_array(\context_module::instance($cm0a->cmid)->id, $contextids)); $this->assertFalse(in_array(\context_module::instance($cm1a->cmid)->id, $contextids)); $this->assertFalse(in_array(\context_module::instance($cm2a->cmid)->id, $contextids)); $this->assertTrue(in_array(\context_module::instance($cm1b->cmid)->id, $contextids)); $this->assertFalse(in_array(\context_module::instance($cm2b->cmid)->id, $contextids)); $this->assertTrue(in_array(\context_module::instance($cm2c->cmid)->id, $contextids)); } /** * Test getting the users in a context. */ public function test_get_users_in_context(): void { global $DB; $dg = $this->getDataGenerator(); $fg = $dg->get_plugin_generator('mod_feedback'); $component = 'mod_feedback'; $c1 = $dg->create_course(); $c2 = $dg->create_course(); $cm0 = $dg->create_module('feedback', ['course' => SITEID]); $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); $cm1b = $dg->create_module('feedback', ['course' => $c1]); $cm2 = $dg->create_module('feedback', ['course' => $c2]); $u1 = $dg->create_user(); $u2 = $dg->create_user(); foreach ([$cm0, $cm1a, $cm1b, $cm2] as $feedback) { $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; if ($feedback == $cm1b) { $this->create_submission_with_answers($feedback, $u2, $answers); } else { $this->create_submission_with_answers($feedback, $u1, $answers); } } // Unsaved submission for u2 in cm1a. $feedback = $cm1a; $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; $this->create_tmp_submission_with_answers($feedback, $u2, $answers); // Only u1 in cm0. $context = \context_module::instance($cm0->cmid); $userlist = new \core_privacy\local\request\userlist($context, $component); provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $this->assertEquals([$u1->id], $userlist->get_userids()); $context = \context_module::instance($cm1a->cmid); $userlist = new \core_privacy\local\request\userlist($context, $component); provider::get_users_in_context($userlist); // Two submissions in cm1a: saved for u1, unsaved for u2. $this->assertCount(2, $userlist); $expected = [$u1->id, $u2->id]; $actual = $userlist->get_userids(); sort($expected); sort($actual); $this->assertEquals($expected, $actual); // Only u2 in cm1b. $context = \context_module::instance($cm1b->cmid); $userlist = new \core_privacy\local\request\userlist($context, $component); provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $this->assertEquals([$u2->id], $userlist->get_userids()); // Only u1 in cm2. $context = \context_module::instance($cm2->cmid); $userlist = new \core_privacy\local\request\userlist($context, $component); provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $this->assertEquals([$u1->id], $userlist->get_userids()); } /** * Test deleting user data. */ public function test_delete_data_for_user(): void { global $DB; $dg = $this->getDataGenerator(); $fg = $dg->get_plugin_generator('mod_feedback'); $c1 = $dg->create_course(); $c2 = $dg->create_course(); $cm0a = $dg->create_module('feedback', ['course' => SITEID]); $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); $cm2a = $dg->create_module('feedback', ['course' => $c2]); $u1 = $dg->create_user(); $u2 = $dg->create_user(); // Create a bunch of data. foreach ([$cm1a, $cm0a, $cm2a] as $feedback) { $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; // Create u2 user data for this module. if ($feedback == $cm1a) { $this->create_submission_with_answers($feedback, $u2, $answers); $this->create_tmp_submission_with_answers($feedback, $u2, $answers); } $this->create_submission_with_answers($feedback, $u1, $answers); $this->create_tmp_submission_with_answers($feedback, $u1, $answers); } $appctx = new approved_contextlist($u1, 'mod_feedback', [ \context_module::instance($cm0a->cmid)->id, \context_module::instance($cm1a->cmid)->id ]); provider::delete_data_for_user($appctx); // Confirm all data is gone in those, except for u2. foreach ([$cm0a, $cm1a] as $feedback) { $this->assert_no_feedback_data_for_user($feedback, $u1); if ($feedback == $cm1a) { $this->assert_feedback_data_for_user($feedback, $u2); $this->assert_feedback_tmp_data_for_user($feedback, $u2); } } // Confirm cm2a wasn't affected. $this->assert_feedback_data_for_user($cm2a, $u1); $this->assert_feedback_tmp_data_for_user($cm2a, $u1); } /** * Test deleting data within a context for an approved userlist. */ public function test_delete_data_for_users(): void { global $DB; $dg = $this->getDataGenerator(); $fg = $dg->get_plugin_generator('mod_feedback'); $c1 = $dg->create_course(); $c2 = $dg->create_course(); $cm0 = $dg->create_module('feedback', ['course' => SITEID]); $cm1 = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); $cm2 = $dg->create_module('feedback', ['course' => $c2]); $context0 = \context_module::instance($cm0->cmid); $context1 = \context_module::instance($cm1->cmid); $u1 = $dg->create_user(); $u2 = $dg->create_user(); // Create a bunch of data. foreach ([$cm0, $cm1, $cm2] as $feedback) { $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; $this->create_submission_with_answers($feedback, $u1, $answers); $this->create_tmp_submission_with_answers($feedback, $u1, $answers); $this->create_submission_with_answers($feedback, $u2, $answers); $this->create_tmp_submission_with_answers($feedback, $u2, $answers); } // Delete u1 from cm0, ensure u2 data is retained. $approveduserlist = new approved_userlist($context0, 'mod_feedback', [$u1->id]); provider::delete_data_for_users($approveduserlist); $this->assert_no_feedback_data_for_user($cm0, $u1); $this->assert_feedback_data_for_user($cm0, $u2); $this->assert_feedback_tmp_data_for_user($cm0, $u2); // Ensure cm1 unaffected by cm1 deletes. $this->assert_feedback_data_for_user($cm1, $u1); $this->assert_feedback_tmp_data_for_user($cm1, $u1); $this->assert_feedback_data_for_user($cm1, $u2); $this->assert_feedback_tmp_data_for_user($cm1, $u2); // Delete u1 and u2 from cm1, ensure no data is retained. $approveduserlist = new approved_userlist($context1, 'mod_feedback', [$u1->id, $u2->id]); provider::delete_data_for_users($approveduserlist); $this->assert_no_feedback_data_for_user($cm1, $u1); $this->assert_no_feedback_data_for_user($cm1, $u2); // Ensure cm2 is unaffected by any of the deletes. $this->assert_feedback_data_for_user($cm2, $u1); $this->assert_feedback_tmp_data_for_user($cm2, $u1); $this->assert_feedback_data_for_user($cm2, $u2); $this->assert_feedback_tmp_data_for_user($cm2, $u2); } /** * Test deleting a whole context. */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $dg = $this->getDataGenerator(); $fg = $dg->get_plugin_generator('mod_feedback'); $c1 = $dg->create_course(); $c2 = $dg->create_course(); $cm0a = $dg->create_module('feedback', ['course' => SITEID]); $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); $u1 = $dg->create_user(); $u2 = $dg->create_user(); // Create a bunch of data. foreach ([$cm1a, $cm0a] as $feedback) { $i1 = $fg->create_item_numeric($feedback); $i2 = $fg->create_item_multichoice($feedback); $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; $this->create_submission_with_answers($feedback, $u1, $answers); $this->create_tmp_submission_with_answers($feedback, $u1, $answers); $this->create_submission_with_answers($feedback, $u2, $answers); $this->create_tmp_submission_with_answers($feedback, $u2, $answers); } provider::delete_data_for_all_users_in_context(\context_module::instance($cm1a->cmid)); $this->assert_no_feedback_data_for_user($cm1a, $u1); $this->assert_no_feedback_data_for_user($cm1a, $u2); $this->assert_feedback_data_for_user($cm0a, $u1); $this->assert_feedback_data_for_user($cm0a, $u2); $this->assert_feedback_tmp_data_for_user($cm0a, $u1); $this->assert_feedback_tmp_data_for_user($cm0a, $u2); } /** * Test exporting data. */ public function test_export_user_data(): void { global $DB; $dg = $this->getDataGenerator(); $fg = $dg->get_plugin_generator('mod_feedback'); $c1 = $dg->create_course(); $c2 = $dg->create_course(); $cm0a = $dg->create_module('feedback', ['course' => SITEID]); $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]); $cm2a = $dg->create_module('feedback', ['course' => $c2, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'multiple_submit' => 1]); $cm2b = $dg->create_module('feedback', ['course' => $c2]); $cm2c = $dg->create_module('feedback', ['course' => $c2]); $u1 = $dg->create_user(); $u2 = $dg->create_user(); // Create a bunch of data. foreach ([$cm0a, $cm1a, $cm2a, $cm2b] as $feedback) { $i1 = $fg->create_item_numeric($feedback, ['name' => 'Q1', 'label' => 'L1']); $i2 = $fg->create_item_multichoice($feedback, ['name' => 'Q2', 'label' => 'L2']); $answersu1 = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]]; $answersu2 = ['numeric_' . $i1->id => '2', 'multichoice_' . $i2->id => [2]]; if ($cm0a == $feedback) { $this->create_submission_with_answers($feedback, $u1, $answersu1); $this->create_tmp_submission_with_answers($feedback, $u1, $answersu1); } else if ($cm1a == $feedback) { $this->create_tmp_submission_with_answers($feedback, $u1, $answersu1); } else if ($cm2a == $feedback) { $this->create_submission_with_answers($feedback, $u1, $answersu1); $this->create_submission_with_answers($feedback, $u1, ['numeric_' . $i1->id => '1337'], 2); } else if ($cm2c == $feedback) { $this->create_submission_with_answers($feedback, $u1, $answersu1); $this->create_tmp_submission_with_answers($feedback, $u1, $answersu1); } $this->create_submission_with_answers($feedback, $u2, $answersu2); $this->create_tmp_submission_with_answers($feedback, $u2, $answersu2); } $appctx = new approved_contextlist($u1, 'mod_feedback', [ \context_module::instance($cm0a->cmid)->id, \context_module::instance($cm1a->cmid)->id, \context_module::instance($cm2a->cmid)->id, \context_module::instance($cm2b->cmid)->id, ]); provider::export_user_data($appctx); // CM0A. $data = writer::with_context(\context_module::instance($cm0a->cmid))->get_data(); $this->assertCount(2, $data->submissions); $submission = $data->submissions[0]; $this->assertEquals(transform::yesno(false), $submission['inprogress']); $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); $this->assertCount(2, $submission['answers']); $this->assertEquals('Q1', $submission['answers'][0]['question']); $this->assertEquals('1', $submission['answers'][0]['answer']); $this->assertEquals('Q2', $submission['answers'][1]['question']); $this->assertEquals('a', $submission['answers'][1]['answer']); $submission = $data->submissions[1]; $this->assertEquals(transform::yesno(true), $submission['inprogress']); $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); $this->assertCount(2, $submission['answers']); $this->assertEquals('Q1', $submission['answers'][0]['question']); $this->assertEquals('1', $submission['answers'][0]['answer']); $this->assertEquals('Q2', $submission['answers'][1]['question']); $this->assertEquals('a', $submission['answers'][1]['answer']); // CM1A. $data = writer::with_context(\context_module::instance($cm1a->cmid))->get_data(); $this->assertCount(1, $data->submissions); $submission = $data->submissions[0]; $this->assertEquals(transform::yesno(true), $submission['inprogress']); $this->assertEquals(transform::yesno(false), $submission['anonymousresponse']); $this->assertCount(2, $submission['answers']); $this->assertEquals('Q1', $submission['answers'][0]['question']); $this->assertEquals('1', $submission['answers'][0]['answer']); $this->assertEquals('Q2', $submission['answers'][1]['question']); $this->assertEquals('a', $submission['answers'][1]['answer']); // CM2A. $data = writer::with_context(\context_module::instance($cm2a->cmid))->get_data(); $this->assertCount(2, $data->submissions); $submission = $data->submissions[0]; $this->assertEquals(transform::yesno(false), $submission['inprogress']); $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); $this->assertCount(2, $submission['answers']); $this->assertEquals('Q1', $submission['answers'][0]['question']); $this->assertEquals('1', $submission['answers'][0]['answer']); $this->assertEquals('Q2', $submission['answers'][1]['question']); $this->assertEquals('a', $submission['answers'][1]['answer']); $submission = $data->submissions[1]; $this->assertEquals(transform::yesno(false), $submission['inprogress']); $this->assertEquals(transform::yesno(true), $submission['anonymousresponse']); $this->assertCount(1, $submission['answers']); $this->assertEquals('Q1', $submission['answers'][0]['question']); $this->assertEquals('1337', $submission['answers'][0]['answer']); // CM2B (no data). $data = writer::with_context(\context_module::instance($cm2b->cmid))->get_data(); $this->assertEmpty($data); // CM2C (not exported). $data = writer::with_context(\context_module::instance($cm2b->cmid))->get_data(); $this->assertEmpty($data); } /** * Assert there is no feedback data for a user. * * @param object $feedback The feedback. * @param object $user The user. * @return void */ protected function assert_no_feedback_data_for_user($feedback, $user) { global $DB; $this->assertFalse($DB->record_exists('feedback_completed', ['feedback' => $feedback->id, 'userid' => $user->id])); $this->assertFalse($DB->record_exists('feedback_completedtmp', ['feedback' => $feedback->id, 'userid' => $user->id])); // Check that there aren't orphan values because we can't check by userid. $sql = " SELECT fv.id FROM {%s} fv LEFT JOIN {%s} fc ON fc.id = fv.completed WHERE fc.id IS NULL"; $this->assertFalse($DB->record_exists_sql(sprintf($sql, 'feedback_value', 'feedback_completed'), [])); $this->assertFalse($DB->record_exists_sql(sprintf($sql, 'feedback_valuetmp', 'feedback_completedtmp'), [])); } /** * Assert there are submissions and answers for user. * * @param object $feedback The feedback. * @param object $user The user. * @param int $submissioncount The number of submissions. * @param int $valuecount The number of values per submission. * @return void */ protected function assert_feedback_data_for_user($feedback, $user, $submissioncount = 1, $valuecount = 2) { global $DB; $completeds = $DB->get_records('feedback_completed', ['feedback' => $feedback->id, 'userid' => $user->id]); $this->assertCount($submissioncount, $completeds); foreach ($completeds as $record) { $this->assertEquals($valuecount, $DB->count_records('feedback_value', ['completed' => $record->id])); } } /** * Assert there are temporary submissions and answers for user. * * @param object $feedback The feedback. * @param object $user The user. * @param int $submissioncount The number of submissions. * @param int $valuecount The number of values per submission. * @return void */ protected function assert_feedback_tmp_data_for_user($feedback, $user, $submissioncount = 1, $valuecount = 2) { global $DB; $completedtmps = $DB->get_records('feedback_completedtmp', ['feedback' => $feedback->id, 'userid' => $user->id]); $this->assertCount($submissioncount, $completedtmps); foreach ($completedtmps as $record) { $this->assertEquals($valuecount, $DB->count_records('feedback_valuetmp', ['completed' => $record->id])); } } /** * Create an submission with answers. * * @param object $feedback The feedback. * @param object $user The user. * @param array $answers Answers. * @param int $submissioncount The number of submissions expected after this entry. * @return void */ protected function create_submission_with_answers($feedback, $user, $answers, $submissioncount = 1) { global $DB; $modinfo = get_fast_modinfo($feedback->course); $cm = $modinfo->get_cm($feedback->cmid); $feedbackcompletion = new \mod_feedback_completion($feedback, $cm, $feedback->course, false, null, null, $user->id); $feedbackcompletion->save_response_tmp((object) $answers); $feedbackcompletion->save_response(); $this->assertEquals($submissioncount, $DB->count_records('feedback_completed', ['feedback' => $feedback->id, 'userid' => $user->id])); $this->assertEquals(count($answers), $DB->count_records('feedback_value', [ 'completed' => $feedbackcompletion->get_completed()->id])); } /** * Create a temporary submission with answers. * * @param object $feedback The feedback. * @param object $user The user. * @param array $answers Answers. * @return void */ protected function create_tmp_submission_with_answers($feedback, $user, $answers) { global $DB; $modinfo = get_fast_modinfo($feedback->course); $cm = $modinfo->get_cm($feedback->cmid); $feedbackcompletion = new \mod_feedback_completion($feedback, $cm, $feedback->course, false, null, null, $user->id); $feedbackcompletion->save_response_tmp((object) $answers); $this->assertEquals(1, $DB->count_records('feedback_completedtmp', ['feedback' => $feedback->id, 'userid' => $user->id])); $this->assertEquals(2, $DB->count_records('feedback_valuetmp', [ 'completed' => $feedbackcompletion->get_current_completed_tmp()->id])); } } PKB\G(wtests/lib_test.phpnu[. /** * Unit tests for (some of) mod/feedback/lib.php. * * @package mod_feedback * @copyright 2016 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_feedback; use mod_feedback_completion; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/feedback/lib.php'); /** * Unit tests for (some of) mod/feedback/lib.php. * * @copyright 2016 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { public function test_feedback_initialise(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $params['course'] = $course->id; $params['timeopen'] = time() - 5 * MINSECS; $params['timeclose'] = time() + DAYSECS; $params['anonymous'] = 1; $params['intro'] = 'Some introduction text'; $feedback = $this->getDataGenerator()->create_module('feedback', $params); // Test different ways to construct the structure object. $pseudocm = get_coursemodule_from_instance('feedback', $feedback->id); // Object similar to cm_info. $cm = get_fast_modinfo($course)->instances['feedback'][$feedback->id]; // Instance of cm_info. $constructorparams = [ [$feedback, null], [null, $pseudocm], [null, $cm], [$feedback, $pseudocm], [$feedback, $cm], ]; foreach ($constructorparams as $params) { $structure = new mod_feedback_completion($params[0], $params[1], 0); $this->assertTrue($structure->is_open()); $this->assertTrue($structure->get_cm() instanceof \cm_info); $this->assertEquals($feedback->cmid, $structure->get_cm()->id); $this->assertEquals($feedback->intro, $structure->get_feedback()->intro); } } /** * Tests for mod_feedback_refresh_events. */ public function test_feedback_refresh_events(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $timeopen = time(); $timeclose = time() + 86400; $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $params['course'] = $course->id; $params['timeopen'] = $timeopen; $params['timeclose'] = $timeclose; $feedback = $generator->create_instance($params); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $context = \context_module::instance($cm->id); // Normal case, with existing course. $this->assertTrue(feedback_refresh_events($course->id)); $eventparams = array('modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => 'open'); $openevent = $DB->get_record('event', $eventparams, '*', MUST_EXIST); $this->assertEquals($openevent->timestart, $timeopen); $eventparams = array('modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => 'close'); $closeevent = $DB->get_record('event', $eventparams, '*', MUST_EXIST); $this->assertEquals($closeevent->timestart, $timeclose); // In case the course ID is passed as a numeric string. $this->assertTrue(feedback_refresh_events('' . $course->id)); // Course ID not provided. $this->assertTrue(feedback_refresh_events()); $eventparams = array('modulename' => 'feedback'); $events = $DB->get_records('event', $eventparams); foreach ($events as $event) { if ($event->modulename === 'feedback' && $event->instance === $feedback->id && $event->eventtype === 'open') { $this->assertEquals($event->timestart, $timeopen); } if ($event->modulename === 'feedback' && $event->instance === $feedback->id && $event->eventtype === 'close') { $this->assertEquals($event->timestart, $timeclose); } } } /** * Test check_updates_since callback. */ public function test_check_updates_since(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Create user. $student = self::getDataGenerator()->create_user(); // User enrolment. $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual'); $this->setCurrentTimeStart(); $record = array( 'course' => $course->id, 'custom' => 0, 'feedback' => 1, ); $feedback = $this->getDataGenerator()->create_module('feedback', $record); $cm = get_coursemodule_from_instance('feedback', $feedback->id, $course->id); $cm = \cm_info::create($cm); $this->setUser($student); // Check that upon creation, the updates are only about the new configuration created. $onehourago = time() - HOURSECS; $updates = feedback_check_updates_since($cm, $onehourago); foreach ($updates as $el => $val) { if ($el == 'configuration') { $this->assertTrue($val->updated); $this->assertTimeCurrent($val->timeupdated); } else { $this->assertFalse($val->updated); } } $record = [ 'feedback' => $feedback->id, 'userid' => $student->id, 'timemodified' => time(), 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO, 'courseid' => $course->id, ]; $DB->insert_record('feedback_completed', (object)$record); $DB->insert_record('feedback_completedtmp', (object)$record); // Check now for finished and unfinished attempts. $updates = feedback_check_updates_since($cm, $onehourago); $this->assertTrue($updates->attemptsunfinished->updated); $this->assertCount(1, $updates->attemptsunfinished->itemids); $this->assertTrue($updates->attemptsfinished->updated); $this->assertCount(1, $updates->attemptsfinished->itemids); } /** * Test calendar event provide action open. */ public function test_feedback_core_calendar_provide_event_action_open(): void { $this->resetAfterTest(); $this->setAdminUser(); $now = time(); $course = $this->getDataGenerator()->create_course(); // Enrol admin as a student so they can complete the feedback. $this->getDataGenerator()->enrol_user(get_admin()->id, $course->id, 'student'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id, 'timeopen' => $now - DAYSECS, 'timeclose' => $now + DAYSECS]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory); $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } /** * Test calendar event provide action open, viewed by a different user. */ public function test_feedback_core_calendar_provide_event_action_open_for_user(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $now = time(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id, 'timeopen' => $now - DAYSECS, 'timeclose' => $now + DAYSECS]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $this->setUser($user2); // User2 checking their events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id); $this->assertNull($actionevent); // User2 checking $user's events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id); $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } /** * Test calendar event provide action closed. */ public function test_feedback_core_calendar_provide_event_action_closed(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id, 'timeclose' => time() - DAYSECS)); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory); // No event on the dashboard if feedback is closed. $this->assertNull($actionevent); } /** * Test calendar event provide action closed, viewed by a different user. */ public function test_feedback_core_calendar_provide_event_action_closed_for_user(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual'); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id, 'timeclose' => time() - DAYSECS)); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $this->setUser($user2); // User2 checking their events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id); $this->assertNull($actionevent); // User2 checking $user's events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id); // No event on the dashboard if feedback is closed. $this->assertNull($actionevent); } /** * Test calendar event action open in future. * * @throws coding_exception */ public function test_feedback_core_calendar_provide_event_action_open_in_future(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Enrol admin as a student so they can complete the feedback. $this->getDataGenerator()->enrol_user(get_admin()->id, $course->id, 'student'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id, 'timeopen' => time() + DAYSECS]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory); $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertFalse($actionevent->is_actionable()); } /** * Test calendar event action open in future, viewed by a different user. * * @throws coding_exception */ public function test_feedback_core_calendar_provide_event_action_open_in_future_for_user(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id, 'timeopen' => time() + DAYSECS]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $this->setUser($user2); // User2 checking their events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id); $this->assertNull($actionevent); // User2 checking $user's events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id); $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertFalse($actionevent->is_actionable()); } /** * Test calendar event with no time specified. * * @throws coding_exception */ public function test_feedback_core_calendar_provide_event_action_no_time_specified(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Enrol admin as a student so they can complete the feedback. $this->getDataGenerator()->enrol_user(get_admin()->id, $course->id, 'student'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory); $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } /** * Test calendar event with no time specified, viewed by a different user. * * @throws coding_exception */ public function test_feedback_core_calendar_provide_event_action_no_time_specified_for_user(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $factory = new \core_calendar\action_factory(); $this->setUser($user2); // User2 checking their events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id); $this->assertNull($actionevent); // User2 checking $user's events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id); $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } /** * A user that can not submit feedback should not have an action. */ public function test_feedback_core_calendar_provide_event_action_can_not_submit(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $context = \context_module::instance($cm->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual'); $this->setUser($user); assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $context); $factory = new \core_calendar\action_factory(); $action = mod_feedback_core_calendar_provide_event_action($event, $factory); $this->assertNull($action); } /** * A user that can not submit feedback should not have an action, viewed by a different user. */ public function test_feedback_core_calendar_provide_event_action_can_not_submit_for_user(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $context = \context_module::instance($cm->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id, 'manual'); assign_capability('mod/feedback:complete', CAP_PROHIBIT, $studentrole->id, $context); $factory = new \core_calendar\action_factory(); $this->setUser($user2); // User2 checking their events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id); $this->assertNull($actionevent); // User2 checking $user's events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id); $this->assertNull($actionevent); } /** * A user that has already submitted feedback should not have an action. */ public function test_feedback_core_calendar_provide_event_action_already_submitted(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $context = \context_module::instance($cm->id); $this->setUser($user); $record = [ 'feedback' => $feedback->id, 'userid' => $user->id, 'timemodified' => time(), 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO, 'courseid' => 0, ]; $DB->insert_record('feedback_completed', (object) $record); $factory = new \core_calendar\action_factory(); $action = mod_feedback_core_calendar_provide_event_action($event, $factory); $this->assertNull($action); } /** * A user that has already submitted feedback should not have an action, viewed by a different user. */ public function test_feedback_core_calendar_provide_event_action_already_submitted_for_user(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $context = \context_module::instance($cm->id); $this->setUser($user); $record = [ 'feedback' => $feedback->id, 'userid' => $user->id, 'timemodified' => time(), 'random_response' => 0, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO, 'courseid' => 0, ]; $DB->insert_record('feedback_completed', (object) $record); $factory = new \core_calendar\action_factory(); $this->setUser($user2); // User2 checking their events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user2->id); $this->assertNull($actionevent); // User2 checking $user's events. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $user->id); $this->assertNull($actionevent); } public function test_feedback_core_calendar_provide_event_action_already_completed(): void { $this->resetAfterTest(); set_config('enablecompletion', 1); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Get some additional data. $cm = get_coursemodule_from_instance('feedback', $feedback->id); // Create a calendar event. $event = $this->create_action_event($course->id, $feedback->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed. $completion = new \completion_info($course); $completion->set_module_viewed($cm); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory); // Ensure result was null. $this->assertNull($actionevent); } public function test_feedback_core_calendar_provide_event_action_already_completed_for_user(): void { $this->resetAfterTest(); set_config('enablecompletion', 1); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id), array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS)); // Enrol a student in the course. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Get some additional data. $cm = get_coursemodule_from_instance('feedback', $feedback->id); // Create a calendar event. $event = $this->create_action_event($course->id, $feedback->id, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed for the student. $completion = new \completion_info($course); $completion->set_module_viewed($cm, $student->id); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory, $student->id); // Ensure result was null. $this->assertNull($actionevent); } /** * Creates an action event. * * @param int $courseid The course id. * @param int $instanceid The feedback id. * @param string $eventtype The event type. eg. FEEDBACK_EVENT_TYPE_OPEN. * @return bool|calendar_event */ private function create_action_event($courseid, $instanceid, $eventtype) { $event = new \stdClass(); $event->name = 'Calendar event'; $event->modulename = 'feedback'; $event->courseid = $courseid; $event->instance = $instanceid; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = $eventtype; $event->timestart = time(); return \calendar_event::create($event); } /** * Test the callback responsible for returning the completion rule descriptions. * This function should work given either an instance of the module (cm_info), such as when checking the active rules, * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type. */ public function test_mod_feedback_completion_get_active_rule_descriptions(): void { $this->resetAfterTest(); $this->setAdminUser(); // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]); $feedback1 = $this->getDataGenerator()->create_module('feedback', [ 'course' => $course->id, 'completion' => 2, 'completionsubmit' => 1 ]); $feedback2 = $this->getDataGenerator()->create_module('feedback', [ 'course' => $course->id, 'completion' => 2, 'completionsubmit' => 0 ]); $cm1 = \cm_info::create(get_coursemodule_from_instance('feedback', $feedback1->id)); $cm2 = \cm_info::create(get_coursemodule_from_instance('feedback', $feedback2->id)); // Data for the stdClass input type. // This type of input would occur when checking the default completion rules for an activity type, where we don't have // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info. $moddefaults = new \stdClass(); $moddefaults->customdata = ['customcompletionrules' => ['completionsubmit' => 1]]; $moddefaults->completion = 2; $activeruledescriptions = [get_string('completionsubmit', 'feedback')]; $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions($cm1), $activeruledescriptions); $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions($cm2), []); $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions); $this->assertEquals(mod_feedback_get_completion_active_rule_descriptions(new \stdClass()), []); } /** * An unknown event should not have min or max restrictions. */ public function test_get_valid_event_timestart_range_unknown_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $DB->update_record('feedback', $feedback); $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => 'SOME UNKNOWN EVENT', 'timestart' => $timeopen, 'timeduration' => 86400, 'visible' => 1 ]); list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback); $this->assertNull($min); $this->assertNull($max); } /** * A FEEDBACK_EVENT_TYPE_OPEN should have a max timestart equal to the activity * close time. */ public function test_get_valid_event_timestart_range_event_type_open(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $DB->update_record('feedback', $feedback); $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $timeopen, 'timeduration' => 86400, 'visible' => 1 ]); list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback); $this->assertNull($min); $this->assertEquals($timeclose, $max[0]); $this->assertNotEmpty($max[1]); } /** * A FEEDBACK_EVENT_TYPE_OPEN should not have a max timestamp if the activity * doesn't have a close date. */ public function test_get_valid_event_timestart_range_event_type_open_no_close(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = 0; $DB->update_record('feedback', $feedback); $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $timeopen, 'timeduration' => 86400, 'visible' => 1 ]); list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback); $this->assertNull($min); $this->assertNull($max); } /** * A FEEDBACK_EVENT_TYPE_CLOSE should have a min timestart equal to the activity * open time. */ public function test_get_valid_event_timestart_range_event_type_close(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $DB->update_record('feedback', $feedback); $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $timeopen, 'timeduration' => 86400, 'visible' => 1 ]); list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback); $this->assertEquals($timeopen, $min[0]); $this->assertNotEmpty($min[1]); $this->assertNull($max); } /** * A FEEDBACK_EVENT_TYPE_CLOSE should not have a minimum timestamp if the activity * doesn't have an open date. */ public function test_get_valid_event_timestart_range_event_type_close_no_open(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = 0; $feedback->timeclose = $timeclose; $DB->update_record('feedback', $feedback); $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $timeopen, 'timeduration' => 86400, 'visible' => 1 ]); list($min, $max) = mod_feedback_core_calendar_get_valid_event_timestart_range($event, $feedback); $this->assertNull($min); $this->assertNull($max); } /** * An unkown event type should not change the feedback instance. */ public function test_mod_feedback_core_calendar_event_timestart_updated_unknown_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $DB->update_record('feedback', $feedback); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN . "SOMETHING ELSE", 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); mod_feedback_core_calendar_event_timestart_updated($event, $feedback); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); $this->assertEquals($timeopen, $feedback->timeopen); $this->assertEquals($timeclose, $feedback->timeclose); } /** * A FEEDBACK_EVENT_TYPE_OPEN event should update the timeopen property of * the feedback activity. */ public function test_mod_feedback_core_calendar_event_timestart_updated_open_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeopen = $timeopen - DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $feedback->timemodified = $timemodified; $DB->update_record('feedback', $feedback); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_OPEN, 'timestart' => $newtimeopen, 'timeduration' => 86400, 'visible' => 1 ]); mod_feedback_core_calendar_event_timestart_updated($event, $feedback); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); // Ensure the timeopen property matches the event timestart. $this->assertEquals($newtimeopen, $feedback->timeopen); // Ensure the timeclose isn't changed. $this->assertEquals($timeclose, $feedback->timeclose); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $feedback->timemodified); } /** * A FEEDBACK_EVENT_TYPE_CLOSE event should update the timeclose property of * the feedback activity. */ public function test_mod_feedback_core_calendar_event_timestart_updated_close_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeclose = $timeclose + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $feedback->timemodified = $timemodified; $DB->update_record('feedback', $feedback); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $newtimeclose, 'timeduration' => 86400, 'visible' => 1 ]); mod_feedback_core_calendar_event_timestart_updated($event, $feedback); $feedback = $DB->get_record('feedback', ['id' => $feedback->id]); // Ensure the timeclose property matches the event timestart. $this->assertEquals($newtimeclose, $feedback->timeclose); // Ensure the timeopen isn't changed. $this->assertEquals($timeopen, $feedback->timeopen); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $feedback->timemodified); } /** * If a student somehow finds a way to update the calendar event * then the callback should not be executed to update the activity * properties as well because that would be a security issue. */ public function test_student_role_cant_update_time_close_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeclose = $timeclose + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $feedback->timemodified = $timemodified; $DB->update_record('feedback', $feedback); $generator->enrol_user($user->id, $course->id, 'student'); $generator->role_assign($roleid, $user->id, $context->id); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => $user->id, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $newtimeclose, 'timeduration' => 86400, 'visible' => 1 ]); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true); $this->setUser($user); mod_feedback_core_calendar_event_timestart_updated($event, $feedback); $newfeedback = $DB->get_record('feedback', ['id' => $feedback->id]); // The activity shouldn't have been updated because the user // doesn't have permissions to do it. $this->assertEquals($timeclose, $newfeedback->timeclose); } /** * The activity should update if a teacher modifies the calendar * event. */ public function test_teacher_role_can_update_time_close_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $generator->create_course(); $context = \context_course::instance($course->id); $roleid = $generator->create_role(); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeclose = $timeclose + DAYSECS; $feedback = $feedbackgenerator->create_instance(['course' => $course->id]); $feedback->timeopen = $timeopen; $feedback->timeclose = $timeclose; $feedback->timemodified = $timemodified; $DB->update_record('feedback', $feedback); $generator->enrol_user($user->id, $course->id, 'teacher'); $generator->role_assign($roleid, $user->id, $context->id); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => $user->id, 'modulename' => 'feedback', 'instance' => $feedback->id, 'eventtype' => FEEDBACK_EVENT_TYPE_CLOSE, 'timestart' => $newtimeclose, 'timeduration' => 86400, 'visible' => 1 ]); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); $sink = $this->redirectEvents(); mod_feedback_core_calendar_event_timestart_updated($event, $feedback); $triggeredevents = $sink->get_events(); $moduleupdatedevents = array_filter($triggeredevents, function($e) { return is_a($e, 'core\event\course_module_updated'); }); $newfeedback = $DB->get_record('feedback', ['id' => $feedback->id]); // The activity should have been updated because the user // has permissions to do it. $this->assertEquals($newtimeclose, $newfeedback->timeclose); // A course_module_updated event should be fired if the module // was successfully modified. $this->assertNotEmpty($moduleupdatedevents); } /** * A user who does not have capabilities to add events to the calendar should be able to create an feedback. */ public function test_creation_with_no_calendar_capabilities(): void { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher'); $roleid = self::getDataGenerator()->create_role(); self::getDataGenerator()->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true); $generator = self::getDataGenerator()->get_plugin_generator('mod_feedback'); // Create an instance as a user without the calendar capabilities. $this->setUser($user); $time = time(); $params = array( 'course' => $course->id, 'timeopen' => $time + 200, 'timeclose' => $time + 2000, ); $generator->create_instance($params); } /** * Test that if a teacher (non editing) is not part of any group in separate group mode he will not receive notification emails. * @covers ::feedback_get_receivemail_users */ public function test_feedback_get_receivemail_users(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]); $group = $generator->create_group(['courseid' => $course->id, 'name' => 'group0']); $feedbackgenerator = $generator->get_plugin_generator('mod_feedback'); $feedback = $feedbackgenerator->create_instance(['course' => $course->id, 'email_notification' => true]); $teacher = []; $data = [ 'teacher1' => 'teacher', 'teacher2' => 'teacher', 'teacher3' => 'editingteacher', 'teacher4' => 'editingteacher', ]; foreach ($data as $username => $role) { $teacher[$username] = $generator->create_user(['username' => $username]); $generator->enrol_user($teacher[$username]->id, $course->id, $role); } $generator->create_group_member([ 'groupid' => $group->id, 'userid' => $teacher['teacher1']->id, ]); $generator->create_group_member([ 'groupid' => $group->id, 'userid' => $teacher['teacher4']->id, ]); $usergroup = $group->id; // Non editing Teachers (teacher1) in a group should receive notification emails. // Editing teachers (teacher4), in a group should also receive notification emails. $teachersingroup = feedback_get_receivemail_users($feedback->cmid, $usergroup); $this->assertCount(2, $teachersingroup); $this->assertNotEmpty($teachersingroup[$teacher['teacher1']->id]); $this->assertNotEmpty($teachersingroup[$teacher['teacher4']->id]); // Here we should return only the editing teachers (teacher3 and 4) who have access to all groups. $teacherwithnogroup = feedback_get_receivemail_users($feedback->cmid); $this->assertCount(2, $teacherwithnogroup); $this->assertNotEmpty($teacherwithnogroup[$teacher['teacher3']->id]); $this->assertNotEmpty($teacherwithnogroup[$teacher['teacher4']->id]); } } PKB\n n tests/item_test.phpnu[. namespace mod_feedback; use advanced_testcase; use ReflectionClass; /** * This file contains unit tests for the mod_feedback items. * * @package mod_feedback * @copyright 2020 Mikhail Golenkov * @author 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class item_test extends advanced_testcase { /** * Test that get_analysed() for textarea item returns correctly formatted data for exporting to Excel. * * @covers ::get_analysed */ public function test_get_analysed_textarea_for_excel_export(): void { global $DB; $this->resetAfterTest(); // Create a course, a feedback activity and an item. $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course]); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item = $feedbackgenerator->create_item_textfield($feedback); // Expected text. $valuetext = "First line\nSecond line"; // Create a temporary response. $completedid = $DB->insert_record('feedback_completedtmp', (object)['feedback' => $feedback->id]); $completed = $DB->get_record('feedback_completedtmp', ['id' => $completedid], '*', MUST_EXIST); $value = (object)['course_id' => $course->id, 'item' => $item->id, 'completed' => $completedid, 'value' => $valuetext]; $DB->insert_record('feedback_valuetmp', $value); feedback_save_tmp_values($completed); // Set get_analysed() method accessibility. $itemclass = feedback_get_item_class('textarea'); $reflection = new ReflectionClass($itemclass); $method = $reflection->getMethod('get_analysed'); // Call the method and indicate it is being used for Excel. $actual = $method->invoke(new $itemclass(), $item, false, $course->id, true); // Check returned data maintains the line break. $this->assertCount(1, $actual->data); $datum = reset($actual->data); $this->assertEquals($valuetext, $datum); } } PKB\TgGUUtests/completion_test.phpnu[. namespace mod_feedback; use mod_feedback_completion; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/feedback/classes/completion.php'); /** * Unit tests for (some of) mod/feedback/classes/completion.php. * * @package mod_feedback * @copyright 2019 Tobias Reischmann * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class completion_test extends \advanced_testcase { /** * Returns the number of pages with visible elements for the current state of the feedback completion. * @param mod_feedback_completion $completion * @return int number of pages with at least one visible item. */ private function get_number_of_visible_pages(mod_feedback_completion $completion) { $pages = $completion->get_pages(); $result = 0; foreach ($pages as $items) { if (count($items) > 0) { $result++; } } return $result; } /** * Tests get_pages for transitive dependencies. * @throws coding_exception */ public function test_get_pages(): void { $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id)); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $itemscreated = []; // Create at least one page. $itemscreated[] = $feedbackgenerator->create_item_multichoice($feedback, $record = ['values' => "y\nn"]); $itemscreated[] = $feedbackgenerator->create_item_pagebreak($feedback); $itemscreated[] = $feedbackgenerator->create_item_multichoice($feedback, $record = ['values' => "y\nn", 'dependitem' => $itemscreated[0]->id, 'dependvalue' => 'n']); $itemscreated[] = $feedbackgenerator->create_item_pagebreak($feedback); $itemscreated[] = $feedbackgenerator->create_item_multichoice($feedback, $record = ['values' => "y\nn", 'dependitem' => $itemscreated[0]->id, 'dependvalue' => 'y']); $itemscreated[] = $feedbackgenerator->create_item_pagebreak($feedback); $itemscreated[] = $feedbackgenerator->create_item_multichoice($feedback, $record = ['values' => "y\nn", 'dependitem' => $itemscreated[2]->id, 'dependvalue' => 'y']); // Test hiding item since transitive dependency is not met. // Answering the first multichoice with 'y', should hide the second and therefor also the fourth. $user1 = $this->getDataGenerator()->create_and_enrol($course); $completion = new mod_feedback_completion($feedback, $cm, $course, false, null, $user1->id); // Initially, all pages should be visible. $this->assertEquals(4, $this->get_number_of_visible_pages($completion)); // Answer the first multichoice with 'y', which should exclude the second and the fourth. $answers = ['multichoice_' . $itemscreated[0]->id => [1]]; $completion->save_response_tmp((object) $answers); $this->assertEquals(2, $this->get_number_of_visible_pages($completion)); // Answer the third multichoice with 'n', which should exclude the last one. $answers = ['multichoice_' . $itemscreated[4]->id => [2]]; $completion->save_response_tmp((object) $answers); $this->assertEquals(2, $this->get_number_of_visible_pages($completion)); $completion->save_response(); // Test showing item since transitive dependency is met. // Answering the first multichoice with 'n' should hide the third multichoice. $user2 = $this->getDataGenerator()->create_and_enrol($course); $completion2 = new mod_feedback_completion($feedback, $cm, $course, false, null, $user2->id); // Initially, all pages should be visible. $this->assertEquals(4, $this->get_number_of_visible_pages($completion2)); // Answer the first multichoice with 'n' should hide the third multichoice. $answers = ['multichoice_' . $itemscreated[0]->id => [2]]; $completion2->save_response_tmp((object) $answers); $this->assertEquals(3, $this->get_number_of_visible_pages($completion2)); // Answering the second multichoice with 'n' should hide the fourth one. $answers = ['multichoice_' . $itemscreated[2]->id => [2]]; $completion2->save_response_tmp((object) $answers); $this->assertEquals(2, $this->get_number_of_visible_pages($completion2)); } } PKB\044 tests/custom_completion_test.phpnu[. /** * Contains unit tests for core_completion/activity_custom_completion. * * @package mod_feedback * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_feedback; use advanced_testcase; use cm_info; use coding_exception; use mod_feedback\completion\custom_completion; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); /** * Class for unit testing mod_feedback/activity_custom_completion. * * @package mod_feedback * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class custom_completion_test extends advanced_testcase { /** * Data provider for get_state(). * * @return array[] */ public static function get_state_provider(): array { return [ 'Undefined rule' => [ 'somenonexistentrule', COMPLETION_DISABLED, false, null, coding_exception::class ], 'Rule not available' => [ 'completionsubmit', COMPLETION_DISABLED, false, null, moodle_exception::class ], 'Rule available, user has not submitted' => [ 'completionsubmit', COMPLETION_ENABLED, false, COMPLETION_INCOMPLETE, null ], 'Rule available, user has submitted' => [ 'completionsubmit', COMPLETION_ENABLED, true, COMPLETION_COMPLETE, null ], ]; } /** * Test for get_state(). * * @dataProvider get_state_provider * @param string $rule The custom completion rule. * @param int $available Whether this rule is available. * @param bool $submitted * @param int|null $status Expected status. * @param string|null $exception Expected exception. */ public function test_get_state(string $rule, int $available, ?bool $submitted, ?int $status, ?string $exception): void { global $DB; if (!is_null($exception)) { $this->expectException($exception); } // Custom completion rule data for cm_info::customdata. $customdataval = [ 'customcompletionrules' => [ $rule => $available ] ]; // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of the magic getter method when fetching the cm_info object's customdata and instance values. $mockcminfo->expects($this->any()) ->method('__get') ->will($this->returnValueMap([ ['customdata', $customdataval], ['instance', 1], ])); // Mock the DB calls. $DB = $this->createMock(get_class($DB)); $DB->expects($this->atMost(1)) ->method('record_exists') ->willReturn($submitted); $customcompletion = new custom_completion($mockcminfo, 2); $this->assertEquals($status, $customcompletion->get_state($rule)); } /** * Test for get_defined_custom_rules(). */ public function test_get_defined_custom_rules(): void { $rules = custom_completion::get_defined_custom_rules(); $this->assertCount(1, $rules); $this->assertEquals('completionsubmit', reset($rules)); } /** * Test for get_defined_custom_rule_descriptions(). */ public function test_get_custom_rule_descriptions(): void { // Get defined custom rules. $rules = custom_completion::get_defined_custom_rules(); // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Instantiate a custom_completion object using the mocked cm_info. $customcompletion = new custom_completion($mockcminfo, 1); // Get custom rule descriptions. $ruledescriptions = $customcompletion->get_custom_rule_descriptions(); // Confirm that defined rules and rule descriptions are consistent with each other. $this->assertEquals(count($rules), count($ruledescriptions)); foreach ($rules as $rule) { $this->assertArrayHasKey($rule, $ruledescriptions); } } /** * Test for is_defined(). */ public function test_is_defined(): void { // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->getMock(); $customcompletion = new custom_completion($mockcminfo, 1); // Rule is defined. $this->assertTrue($customcompletion->is_defined('completionsubmit')); // Undefined rule. $this->assertFalse($customcompletion->is_defined('somerandomrule')); } /** * Data provider for test_get_available_custom_rules(). * * @return array[] */ public static function get_available_custom_rules_provider(): array { return [ 'Completion submit available' => [ COMPLETION_ENABLED, ['completionsubmit'] ], 'Completion submit not available' => [ COMPLETION_DISABLED, [] ], ]; } /** * Test for get_available_custom_rules(). * * @dataProvider get_available_custom_rules_provider * @param int $status * @param array $expected */ public function test_get_available_custom_rules(int $status, array $expected): void { $customdataval = [ 'customcompletionrules' => [ 'completionsubmit' => $status ] ]; // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of magic getter for the customdata attribute. $mockcminfo->expects($this->any()) ->method('__get') ->with('customdata') ->willReturn($customdataval); $customcompletion = new custom_completion($mockcminfo, 1); $this->assertEquals($expected, $customcompletion->get_available_custom_rules()); } } PKB\</tests/dates_test.phpnu[. /** * Contains unit tests for mod_feedback\dates. * * @package mod_feedback * @category test * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_feedback; use advanced_testcase; use cm_info; use core\activity_dates; /** * Class for unit testing mod_feedback\dates. * * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class dates_test extends advanced_testcase { /** * Data provider for get_dates_for_module(). * @return array[] */ public static function get_dates_for_module_provider(): array { $now = time(); $before = $now - DAYSECS; $earlier = $before - DAYSECS; $after = $now + DAYSECS; $later = $after + DAYSECS; return [ 'without any dates' => [ null, null, [] ], 'only with opening time' => [ $after, null, [ ['label' => 'Opens:', 'timestamp' => $after, 'dataid' => 'timeopen'], ] ], 'only with closing time' => [ null, $after, [ ['label' => 'Closes:', 'timestamp' => $after, 'dataid' => 'timeclose'], ] ], 'with both times' => [ $after, $later, [ ['label' => 'Opens:', 'timestamp' => $after, 'dataid' => 'timeopen'], ['label' => 'Closes:', 'timestamp' => $later, 'dataid' => 'timeclose'], ] ], 'between the dates' => [ $before, $after, [ ['label' => 'Opened:', 'timestamp' => $before, 'dataid' => 'timeopen'], ['label' => 'Closes:', 'timestamp' => $after, 'dataid' => 'timeclose'], ] ], 'dates are past' => [ $earlier, $before, [ ['label' => 'Opened:', 'timestamp' => $earlier, 'dataid' => 'timeopen'], ['label' => 'Closed:', 'timestamp' => $before, 'dataid' => 'timeclose'], ] ], ]; } /** * Test for get_dates_for_module(). * * @dataProvider get_dates_for_module_provider * @param int|null $timeopen The "allow answers from" time in the feedback activity. * @param int|null $timeclose The "allow answers to" time in the feedback activity. * @param array $expected The expected value of calling get_dates_for_module() */ public function test_get_dates_for_module(?int $timeopen, ?int $timeclose, array $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $data = ['course' => $course->id]; if ($timeopen) { $data['timeopen'] = $timeopen; } if ($timeclose) { $data['timeclose'] = $timeclose; } $feedback = $this->getDataGenerator()->create_module('feedback', $data); $this->setUser($user); $cm = get_coursemodule_from_instance('feedback', $feedback->id); // Make sure we're using a cm_info object. $cm = cm_info::create($cm); $dates = activity_dates::get_dates_for_module($cm, (int) $user->id); $this->assertEquals($expected, $dates); } } PKB\b77tests/event/events_test.phpnu[. /** * Tests for feedback events. * * @package mod_feedback * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace mod_feedback\event; /** * Class mod_feedback_events_testcase * * Class for tests related to feedback events. * * @package mod_feedback * @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 user who likes to interact with feedback activity. */ private $eventuser; /** @var stdClass A course used to hold feedback activities for testing. */ private $eventcourse; /** @var stdClass A feedback activity used for feedback event testing. */ private $eventfeedback; /** @var stdClass course module object . */ private $eventcm; /** @var stdClass A feedback item. */ private $eventfeedbackitem; /** @var stdClass A feedback activity response submitted by user. */ private $eventfeedbackcompleted; /** @var stdClass value associated with $eventfeedbackitem . */ private $eventfeedbackvalue; public function setUp(): void { global $DB; parent::setUp(); $this->setAdminUser(); $gen = $this->getDataGenerator(); $this->eventuser = $gen->create_user(); // Create a user. $course = $gen->create_course(); // Create a course. // Assign manager role, so user can see reports. role_assign(1, $this->eventuser->id, \context_course::instance($course->id)); // Add a feedback activity to the created course. $record = new \stdClass(); $record->course = $course->id; $feedback = $gen->create_module('feedback', $record); $this->eventfeedback = $DB->get_record('feedback', array('id' => $feedback->id), '*', MUST_EXIST); // Get exact copy. $this->eventcm = get_coursemodule_from_instance('feedback', $this->eventfeedback->id, false, MUST_EXIST); // Create a feedback item. $item = new \stdClass(); $item->feedback = $this->eventfeedback->id; $item->type = 'numeric'; $item->presentation = '0|0'; $itemid = $DB->insert_record('feedback_item', $item); $this->eventfeedbackitem = $DB->get_record('feedback_item', array('id' => $itemid), '*', MUST_EXIST); // Create a response from a user. $response = new \stdClass(); $response->feedback = $this->eventfeedback->id; $response->userid = $this->eventuser->id; $response->anonymous_response = FEEDBACK_ANONYMOUS_YES; $completedid = $DB->insert_record('feedback_completed', $response); $this->eventfeedbackcompleted = $DB->get_record('feedback_completed', array('id' => $completedid), '*', MUST_EXIST); $value = new \stdClass(); $value->course_id = $course->id; $value->item = $this->eventfeedbackitem->id; $value->completed = $this->eventfeedbackcompleted->id; $value->value = 25; // User response value. $valueid = $DB->insert_record('feedback_value', $value); $this->eventfeedbackvalue = $DB->get_record('feedback_value', array('id' => $valueid), '*', MUST_EXIST); // Do this in the end to get correct sortorder and cacherev values. $this->eventcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST); } /** * Tests for event response_deleted. */ public function test_response_deleted_event(): void { global $USER, $DB; $this->resetAfterTest(); // Create and delete a module. $sink = $this->redirectEvents(); feedback_delete_completed($this->eventfeedbackcompleted->id); $events = $sink->get_events(); $event = array_pop($events); // Delete feedback event. $sink->close(); // Validate event data. $this->assertInstanceOf('\mod_feedback\event\response_deleted', $event); $this->assertEquals($this->eventfeedbackcompleted->id, $event->objectid); $this->assertEquals($USER->id, $event->userid); $this->assertEquals($this->eventuser->id, $event->relateduserid); $this->assertEquals('feedback_completed', $event->objecttable); $this->assertEquals(null, $event->get_url()); $this->assertEquals($this->eventfeedbackcompleted, $event->get_record_snapshot('feedback_completed', $event->objectid)); $this->assertEquals($this->eventcourse, $event->get_record_snapshot('course', $event->courseid)); $this->assertEquals($this->eventfeedback, $event->get_record_snapshot('feedback', $event->other['instanceid'])); // Test can_view() . $this->setUser($this->eventuser); $this->assertFalse($event->can_view()); $this->assertDebuggingCalled(); $this->setAdminUser(); $this->assertTrue($event->can_view()); $this->assertDebuggingCalled(); // Create a response, with anonymous set to no and test can_view(). $response = new \stdClass(); $response->feedback = $this->eventcm->instance; $response->userid = $this->eventuser->id; $response->anonymous_response = FEEDBACK_ANONYMOUS_NO; $completedid = $DB->insert_record('feedback_completed', $response); $DB->get_record('feedback_completed', array('id' => $completedid), '*', MUST_EXIST); $value = new \stdClass(); $value->course_id = $this->eventcourse->id; $value->item = $this->eventfeedbackitem->id; $value->completed = $completedid; $value->value = 25; // User response value. $DB->insert_record('feedback_valuetmp', $value); // Save the feedback. $sink = $this->redirectEvents(); feedback_delete_completed($completedid); $events = $sink->get_events(); $event = array_pop($events); // Response submitted feedback event. $sink->close(); // Test can_view() . $this->setUser($this->eventuser); $this->assertTrue($event->can_view()); $this->assertDebuggingCalled(); $this->setAdminUser(); $this->assertTrue($event->can_view()); $this->assertDebuggingCalled(); $this->assertEventContextNotUsed($event); } /** * Tests for event validations related to feedback response deletion. */ public function test_response_deleted_event_exceptions(): void { $this->resetAfterTest(); $context = \context_module::instance($this->eventcm->id); // Test not setting other['anonymous']. try { \mod_feedback\event\response_submitted::create(array( 'context' => $context, 'objectid' => $this->eventfeedbackcompleted->id, 'relateduserid' => 2, )); $this->fail("Event validation should not allow \\mod_feedback\\event\\response_deleted to be triggered without other['anonymous']"); } catch (\coding_exception $e) { $this->assertStringContainsString("The 'anonymous' value must be set in other.", $e->getMessage()); } } /** * Tests for event response_submitted. */ public function test_response_submitted_event(): void { global $USER, $DB; $this->resetAfterTest(); $this->setUser($this->eventuser); // Create a temporary response, with anonymous set to yes. $response = new \stdClass(); $response->feedback = $this->eventcm->instance; $response->userid = $this->eventuser->id; $response->anonymous_response = FEEDBACK_ANONYMOUS_YES; $completedid = $DB->insert_record('feedback_completedtmp', $response); $completed = $DB->get_record('feedback_completedtmp', array('id' => $completedid), '*', MUST_EXIST); $value = new \stdClass(); $value->course_id = $this->eventcourse->id; $value->item = $this->eventfeedbackitem->id; $value->completed = $completedid; $value->value = 25; // User response value. $DB->insert_record('feedback_valuetmp', $value); // Save the feedback. $sink = $this->redirectEvents(); $id = feedback_save_tmp_values($completed); $events = $sink->get_events(); $event = array_pop($events); // Response submitted feedback event. $sink->close(); // Validate event data. Feedback is anonymous. $this->assertInstanceOf('\mod_feedback\event\response_submitted', $event); $this->assertEquals($id, $event->objectid); $this->assertEquals($USER->id, $event->userid); $this->assertEquals($USER->id, $event->relateduserid); $this->assertEquals('feedback_completed', $event->objecttable); $this->assertEquals(1, $event->anonymous); $this->assertEquals(FEEDBACK_ANONYMOUS_YES, $event->other['anonymous']); $this->setUser($this->eventuser); $this->assertFalse($event->can_view()); $this->assertDebuggingCalled(); $this->setAdminUser(); $this->assertTrue($event->can_view()); $this->assertDebuggingCalled(); // Create a temporary response, with anonymous set to no. $response = new \stdClass(); $response->feedback = $this->eventcm->instance; $response->userid = $this->eventuser->id; $response->anonymous_response = FEEDBACK_ANONYMOUS_NO; $completedid = $DB->insert_record('feedback_completedtmp', $response); $completed = $DB->get_record('feedback_completedtmp', array('id' => $completedid), '*', MUST_EXIST); $value = new \stdClass(); $value->course_id = $this->eventcourse->id; $value->item = $this->eventfeedbackitem->id; $value->completed = $completedid; $value->value = 25; // User response value. $DB->insert_record('feedback_valuetmp', $value); // Save the feedback. $sink = $this->redirectEvents(); feedback_save_tmp_values($completed); $events = $sink->get_events(); $event = array_pop($events); // Response submitted feedback event. $sink->close(); // Test can_view(). $this->assertTrue($event->can_view()); $this->assertDebuggingCalled(); $this->setAdminUser(); $this->assertTrue($event->can_view()); $this->assertDebuggingCalled(); $this->assertEventContextNotUsed($event); } /** * Tests for event validations related to feedback response submission. */ public function test_response_submitted_event_exceptions(): void { $this->resetAfterTest(); $context = \context_module::instance($this->eventcm->id); // Test not setting instanceid. try { \mod_feedback\event\response_submitted::create(array( 'context' => $context, 'objectid' => $this->eventfeedbackcompleted->id, 'relateduserid' => 2, 'anonymous' => 0, 'other' => array('cmid' => $this->eventcm->id, 'anonymous' => 2) )); $this->fail("Event validation should not allow \\mod_feedback\\event\\response_deleted to be triggered without other['instanceid']"); } catch (\coding_exception $e) { $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage()); } // Test not setting cmid. try { \mod_feedback\event\response_submitted::create(array( 'context' => $context, 'objectid' => $this->eventfeedbackcompleted->id, 'relateduserid' => 2, 'anonymous' => 0, 'other' => array('instanceid' => $this->eventfeedback->id, 'anonymous' => 2) )); $this->fail("Event validation should not allow \\mod_feedback\\event\\response_deleted to be triggered without other['cmid']"); } catch (\coding_exception $e) { $this->assertStringContainsString("The 'cmid' value must be set in other.", $e->getMessage()); } // Test not setting anonymous. try { \mod_feedback\event\response_submitted::create(array( 'context' => $context, 'objectid' => $this->eventfeedbackcompleted->id, 'relateduserid' => 2, 'other' => array('cmid' => $this->eventcm->id, 'instanceid' => $this->eventfeedback->id) )); $this->fail("Event validation should not allow \\mod_feedback\\event\\response_deleted to be triggered without other['anonymous']"); } catch (\coding_exception $e) { $this->assertStringContainsString("The 'anonymous' value must be set in other.", $e->getMessage()); } } /** * Test that event observer is executed on course deletion and the templates are removed. */ public function test_delete_course(): void { global $DB; $this->resetAfterTest(); feedback_save_as_template($this->eventfeedback, 'my template', 0); $courseid = $this->eventcourse->id; $this->assertNotEmpty($DB->get_records('feedback_template', array('course' => $courseid))); delete_course($this->eventcourse, false); $this->assertEmpty($DB->get_records('feedback_template', array('course' => $courseid))); } } PKB\.)))tests/access_test.phpnu[. namespace mod_feedback; use mod_feedback_completion; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/feedback/lib.php'); /** * Unit tests to check group membership for teacher access to responses. * @package mod_feedback * @copyright 2024 Leon Stringer * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class access_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Submit feedback response. * @param \stdClass $feedback Mod_feedback instance. * @param \stdClass $cm Course module. * @param int $itemid ID of a textfield on the form to complete. * @param int $userid ID of the user submitting the response. * @return mod_feedback_completion */ private function student_response(\stdClass $feedback, \stdClass $cm, int $itemid, int $userid): mod_feedback_completion { $completion = new mod_feedback_completion($feedback, $cm, $cm->course, false, null, $userid, $userid); $answers = ['textfield_' . $itemid => "test"]; $completion->save_response_tmp((object) $answers); $completion->save_response(); return $completion; } /** * Test access to feedback responses is allowed or denied correctly for * activity group mode and users group membership. * @param int $groupmode NOGROUPS, SEPARATEGROUPS, etc. * @param int $anonymous FEEDBACK_ANONYMOUS_NO or FEEDBACK_ANONYMOUS_YES. * @param array $studentgroups Zero or more names of groups to add the * student to, for example, ['group1']. * @param array $teacheraccess List of teachers' usernames and whether they * should be able to access the submitted feedback, for example, * ['teacher1' => true, 'teacher2' => false, ...]. * @dataProvider response_access_provider */ public function test_response_access(int $groupmode, int $anonymous, array $studentgroups, array $teacheraccesses): void { global $DB; $course = $this->getDataGenerator()->create_course([ 'groupmode' => $groupmode, 'groupmodeforce' => true, ]); $groups['group1'] = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $groups['group2'] = $this->getDataGenerator()->create_group(['courseid' => $course->id]); /* * Participant: Role: Groups: * teacher1 editingteacher group1 * teacher2 teacher group1 * teacher3 teacher (no group) * teacher4 editingteacher (no group) * student student Set by data provider */ $teachers['teacher1'] = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->getDataGenerator()->create_group_member(['groupid' => $groups['group1']->id, 'userid' => $teachers['teacher1']->id]); $teachers['teacher2'] = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $this->getDataGenerator()->create_group_member(['groupid' => $groups['group1']->id, 'userid' => $teachers['teacher2']->id]); $teachers['teacher3'] = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $teachers['teacher4'] = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); foreach ($studentgroups as $group) { $this->getDataGenerator()->create_group_member(['groupid' => $groups[$group]->id, 'userid' => $student->id]); } $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id, 'anonymous' => $anonymous]); $cm = get_coursemodule_from_instance('feedback', $feedback->id); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item = $feedbackgenerator->create_item_textfield($feedback); $completion = $this->student_response($feedback, $cm, $item->id, $student->id); $showcompleted = $completion->get_completed()->id; $userid = ''; // Value used on mod/feedback/show_entries.php. foreach ($teacheraccesses as $teacher => $access) { $this->setUser($teachers[$teacher]); // moodle_exception should be thrown if teacher doesn't have // access. try { new mod_feedback_completion($feedback, $cm, 0, true, $showcompleted, $userid); $this->assertTrue($access); } catch (\moodle_exception $ex) { $this->assertTrue(!$access); } } } /** * Group mode and group membership combinations. * @return array */ public static function response_access_provider(): array { return [ /* * Student is in group1 so response should visible to: * 1. Teacher1 (same groups and has accessallgroups) * 2. Teachers (same group) * 3. Teacher4 (has accessallgroups) */ 'separate_groups_student1' => [ 'groupmode' => SEPARATEGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_NO, 'studentgroups' => ['group1'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => false, 'teacher4' => true], ], /* * Student is in group2 so response should visible to: * 1. Teacher1 (same groups and has accessallgroups) * 2. Teacher4 (has accessallgroups) */ 'separate_groups_student2' => [ 'groupmode' => SEPARATEGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_NO, 'studentgroups' => ['group2'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => false, 'teacher3' => false, 'teacher4' => true], ], /* * Student is in no groups so response should visible to: * 1. Teacher1 (same groups and has accessallgroups) * 2. Teacher4 (has accessallgroups) */ 'separate_groups_student3' => [ 'groupmode' => SEPARATEGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_NO, 'studentgroups' => [], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => false, 'teacher3' => false, 'teacher4' => true], ], /* * Same three tests with FEEDBACK_ANONYMOUS_YES. */ 'separate_groups_anon_student1' => [ 'groupmode' => SEPARATEGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'studentgroups' => ['group1'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => false, 'teacher4' => true], ], 'separate_groups_anon_student2' => [ 'groupmode' => SEPARATEGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'studentgroups' => ['group2'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => false, 'teacher3' => false, 'teacher4' => true], ], 'separate_groups_anon_student3' => [ 'groupmode' => SEPARATEGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'studentgroups' => [], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => false, 'teacher3' => false, 'teacher4' => true], ], /* * Same three tests with NOGROUPS and FEEDBACK_ANONYMOUS_NO. */ 'no_groups_student1' => [ 'groupmode' => NOGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_NO, 'studentgroups' => ['group1'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => true, 'teacher4' => true], ], 'no_groups_student2' => [ 'groupmode' => NOGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_NO, 'studentgroups' => ['group2'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => true, 'teacher4' => true], ], 'no_groups_student3' => [ 'groupmode' => NOGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_NO, 'studentgroups' => [], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => true, 'teacher4' => true], ], /* * Same three tests with NOGROUPS and FEEDBACK_ANONYMOUS_YES. */ 'no_groups_anon_student1' => [ 'groupmode' => NOGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'studentgroups' => ['group1'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => true, 'teacher4' => true], ], 'no_groups_anon_student2' => [ 'groupmode' => NOGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'studentgroups' => ['group2'], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => true, 'teacher4' => true], ], 'no_groups_anon_student3' => [ 'groupmode' => NOGROUPS, 'anonymous' => FEEDBACK_ANONYMOUS_YES, 'studentgroups' => [], 'teacheraccesses' => ['teacher1' => true, 'teacher2' => true, 'teacher3' => true, 'teacher4' => true], ], ]; } } PKB\B~w+w+tests/generator_test.phpnu[. namespace mod_feedback; /** * Generator tests class. * * @package mod_feedback * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends \advanced_testcase { public function test_create_instance(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $this->assertFalse($DB->record_exists('feedback', array('course' => $course->id))); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course->id)); $this->assertEquals(1, $DB->count_records('feedback', array('course' => $course->id))); $this->assertTrue($DB->record_exists('feedback', array('course' => $course->id))); $this->assertTrue($DB->record_exists('feedback', array('id' => $feedback->id))); $params = array('course' => $course->id, 'name' => 'One more feedback'); $feedback = $this->getDataGenerator()->create_module('feedback', $params); $this->assertEquals(2, $DB->count_records('feedback', array('course' => $course->id))); $this->assertEquals('One more feedback', $DB->get_field_select('feedback', 'name', 'id = :id', array('id' => $feedback->id))); } public function test_create_item_info(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_info($feedback); $item2 = $feedbackgenerator->create_item_info($feedback, array('name' => 'Custom name')); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($item1->id, $records[$item1->id]->id); $this->assertEquals($item2->id, $records[$item2->id]->id); $this->assertEquals('Custom name', $records[$item2->id]->name); $this->assertEquals('info', $records[$item1->id]->typ); } public function test_create_item_label(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_label($feedback); $editor = array( 'text' => "Custom editor", 'format' => FORMAT_HTML, 'itemid' => 0 ); $item2 = $feedbackgenerator->create_item_label($feedback, array('presentation_editor' => $editor)); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($item1->id, $records[$item1->id]->id); $this->assertEquals($item2->id, $records[$item2->id]->id); $this->assertEquals($editor['text'], $records[$item2->id]->presentation); $this->assertEquals('label', $records[$item1->id]->typ); } public function test_create_item_multichoice(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_multichoice($feedback); $item2 = $feedbackgenerator->create_item_multichoice($feedback, array('values' => "1\n2\n3\n4\n5", 'horizontal' => 1)); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($item1->id, $records[$item1->id]->id); $this->assertEquals($item2->id, $records[$item2->id]->id); $this->assertEquals('r>>>>>a|b|c|d|e', $records[$item1->id]->presentation); $this->assertEquals('r>>>>>1|2|3|4|5<<<<<1', $records[$item2->id]->presentation); $this->assertEquals('multichoice', $records[$item1->id]->typ); } public function test_create_item_multichoicerated(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_multichoicerated($feedback); $item2 = $feedbackgenerator->create_item_multichoicerated($feedback, array( 'values' => "0/1\n1/2\n2/3\n3/4\n4/5", 'horizontal' => 1)); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($item1->id, $records[$item1->id]->id); $this->assertEquals($item2->id, $records[$item2->id]->id); $this->assertEquals('r>>>>>0####a|1####b|2####c|3####d|4####e', $records[$item1->id]->presentation); $this->assertEquals('r>>>>>0####1|1####2|2####3|3####4|4####5<<<<<1', $records[$item2->id]->presentation); $this->assertEquals('multichoicerated', $records[$item1->id]->typ); } public function test_create_item_numeric(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_numeric($feedback); $item2 = $feedbackgenerator->create_item_numeric($feedback, array('rangefrom' => '0', 'rangeto' => '10')); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($item1->id, $records[$item1->id]->id); $this->assertEquals($item2->id, $records[$item2->id]->id); $this->assertEquals('-|-', $records[$item1->id]->presentation); $this->assertEquals('0|10', $records[$item2->id]->presentation); $this->assertEquals('numeric', $records[$item1->id]->typ); } public function test_create_item_textarea(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_textarea($feedback); $item2 = $feedbackgenerator->create_item_textarea($feedback, array('itemwidth' => '20', 'itemheight' => '10')); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($item1->id, $records[$item1->id]->id); $this->assertEquals($item2->id, $records[$item2->id]->id); $this->assertEquals('40|20', $records[$item1->id]->presentation); $this->assertEquals('20|10', $records[$item2->id]->presentation); $this->assertEquals('textarea', $records[$item1->id]->typ); } public function test_create_item_textfield(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $item1 = $feedbackgenerator->create_item_textfield($feedback); $item2 = $feedbackgenerator->create_item_textfield($feedback, array('itemsize' => '20', 'itemmaxlength' => '10')); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); $this->assertEquals($item1->id, $records[$item1->id]->id); $this->assertEquals($item2->id, $records[$item2->id]->id); $this->assertEquals('20|30', $records[$item1->id]->presentation); $this->assertEquals('20|10', $records[$item2->id]->presentation); $this->assertEquals('textfield', $records[$item1->id]->typ); } public function test_create_item_pagebreak(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $feedback = $this->getDataGenerator()->create_module('feedback', array('course' => $course)); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); // Try to create a page break in an empty feedback (no items). $feedbackgenerator->create_item_pagebreak($feedback); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(0, $records); // Create at least one item before the pagebreak. $feedbackgenerator->create_item_textfield($feedback); // Now, create one pagebreak. $item1 = $feedbackgenerator->create_item_pagebreak($feedback); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); // This pagebreak won't be created (there is already one in the last position). $item2 = $feedbackgenerator->create_item_pagebreak($feedback); $this->assertFalse($item2); $records = $DB->get_records('feedback_item', array('feedback' => $feedback->id), 'id'); $this->assertCount(2, $records); } } PKB\,,tests/behat/anonymous.featurenu[@mod @mod_feedback Feature: Anonymous feedback In order to collect feedbacks As an admin I need to be able to allow anonymous feedbacks Background: Given the following "users" exist: | username | firstname | lastname | | user1 | Username | 1 | | user2 | Username | 2 | | teacher | Teacher | 3 | | manager | Manager | 4 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | student | | user2 | C1 | student | | teacher | C1 | editingteacher | And the following "system role assigns" exist: | user | course | role | | manager | Acceptance test site | manager | And the following "activities" exist: | activity | name | course | idnumber | anonymous | publish_stats | section | | feedback | Site feedback | Acceptance test site | feedback0 | 1 | 1 | 1 | | feedback | Course feedback | C1 | feedback1 | 1 | 1 | 0 | When I log in as "manager" And I am on site homepage And I follow "Site feedback" And I navigate to "Questions" in current page administration And I add a "Multiple choice" question to the feedback with: | Question | Do you like our site? | | Label | multichoice2 | | Multiple choice type | Multiple choice - single answer | | Hide the "Not selected" option | Yes | | Multiple choice values | Yes\nNo\nI don't know | And I log out Scenario: Guests can see anonymous feedback on front page but can not complete When I follow "Site feedback" Then I should not see "Answer the questions" And I should not see "Preview questions" Scenario: Complete anonymous feedback on the front page as an authenticated user And I log in as "user1" And I am on site homepage When I follow "Site feedback" And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like our site?" And I set the following fields to these values: | Yes | 1 | And I press "Submit your answers" And I should not see "Analysis" And I press "Continue" @javascript Scenario: Complete anonymous feedback and view analysis on the front page as an authenticated user Given the following "role capability" exists: | role | frontpage | | mod/feedback:viewanalysepage | allow | And I log in as "user1" And I am on site homepage When I follow "Site feedback" And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like our site?" And I set the following fields to these values: | Yes | 1 | And I press "Submit your answers" And I log out And I log in as "user2" And I am on site homepage And I follow "Site feedback" And I should not see "Preview questions" And I follow "Answer the questions" And I set the following fields to these values: | No | 1 | And I press "Submit your answers" And I follow "Analysis" And I should see "Submitted answers: 2" And I should see "Questions: 1" # And I should not see "multichoice2" # TODO MDL-29303 do not show labels to users who can not edit feedback And I show chart data for the "multichoice2" feedback And I should see "Do you like our site?" And I should see "1 (50.00 %)" in the "Yes" "table_row" And I should see "1 (50.00 %)" in the "No" "table_row" And I log out And I log in as "manager" And I am on site homepage And I follow "Site feedback" And I navigate to "Responses" in current page administration And I should not see "Username" And I should see "Anonymous entries (2)" And I follow "Response number: 1" And I should not see "Username" And I should see "Response number: 1 (Anonymous)" And I log out Scenario: Complete fully anonymous feedback on the front page as a guest Given the following config values are set as admin: | feedback_allowfullanonymous | 1 | When I follow "Site feedback" And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like our site?" And I set the following fields to these values: | Yes | 1 | And I press "Submit your answers" And I should not see "Analysis" And I press "Continue" @javascript Scenario: Complete fully anonymous feedback and view analyze on the front page as a guest Given the following config values are set as admin: | feedback_allowfullanonymous | 1 | And the following "role capability" exists: | role | guest | | mod/feedback:viewanalysepage | allow | When I follow "Site feedback" And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like our site?" And I set the following fields to these values: | Yes | 1 | And I press "Submit your answers" And I press "Continue" # Starting new feedback When I follow "Site feedback" And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like our site?" And I set the following fields to these values: | No | 1 | And I press "Submit your answers" And I follow "Analysis" And I should see "Submitted answers: 2" And I should see "Questions: 1" # And I should not see "multichoice2" # TODO MDL-29303 And I show chart data for the "multichoice2" feedback And I should see "Do you like our site?" And I should see "1 (50.00 %)" in the "Yes" "table_row" And I should see "1 (50.00 %)" in the "No" "table_row" And I log in as "manager" And I am on site homepage And I follow "Site feedback" And I navigate to "Responses" in current page administration And I should see "Anonymous entries (2)" And I follow "Response number: 1" And I should see "Response number: 1 (Anonymous)" And I log out @javascript Scenario: Anonymous feedback in a course # Teacher can not When I am on the "Course feedback" "feedback activity" page logged in as teacher And I navigate to "Questions" in current page administration And I add a "Multiple choice" question to the feedback with: | Question | Do you like this course? | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer | | Hide the "Not selected" option | Yes | | Multiple choice values | Yes\nNo\nI don't know | And I log out And I am on the "Course feedback" "feedback activity" page logged in as user1 And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like this course?" And I set the following fields to these values: | Yes | 1 | And I press "Submit your answers" And I log out And I am on the "Course feedback" "feedback activity" page logged in as user2 And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like this course?" And I set the following fields to these values: | No | 1 | And I press "Submit your answers" And I follow "Analysis" And I should see "Submitted answers: 2" And I should see "Questions: 1" # And I should not see "multichoice2" # TODO MDL-29303 And I show chart data for the "multichoice1" feedback And I should see "Do you like this course?" And I should see "1 (50.00 %)" in the "Yes" "table_row" And I should see "1 (50.00 %)" in the "No" "table_row" And I log out And I am on the "Course feedback" "feedback activity" page logged in as teacher And I follow "Preview" And I should see "Do you like this course?" And I press "Continue" And I should not see "Answer the questions" And I navigate to "Responses" in current page administration And I should not see "Username" And I should see "Anonymous entries (2)" And I follow "Response number: 1" And I should not see "Username" And I should see "Response number: 1 (Anonymous)" And I should not see "Prev" And I follow "Next" And I should see "Response number: 2 (Anonymous)" And I should see "Prev" And I should not see "Next" And I click on "Back" "link" in the "[role=main]" "css_element" # Delete anonymous response And I click on "Delete entry" "link" in the "Response number: 1" "table_row" And I press "Yes" And I should see "Anonymous entries (1)" And I should not see "Response number: 1" And I should see "Response number: 2" Scenario: Collecting new non-anonymous feedback from a previously anonymous feedback activity When I am on the "Course feedback" "feedback activity" page logged in as teacher And I navigate to "Settings" in current page administration And I set the following fields to these values: | Allow multiple submissions | Yes | And I press "Save and display" And I follow "Edit questions" And I add a "Short text answer" question to the feedback with: | Question | this is a short text answer | | Label | shorttext | | Maximum characters accepted | 200 | And I log out When I am on the "Course feedback" "feedback activity" page logged in as user1 And I follow "Answer the questions" And I set the following fields to these values: | this is a short text answer | anontext | And I press "Submit your answers" And I log out # Switch to non-anon responses. And I am on the "Course feedback" "feedback activity editing" page logged in as teacher And I set the following fields to these values: | Record user names | User's name will be logged and shown with answers | And I press "Save and display" And I log out # Now leave a non-anon feedback as user1 And I am on the "Course feedback" "feedback activity" page logged in as user1 And I follow "Answer the questions" And I set the following fields to these values: | this is a short text answer | usertext | And I press "Submit your answers" And I log out # Now check the responses are correct. And I am on the "Course feedback" "feedback activity" page logged in as teacher And I follow "Responses" And I should see "Anonymous entries (1)" And I should see "Non anonymous entries (1)" And I click on "," "link" in the "Username 1" "table_row" And I should see "(Username 1)" Then the field "this is a short text answer" matches value "usertext" And I navigate to "Responses" in current page administration And I follow "Response number: 1" And I should see "Response number: 1 (Anonymous)" And the field "this is a short text answer" matches value "anontext" PKB\ҷ))"tests/behat/behat_mod_feedback.phpnu[. /** * Steps definitions related to mod_feedback. * * @package mod_feedback * @category test * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode as TableNode, Behat\Mink\Exception\ExpectationException as ExpectationException; /** * Steps definitions related to mod_feedback. * * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_feedback extends behat_base { /** * Adds a question to the existing feedback with filling the form. * * The form for creating a question should be on one page. * * @When /^I add a "(?P(?:[^"]|\\")*)" question to the feedback with:$/ * @param string $questiontype * @param TableNode $questiondata with data for filling the add question form */ public function i_add_question_to_the_feedback_with($questiontype, TableNode $questiondata) { $questiontype = $this->escape($questiontype); $this->execute('behat_general::i_click_on', [get_string('add_item', 'mod_feedback'), 'link']); $this->execute('behat_general::i_click_on', [$questiontype, 'link']); // Wait again, for page to reloaded. $this->execute('behat_general::i_wait_to_be_redirected'); $rows = $questiondata->getRows(); $modifiedrows = array(); foreach ($rows as $row) { foreach ($row as $key => $value) { $row[$key] = preg_replace('|\\\\n|', "\n", $value); } $modifiedrows[] = $row; } $newdata = new TableNode($modifiedrows); $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $newdata); $saveitem = $this->escape(get_string('save')); $this->execute("behat_forms::press_button", $saveitem); } /** * Adds a question to the existing feedback with filling the form. * * The form for creating a question should be on one page. * * @When /^I add a page break to the feedback$/ */ public function i_add_a_page_break_to_the_feedback() { $questiontype = $this->escape(get_string('add_pagebreak', 'feedback')); $this->execute('behat_general::i_click_on', [get_string('add_item', 'mod_feedback'), 'link']); $this->execute('behat_general::i_click_on', [$questiontype, 'link']); // Wait again, for page to reloaded. $this->execute('behat_general::i_wait_to_be_redirected'); } /** * Quick way to generate answers to a one-page feedback. * * @When /^I log in as "(?P(?:[^"]|\\")*)" and complete feedback "(?P(?:[^"]|\\")*)" in course "(?P(?:[^"]|\\")*)" with:$/ * @param string $questiontype * @param TableNode $questiondata with data for filling the add question form */ public function i_log_in_as_and_complete_feedback_in_course($username, $feedbackname, $coursename, TableNode $answers) { $username = $this->escape($username); $coursename = $this->escape($coursename); $feedbackname = $this->escape($feedbackname); $completeform = $this->escape(get_string('complete_the_form', 'feedback')); // Log in as user. $this->execute('behat_auth::i_log_in_as', $username); // Navigate to feedback complete form. $this->execute('behat_navigation::i_am_on_page_instance', [$feedbackname, 'feedback activity']); $this->execute('behat_general::click_link', $completeform); // Fill form and submit. $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $answers); $this->execute("behat_forms::press_button", 'Submit your answers'); // Log out. $this->execute('behat_auth::i_log_out'); } /** * Exports feedback and makes sure the export file is the same as in the fixture * * @Then /^following "(?P(?:[^"]|\\")*)" should export feedback identical to "(?P(?:[^"]|\\")*)"$/ * @param string $link * @param string $filename */ public function following_should_export_feedback_identical_to($link, $filename) { global $CFG; $exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession()); // It will stop spinning once file is downloaded or time out. $behatgeneralcontext = behat_context_helper::get('behat_general'); $result = $this->spin( function($context, $args) use ($behatgeneralcontext) { $link = $args['link']; return $behatgeneralcontext->download_file_from_link($link); }, array('link' => $link), behat_base::get_extended_timeout(), $exception ); $this->compare_exports(file_get_contents($CFG->dirroot . '/' . $filename), $result); } /** * Clicks on Show chart data to display chart data if not visible. * * @Then /^I show chart data for the "(?P(?:[^"]|\\")*)" feedback$/ * @param string $feedbackname name of the feedback for which chart data needs to be shown. */ public function i_show_chart_data_for_the_feedback($feedbackname) { $feedbackxpath = "//th[contains(normalize-space(string(.)), \"" . $feedbackname . "\")]/ancestor::table//" . "div[contains(concat(' ', normalize-space(@class), ' '), ' chart-table ')]" . "//p[contains(concat(' ', normalize-space(@class), ' '), ' chart-table-expand ') and ". "//a[contains(normalize-space(string(.)), '".get_string('showchartdata')."')]]"; $charttabledataxpath = $feedbackxpath . "/following-sibling::div[contains(concat(' ', normalize-space(@class), ' '), ' chart-table-data ')][1]"; // If chart data is not visible then expand. $node = $this->get_selected_node("xpath_element", $charttabledataxpath); if ($node) { if ($node->getAttribute('aria-expanded') === 'false') { $this->execute('behat_general::i_click_on_in_the', array( get_string('showchartdata'), 'link', $feedbackxpath, 'xpath_element' )); } } } /** * Ensures two feedback export files are identical * * Maps the itemids and converts DEPENDITEM if necessary * * Throws ExpectationException if exports are different * * @param string $expected * @param string $actual * @throws ExpectationException */ protected function compare_exports($expected, $actual) { $dataexpected = xmlize($expected, 1, 'UTF-8'); $dataexpected = $dataexpected['FEEDBACK']['#']['ITEMS'][0]['#']['ITEM']; $dataactual = xmlize($actual, 1, 'UTF-8'); $dataactual = $dataactual['FEEDBACK']['#']['ITEMS'][0]['#']['ITEM']; if (count($dataexpected) != count($dataactual)) { throw new ExpectationException('Expected ' . count($dataexpected) . ' items in the export file, found ' . count($dataactual), $this->getSession()); } $itemmapping = array(); $itemactual = reset($dataactual); foreach ($dataexpected as $idx => $itemexpected) { // Map ITEMID and DEPENDITEM. $itemmapping[intval($itemactual['#']['ITEMID'][0]['#'])] = intval($itemexpected['#']['ITEMID'][0]['#']); $itemactual['#']['ITEMID'][0]['#'] = $itemexpected['#']['ITEMID'][0]['#']; $expecteddependitem = $actualdependitem = 0; if (isset($itemexpected['#']['DEPENDITEM'][0]['#'])) { $expecteddependitem = intval($itemexpected['#']['DEPENDITEM'][0]['#']); } if (isset($itemactual['#']['DEPENDITEM'][0]['#'])) { $actualdependitem = intval($itemactual['#']['DEPENDITEM'][0]['#']); } if ($expecteddependitem && !$actualdependitem) { throw new ExpectationException('Expected DEPENDITEM in ' . ($idx + 1) . 'th item', $this->getSession()); } if (!$expecteddependitem && $actualdependitem) { throw new ExpectationException('Unexpected DEPENDITEM in ' . ($idx + 1) . 'th item', $this->getSession()); } if ($expecteddependitem && $actualdependitem) { if (!isset($itemmapping[$actualdependitem]) || $itemmapping[$actualdependitem] != $expecteddependitem) { throw new ExpectationException('Unknown DEPENDITEM in ' . ($idx + 1) . 'th item', $this->getSession()); } $itemactual['#']['DEPENDITEM'][0]['#'] = $itemexpected['#']['DEPENDITEM'][0]['#']; } // Now, after mapping, $itemexpected should be exactly the same as $itemactual. if (json_encode($itemexpected) !== json_encode($itemactual)) { throw new ExpectationException('Actual ' . ($idx + 1) . 'th item does not match expected', $this->getSession()); } // Get the next itemactual. $itemactual = next($dataactual); } } /** * Return the list of partial named selectors * * @return behat_component_named_selector[] */ public static function get_partial_named_selectors(): array { return [ new behat_component_named_selector('Question', [ ".//*[starts-with(@id, 'fitem_feedback_item_') or starts-with(@id, 'fgroup_feedback_item_')]" . "[.//*[contains(text(), %locator%)]]", ]), ]; } } PKB\~,,!tests/behat/coursemapping.featurenu[@mod @mod_feedback Feature: Mapping courses in a feedback In order to collect the same feedback about multiple courses As a manager I need to be able to map site feedback to courses Background: Given the following "users" exist: | username | firstname | lastname | | user1 | Username | 1 | | user2 | Username | 2 | | user3 | Username | 3 | | teacher | Teacher | 4 | | manager | Manager | 5 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | | Course 2 | C2 | | Course 3 | C3 | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | student | | user1 | C2 | student | | user2 | C1 | student | | user2 | C2 | student | | user3 | C3 | student | | teacher | C1 | editingteacher | | teacher | C2 | editingteacher | | teacher | C3 | editingteacher | And the following "system role assigns" exist: | user | course | role | | manager | Acceptance test site | manager | And the following "activities" exist: | activity | name | course | idnumber | anonymous | publish_stats | section | | feedback | Course feedback | Acceptance test site | feedback0 | 1 | 1 | 1 | | feedback | Another feedback | C1 | feedback1 | 1 | 1 | 0 | And I enable "feedback" "block" plugin And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | feedback | Course | C1 | course-view-* | side-pre | | feedback | Course | C2 | course-view-* | side-pre | | feedback | Course | C3 | course-view-* | side-pre | When I log in as "manager" And I am on site homepage And I follow "Course feedback" And I navigate to "Questions" in current page administration And I add a "Information" question to the feedback with: | Question | this is an information question | | Label | info | | Information type | Course | And I add a "Multiple choice (rated)" question to the feedback with: | Question | this is a multiple choice rated | | Label | multichoicerated | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | 0/option a\n1/option b\n5/option c | And I add a "Multiple choice" question to the feedback with: | Question | this is a simple multiple choice | | Label | multichoicesimple | | Multiple choice type | Multiple choice - single answer allowed (drop-down menu) | | Multiple choice values | option d\noption e\noption f | And I log out Scenario: Course feedback can not be mapped And I log in as "manager" And I am on "Course 1" course homepage And I follow "Another feedback" And I should not see "Mapped courses" @javascript Scenario: Site feedback is not mapped to any course And I log in as "user1" And I am on site homepage And I follow "Course feedback" And I follow "Answer the questions" And I should see "Acceptance test site" in the ".feedback_form" "css_element" And I set the following fields to these values: | option a | 1 | | this is a simple multiple choice | option d | And I press "Submit your answers" And I press "Continue" And I am on "Course 1" course homepage And I click on "Course feedback" "link" in the "Feedback" "block" And I follow "Answer the questions" And I should not see "Acceptance test site" in the ".feedback_form" "css_element" And I should see "C1" in the ".feedback_form" "css_element" And I set the following fields to these values: | option b | 1 | | this is a simple multiple choice | option e | And I press "Submit your answers" And I press "Continue" And I click on "Course feedback" "link" in the "Feedback" "block" And I should not see "Answer the questions" And I log out And I log in as "user2" And I am on "Course 1" course homepage And I click on "Course feedback" "link" in the "Feedback" "block" And I follow "Answer the questions" And I should not see "Acceptance test site" in the ".feedback_form" "css_element" And I should see "C1" in the ".feedback_form" "css_element" And I set the following fields to these values: | option c | 1 | | this is a simple multiple choice | option e | And I press "Submit your answers" And I press "Continue" And I log out And I log in as "manager" And I am on site homepage And I follow "Course feedback" And I navigate to "Analysis" in current page administration And I should see "All courses" in the "#feedback_course_filter [data-fieldtype=autocomplete] .form-autocomplete-selection [role=option]" "css_element" And I show chart data for the "multichoicerated" feedback And I should see "1 (33.33 %)" in the "option a" "table_row" And I should see "1 (33.33 %)" in the "option b" "table_row" And I should see "1 (33.33 %)" in the "option c" "table_row" And I should see "Average: 2.00" And I follow "Sort by course" And I should see "2.50" in the "C1" "table_row" And I should see "1.00" in the "Acceptance test site" "table_row" And I click on "Back" "link" in the "region-main" "region" And I set the field "Filter by course" to "Course 1" And I press "Filter" And I should see "Course 1" in the "#feedback_course_filter [data-fieldtype=autocomplete] .form-autocomplete-selection [role=option]" "css_element" And I show chart data for the "multichoicerated" feedback And I should see "0" in the "option a" "table_row" And I should see "1 (50.00 %)" in the "option b" "table_row" And I should see "1 (50.00 %)" in the "option c" "table_row" And I log out @javascript Scenario: Site feedback is mapped to courses And I log in as "manager" And I am on site homepage And I follow "Course feedback" And I navigate to "Mapped courses" in current page administration And I set the field "Courses" to "Course 2, Course 3" And I press "Save changes" And I should see "Course mapping has been changed" And I log out And I log in as "user1" And I am on site homepage And I follow "Course feedback" And I should see "You can only access this feedback from a course" And I should not see "Answer the questions" And I am on "Course 1" course homepage And "Feedback" "block" should not exist And I should not see "Course feedback" And I am on "Course 2" course homepage And I click on "Course feedback" "link" in the "Feedback" "block" And I follow "Answer the questions" And I should not see "Acceptance test site" in the ".feedback_form" "css_element" And I should see "C2" in the ".feedback_form" "css_element" And I set the following fields to these values: | option b | 1 | | this is a simple multiple choice | option e | And I press "Submit your answers" And I press "Continue" And I click on "Course feedback" "link" in the "Feedback" "block" And I should not see "Answer the questions" And I log out And I log in as "user2" And I am on "Course 2" course homepage And I click on "Course feedback" "link" in the "Feedback" "block" And I follow "Answer the questions" And I should not see "Acceptance test site" in the ".feedback_form" "css_element" And I should see "C2" in the ".feedback_form" "css_element" And I set the following fields to these values: | option c | 1 | | this is a simple multiple choice | option e | And I press "Submit your answers" And I press "Continue" And I log out And I log in as "user3" And I am on "Course 3" course homepage And I click on "Course feedback" "link" in the "Feedback" "block" And I follow "Answer the questions" And I should not see "Acceptance test site" in the ".feedback_form" "css_element" And I should see "C3" in the ".feedback_form" "css_element" And I set the following fields to these values: | option c | 1 | | this is a simple multiple choice | option d | And I press "Submit your answers" And I press "Continue" And I log out And I log in as "manager" And I am on site homepage And I follow "Course feedback" And I navigate to "Analysis" in current page administration And I should see "All courses" in the "#feedback_course_filter [data-fieldtype=autocomplete] .form-autocomplete-selection [role=option]" "css_element" And I show chart data for the "multichoicerated" feedback And I should see "0" in the "option a" "table_row" And I should see "1 (33.33 %)" in the "option b" "table_row" And I should see "2 (66.67 %)" in the "option c" "table_row" And I should see "Average: 3.67" And I click on "Sort by course" "link" And I should see "3.00" in the "C3" "table_row" And I should see "2.50" in the "C2" "table_row" And I click on "Back" "link" in the "region-main" "region" And I set the field "Filter by course" to "Course 2" And I press "Filter" And I show chart data for the "multichoicerated" feedback And I should see "0" in the "option a" "table_row" And I should see "1 (50.00 %)" in the "option b" "table_row" And I should see "1 (50.00 %)" in the "option c" "table_row" And I show chart data for the "multichoicesimple" feedback And I should see "2 (100.00 %)" in the "option e" "table_row" And I set the field "Filter by course" to "Course 3" And I press "Filter" And I show chart data for the "multichoicerated" feedback And I should see "0" in the "option a" "table_row" And I should see "0" in the "option b" "table_row" And I should see "1 (100.00 %)" in the "option c" "table_row" And I show chart data for the "multichoicesimple" feedback And I should see "1 (100.00 %)" in the "option d" "table_row" And I follow "Show all" And I show chart data for the "multichoicesimple" feedback And I should see "1 (33.33 %)" in the "option d" "table_row" And I should see "2 (66.67 %)" in the "option e" "table_row" And I should see "0" in the "option f" "table_row" Scenario: Site feedback deletion hides feedback block completely When I log in as "manager" And I am on site homepage And I turn editing mode on And I add the "Feedback" block And I add the "Main menu" block And I click on "Delete" "link" in the "Course feedback" activity And I press "Delete" And I turn editing mode off And I am on site homepage Then "Feedback" "block" should not exist And I am on "Course 1" course homepage And "Feedback" "block" should not exist PKB\OD!tests/behat/non_anonymous.featurenu[@mod @mod_feedback Feature: Non anonymous feedback In order to collect feedbacks As an teacher I need to be able to create and collect feedbacks Background: Given the following "users" exist: | username | firstname | lastname | | user1 | Username | 1 | | user2 | Username | 2 | | teacher | Teacher | 3 | | manager | Manager | 4 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | student | | user2 | C1 | student | | teacher | C1 | editingteacher | And the following "system role assigns" exist: | user | course | role | | manager | Acceptance test site | manager | And the following "activities" exist: | activity | name | course | idnumber | anonymous | publish_stats | section | | feedback | Site feedback | Acceptance test site | feedback0 | 2 | 1 | 1 | | feedback | Course feedback | C1 | feedback1 | 2 | 1 | 0 | When I am on the "Site feedback" "feedback activity" page logged in as manager And I navigate to "Questions" in current page administration And I add a "Multiple choice" question to the feedback with: | Question | Do you like our site? | | Label | multichoice2 | | Multiple choice type | Multiple choice - single answer | | Hide the "Not selected" option | Yes | | Multiple choice values | Yes of course\nNot at all\nI don't know | And I log out Scenario: Guests can see non anonymous feedback on front page but can not complete When I am on the "Site feedback" "feedback activity" page Then I should not see "Answer the questions" And I should not see "Preview questions" Scenario: Complete non anonymous feedback on the front page as an authenticated user When I am on the "Site feedback" "feedback activity" page logged in as user1 And I should not see "Preview questions" And I follow "Answer the questions" And I should see "Do you like our site?" And I set the following fields to these values: | Yes of course | 1 | And I press "Submit your answers" And I should not see "Analysis" And I press "Continue" @javascript Scenario: Complete non anonymous feedback and view analysis on the front page as an authenticated user Given the following "role capability" exists: | role | frontpage | | mod/feedback:viewanalysepage | allow | When I am on the "Site feedback" "feedback activity" page logged in as user1 And I follow "Answer the questions" And I should see "Do you like our site?" And I set the following fields to these values: | Yes of course | 1 | And I press "Submit your answers" And I log out And I am on the "Site feedback" "feedback activity" page logged in as user2 And I follow "Answer the questions" And I set the following fields to these values: | Not at all | 1 | And I press "Submit your answers" And I follow "Analysis" And I should see "Submitted answers: 2" And I should see "Questions: 1" # And I should not see "multichoice2" # TODO MDL-29303 do not show labels to users who can not edit feedback And I show chart data for the "multichoice2" feedback And I should see "Do you like our site?" And I should see "1 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (50.00 %)" in the "Not at all" "table_row" And I log out And I am on the "Site feedback" "feedback activity" page logged in as manager And I navigate to "Responses" in current page administration And I should see "Username" And I should see "Non anonymous entries (2)" And I should not see "Anonymous entries" And I click on "," "link" in the "Username 1" "table_row" And I should see "(Username 1)" And I should see "Yes of course" And I log out @javascript Scenario: Non anonymous feedback in a course When I am on the "Course feedback" "feedback activity" page logged in as teacher And I navigate to "Questions" in current page administration And I add a "Multiple choice" question to the feedback with: | Question | Do you like this course? | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer | | Hide the "Not selected" option | Yes | | Multiple choice values | Yes of course\nNot at all\nI don't know | And I log out When I am on the "Course feedback" "feedback activity" page logged in as user1 And I follow "Answer the questions" And I should see "Do you like this course?" And I set the following fields to these values: | Yes of course | 1 | And I press "Submit your answers" And I log out When I am on the "Course feedback" "feedback activity" page logged in as user2 And I follow "Answer the questions" And I should see "Do you like this course?" And I set the following fields to these values: | Not at all | 1 | And I press "Submit your answers" And I follow "Analysis" And I should see "Submitted answers: 2" And I should see "Questions: 1" # And I should not see "multichoice2" # TODO MDL-29303 And I show chart data for the "multichoice1" feedback And I should see "Do you like this course?" And I should see "1 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (50.00 %)" in the "Not at all" "table_row" And I log out When I am on the "Course feedback" "feedback activity" page logged in as teacher And I follow "Preview" And I should see "Do you like this course?" And I press "Continue" And I should not see "Answer the questions" And I navigate to "Responses" in current page administration And I should see "Non anonymous entries (2)" And I should not see "Anonymous" And I click on "," "link" in the "Username 1" "table_row" And I should see "(Username 1)" And I should see "Yes of course" And I should not see "Prev" And I follow "Next" And I should see "(Username 2)" And I should not see "Next" And I should see "Prev" And I click on "Back" "link" in the "region-main" "region" # Sort the feedback responses. And I click on "Do you like this course?" "link" in the "generaltable" "table" And "Username 1" "table_row" should appear before "Username 2" "table_row" # Now sort descending. And I click on "Do you like this course?" "link" in the "generaltable" "table" And "Username 2" "table_row" should appear before "Username 1" "table_row" # Delete non anonymous response And I click on "Delete entry" "link" in the "Username 1" "table_row" And I press "Yes" And I should see "Non anonymous entries (1)" And I should not see "Username 1" And I should see "Username 2" PKB\؟!tests/behat/export_import.featurenu[@mod @mod_feedback Feature: Exporting and importing feedbacks In order to quickly copy feedbacks across courses and sites As a teacher I need to be able to export and import feedbacks Background: Given the following "users" exist: | username | firstname | lastname | | teacher | Teacher | 1 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | | Course 2 | C2 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | | teacher | C1 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | | feedback | Learning experience | C1 | feedback0 | And I change window size to "large" Scenario: Export sample feedback and compare with the fixture When I am on the "Learning experience" "feedback activity" page logged in as teacher And I navigate to "Questions" in current page administration And "Export questions" "link" should not exist in the ".tertiary-navigation" "css_element" And I add a "Information" question to the feedback with: | Question | this is an information question | | Label | info | | Information type | Course | And I add a "Text and media area" question to the feedback with: | Contents | label text | And I add a "Longer text answer" question to the feedback with: | Question | this is a longer text answer | | Label | longertext | | Required | 1 | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 1 | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | option a\noption b\noption c | And I add a page break to the feedback And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 2 | | Label | multichoice2 | | Multiple choice type | Multiple choice - multiple answers | | Hide the "Not selected" option | Yes | | Multiple choice values | option d\noption e\noption f | | Dependence item | multichoice1 | | Dependence value | option a | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 3 | | Label | multichoice3 | | Multiple choice type | Multiple choice - single answer allowed (drop-down menu) | | Multiple choice values | option g\noption h\noption i | And I add a "Multiple choice (rated)" question to the feedback with: | Question | this is a multiple choice rated | | Label | multichoice4 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | 0/option k\n1/option l\n5/option m | And I add a "Numeric answer" question to the feedback with: | Question | this is a numeric answer | | Label | numeric | | Range to | 100 | And I add a "Short text answer" question to the feedback with: | Question | this is a short text answer | | Label | shorttext | | Maximum characters accepted | 200 | Then following "Export questions" should export feedback identical to "mod/feedback/tests/fixtures/testexport.xml" @javascript @_file_upload Scenario: Import feedback deleting old items When I am on the "Learning experience" "feedback activity" page logged in as teacher And I navigate to "Questions" in current page administration And I add a "Numeric answer" question to the feedback with: | Question | Existing question | | Label | numeric | | Range to | 100 | And I press "Actions" And I choose "Import" in the open action menu And I upload "mod/feedback/tests/fixtures/testexport.xml" file to "File" filemanager And I press "Import" Then I should not see "Existing question" And I should see "this is an information question" And I should see "label text" And I should see "this is a longer text answer" And I should see "this is a multiple choice 1" And I should see "this is a multiple choice 2" And I should see "this is a multiple choice 3" And I should see "this is a multiple choice rated" And I should see "this is a numeric answer" And I should see "this is a short text answer" @javascript @_file_upload Scenario: Import feedback appending new items When I am on the "Learning experience" "feedback activity" page logged in as teacher And I navigate to "Questions" in current page administration And I add a "Numeric answer" question to the feedback with: | Question | Existing question | | Label | numeric | | Range to | 100 | And I press "Actions" And I choose "Import" in the open action menu And I set the field "Keep existing questions and add new questions at the end" to "1" And I upload "mod/feedback/tests/fixtures/testexport.xml" file to "File" filemanager And I press "Import" Then I should see "Existing question" And "Existing question" "text" should appear before "this is an information question" "text" And I should see "this is an information question" And I should see "label text" And I should see "this is a longer text answer" And I should see "this is a multiple choice 1" And I should see "this is a multiple choice 2" And I should see "this is a multiple choice 3" And I should see "this is a multiple choice rated" And I should see "this is a numeric answer" And I should see "this is a short text answer" PKB\GOtests/behat/questions.featurenu[@mod @mod_feedback Feature: Managing feedback questions In order to manage feedback questions As a teacher I need to be able to create, edit and delete feedback questions Background: Given the following "users" exist: | username | firstname | lastname | | teacher | Teacher | 1 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | | feedback | Learning experience course 1 | C1 | feedback1 | And the following "mod_feedback > question" exists: | activity | feedback1 | | name | Is it me you're looking for? | | label | q1 | Scenario: Teacher can create a new feedback question Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" When I add a "Short text answer" question to the feedback with: | Question | I can see it in your eyes | | Label | q2 | Then I should see "(q2) I can see it in your eyes" @javascript Scenario: Teacher can edit feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Question" to "Can you see it in my eyes?" And I press "Save changes to question" Then I should see "(q1) Can you see it in my eyes?" And I should not see "(q1) Is it me you're looking for?" @javascript Scenario: Teacher can edit and save as new feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Question" to "You can se it in my eyes?" And I press "Save as new question" Then I should see "(q1) Is it me you're looking for?" And I should see "(q1) You can se it in my eyes?" @javascript Scenario: Teacher can delete feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Delete question" in the open action menu And I click on "Yes" "button" in the "Confirmation" "dialogue" Then I should not see "(q1) Is it me you're looking for?" @javascript Scenario: Teacher can mark as required feedback questions Given I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" When I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Set as required" in the open action menu And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu Then the field "Required" matches value "1" And I press "Cancel" And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Set as not required" in the open action menu And I click on "Edit" "link" in the "Is it me you're looking for?" "mod_feedback > Question" And I choose "Edit question" in the open action menu And the field "Required" matches value "0" @javascript Scenario: Teacher can move questions Given the following "mod_feedback > questions" exist: | activity | label | name | | feedback1 | q2 | I can see it in your eyes | | feedback1 | q3 | I can see it in your smile | And I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I click on "Edit questions" "link" in the "region-main" "region" When I click on "Move this question" "button" in the "Is it me you're looking for?" "mod_feedback > Question" Then I should see "After \"(q2) I can see it in your eyes\"" in the "Move this question" "dialogue" And I should not see "To the top of the list" in the "Move this question" "dialogue" And I click on "After \"(q3) I can see it in your smile\"" "link" in the "Move this question" "dialogue" And I click on "Move this question" "button" in the "Is it me you're looking for?" "mod_feedback > Question" And I click on "To the top of the list" "link" in the "Move this question" "dialogue" Scenario: Admin cannot answer questions if not enrolled as student When I am on the "Learning experience course 1" "feedback activity" page logged in as admin Then I should not see "Answer the questions" But the following "course enrolments" exist: | user | course | role | | admin | C1 | student | And I am on the "Learning experience course 1" "feedback activity" page logged in as admin And I should see "Answer the questions" PKB\qN& #tests/behat/multipleattempt.featurenu[@mod @mod_feedback Feature: Non anonymous feedback with multiple submissions In order to modify feedback response As a student I need to be able to see previous response when I re-submit feedback Background: Given the following "users" exist: | username | firstname | lastname | | user1 | Username | 1 | | user2 | Username | 2 | | teacher | Teacher | 3 | | manager | Manager | 4 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | student | | user2 | C1 | student | | teacher | C1 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | anonymous | publish_stats | multiple_submit | section | | feedback | Course feedback | C1 | feedback1 | 2 | 1 | 1 | 0 | Scenario: Completing a feedback second time When I log in as "teacher" And I am on "Course 1" course homepage And I follow "Course feedback" And I navigate to "Questions" in current page administration And I add a "Short text answer" question to the feedback with: | Question | first | | Label | shorttext1 | | Maximum characters accepted | 200 | And I add a page break to the feedback And I add a "Short text answer" question to the feedback with: | Question | second | | Label | shorttext2 | | Maximum characters accepted | 200 | And I log out And I log in as "user1" And I am on "Course 1" course homepage And I follow "Course feedback" And I follow "Answer the questions" And I set the following fields to these values: | first | 111 | And I press "Next" And I set the following fields to these values: | second | 222 | And I press "Submit your answers" And I log out And I log in as "user1" And I am on "Course 1" course homepage And I follow "Course feedback" And I follow "Answer the questions" Then the field "first" matches value "111" And I press "Next" And the field "second" matches value "222" And I set the following fields to these values: | second | 333 | And I press "Submit your answers" PKB\q=K 'tests/behat/show_nonrespondents.featurenu[@mod @mod_feedback Feature: Show users who have not responded to the feedback survey In order to harass students about completing the feedback As a teacher I need to see which students haven't responded Background: Given the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | 1 | | student1 | Student | 1 | | student2 | Student | 2 | | student3 | Student | 3 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "groups" exist: | course | name | idnumber | | C1 | G1 | GI1 | And the following "group members" exist: | user | group | | student1 | GI1 | | student2 | GI1 | And the following "groupings" exist: | name | course | idnumber | | GX1 | C1 | GXI1 | And the following "grouping groups" exist: | grouping | group | | GXI1 | GI1 | Scenario: See users who have not responded # Set up a feedback. When I log in as "teacher1" And I add a feedback activity to course "Course 1" section "1" and I fill the form with: | Name | Frogs | | Description | x | | Record user names | User's name will be logged and shown with answers | | Access restrictions | Grouping: GX1 | And I am on the Frogs "feedback activity" page And I navigate to "Questions" in current page administration And I add a "Short text answer" question to the feedback with: | Question | Y/N? | And I log out # Go in as student 1 and do the feedback. And I am on the Frogs "feedback activity" page logged in as student1 And I follow "Answer the questions" And I set the field "Y/N?" to "Y" And I press "Submit your answers" And I log out # Go in as teacher and check the users who haven't completed it. And I am on the Frogs "feedback activity" page logged in as teacher1 And I navigate to "Responses" in current page administration And I select "Show non-respondents" from the "jump" singleselect # Should only show student 2; not student 1 (they did it) or 3 (not in grouping). Then I should see "Student 2" And I should not see "Student 1" And I should not see "Student 3" PKB\ؕp5 5 "tests/behat/show_responses.featurenu[@mod @mod_feedback Feature: As a teacher, I can see users who have responded or not responded to a feedback activity. As a non editing teacher not in a group I cannot see the responses. Background: Given the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | 1 | | teacher2 | Teacher | 2 | | teacher3 | Teacher | 3 | | student1 | Student | 1 | | student2 | Student | 2 | | student3 | Student | 3 | And the following "courses" exist: | fullname | shortname | groupmode | | Course 1 | C1 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | | teacher3 | C1 | teacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "groups" exist: | course | name | idnumber | | C1 | G1 | GI1 | And the following "group members" exist: | user | group | | student1 | GI1 | | student2 | GI1 | | teacher2 | GI1 | When I log in as "teacher1" And I add a feedback activity to course "Course 1" section "1" and I fill the form with: | Name | Frogs | | Description | x | | Record user names | User's name will be logged and shown with answers | And I am on the Frogs "feedback activity" page And I navigate to "Questions" in current page administration And I add a "Short text answer" question to the feedback with: | Question | Y/N? | And I log out # Go in as student 1 and do the feedback. And I am on the Frogs "feedback activity" page logged in as student1 And I follow "Answer the questions" And I set the field "Y/N?" to "Y" And I press "Submit your answers" And I log out Scenario Outline: If a teacher or non editing teacher is in a group, they can see the responses in separate group mode. # Go in as teacher and check the users who haven't completed it. Given I am on the Frogs "feedback activity" page logged in as Then "Responses" "link" in current page administration Examples: | user | existsornot | | teacher1 | should exist | | teacher2 | should exist | | teacher3 | should not exist | Scenario Outline: Teachers and non editing teachers in a group can see the responses # Go in as teacher and check the users who haven't completed it. Given I am on the Frogs "feedback activity" page logged in as And I navigate to "Responses" in current page administration And I select "Show non-respondents" from the "jump" singleselect # Should only show student 2; not student 1 (they did it) or 3 (not in grouping). Then I And I Examples: | user | studentshouldsee | studentshouldnotsee | | teacher1 | should see "Student 2" | should see "Student 3" | | teacher2 | should see "Student 2" | should not see "Student 3" | PKB\ָ{U{Utests/behat/multichoice.featurenu[@mod @mod_feedback Feature: Testing multichoice questions in feedback In order to create feedbacks As a teacher I need to be able to create different types of multichoice questions Background: Given the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | 1 | | student1 | Student | 1 | | student2 | Student | 2 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | feedback | Learning experience | C1 | feedback0 | And I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Questions" in current page administration @javascript Scenario: Non-rated single-answer multiple choice questions in feedback When I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 1 | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | option a\noption b\noption c | And I add a "Text and media area" question to the feedback with: | Contents | this is the first page of the feedback | And I add a page break to the feedback And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 2 | | Label | multichoice2 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | option d\noption e\noption f | | Required | 1 | And I add a "Text and media area" question to the feedback with: | Contents | this is the second page of the feedback | And I add a page break to the feedback And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 3 | | Label | multichoice3 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | option g\noption h\noption i | | Hide the "Not selected" option | Yes | | Dependence item | multichoice2 | | Dependence value | option d | And I add a "Text and media area" question to the feedback with: | Contents | this is the third page of the feedback | And I log out And I am on the "Learning experience" "feedback activity" page logged in as student1 And I follow "Answer the questions" # Examine the first page, select nothing, go to the next page Then the following fields match these values: | Not selected | 1 | | option a | 0 | | option b | 0 | | option c | 0 | And "Previous page" "button" should not exist And "Submit your answers" "button" should not exist And I should see "this is the first page of the feedback" And I should not see "this is the second page of the feedback" And I should not see "this is the third page of the feedback" And I press "Next page" # Examine the second page, select nothing, try to go to the next page And I should see "Not selected" And the following fields match these values: | Not selected | 1 | | option d | 0 | | option e | 0 | | option f | 0 | And "Previous page" "button" should exist And "Submit your answers" "button" should not exist And I should not see "this is the first page of the feedback" And I should see "this is the second page of the feedback" And I should not see "this is the third page of the feedback" And I press "Next page" # We are still on the second page because the field is required And I should see "Required" in the "form" "css_element" And I should see "this is the second page of the feedback" And I set the field "option e" to "1" And the following fields match these values: | Not selected | 0 | | option d | 0 | | option e | 1 | | option f | 0 | And I press "Next page" # Now we are on the third page, element is not shown because of wrong dependency. And I should see "this is the third page of the feedback" And I should not see "this is a multiple choice 3" # Go back, check that values are preserved and change the option to enable dependency. And I press "Previous page" And the following fields match these values: | Not selected | 0 | | option d | 0 | | option e | 1 | | option f | 0 | And I set the field "option d" to "1" And I press "Next page" # Now third page contains all items. And I should see "this is a multiple choice 3" And I should see "this is the third page of the feedback" And I should not see "Not selected" And the following fields match these values: | option g | 0 | | option h | 0 | | option i | 0 | And "Previous page" "button" should exist And "Next page" "button" should not exist And "Submit your answers" "button" should exist And I set the field "option i" to "1" And I press "Submit your answers" And I log out # Student 2 tries to trick - he answers the third question and then # goes back and changes dependency question. Analysis should not show this answer! And I am on the "Learning experience" "feedback activity" page logged in as student2 And I follow "Answer the questions" And I set the field "option a" to "1" And I press "Next page" And I set the field "option d" to "1" And I press "Next page" And I set the field "option g" to "1" And I press "Previous page" And I set the field "option f" to "1" And I press "Next page" And I press "Submit your answers" And I log out # Login as teacher and check analysis And I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Analysis" in current page administration And I should see "Submitted answers: 2" And I should see "Questions: 3" And I show chart data for the "multichoice1" feedback And I should see "1 (50.00 %)" in the "option a" "table_row" And I should not see "%" in the "option b" "table_row" And I should not see "%" in the "option c" "table_row" And I show chart data for the "multichoice2" feedback And I should see "1 (50.00 %)" in the "option d" "table_row" And I should not see "%" in the "option e" "table_row" And I should see "1 (50.00 %)" in the "option f" "table_row" And I show chart data for the "multichoice3" feedback And I should not see "%" in the "option g" "table_row" And I should not see "%" in the "option h" "table_row" And I should see "1 (100.00 %)" in the "option i" "table_row" # Change the settings so we don't analyse empty submits And I am on the "Learning experience" "feedback activity" page And I navigate to "Questions" in current page administration And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Omit empty submits in analysis" to "Yes" And I press "Save changes to question" And I am on the "Learning experience" "feedback activity" page And I navigate to "Analysis" in current page administration And I show chart data for the "multichoice1" feedback And I should see "1 (100.00 %)" in the "option a" "table_row" And I should not see "%" in the "option b" "table_row" And I should not see "%" in the "option c" "table_row" @javascript Scenario: Non-rated multiple-answers multiple choice questions in feedback # Create a feedback with three pages, required and dependent questions. When I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 1 | | Label | multichoice1 | | Multiple choice type | Multiple choice - multiple answers | | Multiple choice values | option a\noption b\noption c | And I add a "Text and media area" question to the feedback with: | Contents | this is the first page of the feedback | And I add a page break to the feedback And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 2 | | Label | multichoice2 | | Multiple choice type | Multiple choice - multiple answers | | Multiple choice values | option d\noption e\noption f | | Required | 1 | And I add a "Text and media area" question to the feedback with: | Contents | this is the second page of the feedback | And I add a page break to the feedback And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 3 | | Label | multichoice3 | | Multiple choice type | Multiple choice - multiple answers | | Multiple choice values | option g\noption h\noption i | | Dependence item | multichoice2 | | Dependence value | option d | And I add a "Text and media area" question to the feedback with: | Contents | this is the third page of the feedback | And I log out # Login as the first student. And I am on the "Learning experience" "feedback activity" page logged in as student1 And I follow "Answer the questions" # Examine the first page, select nothing, go to the next page And I should not see "Not selected" And the following fields match these values: | option a | 0 | | option b | 0 | | option c | 0 | And "Previous page" "button" should not exist And "Submit your answers" "button" should not exist And I should see "this is the first page of the feedback" And I should not see "this is the second page of the feedback" And I should not see "this is the third page of the feedback" And I press "Next page" # Examine the second page, select nothing, try to go to the next page Then I should not see "Not selected" And the following fields match these values: | option d | 0 | | option e | 0 | | option f | 0 | And "Previous page" "button" should exist And "Submit your answers" "button" should not exist And I should not see "this is the first page of the feedback" And I should see "this is the second page of the feedback" And I should not see "this is the third page of the feedback" And I press "Next page" # We are still on the second page because the field is required And I should see "Required" in the "form" "css_element" And I should see "this is the second page of the feedback" And I set the field "option e" to "1" And I set the field "option f" to "1" And the following fields match these values: | option d | 0 | | option e | 1 | | option f | 1 | And I press "Next page" # Now we are on the third page, element is not shown because of wrong dependency. And I should see "this is the third page of the feedback" And I should not see "this is a multiple choice 3" # Go back, check that values are preserved and change the option to enable dependency. And I press "Previous page" And the following fields match these values: | option d | 0 | | option e | 1 | | option f | 1 | And I set the field "option d" to "1" And I set the field "option e" to "0" And I press "Next page" # Now third page contains all items. And I should see "this is a multiple choice 3" And I should see "this is the third page of the feedback" And the following fields match these values: | option g | 0 | | option h | 0 | | option i | 0 | And "Previous page" "button" should exist And "Next page" "button" should not exist And "Submit your answers" "button" should exist And I set the field "option i" to "1" And I press "Submit your answers" And I log out # Student 2 tries to trick - he answers the third question and then # goes back and changes dependency question. Analysis should not show this answer! And I am on the "Learning experience" "feedback activity" page logged in as student2 And I follow "Answer the questions" And I set the field "option a" to "1" And I set the field "option b" to "1" And I press "Next page" And I set the field "option d" to "1" And I press "Next page" And I set the field "option g" to "1" And I press "Previous page" And I set the field "option d" to "0" And I set the field "option f" to "1" And I press "Next page" And I press "Submit your answers" And I log out # Login as teacher and check analysis And I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Analysis" in current page administration And I should see "Submitted answers: 2" And I should see "Questions: 3" And I show chart data for the "multichoice1" feedback And I should see "1 (50.00 %)" in the "option a" "table_row" And I should see "1 (50.00 %)" in the "option b" "table_row" And I should not see "%" in the "option c" "table_row" And I show chart data for the "multichoice2" feedback And I should see "1 (50.00 %)" in the "option d" "table_row" And I should not see "%" in the "option e" "table_row" And I should see "2 (100.00 %)" in the "option f" "table_row" And I show chart data for the "multichoice3" feedback And I should not see "%" in the "option g" "table_row" And I should not see "%" in the "option h" "table_row" And I should see "1 (100.00 %)" in the "option i" "table_row" # Change the settings so we don't analyse empty submits And I am on the "Learning experience" "feedback activity" page And I navigate to "Questions" in current page administration And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Omit empty submits in analysis" to "Yes" And I press "Save changes to question" And I am on the "Learning experience" "feedback activity" page And I navigate to "Analysis" in current page administration And I show chart data for the "multichoice1" feedback And I should see "1 (100.00 %)" in the "option a" "table_row" And I should see "1 (100.00 %)" in the "option b" "table_row" And I should not see "%" in the "option c" "table_row" @javascript Scenario: Non-rated single-answer dropdown multiple choice questions in feedback When I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 1 | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer allowed (drop-down menu) | | Multiple choice values | option a\noption b\noption c | And I add a "Text and media area" question to the feedback with: | Contents | this is the first page of the feedback | And I add a page break to the feedback And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 2 | | Label | multichoice2 | | Multiple choice type | Multiple choice - single answer allowed (drop-down menu) | | Multiple choice values | option d\noption e\noption f | | Required | 1 | And I add a "Text and media area" question to the feedback with: | Contents | this is the second page of the feedback | And I add a page break to the feedback And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 3 | | Label | multichoice3 | | Multiple choice type | Multiple choice - single answer allowed (drop-down menu) | | Multiple choice values | option g\noption h\noption i | | Dependence item | multichoice2 | | Dependence value | option d | And I add a "Text and media area" question to the feedback with: | Contents | this is the third page of the feedback | And I log out And I am on the "Learning experience" "feedback activity" page logged in as student1 And I follow "Answer the questions" # Examine the first page, select nothing, go to the next page Then the following fields match these values: | this is a multiple choice 1 | 0 | And "Previous page" "button" should not exist And "Submit your answers" "button" should not exist And I should see "this is the first page of the feedback" And I should not see "this is the second page of the feedback" And I should not see "this is the third page of the feedback" And I press "Next page" # Examine the second page, select nothing, try to go to the next page And the following fields match these values: | this is a multiple choice 2 | 0 | And "Previous page" "button" should exist And "Submit your answers" "button" should not exist And I should not see "this is the first page of the feedback" And I should see "this is the second page of the feedback" And I should not see "this is the third page of the feedback" And I press "Next page" # We are still on the second page because the field is required And I should see "Required" in the "form" "css_element" And I should see "this is the second page of the feedback" And I set the field "this is a multiple choice 2" to "option e" And I press "Next page" # Now we are on the third page, element is not shown because of wrong dependency. And I should see "this is the third page of the feedback" And I should not see "this is a multiple choice 3" # Go back, check that values are preserved and change the option to enable dependency. And I press "Previous page" And the following fields match these values: | this is a multiple choice 2 | option e | And I set the field "this is a multiple choice 2" to "option d" And I press "Next page" # Now third page contains all items. And I should see "this is the third page of the feedback" And the following fields match these values: | this is a multiple choice 3 | 0 | And "Previous page" "button" should exist And "Next page" "button" should not exist And "Submit your answers" "button" should exist And I set the field "this is a multiple choice 3" to "option i" And I press "Submit your answers" And I log out # Student 2 tries to trick - he answers the third question and then # goes back and changes dependency question. Analysis should not show this answer! And I am on the "Learning experience" "feedback activity" page logged in as student2 And I follow "Answer the questions" And I set the field "this is a multiple choice 1" to "option a" And I press "Next page" And I set the field "this is a multiple choice 2" to "option d" And I press "Next page" And I set the field "this is a multiple choice 3" to "option g" And I press "Previous page" And I set the field "this is a multiple choice 2" to "option f" And I press "Next page" And I press "Submit your answers" And I log out # Login as teacher and check analysis And I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Analysis" in current page administration And I should see "Submitted answers: 2" And I should see "Questions: 3" And I show chart data for the "multichoice1" feedback And I should see "1 (50.00 %)" in the "option a" "table_row" And I should not see "%" in the "option b" "table_row" And I should not see "%" in the "option c" "table_row" And I show chart data for the "multichoice2" feedback And I should see "1 (50.00 %)" in the "option d" "table_row" And I should not see "%" in the "option e" "table_row" And I should see "1 (50.00 %)" in the "option f" "table_row" And I show chart data for the "multichoice3" feedback And I should not see "%" in the "option g" "table_row" And I should not see "%" in the "option h" "table_row" And I should see "1 (100.00 %)" in the "option i" "table_row" # Change the settings so we don't analyse empty submits And I am on the "Learning experience" "feedback activity" page And I navigate to "Questions" in current page administration And I click on "Edit" "link" in the "this is a multiple choice 1" "mod_feedback > Question" And I choose "Edit question" in the open action menu And I set the field "Omit empty submits in analysis" to "Yes" And I press "Save changes to question" And I am on the "Learning experience" "feedback activity" page And I navigate to "Analysis" in current page administration And I show chart data for the "multichoice1" feedback And I should see "1 (100.00 %)" in the "option a" "table_row" And I should not see "%" in the "option b" "table_row" And I should not see "%" in the "option c" "table_row" PKB\  #tests/behat/overview_report.featurenu[@mod @mod_feedback Feature: Testing overview integration in mod_feedback In order to list all feedbacks in a course As a user I need to be able to see the feedback overview Background: Given the following "users" exist: | username | firstname | lastname | | student1 | Username | 1 | | student2 | Username | 2 | | student3 | Username | 3 | | student4 | Username | 4 | | student5 | Username | 5 | | student6 | Username | 6 | | student7 | Username | 7 | | student8 | Username | 8 | | teacher1 | Teacher | T | And the following "courses" exist: | fullname | shortname | groupmode | | Course 1 | C1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | student4 | C1 | student | | student5 | C1 | student | | student6 | C1 | student | | student7 | C1 | student | | student8 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | timeclose | | feedback | Date feedback | C1 | feedback1 | ##1 Jan 2040 08:00## | | feedback | Not responded feedback | C1 | feedback2 | ##1 Jan 2040 08:00## | | feedback | No date feedback | C1 | feedback3 | | Given the following "mod_feedback > question" exists: | activity | feedback1 | | name | Do you like this course? | | questiontype | multichoice | | label | multichoice1 | | subtype | r | | hidenoselect | 1 | | values | Yes of course\nNot at all\nI don't know | And the following "mod_feedback > responses" exist: | activity | user | Do you like this course? | | feedback1 | student1 | Not at all | | feedback1 | student2 | I don't know | | feedback1 | student3 | Not at all | | feedback1 | student4 | Yes of course | | feedback3 | student1 | Not at all | | feedback3 | student2 | I don't know | | feedback3 | student3 | Not at all | Scenario: Teacher can see the feedback relevant information in the feedback overview When I am on the "Course 1" "course > activities > feedback" page logged in as "teacher1" Then I should see "Responses" in the "feedback_overview_collapsible" "region" And I should not see "Responded" in the "feedback_overview_collapsible" "region" And I should see "Allow answers until" in the "feedback_overview_collapsible" "region" And I should see "1 January 2040" in the "Date feedback" "table_row" And I should see "4 of 8" in the "Date feedback" "table_row" And I should see "1 January 2040" in the "Not responded feedback" "table_row" And I should see "0 of 8" in the "Not responded feedback" "table_row" And I should see "-" in the "No date feedback" "table_row" And I should see "3 of 8" in the "No date feedback" "table_row" And I click on "4 of 8" "link" in the "Date feedback" "table_row" And I should see "Show responses" Scenario: Students can see the feedback relevant information in the feedback overview When I am on the "Course 1" "course > activities > feedback" page logged in as "student1" Then I should not see "Responses" in the "feedback_overview_collapsible" "region" And I should see "Responded" in the "feedback_overview_collapsible" "region" And I should see "Allow answers until" in the "feedback_overview_collapsible" "region" And I should see "1 January 2040" in the "Date feedback" "table_row" And "You have already submitted this feedback" "icon" should exist in the "Date feedback" "table_row" And I should see "1 January 2040" in the "Not responded feedback" "table_row" And I should see "-" in the "Not responded feedback" "table_row" And I should see "-" in the "No date feedback" "table_row" And "You have already submitted this feedback" "icon" should exist in the "No date feedback" "table_row" Scenario: The feedback overview report should generate log events Given I am on the "Course 1" "course > activities > feedback" page logged in as "teacher1" When I am on the "Course 1" "course" page logged in as "teacher1" And I navigate to "Reports" in current page administration And I click on "Logs" "link" And I click on "Get these logs" "button" Then I should see "Course activities overview page viewed" And I should see "viewed the instance list for the module 'feedback'" PKB\T L L %tests/behat/preview_questions.featurenu[@mod @mod_feedback Feature: Preview feedback questions In order to view the feedback questions As a teacher I need to be able to preview them Background: Given the following "users" exist: | username | firstname | lastname | | student | Student | User | | editingteacher | Editing | Teacher | | teacher | NonEditing | Teacher | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | student | C1 | student | | editingteacher | C1 | editingteacher | | teacher | C1 | teacher | And the following "activities" exist: | activity | name | course | idnumber | | feedback | Music history | C1 | feedback0 | And the following "mod_feedback > question" exists: | activity | feedback0 | | name | What is your favourite instrument | | questiontype | multichoice | | label | instrument1 | | values | Drums\nGuitar\nHurdy-gurdy | Scenario: Students cannot see the Preview questions button When I am on the "Music history" "feedback activity" page logged in as student Then I should not see "Preview questions" Scenario: Non-editing teachers can see the Preview questions button When I am on the "Music history" "feedback activity" page logged in as teacher Then I should see "Preview questions" And I follow "Preview questions" And I should see "What is your favourite instrument" Scenario: Editing teachers can see the Preview questions button When I am on the "Music history" "feedback activity" page logged in as editingteacher Then I should see "Preview questions" And I follow "Preview questions" And I should see "What is your favourite instrument" @javascript @accessibility Scenario: Feedback questionnaire pages should be accessible Given the following "mod_feedback > question" exists: | activity | feedback0 | | name | What type of guitar is your favourite? | | questiontype | multichoice | | label | whatguitar | | dependitem | instrument1 | | dependvalue | Guitar | | values | Electric\nAcoustic\nBass | When I am on the "Music history" "feedback activity" page logged in as editingteacher And I navigate to "Questions" in current page administration Then the "region-main" "region" should meet accessibility standards with "best-practice" extra tests And I am on the "Music history" "feedback activity" page logged in as student And I follow "Answer the questions" And the "region-main" "region" should meet accessibility standards with "best-practice" extra tests PKB\E"tests/behat/question_types.featurenu[@mod @mod_feedback Feature: Test creating different types of feedback questions for anonymous feedback In order to create feedbacks As a teacher I need to be able to add different question types @javascript Scenario: Create different types of questions in anonymous feedback with javascript enabled Given the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | 1 | | student1 | Student | 1 | | student2 | Student | 2 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | feedback | Learning experience | C1 | feedback0 | When I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Questions" in current page administration And I add a "Information" question to the feedback with: | Question | this is an information question | | Label | info | | Information type | Course | And I add a "Text and media area" question to the feedback with: | Contents | label text | And I add a "Longer text answer" question to the feedback with: | Question | this is a longer text answer | | Label | longertext | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 1 | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | option a\noption b\noption c | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 2 | | Label | multichoice2 | | Multiple choice type | Multiple choice - multiple answers | | Multiple choice values | option d\noption e\noption f | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 3 | | Label | multichoice3 | | Multiple choice type | Multiple choice - single answer allowed (drop-down menu) | | Multiple choice values | option g\noption h\noption i | And I add a "Multiple choice (rated)" question to the feedback with: | Question | this is a multiple choice rated | | Label | multichoice4 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | 0/option k\n1/option l\n5/option m | And I add a "Numeric answer" question to the feedback with: | Question | this is a numeric answer | | Label | numeric | | Range from | 0 | | Range to | 100 | And I add a "Short text answer" question to the feedback with: | Question | this is a short text answer | | Label | shorttext | | Maximum characters accepted | 200 | And I log out And I am on the "Learning experience" "feedback activity" page logged in as student1 And I follow "Answer the questions" And I set the following fields to these values: | this is a longer text answer | my long answer | | option b | 1 | | option d | 1 | | option f | 1 | | this is a multiple choice 3 | option h | | option l | 1 | | this is a numeric answer (0 - 100) | 35 | | this is a short text answer | hello | And I press "Submit your answers" And I log out And I am on the "Learning experience" "feedback activity" page logged in as student2 And I follow "Answer the questions" And I set the following fields to these values: | this is a longer text answer | lots of feedbacks | | option a | 1 | | option d | 1 | | option e | 1 | | this is a multiple choice 3 | option i | | option m | 1 | | this is a numeric answer (0 - 100) | 71 | | this is a short text answer | no way | And I press "Submit your answers" And I log out When I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Analysis" in current page administration And I should see "Submitted answers: 2" And I should see "Questions: 8" And I am on the "Learning experience" "feedback activity" page And I navigate to "Analysis" in current page administration And I should see "C1" in the "(info)" "table" And I should see "my long answer" in the "(longertext)" "table" And I should see "lots of feedbacks" in the "(longertext)" "table" And I show chart data for the "multichoice2" feedback And I should see "2 (100.00 %)" in the "option d" "table_row" And I should see "1 (50.00 %)" in the "option e" "table_row" And I should see "1 (50.00 %)" in the "option f" "table_row" And I show chart data for the "multichoice3" feedback And I should see "0" in the "option g" "table_row" And I should not see "%" in the "option g" "table_row" And I should see "1 (50.00 %)" in the "option h" "table_row" And I should see "1 (50.00 %)" in the "option i" "table_row" And I show chart data for the "multichoice4" feedback And I should see "0" in the "option k" "table_row" And I should not see "%" in the "(0) option k" "table_row" And I should see "1 (50.00 %)" in the "(1) option l" "table_row" And I should see "1 (50.00 %)" in the "(5) option m" "table_row" And I should see "Average: 3.00" And I should see "35" in the "(numeric)" "table" And I should see "71" in the "(numeric)" "table" And I should see "Average: 53.00" in the "(numeric)" "table" And I should see "no way" in the "(shorttext)" "table" And I should see "hello" in the "(shorttext)" "table" And I show chart data for the "multichoice1" feedback And I should see "1 (50.00 %)" in the "option a" "table_row" And I should see "1 (50.00 %)" in the "option b" "table_row" And I log out PKB\HjQ:Q:tests/behat/groups.featurenu[@mod @mod_feedback Feature: Feedbacks in courses with groups In order to collect feedbacks per group As an teacher I need to be able to filter feedback replies by groups Background: Given the following "users" exist: | username | firstname | lastname | | user1 | Username | 1 | | user2 | Username | 2 | | user3 | Username | 3 | | user4 | Username | 4 | | user5 | Username | 5 | | user6 | Username | 6 | | user7 | Username | 7 | | user8 | Username | 8 | | teacher | Teacher | T | | manager | Manager | M | And the following "courses" exist: | fullname | shortname | groupmode | | Course 1 | C1 | 1 | And the following "course enrolments" exist: | user | course | role | | user1 | C1 | student | | user2 | C1 | student | | user3 | C1 | student | | user4 | C1 | student | | user5 | C1 | student | | user6 | C1 | student | | user7 | C1 | student | | user8 | C1 | student | | teacher | C1 | editingteacher | And the following "groups" exist: | name | course | idnumber | participation | | Group 1 | C1 | G1 | 1 | | Group 2 | C1 | G2 | 1 | | Group 3 | C1 | G3 | 0 | And the following "group members" exist: | user | group | | user1 | G1 | | user2 | G1 | | user2 | G2 | | user3 | G2 | | user4 | G1 | | user5 | G1 | | user6 | G2 | | user8 | G3 | And the following "system role assigns" exist: | user | course | role | | manager | Acceptance test site | manager | And the following "activities" exist: | activity | name | course | idnumber | anonymous | publish_stats | groupmode | section | | feedback | Site feedback | Acceptance test site | feedback0 | 2 | 1 | 1 | 1 | | feedback | Course feedback | C1 | feedback1 | 2 | 1 | 1 | 0 | | feedback | Course anon feedback | C1 | feedback2 | 1 | 1 | 1 | 0 | @javascript Scenario: Non anonymous feedback with groups in a course Given the following "mod_feedback > question" exists: | activity | feedback1 | | name | Do you like this course? | | questiontype | multichoice | | label | multichoice1 | | subtype | r | | hidenoselect | 1 | | values | Yes of course\nNot at all\nI don't know | And the following "mod_feedback > responses" exist: | activity | user | Do you like this course? | | feedback1 | user1 | Not at all | | feedback1 | user2 | I don't know | | feedback1 | user3 | Not at all | | feedback1 | user4 | Yes of course | | feedback1 | user5 | Yes of course | | feedback1 | user6 | Not at all | | feedback1 | user7 | I don't know | | feedback1 | user8 | Yes of course | # View analysis, user1 should only see one group - group 1 When I am on the "Course feedback" "feedback activity" page logged in as user1 And I follow "Analysis" And I should see "Separate groups: Group 1" And I show chart data for the "multichoice1" feedback And I should see "2 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (25.00 %)" in the "Not at all" "table_row" And I log out # View analysis, user3 should only see one group - group 2 And I am on the "Course feedback" "feedback activity" page logged in as user3 And I follow "Analysis" And I should see "Separate groups: Group 2" And I show chart data for the "multichoice1" feedback And I should see "0" in the "Yes of course" "table_row" And I should see "2 (66.67 %)" in the "Not at all" "table_row" And I log out # View analysis, user2 should see a group selector and be able to change the group but not view all. And I am on the "Course feedback" "feedback activity" page logged in as user2 And I follow "Analysis" And the field "Separate groups" matches value "Group 1" And I should not see "Group 3" in the "Separate groups" "select" And I show chart data for the "multichoice1" feedback And I should see "2 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (25.00 %)" in the "Not at all" "table_row" And I select "Group 2" from the "Separate groups" singleselect And I show chart data for the "multichoice1" feedback And I should see "0" in the "Yes of course" "table_row" And I should see "2 (66.67 %)" in the "Not at all" "table_row" And the "Separate groups" select box should not contain "All participants" And I log out # User without group can see all participants only And I am on the "Course feedback" "feedback activity" page logged in as user7 And I follow "Analysis" And I should see "Separate groups: All participants" And I show chart data for the "multichoice1" feedback And I should see "3 (37.50 %)" in the "Yes of course" "table_row" And I should see "3 (37.50 %)" in the "Not at all" "table_row" And I should see "2 (25.00 %)" in the "I don't know" "table_row" And I log out # User in non-participation group sees the same as no groups And I am on the "Course feedback" "feedback activity" page logged in as user8 And I follow "Analysis" And I should see "Separate groups: All participants" And I show chart data for the "multichoice1" feedback And I should see "3 (37.50 %)" in the "Yes of course" "table_row" And I should see "3 (37.50 %)" in the "Not at all" "table_row" And I should see "2 (25.00 %)" in the "I don't know" "table_row" And I log out # Teacher can browse everybody And I am on the "Course feedback" "feedback activity" page logged in as teacher And I navigate to "Analysis" in current page administration And the field "Separate groups" matches value "All participants" And I should not see "Group 3" in the "Separate groups" "select" And I show chart data for the "multichoice1" feedback And I should see "3 (37.50 %)" in the "Yes of course" "table_row" And I should see "3 (37.50 %)" in the "Not at all" "table_row" And I should see "2 (25.00 %)" in the "I don't know" "table_row" And I select "Group 1" from the "Separate groups" singleselect And I show chart data for the "multichoice1" feedback And I should see "2 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (25.00 %)" in the "Not at all" "table_row" And I select "Group 2" from the "Separate groups" singleselect And I show chart data for the "multichoice1" feedback And I should see "0" in the "Yes of course" "table_row" And I should see "2 (66.67 %)" in the "Not at all" "table_row" And I am on the "Course feedback" "feedback activity" page And I navigate to "Responses" in current page administration And the field "Separate groups" matches value "Group 2" And I should not see "Username 1" And I should see "Username 3" And I select "Group 1" from the "Separate groups" singleselect And I should see "Username 1" And I should not see "Username 3" And I select "All participants" from the "Separate groups" singleselect And I should see "Username 1" And I should see "Username 3" @javascript Scenario: Anonymous feedback with groups in a course - insufficient responses Given the following "mod_feedback > question" exists: | activity | feedback2 | | name | Do you like this course? | | questiontype | multichoice | | label | multichoice1 | | subtype | r | | hidenoselect | 1 | | values | Yes of course\nNot at all\nI don't know | And the following "mod_feedback > responses" exist: | activity | user | Do you like this course? | | feedback2 | user1 | Not at all | When I am on the "Course anon feedback" "feedback activity" page logged in as user1 And I follow "Analysis" Then I should not see "Yes of course" And I should see "There are insufficient responses for this group" @javascript Scenario: Anonymous feedback with groups in a course Given the following "mod_feedback > question" exists: | activity | feedback2 | | name | Do you like this course? | | questiontype | multichoice | | label | multichoice1 | | subtype | r | | hidenoselect | 1 | | values | Yes of course\nNot at all\nI don't know | And the following "mod_feedback > responses" exist: | activity | user | Do you like this course? | | feedback2 | user1 | Not at all | | feedback2 | user2 | I don't know | | feedback2 | user3 | Not at all | | feedback2 | user4 | Yes of course | | feedback2 | user5 | Yes of course | | feedback2 | user6 | Not at all | | feedback2 | user7 | I don't know | | feedback2 | user8 | Yes of course | # View analysis, user1 should only see one group - group 1 When I am on the "Course anon feedback" "feedback activity" page logged in as user1 And I follow "Analysis" And I should see "Separate groups: Group 1" And I show chart data for the "multichoice1" feedback And I should see "2 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (25.00 %)" in the "Not at all" "table_row" And I log out # View analysis, user3 should only see one group - group 2 And I am on the "Course anon feedback" "feedback activity" page logged in as user3 And I follow "Analysis" And I should see "Separate groups: Group 2" And I show chart data for the "multichoice1" feedback And I should see "0" in the "Yes of course" "table_row" And I should see "2 (66.67 %)" in the "Not at all" "table_row" And I log out # View analysis, user2 should see a group selector and be able to change the group but not view all. And I am on the "Course anon feedback" "feedback activity" page logged in as user2 And I follow "Analysis" And the field "Separate groups" matches value "Group 1" And I show chart data for the "multichoice1" feedback And I should see "2 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (25.00 %)" in the "Not at all" "table_row" And I select "Group 2" from the "Separate groups" singleselect And I show chart data for the "multichoice1" feedback And I should see "0" in the "Yes of course" "table_row" And I should see "2 (66.67 %)" in the "Not at all" "table_row" And the "Separate groups" select box should not contain "All participants" And I log out # User without group can see all participants only And I am on the "Course anon feedback" "feedback activity" page logged in as user7 And I follow "Analysis" And I should see "Separate groups: All participants" And I show chart data for the "multichoice1" feedback And I should see "3 (37.50 %)" in the "Yes of course" "table_row" And I should see "3 (37.50 %)" in the "Not at all" "table_row" And I should see "2 (25.00 %)" in the "I don't know" "table_row" And I log out # User in non-participation group is the same as no groups. And I am on the "Course anon feedback" "feedback activity" page logged in as user8 And I follow "Analysis" And I should see "Separate groups: All participants" And I show chart data for the "multichoice1" feedback And I should see "3 (37.50 %)" in the "Yes of course" "table_row" And I should see "3 (37.50 %)" in the "Not at all" "table_row" And I should see "2 (25.00 %)" in the "I don't know" "table_row" And I log out # Teacher can browse everybody And I am on the "Course anon feedback" "feedback activity" page logged in as teacher And I navigate to "Analysis" in current page administration And the field "Separate groups" matches value "All participants" And I show chart data for the "multichoice1" feedback And I should see "3 (37.50 %)" in the "Yes of course" "table_row" And I should see "3 (37.50 %)" in the "Not at all" "table_row" And I should see "2 (25.00 %)" in the "I don't know" "table_row" And I select "Group 1" from the "Separate groups" singleselect And I show chart data for the "multichoice1" feedback And I should see "2 (50.00 %)" in the "Yes of course" "table_row" And I should see "1 (25.00 %)" in the "Not at all" "table_row" And I select "Group 2" from the "Separate groups" singleselect And I show chart data for the "multichoice1" feedback And I should see "0" in the "Yes of course" "table_row" And I should see "2 (66.67 %)" in the "Not at all" "table_row" And I am on the "Course anon feedback" "feedback activity" page And I navigate to "Responses" in current page administration # The response numbers were randomly allocated, we only can assert the number of visible responses here: And the field "Separate groups" matches value "Group 2" And "//tr[contains(@id,'_r2') and contains(.,'Response number')]" "xpath_element" should exist And "//tr[contains(@id,'_r3') and contains(@class,'emptyrow')]" "xpath_element" should exist And I select "Group 1" from the "Separate groups" singleselect And "//tr[contains(@id,'_r3') and contains(.,'Response number')]" "xpath_element" should exist And "//tr[contains(@id,'_r4') and contains(@class,'emptyrow')]" "xpath_element" should exist And I select "All participants" from the "Separate groups" singleselect And "//tr[contains(@id,'_r7') and contains(.,'Response number')]" "xpath_element" should exist And "//tr[contains(@id,'_r8') and contains(@class,'emptyrow')]" "xpath_element" should exist PKB\$y6V 0tests/behat/feedback_activity_completion.featurenu[@mod @mod_feedback @core_completion Feature: View activity completion in the feedback activity In order to have visibility of feedback completion requirements As a student I need to be able to view my feedback completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | enablecompletion | showcompletionconditions | | Course 1 | C1 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | feedback | | course | C1 | | idnumber | mh1 | | name | Music history | | section | 1 | | completion | 2 | | completionview | 1 | | completionsubmit | 1 | And I am on the "Music history" "feedback activity" page logged in as teacher1 And I navigate to "Questions" in current page administration And I add a "Multiple choice" question to the feedback with: | Question | What is your favourite instrument | | Label | instrument1 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | drums\guitar\hurdygurdy | And I log out Scenario: Feedback module displays completion conditions to teachers When I am on the "Music history" "feedback activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Submit feedback" completion condition Scenario: Student can complete a feedback activity when all conditions are met Given I am on the "Music history" "feedback activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "todo" And the "Submit feedback" completion condition of "Music history" is displayed as "todo" When I follow "Answer the questions" And I set the field "drums" to "1" And I press "Submit your answers" And I press "Continue" And I am on the "Music history" "feedback activity" page Then the "View" completion condition of "Music history" is displayed as "done" And the "Submit feedback" completion condition of "Music history" is displayed as "done" @javascript Scenario: A student can manually mark the feedback activity as done but a teacher cannot Given I am on the "Music history" "feedback activity editing" page logged in as teacher1 And I expand all fieldsets And I set the field "Students must manually mark the activity as done" to "1" And I press "Save and display" # Teacher view. And the manual completion button for "Music history" should be disabled And I log out # Student view. When I am on the "Music history" "feedback activity" page logged in as student1 Then the manual completion button of "Music history" is displayed as "Mark as done" And I toggle the manual completion state of "Music history" And the manual completion button of "Music history" is displayed as "Done" PKB\Rn?+tests/behat/question_types_non_anon.featurenu[@mod @mod_feedback Feature: Test creating different types of feedback questions for non-anonymous feedback In order to create feedbacks As a teacher I need to be able to add different question types @javascript Scenario: Create different types of questions in non-anonymous feedback with javascript enabled Given the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | 1 | | student1 | Student | 1 | | student2 | Student | 2 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | anonymous | | feedback | Learning experience | C1 | feedback0 | 2 | When I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Questions" in current page administration And I add a "Information" question to the feedback with: | Question | this is an information question | | Label | info | | Information type | Course | And I add a "Information" question to the feedback with: | Question | this is a response time question | | Label | curtime | | Information type | Responses time | And I add a "Text and media area" question to the feedback with: | Contents | label text | And I add a "Longer text answer" question to the feedback with: | Question | this is a longer text answer | | Label | longertext | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 1 | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | option a\noption b\noption c | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 2 | | Label | multichoice2 | | Multiple choice type | Multiple choice - multiple answers | | Multiple choice values | option d\noption e\noption f | And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 3 | | Label | multichoice3 | | Multiple choice type | Multiple choice - single answer allowed (drop-down menu) | | Multiple choice values | option g\noption h\noption i | And I add a "Multiple choice (rated)" question to the feedback with: | Question | this is a multiple choice rated | | Label | multichoice4 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | 0/option k\n1/option l\n5/option m | And I add a "Numeric answer" question to the feedback with: | Question | this is a numeric answer | | Label | numeric | | Range from | 0 | | Range to | 100 | And I add a "Short text answer" question to the feedback with: | Question | this is a short text answer | | Label | shorttext | | Maximum characters accepted | 200 | And I log out When I am on the "Learning experience" "feedback activity" page logged in as student1 And I follow "Answer the questions" And I set the following fields to these values: | this is a longer text answer | my long answer | | option b | 1 | | option d | 1 | | option f | 1 | | this is a multiple choice 3 | option h | | option l | 1 | | this is a numeric answer (0 - 100) | 35 | | this is a short text answer | hello | And I press "Submit your answers" And I log out When I am on the "Learning experience" "feedback activity" page logged in as student2 And I follow "Answer the questions" And I set the following fields to these values: | this is a longer text answer | lots of feedbacks | | option a | 1 | | option d | 1 | | option e | 1 | | this is a multiple choice 3 | option i | | option m | 1 | | this is a numeric answer (0 - 100) | 71 | | this is a short text answer | no way | And I press "Submit your answers" And I log out When I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Analysis" in current page administration And I should see "Submitted answers: 2" And I should see "Questions: 9" And I log out And I am on the "Learning experience" "feedback activity" page logged in as teacher1 And I navigate to "Analysis" in current page administration And I should see "C1" in the "(info)" "table" And I should see "my long answer" in the "(longertext)" "table" And I should see "lots of feedbacks" in the "(longertext)" "table" And I show chart data for the "multichoice2" feedback And I should see "2 (100.00 %)" in the "option d" "table_row" And I should see "1 (50.00 %)" in the "option e" "table_row" And I should see "1 (50.00 %)" in the "option f" "table_row" And I show chart data for the "multichoice3" feedback And I should see "0" in the "option g" "table_row" And I should not see "%" in the "option g" "table_row" And I should see "1 (50.00 %)" in the "option h" "table_row" And I should see "1 (50.00 %)" in the "option i" "table_row" And I show chart data for the "multichoice4" feedback And I should see "0" in the "(0) option k" "table_row" And I should not see "%" in the "(0) option k" "table_row" And I should see "1 (50.00 %)" in the "(1) option l" "table_row" And I should see "1 (50.00 %)" in the "(5) option m" "table_row" And I should see "Average: 3.00" And I should see "35" in the "(numeric)" "table" And I should see "71" in the "(numeric)" "table" And I should see "Average: 53.00" in the "(numeric)" "table" And I should see "no way" in the "(shorttext)" "table" And I should see "hello" in the "(shorttext)" "table" And I show chart data for the "multichoice1" feedback And I should see "1 (50.00 %)" in the "option a" "table_row" And I should see "1 (50.00 %)" in the "option b" "table_row" And I log out PKB\F&&5tests/behat/feedback_no_calendar_capabilities.featurenu[@mod @mod_feedback Feature: Feedback with no calendar capabilites In order to allow work effectively As a teacher I need to be able to create feedbacks even when I cannot edit calendar events Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "admin" And I am on the "Course 1" "permissions" page And I override the system permissions of "Teacher" role with: | capability | permission | | moodle/calendar:manageentries | Prohibit | And I log out Scenario: Editing a feedback Given the following "activities" exist: | activity | name | intro | course | idnumber | | feedback | Test feedback name | Test feedback description | C1 | feedback1 | And I log in as "admin" And I am on "Course 1" course homepage And I follow "Test feedback name" And I navigate to "Settings" in current page administration And I set the following fields to these values: | id_timeopen_enabled | 1 | | id_timeopen_day | 1 | | id_timeopen_month | 1 | | id_timeopen_year | 2017 | | id_timeclose_enabled | 1 | | id_timeclose_day | 1 | | id_timeclose_month | 2 | | id_timeclose_year | 2017 | And I press "Save and return to course" And I log out When I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I follow "Test feedback name" And I navigate to "Settings" in current page administration And I set the following fields to these values: | id_timeopen_year | 2018 | | id_timeclose_year | 2018 | And I press "Save and return to course" Then I should see "Test feedback name" PKB\+l4Y+Y+tests/behat/templates.featurenu[@mod @mod_feedback Feature: Saving, using and deleting feedback templates In order to quickly create feedbacks As a manager I need to be able to create feedback templates Background: Given the following "users" exist: | username | firstname | lastname | | teacher | Teacher | 1 | | manager | Manager | 1 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | | Course 2 | C2 | And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | | teacher | C2 | editingteacher | And the following "system role assigns" exist: | user | course | role | | manager | Acceptance test site | manager | And the following "activities" exist: | activity | name | course | idnumber | | feedback | Learning experience course 1 | C1 | feedback1 | | feedback | Another feedback in course 1 | C1 | feedback2 | | feedback | Learning experience course 2 | C2 | feedback3 | And I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I navigate to "Questions" in current page administration And I add a "Multiple choice" question to the feedback with: | Question | this is a multiple choice 1 | | Label | multichoice1 | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | option a\noption b\noption c | And I log out @javascript Scenario: Teacher can save template and re-use it in the same course only # Go to feedback templates and make sure none exist yet When I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I navigate to "Templates" in current page administration Then I should see "No templates available yet" # Save as a course template And I navigate to "Feedback" in current page administration And I click on "Edit questions" "link" in the "[role=main]" "css_element" And I press "Actions" And I choose "Save as template" in the open action menu And I should see "Save as template" in the ".modal-title" "css_element" And I should see "Name" in the ".modal-body" "css_element" And I set the field "Name" to "My first template" And I click on "Save" "button" in the ".modal-dialog" "css_element" And I should see "Template saved" And I navigate to "Templates" in current page administration And I should see "My first template" # Create a feedback from this template in the same course And I am on the "Another feedback in course 1" "feedback activity" page And I navigate to "Templates" in current page administration And I open the action menu in "My first template" "table_row" And I choose "Use template" in the open action menu And I click on "Save" "button" in the "Use template" "dialogue" And I should see "this is a multiple choice 1" # Make sure this template is not available in another course And I am on the "Learning experience course 2" "feedback activity" page And I navigate to "Templates" in current page administration And I should see "No templates available yet" # Make sure Template cannot be created without questions And I am on the "Learning experience course 2" "feedback activity" page And I navigate to "Questions" in current page administration And I press "Actions" And I should not see "Save as template" @javascript Scenario: Teacher can append template to existing questions or remove them # Save feedback as a course template When I am on the "Learning experience course 1" "feedback activity" page logged in as teacher And I navigate to "Questions" in current page administration And I press "Actions" And I choose "Save as template" in the open action menu And I should see "Save as template" in the ".modal-title" "css_element" And I should see "Name" in the ".modal-body" "css_element" And I set the field "Name" to "My first template" And I click on "Save" "button" in the ".modal-dialog" "css_element" And I press "Actions" And I choose "Save as template" in the open action menu # Add questions to another feedback And I am on the "Another feedback in course 1" "feedback activity" page And I navigate to "Questions" in current page administration And I add a "Multiple choice" question to the feedback with: | Question | What is your favourite subject | | Label | subjectchoice | | Multiple choice type | Multiple choice - single answer | | Multiple choice values | Maths\bScience\nEnglish\nOther | # Import template appending items And I navigate to "Templates" in current page administration And I open the action menu in "My first template" "table_row" And I choose "Use template" in the open action menu And I set the field "Keep existing questions and add new questions at the end" to "1" And I click on "Save" "button" in the "Use template" "dialogue" Then "What is your favourite subject" "text" should appear before "this is a multiple choice 1" "text" # Import template replacing items (through Preview page) And I navigate to "Templates" in current page administration And I open the action menu in "My first template" "table_row" And I choose "Preview" in the open action menu And I press "Actions" And I choose "Use template" in the open action menu And I set the field "Override existing questions" to "1" And I click on "Save" "button" in the "Use template" "dialogue" And I should see "this is a multiple choice 1" And I should not see "What is your favourite subject" And I should see "this is a multiple choice 1" @javascript Scenario: Manager can save template as public and it will be available in any course When I am on the "Learning experience course 1" "feedback activity" page logged in as manager And I navigate to "Questions" in current page administration And I press "Actions" And I choose "Save as template" in the open action menu And I set the field "Name" to "My first template" And I set the field "Available for all courses" to "1" And I click on "Save" "button" in the ".modal-dialog" "css_element" And I log out And I am on the "Learning experience course 2" "feedback activity" page logged in as teacher And I navigate to "Templates" in current page administration And I open the action menu in "My first template" "table_row" And I choose "Use template" in the open action menu And I set the field "Override existing questions" to "1" And I click on "Save" "button" in the "Use template" "dialogue" Then I should see "this is a multiple choice 1" @javascript Scenario: Teacher can delete course templates but can not delete public templates # Save feedback as both public and course template When I am on the "Learning experience course 1" "feedback activity" page logged in as manager And I navigate to "Questions" in current page administration And I press "Actions" And I choose "Save as template" in the open action menu And I set the field "Name" to "My public template" And I set the field "Available for all courses" to "1" And I click on "Save" "button" in the ".modal-dialog" "css_element" And I press "Actions" And I choose "Save as template" in the open action menu And I set the field "Name" to "My course template" And I click on "Save" "button" in the ".modal-dialog" "css_element" And I log out # Login as teacher and try to delete templates And I am on the "Another feedback in course 1" "feedback activity" page logged in as teacher And I navigate to "Templates" in current page administration And I open the action menu in "My public template" "table_row" Then I should not see "Delete" And I choose "Preview" in the open action menu And I press "Actions" And I should not see "Delete" And I click on "Back" "link" in the ".tertiary-navigation" "css_element" And "My course template" "text" should exist in the ".coursetemplates" "css_element" And I open the action menu in "My course template" "table_row" And I choose "Delete" in the open action menu And I click on "Yes" "button" And I should see "Template deleted" And "My course template" "text" should not exist in the ".coursetemplates" "css_element" And "No templates available yet" "text" should exist in the ".coursetemplates" "css_element" @javascript @accessibility Scenario: Manager can delete both course and public templates # Save feedback as both public and course template When I am on the "Learning experience course 1" "feedback activity" page logged in as manager And I navigate to "Questions" in current page administration And the "region-main" "region" should meet accessibility standards with "best-practice" extra tests And I press "Actions" And I choose "Save as template" in the open action menu And I set the field "Name" to "My public template" And I set the field "Available for all courses" to "1" And I click on "Save" "button" in the ".modal-dialog" "css_element" And I press "Actions" And I choose "Save as template" in the open action menu And I set the field "Name" to "My course template" And I click on "Save" "button" in the ".modal-dialog" "css_element" And I navigate to "Templates" in current page administration And the "region-main" "region" should meet accessibility standards with "best-practice" extra tests And "My course template" "text" should exist in the ".coursetemplates" "css_element" # Delete course template And I open the action menu in "My course template" "table_row" And I choose "Delete" in the open action menu And I should see "Are you sure you want to delete this template?" And I press "Yes" Then I should see "Template deleted" And "My course template" "text" should not exist in the ".coursetemplates" "css_element" And "No templates available yet" "text" should exist in the ".coursetemplates" "css_element" And "My public template" "text" should exist in the ".publictemplates" "css_element" # Delete course template (through Preview page) And I open the action menu in "My public template" "table_row" And I choose "Preview" in the open action menu And I press "Actions" And I choose "Delete" in the open action menu And I should see "Are you sure you want to delete this template?" And I press "Yes" And I should see "Template deleted" And "My public template" "text" should not exist in the ".publictemplates" "css_element" And "No templates available yet" "text" should exist in the ".publictemplates" "css_element" PKB\M##$tests/courseformat/overview_test.phpnu[. namespace mod_feedback\courseformat; use core_courseformat\local\overview\overviewfactory; /** * Tests for Feedback * * @covers \mod_feedback\courseformat\overview * @package mod_feedback * @category test * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class overview_test extends \advanced_testcase { #[\Override] public static function setUpBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/mod/feedback/lib.php'); parent::setUpBeforeClass(); } /** * Test get_actions_overview. * * @covers ::get_actions_overview * @dataProvider provider_test_get_actions_overview * * @param string $user * @param bool $expectnull * @param bool $hasresponses * @return void */ public function test_get_actions_overview(string $user, bool $expectnull, bool $hasresponses): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module( 'feedback', ['course' => $course->id], ); $cm = get_fast_modinfo($course)->get_cm($activity->cmid); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $itemcreated = $feedbackgenerator->create_item_multichoice($activity, ['values' => "y\nn"]); $expectedresonses = 0; if ($hasresponses) { $this->setUser($student); $feedbackgenerator->create_response([ 'userid' => $student->id, 'cmid' => $cm->id, 'anonymous' => false, $itemcreated->name => 'y', ]); $expectedresonses = 1; } $currentuser = ($user == 'teacher') ? $teacher : $student; $this->setUser($currentuser); $item = overviewfactory::create($cm)->get_actions_overview(); // Students should not see item. if ($expectnull) { $this->assertNull($item); return; } // Teachers should see item. $this->assertEquals(get_string('responses', 'mod_feedback'), $item->get_name()); $this->assertEquals($expectedresonses, $item->get_value()); } /** * Data provider for test_get_actions_overview. * * @return array */ public static function provider_test_get_actions_overview(): array { return [ 'Teacher with responses' => [ 'user' => 'teacher', 'expectnull' => false, 'hasresponses' => true, ], 'Student with responses' => [ 'user' => 'student', 'expectnull' => true, 'hasresponses' => true, ], 'Teacher without responses' => [ 'user' => 'teacher', 'expectnull' => false, 'hasresponses' => false, ], 'Student without responses' => [ 'user' => 'student', 'expectnull' => true, 'hasresponses' => false, ], ]; } /** * Test get_due_date_overview. * @covers ::get_due_date_overview * @dataProvider provider_test_get_due_date_overview * @param string $user * @param bool $hasduedate * @return void */ public function test_get_due_date_overview(string $user, bool $hasduedate): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $moddata = [ 'course' => $course->id, 'timeclose' => $hasduedate ? time() + 3600 : 0, ]; $activity = $this->getDataGenerator()->create_module('feedback', $moddata); $cm = get_fast_modinfo($course)->get_cm($activity->cmid); $currentuser = ($user == 'teacher') ? $teacher : $student; $this->setUser($currentuser); $item = overviewfactory::create($cm)->get_due_date_overview(); // Teachers should see item. $this->assertEquals(get_string('feedbackclose', 'mod_feedback'), $item->get_name()); $expectedvalue = $hasduedate ? $moddata['timeclose'] : null; $this->assertEquals($expectedvalue, $item->get_value()); } /** * Data provider for test_get_due_date_overview. * * @return array */ public static function provider_test_get_due_date_overview(): array { return [ 'Teacher with due date' => [ 'user' => 'teacher', 'hasduedate' => true, ], 'Student with due date' => [ 'user' => 'student', 'hasduedate' => true, ], 'Teacher without due date' => [ 'user' => 'teacher', 'hasduedate' => false, ], 'Student without due date' => [ 'user' => 'student', 'hasduedate' => false, ], ]; } /** * Test get_extra_submitted_overview. * * @covers ::get_extra_submitted_overview * @dataProvider provider_test_get_extra_submitted_overview * * @param string $user * @param bool $expectnull * @param bool $hasresponses * @return void */ public function test_get_extra_submitted_overview(string $user, bool $expectnull, bool $hasresponses = false): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module( 'feedback', ['course' => $course->id], ); $cm = get_fast_modinfo($course)->get_cm($activity->cmid); $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback'); $itemcreated = $feedbackgenerator->create_item_multichoice($activity, ['values' => "y\nn"]); $expectedresonses = 0; if ($hasresponses) { $this->setUser($student); $feedbackgenerator->create_response([ 'userid' => $student->id, 'cmid' => $cm->id, 'anonymous' => false, $itemcreated->name => 'y', ]); $expectedresonses = 1; } if ($user == 'admin') { $this->setAdminUser(); } else { $this->setUser(($user == 'teacher') ? $teacher : $student); } $overview = overviewfactory::create($cm); $reflection = new \ReflectionClass($overview); $method = $reflection->getMethod('get_extra_submitted_overview'); $method->setAccessible(true); $item = $method->invoke($overview); // Students should not see item. if ($expectnull) { $this->assertNull($item); return; } // Teachers should see item. $this->assertEquals(get_string('responded', 'mod_feedback'), $item->get_name()); $this->assertEquals($hasresponses, $item->get_value()); } /** * Data provider for test_get_extra_submitted_overview. * * @return array */ public static function provider_test_get_extra_submitted_overview(): array { return [ 'Teacher' => [ 'user' => 'teacher', 'expectnull' => true, ], 'Admin' => [ 'user' => 'admin', 'expectnull' => true, ], 'Student without responses' => [ 'user' => 'student', 'expectnull' => false, 'hasresponses' => false, ], 'Student with responses' => [ 'user' => 'student', 'expectnull' => false, 'hasresponses' => true, ], ]; } } PKB\p.tests/manager_test.phpnu[. namespace mod_feedback; use advanced_testcase; /** * Class for unit testing mod_feedback\dates. * * @category test * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright 2025 Laurent David * @package mod_feedback * @covers \mod_feedback\manager */ final class manager_test extends advanced_testcase { /** * Data provider for test_can_see_others_in_groups. * * @return array[] */ public static function can_see_others_in_groups_provider(): array { return [ 'student no groups, separate group' => [ 'mode' => SEPARATEGROUPS, 'username' => 's1', 'expected' => false, ], 'student in groups, separate group' => [ 'mode' => SEPARATEGROUPS, 'username' => 's2', 'expected' => true, ], 'non editing teacher no groups, separate group' => [ 'mode' => SEPARATEGROUPS, 'username' => 't1', 'expected' => false, ], 'non editing teacher in groups, separate group' => [ 'mode' => SEPARATEGROUPS, 'username' => 't2', 'expected' => true, ], 'editing teacher no groups, separate group' => [ 'mode' => SEPARATEGROUPS, 'username' => 't3', 'expected' => true, ], 'editing teacher in groups, separate group' => [ 'mode' => SEPARATEGROUPS, 'username' => 't4', 'expected' => true, ], 'student no groups, visible group' => [ 'mode' => VISIBLEGROUPS, 'username' => 's1', 'expected' => true, ], 'student in groups, visible group' => [ 'mode' => VISIBLEGROUPS, 'username' => 's2', 'expected' => true, ], 'non editing teacher no groups, visible group' => [ 'mode' => VISIBLEGROUPS, 'username' => 't1', 'expected' => true, ], 'non editing teacher in groups, visible group' => [ 'mode' => VISIBLEGROUPS, 'username' => 't2', 'expected' => true, ], 'editing teacher no groups, visible group' => [ 'mode' => VISIBLEGROUPS, 'username' => 't3', 'expected' => true, ], 'editing teacher in groups, visible group' => [ 'mode' => VISIBLEGROUPS, 'username' => 't4', 'expected' => true, ], ]; } /** * Test if we can see or not others in groups. * * @param int $mode The group mode. * @param string $username The username of the user to test. * @param bool $expected The expected result. * * @covers ::can_see_others_in_groups * @dataProvider can_see_others_in_groups_provider */ public function test_can_see_others_in_groups(int $mode, string $username, bool $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['groupmode' => $mode, 'groupmodeforce' => 1]); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Group 1']); $users = []; $data = [ 's1' => 'student', 's2' => 'student', 't1' => 'teacher', 't2' => 'teacher', 't3' => 'editingteacher', 't4' => 'editingteacher', ]; foreach ($data as $user => $role) { $users[$user] = $this->getDataGenerator()->create_and_enrol($course, $role, $user); } foreach (['s2', 't2', 't3'] as $uname) { $user = $users[$uname]; $this->getDataGenerator()->create_group_member( ['groupid' => $group->id, 'userid' => $user->id] ); } $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course]); $this->setUser($users[$username]); $cm = get_fast_modinfo($course)->cms[$feedback->cmid]; $this->assertEquals($expected, manager::can_see_others_in_groups($cm)); } } PKB\3ܚ{U0U0show_nonrespondents.phpnu[. /** * print the single entries * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ use mod_feedback\manager; require_once("../../config.php"); require_once("lib.php"); require_once($CFG->libdir.'/tablelib.php'); //////////////////////////////////////////////////////// //get the params //////////////////////////////////////////////////////// $id = required_param('id', PARAM_INT); $subject = optional_param('subject', '', PARAM_CLEANHTML); $message = optional_param_array('message', '', PARAM_CLEANHTML); $format = optional_param('format', FORMAT_MOODLE, PARAM_INT); $messageuser = optional_param_array('messageuser', false, PARAM_INT); $action = optional_param('action', '', PARAM_ALPHA); $perpage = optional_param('perpage', FEEDBACK_DEFAULT_PAGE_COUNT, PARAM_INT); // how many per page $showall = optional_param('showall', false, PARAM_INT); // should we show all users //////////////////////////////////////////////////////// //get the objects //////////////////////////////////////////////////////// if ($message) { $message = $message['text']; } list ($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); if (! $feedback = $DB->get_record("feedback", array("id"=>$cm->instance))) { throw new \moodle_exception('invalidcoursemodule'); } //this page only can be shown on nonanonymous feedbacks in courses //we should never reach this page if ($feedback->anonymous != FEEDBACK_ANONYMOUS_NO OR $feedback->course == SITEID) { throw new \moodle_exception('error'); } $url = new moodle_url('/mod/feedback/show_nonrespondents.php', array('id'=>$cm->id)); $PAGE->set_url($url); $context = context_module::instance($cm->id); //we need the coursecontext to allow sending of mass mails $coursecontext = context_course::instance($course->id); require_login($course, true, $cm); $actionbar = new \mod_feedback\output\responses_action_bar($cm->id, $url); require_capability('mod/feedback:viewreports', $context); $currentgroup = groups_get_activity_group($cm, true); $incompleteusers = feedback_get_incomplete_users($cm, $currentgroup); $canbulkmessaging = has_capability('moodle/course:bulkmessaging', $coursecontext); if ($action == 'sendmessage' && $canbulkmessaging) { require_sesskey(); $shortname = format_string($course->shortname, true, array('context' => $coursecontext)); $strfeedbacks = get_string("modulenameplural", "feedback"); $htmlmessage = ""; $link1 = $CFG->wwwroot.'/course/view.php?id='.$course->id; $link2 = $CFG->wwwroot.'/mod/feedback/index.php?id='.$course->id; $link3 = $CFG->wwwroot.'/mod/feedback/view.php?id='.$cm->id; $htmlmessage .= ''; $htmlmessage .= $message; $htmlmessage .= ''; $good = 1; if (is_array($messageuser)) { // Ensure selected users are part of the "incomplete users" set. $messageuser = array_intersect($messageuser, $incompleteusers); foreach ($messageuser as $userid) { $senduser = $DB->get_record('user', array('id'=>$userid)); $eventdata = new \core\message\message(); $eventdata->courseid = $course->id; $eventdata->name = 'message'; $eventdata->component = 'mod_feedback'; $eventdata->userfrom = $USER; $eventdata->userto = $senduser; $eventdata->subject = $subject; $eventdata->fullmessage = html_to_text($htmlmessage); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $htmlmessage; $eventdata->smallmessage = ''; $eventdata->courseid = $course->id; $eventdata->contexturl = $link3; $eventdata->contexturlname = $feedback->name; $good = $good && message_send($eventdata); } if (!empty($good)) { $msg = $OUTPUT->heading(get_string('messagedselectedusers')); } else { $msg = $OUTPUT->heading(get_string('messagedselectedusersfailed')); } redirect($url, $msg, 4); exit; } } //////////////////////////////////////////////////////// //get the responses of given user //////////////////////////////////////////////////////// /// Print the page header $PAGE->set_heading($course->fullname); $PAGE->set_title($feedback->name); $PAGE->set_secondary_active_tab('responses'); if ($responsesnode = $PAGE->settingsnav->find('responses', navigation_node::TYPE_CUSTOM)) { $responsesnode->make_active(); } $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); echo $OUTPUT->header(); /** @var \mod_feedback\output\renderer $renderer */ $renderer = $PAGE->get_renderer('mod_feedback'); echo $renderer->main_action_bar($actionbar); if (!manager::can_see_others_in_groups($cm)) { // The user is not in a group so show message and exit. echo $OUTPUT->notification(get_string('notingroup')); echo $OUTPUT->footer(); exit(); } /// Print the main part of the page /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////// /// Print the users with no responses //////////////////////////////////////////////////////// $groupselect = groups_print_activity_menu($cm, $url->out(), true); // preparing the table for output $baseurl = new moodle_url('/mod/feedback/show_nonrespondents.php'); $baseurl->params(array('id'=>$id, 'showall'=>$showall)); $tablecolumns = array('userpic', 'fullname', 'status'); $tableheaders = array(get_string('userpic'), get_string('fullnameuser'), get_string('status')); if ($canbulkmessaging) { $tablecolumns[] = 'select'; // Build the select/deselect all control. $selectallid = 'selectall-non-respondents'; $mastercheckbox = new \core\output\checkbox_toggleall('feedback-non-respondents', true, [ 'id' => $selectallid, 'name' => $selectallid, 'value' => 1, 'label' => get_string('select'), // Consistent label to prevent the select column from resizing. 'selectall' => get_string('select'), 'deselectall' => get_string('select'), 'labelclasses' => 'm-0', ]); $tableheaders[] = $OUTPUT->render($mastercheckbox); } $table = new flexible_table('feedback-shownonrespondents-'.$course->id); $table->define_columns($tablecolumns); $table->define_headers($tableheaders); $table->define_baseurl($baseurl); $table->sortable(true, 'lastname', SORT_DESC); $table->set_attribute('cellspacing', '0'); $table->set_attribute('id', 'showentrytable'); $table->set_attribute('class', 'generaltable generalbox'); $table->set_control_variables(array( TABLE_VAR_SORT => 'ssort', TABLE_VAR_IFIRST => 'sifirst', TABLE_VAR_ILAST => 'silast', TABLE_VAR_PAGE => 'spage' )); $table->no_sorting('select'); $table->no_sorting('status'); $table->setup(); if ($table->get_sql_sort()) { $sort = $table->get_sql_sort(); } else { $sort = ''; } $matchcount = count($incompleteusers); $table->initialbars(false); if ($showall) { $startpage = false; $pagecount = false; } else { $table->pagesize($perpage, $matchcount); $startpage = $table->get_page_start(); $pagecount = $table->get_page_size(); } // Return students record including if they started or not the feedback. $students = feedback_get_incomplete_users($cm, $currentgroup, $sort, $startpage, $pagecount, true); //####### viewreports-start //print the list of students echo $OUTPUT->heading(get_string('non_respondents_students', 'feedback', $matchcount), 4); echo isset($groupselect) ? $groupselect : ''; echo '
'; if (empty($students)) { echo $OUTPUT->notification(get_string('noexistingparticipants', 'enrol')); } else { if ($canbulkmessaging) { echo '
'; } foreach ($students as $student) { //userpicture and link to the profilepage $profileurl = $CFG->wwwroot.'/user/view.php?id='.$student->id.'&course='.$course->id; $profilelink = ''.fullname($student).''; $data = array($OUTPUT->user_picture($student, array('courseid' => $course->id)), $profilelink); if ($student->feedbackstarted) { $data[] = get_string('started', 'feedback'); } else { $data[] = get_string('not_started', 'feedback'); } //selections to bulk messaging if ($canbulkmessaging) { $checkbox = new \core\output\checkbox_toggleall('feedback-non-respondents', false, [ 'id' => 'messageuser-' . $student->id, 'name' => 'messageuser[]', 'classes' => 'me-1', 'value' => $student->id, 'label' => get_string('includeuserinrecipientslist', 'mod_feedback', fullname($student)), 'labelclasses' => 'accesshide', ]); $data[] = $OUTPUT->render($checkbox); } $table->add_data($data); } $table->print_html(); $allurl = new moodle_url($baseurl); if ($showall) { $allurl->param('showall', 0); echo $OUTPUT->container(html_writer::link($allurl, get_string('showperpage', '', FEEDBACK_DEFAULT_PAGE_COUNT)), array(), 'showall'); } else if ($matchcount > 0 && $perpage < $matchcount) { $allurl->param('showall', 1); echo $OUTPUT->container(html_writer::link($allurl, get_string('showall', '', $matchcount)), array(), 'showall'); } if ($canbulkmessaging) { echo '
'; echo ''.get_string('send_message', 'feedback').''; echo '
'; echo ''; echo ''; echo '
'; echo $OUTPUT->print_textarea('message', 'edit-message', $message, 15, 25); print_string('formathtml'); echo ''; echo '
'; echo ''; echo '
'; echo ''; echo ''; echo ''; echo '
'; echo '
'; } } /// Finish the page /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// echo $OUTPUT->footer(); PKB\&FFanalysis_to_excel.phpnu[. /** * prints an analysed excel-spreadsheet of the feedback * * @copyright Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ require_once("../../config.php"); require_once("lib.php"); require_once("$CFG->libdir/excellib.class.php"); $id = required_param('id', PARAM_INT); // Course module id. $courseid = optional_param('courseid', '0', PARAM_INT); $url = new moodle_url('/mod/feedback/analysis_to_excel.php', array('id' => $id)); if ($courseid) { $url->param('courseid', $courseid); } $PAGE->set_url($url); list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback'); require_login($course, false, $cm); $context = context_module::instance($cm->id); require_capability('mod/feedback:viewreports', $context); $feedback = $PAGE->activityrecord; // Buffering any output. This prevents some output before the excel-header will be send. ob_start(); ob_end_clean(); // Get the questions (item-names). $feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id); if (!$items = $feedbackstructure->get_items(true)) { throw new \moodle_exception('no_items_available_yet', 'feedback', $cm->url); } $mygroupid = groups_get_activity_group($cm); // Creating a workbook. $filename = "feedback_" . clean_filename($cm->get_formatted_name()) . ".xls"; $workbook = new MoodleExcelWorkbook($filename); // Creating the worksheet. error_reporting(0); $worksheet1 = $workbook->add_worksheet(); error_reporting($CFG->debug); $worksheet1->hide_gridlines(); $worksheet1->set_column(0, 0, 10); $worksheet1->set_column(1, 1, 30); $worksheet1->set_column(2, 20, 15); // Creating the needed formats. $xlsformats = new stdClass(); $xlsformats->head1 = $workbook->add_format(['bold' => 1, 'size' => 12]); $xlsformats->head2 = $workbook->add_format(['align' => 'left', 'bold' => 1, 'bottum' => 2]); $xlsformats->default = $workbook->add_format(['align' => 'left', 'v_align' => 'top']); $xlsformats->value_bold = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top']); $xlsformats->procent = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top', 'num_format' => '#,##0.00']); // Writing the table header. $rowoffset1 = 0; $worksheet1->write_string($rowoffset1, 0, userdate(time()), $xlsformats->head1); // Get the completeds. $completedscount = $feedbackstructure->count_completed_responses($mygroupid); // Write the count of completeds. // Keep consistency and write count of completeds even when they are 0. $rowoffset1++; $worksheet1->write_string($rowoffset1, 0, get_string('completed_feedbacks', 'feedback').': '.strval($completedscount), $xlsformats->head1); $rowoffset1++; $worksheet1->write_string($rowoffset1, 0, get_string('questions', 'feedback').': '. strval(count($items)), $xlsformats->head1); $rowoffset1 += 2; $worksheet1->write_string($rowoffset1, 0, get_string('item_label', 'feedback'), $xlsformats->head1); $worksheet1->write_string($rowoffset1, 1, get_string('question', 'feedback'), $xlsformats->head1); $worksheet1->write_string($rowoffset1, 2, get_string('responses', 'feedback'), $xlsformats->head1); $rowoffset1++; foreach ($items as $item) { // Get the class of item-typ. $itemobj = feedback_get_item_class($item->typ); $rowoffset1 = $itemobj->excelprint_item($worksheet1, $rowoffset1, $xlsformats, $item, $mygroupid, $courseid); } $workbook->close(); PKB\z3sbbimport_form.phpnu[. /** * prints the forms to choose an xml-template file to import items * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ //It must be included from a Moodle page if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); } require_once($CFG->libdir.'/formslib.php'); class feedback_import_form extends moodleform { public function definition() { global $CFG; $mform =& $this->_form; $strdeleteolditmes = get_string('delete_old_items', 'feedback'). ' ('.get_string('oldvalueswillbedeleted', 'feedback').')'; $strnodeleteolditmes = get_string('append_new_items', 'feedback'). ' ('.get_string('oldvaluespreserved', 'feedback').')'; // hidden elements $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('filepicker', 'choosefile', get_string('file'), null, array('maxbytes' => $CFG->maxbytes, 'filetypes' => '*')); $mform->addElement('radio', 'deleteolditems', '', $strdeleteolditmes, true); $mform->addElement('radio', 'deleteolditems', '', $strnodeleteolditmes); // buttons $this->add_action_buttons(true, get_string('import')); } } PKB\Jueedeprecatedlib.phpnu[. /** * List of deprecated mod_feedback functions. * * @package mod_feedback * @copyright 2021 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Returns true if the current role is faked by switching role feature * * @return bool * @deprecated since Moodle 4.5 - please do not use this function any more, {@see is_role_switched} */ #[\core\attribute\deprecated('is_role_switched', since: '4.5', mdl: 'MDL-72424')] function feedback_check_is_switchrole(): bool { global $USER; \core\deprecation::emit_deprecation(__FUNCTION__); return isset($USER->switchrole) && is_array($USER->switchrole) && count($USER->switchrole) > 0; } PKB\)LL settings.phpnu[. defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { $options = array(0=>get_string('no'), 1=>get_string('yes')); $str = get_string('configallowfullanonymous', 'feedback'); $settings->add(new admin_setting_configselect('feedback_allowfullanonymous', get_string('allowfullanonymous', 'feedback'), $str, 0, $options)); } PKB\R,Y22use_templ_form.phpnu[. /** * prints the form to confirm use template * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ 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'); /** * The mod_feedback_use_templ_form * * @deprecated since 4.0. New dynamic forms have been created instead. * @todo MDL-83522 This class will be deleted in Moodle 6.0. */ #[\core\attribute\deprecated( replacement: 'mod_feedback\form\use_template_form', since: '4.0', mdl: 'MDL-71914', reason: 'New dynamic forms have been created instead.' )] class mod_feedback_use_templ_form extends moodleform { public function __construct($action = null, $customdata = null, $method = 'post', $target = '', $attributes = null, $editable = true, $ajaxformdata = null) { debugging('Class mod_feedback_use_templ_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); } public function definition() { $mform =& $this->_form; // visible elements $mform->addElement('radio', 'deleteolditems', '', get_string('delete_old_items', 'feedback'), 1); $mform->addElement('radio', 'deleteolditems', '', get_string('append_new_items', 'feedback'), 0); $mform->setType('deleteolditems', PARAM_INT); $mform->setDefault('deleteolditems', 1); // hidden elements $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'templateid'); $mform->setType('templateid', PARAM_INT); $mform->addElement('hidden', 'do_show'); $mform->setType('do_show', PARAM_INT); $mform->setConstant('do_show', 'edit'); //------------------------------------------------------------------------------- // buttons $this->add_action_buttons(); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @return array */ public static function get_js_module() { debugging('Class mod_feedback_use_templ_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::get_js_module(); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $simulatedsubmitteddata * @param array $simulatedsubmittedfiles * @param string $method * @param null $formidentifier * @return array */ public static function mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post', $formidentifier = null) { debugging('Class mod_feedback_use_templ_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::mock_ajax_submit($simulatedsubmitteddata, $simulatedsubmittedfiles, $method, $formidentifier); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $data * @return array */ public static function mock_generate_submit_keys($data = []) { debugging('Class mod_feedback_use_templ_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); return parent::mock_generate_submit_keys($data); } /** * Overrides parent static method for deprecation purposes. * * @deprecated since 4.0 * @param array $simulatedsubmitteddata * @param array $simulatedsubmittedfiles * @param string $method * @param null $formidentifier */ public static function mock_submit($simulatedsubmitteddata, $simulatedsubmittedfiles = array(), $method = 'post', $formidentifier = null) { debugging('Class mod_feedback_use_templ_form is deprecated. Replaced with dynamic forms.', DEBUG_DEVELOPER); parent::mock_submit($simulatedsubmitteddata, $simulatedsubmittedfiles, $method, $formidentifier); } } PKB\6mY&Y& mod_form.phpnu[. /** * print the form to add or edit a feedback-instance * * @author Andreas Grabs * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package mod_feedback */ //It must be included from a Moodle page if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); } require_once($CFG->dirroot.'/course/moodleform_mod.php'); class mod_feedback_mod_form extends moodleform_mod { public function definition() { global $CFG, $DB; $editoroptions = feedback_get_editor_options(); $mform =& $this->_form; //------------------------------------------------------------------------------- $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text', 'name', get_string('name', 'feedback'), array('size'=>'64')); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $this->standard_intro_elements(get_string('description', 'feedback')); //------------------------------------------------------------------------------- $mform->addElement('header', 'timinghdr', get_string('availability')); $mform->addElement('date_time_selector', 'timeopen', get_string('feedbackopen', 'feedback'), array('optional' => true)); $mform->addElement('date_time_selector', 'timeclose', get_string('feedbackclose', 'feedback'), array('optional' => true)); //------------------------------------------------------------------------------- $mform->addElement('header', 'feedbackhdr', get_string('questionandsubmission', 'feedback')); $options=array(); $options[1] = get_string('anonymous', 'feedback'); $options[2] = get_string('non_anonymous', 'feedback'); $mform->addElement('select', 'anonymous', get_string('anonymous_edit', 'feedback'), $options); // check if there is existing responses to this feedback if (is_numeric($this->_instance) AND $this->_instance AND $feedback = $DB->get_record("feedback", array("id"=>$this->_instance))) { $completed_feedback_count = feedback_get_completeds_group_count($feedback); } else { $completed_feedback_count = false; } if ($completed_feedback_count) { $multiple_submit_value = $feedback->multiple_submit ? get_string('yes') : get_string('no'); $mform->addElement('text', 'multiple_submit_static', get_string('multiplesubmit', 'feedback'), array('size'=>'4', 'disabled'=>'disabled', 'value'=>$multiple_submit_value)); $mform->setType('multiple_submit_static', PARAM_RAW); $mform->addElement('hidden', 'multiple_submit', ''); $mform->setType('multiple_submit', PARAM_INT); $mform->addHelpButton('multiple_submit_static', 'multiplesubmit', 'feedback'); } else { $mform->addElement('selectyesno', 'multiple_submit', get_string('multiplesubmit', 'feedback')); $mform->addHelpButton('multiple_submit', 'multiplesubmit', 'feedback'); } $mform->addElement('selectyesno', 'email_notification', get_string('email_notification', 'feedback')); $mform->addHelpButton('email_notification', 'email_notification', 'feedback'); $mform->addElement('selectyesno', 'autonumbering', get_string('autonumbering', 'feedback')); $mform->addHelpButton('autonumbering', 'autonumbering', 'feedback'); //------------------------------------------------------------------------------- $mform->addElement('header', 'aftersubmithdr', get_string('after_submit', 'feedback')); $mform->addElement('selectyesno', 'publish_stats', get_string('show_analysepage_after_submit', 'feedback')); $mform->addElement('editor', 'page_after_submit_editor', get_string("page_after_submit", "feedback"), null, $editoroptions); $mform->setType('page_after_submit_editor', PARAM_RAW); $mform->addElement('text', 'site_after_submit', get_string('url_for_continue', 'feedback'), array('size'=>'64', 'maxlength'=>'255')); $mform->setType('site_after_submit', PARAM_TEXT); $mform->addHelpButton('site_after_submit', 'url_for_continue', 'feedback'); //------------------------------------------------------------------------------- $this->standard_coursemodule_elements(); //------------------------------------------------------------------------------- // buttons $this->add_action_buttons(); } public function data_preprocessing(&$default_values) { $editoroptions = feedback_get_editor_options(); if ($this->current->instance) { // editing an existing feedback - let us prepare the added editor elements (intro done automatically) $draftitemid = file_get_submitted_draft_itemid('page_after_submit'); $default_values['page_after_submit_editor']['text'] = file_prepare_draft_area($draftitemid, $this->context->id, 'mod_feedback', 'page_after_submit', false, $editoroptions, $default_values['page_after_submit']); $default_values['page_after_submit_editor']['format'] = $default_values['page_after_submitformat']; $default_values['page_after_submit_editor']['itemid'] = $draftitemid; } else { // adding a new feedback instance $draftitemid = file_get_submitted_draft_itemid('page_after_submit_editor'); // no context yet, itemid not used file_prepare_draft_area($draftitemid, null, 'mod_feedback', 'page_after_submit', false); $default_values['page_after_submit_editor']['text'] = ''; $default_values['page_after_submit_editor']['format'] = editors_get_preferred_format(); $default_values['page_after_submit_editor']['itemid'] = $draftitemid; } } /** * Allows module to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data the form data to be modified. */ public function data_postprocessing($data) { parent::data_postprocessing($data); if (isset($data->page_after_submit_editor)) { $data->page_after_submitformat = $data->page_after_submit_editor['format']; $data->page_after_submit = $data->page_after_submit_editor['text']; if (!empty($data->completionunlocked)) { // Turn off completion settings if the checkboxes aren't ticked. $suffix = $this->get_suffix(); $completion = $data->{'completion' . $suffix}; $autocompletion = !empty($completion) && $completion == COMPLETION_TRACKING_AUTOMATIC; if (!$autocompletion || empty($data->{'completionsubmit' . $suffix})) { $data->{'completionsubmit' . $suffix} = 0; } } } } /** * Enforce validation rules here * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array **/ public function validation($data, $files) { $errors = parent::validation($data, $files); // Check open and close times are consistent. if ($data['timeopen'] && $data['timeclose'] && $data['timeclose'] < $data['timeopen']) { $errors['timeclose'] = get_string('closebeforeopen', 'feedback'); } return $errors; } public function add_completion_rules() { $mform =& $this->_form; $suffix = $this->get_suffix(); $completionsubmitel = 'completionsubmit' . $suffix; $mform->addElement('checkbox', $completionsubmitel, '', get_string('completionsubmit', 'feedback') ); // Enable this completion rule by default. $mform->setDefault($completionsubmitel, 1); return [$completionsubmitel]; } public function completion_rule_enabled($data) { $suffix = $this->get_suffix(); return !empty($data['completionsubmit' . $suffix]); } } PKM\sLlang/en/block_feedback.phpnu[. /** * Strings for component 'block_feedback', language 'en', branch 'MOODLE_20_STABLE' * * @package block_feedback * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['feedback'] = 'Feedback'; $string['feedback:addinstance'] = 'Add a new feedback block'; $string['pluginname'] = 'Feedback'; $string['privacy:metadata'] = 'The Feedback block only shows data stored in other locations.'; PKM\hfL* * block_feedback.phpnu[. /** * Feedback block. * * @package block_feedback * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/feedback/lib.php'); class block_feedback extends block_list { function init() { $this->title = get_string('feedback', 'block_feedback'); } function applicable_formats() { return array('site' => true, 'course' => true); } function get_content() { global $CFG, $OUTPUT; if ($this->content !== NULL) { return $this->content; } $this->content = new stdClass; $this->content->items = array(); $this->content->icons = array(); $this->content->footer = ''; $courseid = $this->page->course->id; if ($courseid <= 0) { $courseid = SITEID; } $icon = $OUTPUT->image_icon('monologo', get_string('pluginname', 'mod_feedback'), 'mod_feedback'); if (empty($this->instance->pageid)) { $this->instance->pageid = SITEID; } if ($feedbacks = feedback_get_feedbacks_from_sitecourse_map($courseid)) { $baseurl = new moodle_url('/mod/feedback/view.php'); foreach ($feedbacks as $feedback) { $url = new moodle_url($baseurl); $url->params(array('id'=>$feedback->cmid, 'courseid'=>$courseid)); $this->content->items[] = ''.$icon.$feedback->name.''; } } return $this->content; } } PKB\Fb & &item/label/lib.phpnu[PKB\uAkkK&item/label/label_form.phpnu[PKB\T-item/info/info_form.phpnu[PKB\117item/info/lib.phpnu[PKB\aniitem/textarea/lib.phpnu[PKB\. item/textarea/textarea_form.phpnu[PKB\Y>}qOqO<item/multichoice/lib.phpnu[PKB\\q9%item/multichoice/multichoice_form.phpnu[PKB\s22*item/feedback_item_class.phpnu[PKB\yn'item/textfield/lib.phpnu[PKB\51 1 !iGitem/textfield/textfield_form.phpnu[PKB\....Qitem/numeric/lib.phpnu[PKB\}!4 4 ]item/numeric/numeric_form.phpnu[PKB\--ތitem/captcha/lib.phpnu[PKB\Q88Oitem/captcha/captcha_form.phpnu[PKB\z܇tԱitem/captcha/print_captcha.phpnu[PKB\3?!\L\Litem/multichoicerated/lib.phpnu[PKB\nUB!/item/multichoicerated/multichoicerated_form.phpnu[PKB\׫WW!#item/feedback_item_form_class.phpnu[PKB\׶7db/services.phpnu[PKB\~ cmmRdb/install.phpnu[PKB\fF Wdb/log.phpnu[PKB\A   h]db/access.phpnu[PKB\~bdb/upgrade.phpnu[PKB\Ώ''kdb/install.xmlnu[PKB\IJj@@ db/events.phpnu[PKB\ƨ'db/messages.phpnu[PKB\ϝlang/en/deprecated.txtnu[PKB\B/G/G(lang/en/feedback.phpnu[PKB\b[ 6backup/moodle2/backup_feedback_activity_task.class.phpnu[PKB\e xx.backup/moodle2/backup_feedback_settingslib.phpnu[PKB\|A,nbackup/moodle2/restore_feedback_stepslib.phpnu[PKB\u7Q backup/moodle2/restore_feedback_activity_task.class.phpnu[PKB\7+nbackup/moodle2/backup_feedback_stepslib.phpnu[PKB\s''j3backup/moodle1/lib.phpnu[PKB\7""Iview.phpnu[PKB\pI,, 1aedit_form.phpnu[PKB\11 edit_item.phpnu[PKB\4Y  amapcourse.phpnu[PKB\aV print.phpnu[PKB\><pix/notrequired.svgnu[PKB\IQpix/required.svgnu[PKB\~<pix/monologo.svgnu[PKB\6Yëëӿlib.phpnu[PKB\P߭ kversion.phpnu[PKB\Apshow_entries.phpnu[PKB\;މ export.phpnu[PKB\] ] ڡmanage_templates.phpnu[PKB\/bM!!{analysis_course.phpnu[PKB\]:}classes/external.phpnu[PKB\lGG%classes/form/create_template_form.phpnu[PKB\Bk=="classes/form/use_template_form.phpnu[PKB\.BB.(classes/external/feedback_summary_exporter.phpnu[PKB\>G?e e &classes/external/questions/reorder.phpnu[PKB\  upgrade.txtnu[PKB\A9    analysis.phpnu[PKB\8Z$ > )q tests/external/questions/reorder_test.phpnu[PKB\i  tests/external/external_test.phpnu[PKB\$Lcc tests/privacy/provider_test.phpnu[PKB\G(we tests/lib_test.phpnu[PKB\n n ' tests/item_test.phpnu[PKB\TgGUU tests/completion_test.phpnu[PKB\044 v tests/custom_completion_test.phpnu[PKB\</tests/dates_test.phpnu[PKB\b77tests/event/events_test.phpnu[PKB\.)))Wtests/access_test.phpnu[PKB\B~w+w+,tests/generator_test.phpnu[PKB\,,tests/behat/anonymous.featurenu[PKB\ҷ))"&tests/behat/behat_mod_feedback.phpnu[PKB\~,,!"tests/behat/coursemapping.featurenu[PKB\OD!W1tests/behat/non_anonymous.featurenu[PKB\؟!Ntests/behat/export_import.featurenu[PKB\GOvhtests/behat/questions.featurenu[PKB\qN& #ctests/behat/multipleattempt.featurenu[PKB\q=K 'tests/behat/show_nonrespondents.featurenu[PKB\ؕp5 5 "tests/behat/show_responses.featurenu[PKB\ָ{U{U5tests/behat/multichoice.featurenu[PKB\  #tests/behat/overview_report.featurenu[PKB\T L L %^ tests/behat/preview_questions.featurenu[PKB\E"tests/behat/question_types.featurenu[PKB\HjQ:Q:4tests/behat/groups.featurenu[PKB\$y6V 0otests/behat/feedback_activity_completion.featurenu[PKB\Rn?+}tests/behat/question_types_non_anon.featurenu[PKB\F&&5 tests/behat/feedback_no_calendar_capabilities.featurenu[PKB\+l4Y+Y+tests/behat/templates.featurenu[PKB\M##$;tests/courseformat/overview_test.phpnu[PKB\p.^tests/manager_test.phpnu[PKB\3ܚ{U0U01show_nonrespondents.phpnu[PKB\&FF6analysis_to_excel.phpnu[PKB\z3sbbXGimport_form.phpnu[PKB\JueeOdeprecatedlib.phpnu[PKB\)LL Usettings.phpnu[PKB\R,Y22'Zuse_templ_form.phpnu[PKB\6mY&Y& mmod_form.phpnu[PKM\sL0lang/en/block_feedback.phpnu[PKM\hfL* * block_feedback.phpnu[PKQ9