'.$str.' | '; - } - $out .= '
'.get_string('noresponsedata', 'questionnaire').'
'; - } - return $output; - } - - /** - * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. - * - * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. - * @return array - */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { - global $DB; - - $values = []; - $sql = 'SELECT a.id as aid, q.id AS qid, q.precise AS precise, c.id AS cid '.$col.', c.content as ccontent, - a.rankvalue as arank '. - 'FROM {'.self::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '. - 'WHERE a.response_id= ? AND a.question_id=q.id AND a.choice_id=c.id '. - 'ORDER BY aid, a.question_id, c.id'; - $records = $DB->get_records_sql($sql, [$rid]); - foreach ($records as $row) { - // Next two are 'qid' and 'cid', each with numeric and hash keys. - $osgood = false; - if ($row->precise == 3) { - $osgood = true; - } - $qid = $row->qid.'_'.$row->cid; - unset($row->aid); // Get rid of the answer id. - unset($row->qid); - unset($row->cid); - unset($row->precise); - $row = (array)$row; - $newrow = []; - foreach ($row as $key => $val) { - if ($key != 'content') { // No need to keep question text - ony keep choice text and rank. - if ($key == 'ccontent') { - if ($osgood) { - list($contentleft, $contentright) = array_merge(preg_split('/[|]/', $val), [' ']); - $contents = questionnaire_choice_values($contentleft); - if ($contents->title) { - $contentleft = $contents->title; - } - $contents = questionnaire_choice_values($contentright); - if ($contents->title) { - $contentright = $contents->title; - } - $val = strip_tags($contentleft.'|'.$contentright); - $val = preg_replace("/[\r\n\t]/", ' ', $val); - } else { - $contents = questionnaire_choice_values($val); - if ($contents->modname) { - $val = $contents->modname; - } else if ($contents->title) { - $val = $contents->title; - } else if ($contents->text) { - $val = strip_tags($contents->text); - $val = preg_replace("/[\r\n\t]/", ' ', $val); - } - } - } - $newrow[] = $val; - } - } - $values[$qid] = $newrow; - } - - return $values; - } - - /** - * Configure bulk sql - * @return bulk_sql_config - */ - protected function bulk_sql_config() { - return new bulk_sql_config(self::response_table(), 'qrr', true, false, true); - } - -} \ No newline at end of file diff --git a/classes/response/single.php b/classes/response/single.php deleted file mode 100644 index cdc4b647..00000000 --- a/classes/response/single.php +++ /dev/null @@ -1,353 +0,0 @@ -. - -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); - -/** - * Class for single response types. - * - * @author Mike Churchward - * @package responsetypes - */ - -class single extends base { - static public function response_table() { - return 'questionnaire_resp_single'; - } - - public function insert_response($rid, $val) { - global $DB; - if (!empty($val)) { - foreach ($this->question->choices as $cid => $choice) { - if (strpos($choice->content, '!other') === 0) { - $other = optional_param('q'.$this->question->id.'_'.$cid, null, PARAM_TEXT); - if (!isset($other)) { - continue; - } - if (preg_match("/[^ \t\n]/", $other)) { - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = $cid; - $record->response = $other; - $resid = $DB->insert_record('questionnaire_response_other', $record); - $val = $cid; - break; - } - } - } - } - if (preg_match("/other_q([0-9]+)/", (isset($val) ? $val : ''), $regs)) { - $cid = $regs[1]; - if (!isset($other)) { - $other = optional_param('q'.$this->question->id.'_'.$cid, null, PARAM_TEXT); - } - if (preg_match("/[^ \t\n]/", $other)) { - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = $cid; - $record->response = $other; - $resid = $DB->insert_record('questionnaire_response_other', $record); - $val = $cid; - } - } - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = isset($val) ? $val : 0; - if ($record->choice_id) {// If "no answer" then choice_id is empty (CONTRIB-846). - try { - return $DB->insert_record(static::response_table(), $record); - } catch (\dml_write_exception $ex) { - return false; - } - } else { - return false; - } - } - - public function get_results($rids=false, $anonymous=false) { - global $DB; - - $rsql = ''; - $params = array($this->question->id); - if (!empty($rids)) { - list($rsql, $rparams) = $DB->get_in_or_equal($rids); - $params = array_merge($params, $rparams); - $rsql = ' AND response_id ' . $rsql; - } - - // Added qc.id to preserve original choices ordering. - $sql = 'SELECT rt.id, qc.id as cid, qc.content ' . - 'FROM {questionnaire_quest_choice} qc, ' . - '{'.static::response_table().'} rt ' . - 'WHERE qc.question_id= ? AND qc.content NOT LIKE \'!other%\' AND ' . - 'rt.question_id=qc.question_id AND rt.choice_id=qc.id' . $rsql . ' ' . - 'ORDER BY qc.id'; - - $rows = $DB->get_records_sql($sql, $params); - - // Handle 'other...'. - $sql = 'SELECT rt.id, rt.response, qc.content ' . - 'FROM {questionnaire_response_other} rt, ' . - '{questionnaire_quest_choice} qc ' . - 'WHERE rt.question_id= ? AND rt.choice_id=qc.id' . $rsql . ' ' . - 'ORDER BY qc.id'; - - if ($recs = $DB->get_records_sql($sql, $params)) { - $i = 1; - foreach ($recs as $rec) { - $rows['other'.$i] = new \stdClass(); - $rows['other'.$i]->content = $rec->content; - $rows['other'.$i]->response = $rec->response; - $i++; - } - } - - return $rows; - } - - /** - * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. - * @param array $rids - * @return array | boolean - */ - public function get_feedback_scores(array $rids) { - global $DB; - - $rsql = ''; - $params = [$this->question->id]; - if (!empty($rids)) { - list($rsql, $rparams) = $DB->get_in_or_equal($rids); - $params = array_merge($params, $rparams); - $rsql = ' AND response_id ' . $rsql; - } - $params[] = 'y'; - - $sql = 'SELECT response_id as rid, c.value AS score ' . - 'FROM {'.$this->response_table().'} r ' . - 'INNER JOIN {questionnaire_quest_choice} c ON r.choice_id = c.id ' . - 'WHERE r.question_id= ? ' . $rsql . ' ' . - 'ORDER BY response_id ASC'; - return $DB->get_records_sql($sql, $params); - } - - /** - * Provide a template for results screen if defined. - * @return mixed The template string or false/ - */ - public function results_template() { - return 'mod_questionnaire/results_choice'; - } - - /** - * Return the JSON structure required for the template. - * - * @param bool $rids - * @param string $sort - * @param bool $anonymous - * @return string - */ - public function display_results($rids=false, $sort='', $anonymous=false) { - global $DB; - - $rows = $this->get_results($rids, $anonymous); - if (is_array($rids)) { - $prtotal = 1; - } else if (is_int($rids)) { - $prtotal = 0; - } - $numresps = count($rids); - - $responsecountsql = 'SELECT COUNT(DISTINCT r.response_id) ' . - 'FROM {' . $this->response_table() . '} r ' . - 'WHERE r.question_id = ? '; - $numrespondents = $DB->count_records_sql($responsecountsql, [$this->question->id]); - - if ($rows) { - foreach ($rows as $idx => $row) { - if (strpos($idx, 'other') === 0) { - $answer = $row->response; - $ccontent = $row->content; - $content = preg_replace(array('/^!other=/', '/^!other/'), - array('', get_string('other', 'questionnaire')), $ccontent); - $content .= ' ' . clean_text($answer); - $textidx = $content; - $this->counts[$textidx] = !empty($this->counts[$textidx]) ? ($this->counts[$textidx] + 1) : 1; - } else { - $contents = questionnaire_choice_values($row->content); - $this->choice = $contents->text.$contents->image; - $textidx = $this->choice; - $this->counts[$textidx] = !empty($this->counts[$textidx]) ? ($this->counts[$textidx] + 1) : 1; - } - } - $pagetags = $this->get_results_tags($this->counts, $numresps, $numrespondents, $prtotal, $sort); - } else { - $pagetags = new \stdClass(); - } - return $pagetags; - } - - /** - * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. - * - * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. - * @return array - */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { - global $DB; - - $values = []; - $sql = 'SELECT q.id '.$col.', q.type_id as q_type, c.content as ccontent,c.id as cid '. - 'FROM {'.static::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '. - 'WHERE a.response_id = ? AND a.question_id=q.id AND a.choice_id=c.id '; - $records = $DB->get_records_sql($sql, [$rid]); - foreach ($records as $qid => $row) { - $cid = $row->cid; - if ($csvexport) { - static $i = 1; - $qrecords = $DB->get_records('questionnaire_quest_choice', ['question_id' => $qid]); - foreach ($qrecords as $value) { - if ($value->id == $cid) { - $contents = questionnaire_choice_values($value->content); - if ($contents->modname) { - $row->ccontent = $contents->modname; - } else { - $content = $contents->text; - if (preg_match('/^!other/', $content)) { - $row->ccontent = get_string('other', 'questionnaire'); - } else if (($choicecodes == 1) && ($choicetext == 1)) { - $row->ccontent = "$i : $content"; - } else if ($choicecodes == 1) { - $row->ccontent = "$i"; - } else { - $row->ccontent = $content; - } - } - $i = 1; - break; - } - $i++; - } - } - unset($row->id); - unset($row->cid); - unset($row->q_type); - $arow = get_object_vars($row); - $newrow = []; - foreach ($arow as $key => $val) { - if (!is_numeric($key)) { - $newrow[] = $val; - } - } - if (preg_match('/^!other/', $row->ccontent)) { - $newrow[] = 'other_' . $cid; - } else { - $newrow[] = (int)$cid; - } - $values[$qid] = $newrow; - } - - return $values; - } - - /** - * Return sql and params for getting responses in bulk. - * @author Guy Thomas - * @param int|array $questionnaireids One id, or an array of ids. - * @param bool|int $responseid - * @param bool|int $userid - * @return array - */ - public function get_bulk_sql($questionnaireids, $responseid = false, $userid = false, $groupid = false, $showincompletes = 0) { - global $DB; - - $sql = $this->bulk_sql(); - if (($groupid !== false) && ($groupid > 0)) { - $groupsql = ' INNER JOIN {groups_members} gm ON gm.groupid = ? AND gm.userid = qr.userid '; - $gparams = [$groupid]; - } else { - $groupsql = ''; - $gparams = []; - } - - if (is_array($questionnaireids)) { - list($qsql, $params) = $DB->get_in_or_equal($questionnaireids); - } else { - $qsql = ' = ? '; - $params = [$questionnaireids]; - } - if ($showincompletes == 1) { - $showcompleteonly = ''; - } else { - $showcompleteonly = 'AND qr.complete = ? '; - $params[] = 'y'; - } - - $sql .= " - AND qr.questionnaireid $qsql $showcompleteonly - LEFT JOIN {questionnaire_response_other} qro ON qro.response_id = qr.id AND qro.choice_id = qrs.choice_id - LEFT JOIN {user} u ON u.id = qr.userid - $groupsql - "; - $params = array_merge($params, $gparams); - - if ($responseid) { - $sql .= " WHERE qr.id = ?"; - $params[] = $responseid; - } else if ($userid) { - $sql .= " WHERE qr.userid = ?"; - $params[] = $userid; - } - - return [$sql, $params]; - } - - /** - * Return sql for getting responses in bulk. - * @author Guy Thomas - * @return string - */ - protected function bulk_sql() { - global $DB; - - $userfields = $this->user_fields_sql(); - $alias = 'qrs'; - $extraselect = 'qrs.choice_id, ' . $DB->sql_order_by_text('qro.response', 1000) . ' AS response, 0 AS rankvalue'; - - return " - SELECT " . $DB->sql_concat_join("'_'", ['qr.id', "'".$this->question->helpname()."'", $alias.'.id']) . " AS id, - qr.submitted, qr.complete, qr.grade, qr.userid, $userfields, qr.id AS rid, $alias.question_id, - $extraselect - FROM {questionnaire_response} qr - JOIN {".static::response_table()."} $alias ON $alias.response_id = qr.id - "; - } -} diff --git a/classes/responsetype/answer/answer.php b/classes/responsetype/answer/answer.php new file mode 100644 index 00000000..7f9bab53 --- /dev/null +++ b/classes/responsetype/answer/answer.php @@ -0,0 +1,83 @@ +. + +namespace mod_questionnaire\responsetype\answer; + +/** + * This defines a structured class to hold question answers. + * + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + * @copyright 2019, onwards Poet + */ +class answer { + + // Class properties. + + /** @var int $id The id of the question response data record this applies to. */ + public $id; + + /** @var int $responseid This id of the response record this applies to. */ + public $responseid; + + /** @var int $questionid The id of the question this response applies to. */ + public $questionid; + + /** @var string $content The choiceid of this response (if applicable). */ + public $choiceid; + + /** @var string $value The value of this response (if applicable). */ + public $value; + + /** + * Answer constructor. + * @param null $id + * @param null $responseid + * @param null $questionid + * @param null $choiceid + * @param null $value + */ + public function __construct($id = null, $responseid = null, $questionid = null, $choiceid = null, $value = null) { + $this->id = $id; + $this->responseid = $responseid; + $this->questionid = $questionid; + $this->choiceid = $choiceid; + $this->value = $value; + } + + /** + * Create and return an answer object from data. + * + * @param \stdClass|array $answerdata The data to load. + * @return answer + */ + public static function create_from_data($answerdata) { + if (!is_array($answerdata)) { + $answerdata = (array)$answerdata; + } + + $properties = array_keys(get_class_vars(__CLASS__)); + foreach ($properties as $property) { + if (!isset($answerdata[$property])) { + $answerdata[$property] = null; + } + } + + return new answer($answerdata['id'], $answerdata['responseid'], $answerdata['questionid'], $answerdata['choiceid'], + $answerdata['value']); + } +} diff --git a/classes/response/boolean.php b/classes/responsetype/boolean.php similarity index 52% rename from classes/response/boolean.php rename to classes/responsetype/boolean.php index dddcfcba..6dbe39de 100644 --- a/classes/response/boolean.php +++ b/classes/responsetype/boolean.php @@ -14,45 +14,100 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see') { $question->content = ''; } - $this->page->add_to_page('responses', - $this->renderer->container(format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php', + if ($pdf) { + $response = new stdClass(); + if ($question->is_numbered()) { + $response->qnum = $qnum; + } + $response->qcontent = format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php', $question->context->id, 'mod_questionnaire', 'question', $question->id), - FORMAT_HTML, ['noclean' => true]), 'qn-question')); - $this->page->add_to_page('responses', $this->renderer->results_output($question, $rids, $sort, $anonymous)); - $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-content. - $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-container. + FORMAT_HTML, ['noclean' => true]); + $response->results = $this->renderer->results_output($question, $rids, $sort, $anonymous, $pdf); + $this->page->add_to_page('responses', $response); + } else { + $this->page->add_to_page('responses', + $this->renderer->container(format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php', + $question->context->id, 'mod_questionnaire', 'question', $question->id), + FORMAT_HTML, ['noclean' => true]), 'qn-question')); + $this->page->add_to_page('responses', $this->renderer->results_output($question, $rids, $sort, $anonymous)); + $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-content. + $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-container. + } } return; @@ -2540,12 +2952,9 @@ public function survey_results($precision = 1, $showtotals = 1, $qid = '', $cids /** * Get unique list of question types used in the current survey. - * - * @author: Guy Thomas - * @param int $surveyid + * author: Guy Thomas * @param bool $uniquebytable * @return array - * @throws moodle_exception */ protected function get_survey_questiontypes($uniquebytable = false) { @@ -2580,22 +2989,26 @@ protected function choice_types() { /** * Return all the fields to be used for users in questionnaire sql. - * - * @author: Guy Thomas + * author: Guy Thomas * @return array|string */ protected function user_fields() { - $userfieldsarr = get_all_user_name_fields(); + if (class_exists('\core_user\fields')) { + $userfieldsarr = \core_user\fields::get_name_fields(); + } else { + $userfieldsarr = get_all_user_name_fields(); + } $userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution']); return $userfieldsarr; } /** * Get all survey responses in one go. - * - * @author: Guy Thomas + * author: Guy Thomas * @param string $rid * @param string $userid + * @param bool $groupid + * @param int $showincompletes * @return array */ protected function get_survey_all_responses($rid = '', $userid = '', $groupid = false, $showincompletes = 0) { @@ -2606,25 +3019,25 @@ protected function get_survey_all_responses($rid = '', $userid = '', $groupid = // If a questionnaire is "public", and this is the master course, need to get responses from all instances. if ($this->survey_is_public_master()) { - $qids = array_keys($DB->get_records('questionnaire', ['sid' => $this->sid], 'id')); + $qids = array_keys($DB->get_records('questionnaire', ['sid' => $this->sid], 'id') ?? []); } else { $qids = $this->id; } foreach ($uniquetypes as $type) { - $question = \mod_questionnaire\question\base::question_builder($type); - if (!isset($question->response)) { + $question = \mod_questionnaire\question\question::question_builder($type); + if (!isset($question->responsetype)) { continue; } $allresponsessql .= $allresponsessql == '' ? '' : ' UNION ALL '; - list ($sql, $params) = $question->response->get_bulk_sql($qids, $rid, $userid, $groupid, $showincompletes); + list ($sql, $params) = $question->responsetype->get_bulk_sql($qids, $rid, $userid, $groupid, $showincompletes); $allresponsesparams = array_merge($allresponsesparams, $params); $allresponsessql .= $sql; } $allresponsessql .= " ORDER BY usrid, id"; $allresponses = $DB->get_recordset_sql($allresponsessql, $allresponsesparams); - return $allresponses; + return $allresponses ?? []; } /** @@ -2647,39 +3060,29 @@ public function survey_is_public_master() { /** * Process individual row for csv output - * @param array $outputrow output row + * @param array $row * @param stdClass $resprow resultset row * @param int $currentgroupid * @param array $questionsbyposition * @param int $nbinfocols * @param int $numrespcols + * @param array $options + * @param array $identityfields * @return array - * @throws Exception - * @throws coding_exception - * @throws dml_exception - * @throws dml_missing_record_exception - * @throws dml_multiple_records_exception */ protected function process_csv_row(array &$row, stdClass $resprow, $currentgroupid, array &$questionsbyposition, $nbinfocols, - $numrespcols, $showincompletes = 0) { + $numrespcols, + $options, + $identityfields) { global $DB; - static $config = null; // If using an anonymous response, map users to unique user numbers so that number of unique anonymous users can be seen. static $anonumap = []; - if ($config === null) { - $config = get_config('questionnaire', 'downloadoptions'); - } - $options = empty($config) ? array() : explode(',', $config); - if ($showincompletes == 1) { - $options[] = 'complete'; - } - $positioned = []; $user = new stdClass(); foreach ($this->user_fields() as $userfield) { @@ -2775,6 +3178,9 @@ protected function process_csv_row(array &$row, if (in_array('complete', $options)) { array_push($positioned, $resprow->complete); } + foreach ($identityfields as $field) { + array_push($positioned, $resprow->$field); + } for ($c = $nbinfocols; $c < $numrespcols; $c++) { if (isset($row[$c])) { @@ -2794,10 +3200,19 @@ protected function process_csv_row(array &$row, return $positioned; } - /* {{{ proto array survey_generate_csv(int surveyid) - Exports the results of a survey to an array. - */ - public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $currentgroupid, $showincompletes = 0) { + /** + * Exports the results of a survey to an array. + * @param int $currentgroupid + * @param string $rid + * @param string $userid + * @param int $choicecodes + * @param int $choicetext + * @param int $showincompletes + * @param int $rankaverages + * @return array + */ + public function generate_csv($currentgroupid, $rid='', $userid='', $choicecodes=1, $choicetext=0, $showincompletes=0, + $rankaverages=0) { global $DB; raise_memory_limit('1G'); @@ -2816,11 +3231,18 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, if (in_array($option, array('response', 'submitted', 'id'))) { $columns[] = get_string($option, 'questionnaire'); $types[] = 0; + } else if ($option == 'useridentityfields') { + // Ignore option. + continue; } else { $columns[] = get_string($option); $types[] = 1; } } + $identityfields = $this->get_identity_fields($options); + foreach ($identityfields as $field) { + $columns[] = \core_user\fields::get_display_name($field); + } $nbinfocols = count($columns); $idtocsvmap = array( @@ -2834,11 +3256,12 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, '0', // 7: rating -> number '0', // 8: rate -> number '1', // 9: date -> string - '0' // 10: numeric -> number. + '0', // 10: numeric -> number. + '0', // 11: slider -> number. ); if (!$survey = $DB->get_record('questionnaire_survey', array('id' => $this->survey->id))) { - print_error ('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } // Get all responses for this survey in one go. @@ -2878,7 +3301,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, foreach ($this->questions as $question) { // Skip questions that aren't response capable. - if (!isset($question->response)) { + if (!isset($question->responsetype)) { continue; } // Establish the table's field names. @@ -2904,7 +3327,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, foreach ($choices as $choice) { $content = $choice->content; // If "Other" add a column for the actual "other" text entered. - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { $col = $choice->name.'_'.$stringother; $columns[][$qpos] = $col; $questionidcols[][$qpos] = null; @@ -2932,7 +3355,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, array_push($types, '0'); // If "Other" add a column for the "other" checkbox. // Then add a column for the actual "other" text entered. - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { $content = $stringother; $col = $choice->name.'->['.$content.']'; $columns[][$qpos] = $col; @@ -2948,7 +3371,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $modality = ''; $content = $choice->content; $osgood = false; - if ($choice->precise == 3) { + if (\mod_questionnaire\question\rate::type_is_osgood_rate_scale($choice->precise)) { $osgood = true; } if (preg_match("/^[0-9]{1,3}=/", $content, $ndd)) { @@ -3033,19 +3456,65 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $formatoptions = new stdClass(); $formatoptions->filter = false; // To prevent any filtering in CSV output. + if ($rankaverages) { + $averages = []; + $rids = []; + $allresponsesrs2 = $this->get_survey_all_responses($rid, $userid, $currentgroupid); + foreach ($allresponsesrs2 as $responserow) { + if (!isset($rids[$responserow->rid])) { + $rids[$responserow->rid] = $responserow->rid; + } + } + } + // Get textual versions of responses, add them to output at the correct col position. $prevresprow = false; // Previous response row. $row = []; + if ($rankaverages) { + $averagerow = []; + } + $useridentityfields = []; foreach ($allresponsesrs as $responserow) { $rid = $responserow->rid; $qid = $responserow->question_id; + + // It's possible for a response to exist for a deleted question. Ignore these. + if (!isset($this->questions[$qid])) { + continue; + } + + if (!empty($identityfields)) { + // Get identity fields for user. + if (isset($useridentityfields[$responserow->userid])) { + $customfields = $useridentityfields[$responserow->userid]; + } else { + $customfields = self::get_user_identity_fields($this->context, $responserow->userid); + $useridentityfields[$responserow->userid] = $customfields; + } + + // Set profile fields for user in response row. + foreach ($identityfields as $field) { + $responserow->{$field} = $customfields->{$field}; + } + } + $question = $this->questions[$qid]; $qtype = intval($question->type_id); + if ($rankaverages) { + if ($qtype === QUESRATE) { + if (empty($averages[$qid])) { + $results = $this->questions[$qid]->responsetype->get_results($rids); + foreach ($results as $qresult) { + $averages[$qid][$qresult->id] = $qresult->average; + } + } + } + } $questionobj = $this->questions[$qid]; if ($prevresprow !== false && $prevresprow->rid !== $rid) { $output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition, - $nbinfocols, $numrespcols, $showincompletes); + $nbinfocols, $numrespcols, $options, $identityfields); $row = []; } @@ -3053,10 +3522,13 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $key = $qid.'_'.$responserow->choice_id; $position = $questionpositions[$key]; if ($qtype === QUESRATE) { - $choicetxt = $responserow->rankvalue + 1; + $choicetxt = $responserow->rankvalue; + if ($rankaverages) { + $averagerow[$position] = $averages[$qid][$responserow->choice_id]; + } } else { $content = $choicesbyqid[$qid][$responserow->choice_id]->content; - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { // If this is an "other" column, put the text entered in the next position. $row[$position + 1] = $responserow->response; $choicetxt = empty($responserow->choice_id) ? '0' : '1'; @@ -3085,9 +3557,9 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, } $content = $choicesbyqid[$qid][$responserow->choice_id]->content; - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { // If this has an "other" text, use it. - $responsetxt = get_string('other', 'questionnaire'); + $responsetxt = \mod_questionnaire\question\choice::content_other_choice_display($content); $responsetxt1 = $responserow->response; } else if (($choicecodes == 1) && ($choicetext == 1)) { $responsetxt = $c.' : '.$content; @@ -3112,6 +3584,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $row[$position] = $responsetxt; // Check for "other" text and set it to the next position if present. if (!empty($responsetxt1)) { + $responsetxt1 = preg_replace("/[\r\n\t]/", ' ', $responsetxt1); $row[$position + 1] = $responsetxt1; unset($responsetxt1); } @@ -3123,7 +3596,22 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, if ($prevresprow !== false) { // Add final row to output. May not exist if no response data was ever present. $output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition, - $nbinfocols, $numrespcols, $showincompletes); + $nbinfocols, $numrespcols, $options, $identityfields); + } + + // Add averages row if appropriate. + if ($rankaverages) { + $summaryrow = []; + $summaryrow[0] = get_string('averagesrow', 'questionnaire'); + $i = 1; + for ($i = 1; $i < $nbinfocols; $i++) { + $summaryrow[$i] = ''; + } + $pos = 0; + for ($i = $nbinfocols; $i < $numrespcols; $i++) { + $summaryrow[$i] = isset($averagerow[$i]) ? $averagerow[$i] : ''; + } + $output[] = $summaryrow; } // Change table headers to incorporate actual question numbers. @@ -3168,7 +3656,6 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, * @param int $movetopos The position to move question to. * */ - public function move_question($moveqid, $movetopos) { global $DB; @@ -3195,6 +3682,17 @@ public function move_question($moveqid, $movetopos) { return false; } + /** + * Render the response analysis page. + * @param int $rid + * @param array $resps + * @param bool $compare + * @param bool $isgroupmember + * @param bool $allresponses + * @param int $currentgroupid + * @param array $filteredsections + * @return array|string + */ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid, $filteredsections = null) { global $DB, $CFG; @@ -3209,10 +3707,16 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $action = optional_param('action', 'vall', PARAM_ALPHA); - if ($resp = $DB->get_record('questionnaire_response', ['id' => $rid]) ) { + $resp = $DB->get_record('questionnaire_response', ['id' => $rid]); + if (!empty($resp)) { $userid = $resp->userid; - if ($user = $DB->get_record('user', ['id' => $userid])) { - $ruser = fullname($user); + $user = $DB->get_record('user', ['id' => $userid]); + if (!empty($user)) { + if ($this->respondenttype == 'anonymous') { + $ruser = '- ' . get_string('anonymous', 'questionnaire') . ' -'; + } else { + $ruser = fullname($user); + } } } // Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups). @@ -3307,11 +3811,12 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $sectionlabel = $fbsections[$sectionid]->sectionlabel; $sectionheading = $fbsections[$sectionid]->sectionheading; - $feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $sectionid]); $labels = array(); - foreach ($feedbacks as $feedback) { - if ($feedback->feedbacklabel != '') { - $labels[] = $feedback->feedbacklabel; + if ($feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $sectionid])) { + foreach ($feedbacks as $feedback) { + if ($feedback->feedbacklabel != '') { + $labels[] = $feedback->feedbacklabel; + } } } $feedback = $DB->get_record_select('questionnaire_feedback', @@ -3349,8 +3854,8 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $usergraph = get_config('questionnaire', 'usergraph'); if ($usergraph && $this->survey->chart_type) { $this->page->add_to_page('feedbackcharts', - draw_chart ($feedbacktype = 'global', $this->survey->chart_type, $labels, - $score, $allscore, $sectionlabel, $groupname, $allresponses)); + draw_chart ($feedbacktype = 'global', $labels, $groupname, + $allresponses, $this->survey->chart_type, $score, $allscore, $sectionlabel)); } // Display class or group score. Pending chart library decision to display? // Find out if this feedback sectionlabel has a pipe separator. @@ -3363,6 +3868,7 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $oppositeallscore = ' | '.$allscore[1].'%'; } if ($this->survey->feedbackscores) { + $table = $table ?? new html_table(); if ($compare) { $table->data[] = array($sectionlabel, $score[0].'%'.$oppositescore, $allscore[0].'%'.$oppositeallscore); } else { @@ -3404,13 +3910,13 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre foreach ($fbsections as $key => $fbsection) { if ($fbsection->section == $section) { $feedbacksectionid = $key; - $scorecalculation = unserialize($fbsection->scorecalculation); + $scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation); if (empty($scorecalculation) && !is_array($scorecalculation)) { $scorecalculation = []; } $sectionheading = $fbsection->sectionheading; $imageid = $fbsection->id; - $chartlabels [$section] = $fbsection->sectionlabel; + $chartlabels[$section] = $fbsection->sectionlabel; } } foreach ($scorecalculation as $qid => $key) { @@ -3491,24 +3997,28 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre default: } - foreach ($allscore as $key => $sc) { - if (isset($chartlabels[$key])) { - $lb = explode("|", $chartlabels[$key]); - $oppositescore = ''; - $oppositeallscore = ''; - if (count($lb) > 1) { - $sectionlabel = $lb[0] . ' | ' . $lb[1]; - $oppositescore = ' | ' . $oppositescorepercent[$key] . '%'; - $oppositeallscore = ' | ' . $alloppositescorepercent[$key] . '%'; - } else { - $sectionlabel = $chartlabels[$key]; - } - // If all questions of $section are unseen then don't show feedbackscores for this section. - if ($compare && !is_nan($scorepercent[$key])) { - $table->data[] = array($sectionlabel, $scorepercent[$key] . '%' . $oppositescore, - $allscorepercent[$key] . '%' . $oppositeallscore); - } else if (isset($allscorepercent[$key]) && !is_nan($allscorepercent[$key])) { - $table->data[] = array($sectionlabel, $allscorepercent[$key] . '%' . $oppositeallscore); + if ($this->survey->feedbackscores) { + foreach ($allscore as $key => $sc) { + if (isset($chartlabels[$key])) { + $lb = explode("|", $chartlabels[$key]); + $oppositescore = ''; + $oppositeallscore = ''; + if (count($lb) > 1) { + $sectionlabel = $lb[0] . ' | ' . $lb[1]; + $oppositescore = ' | ' . $oppositescorepercent[$key] . '%'; + $oppositeallscore = ' | ' . $alloppositescorepercent[$key] . '%'; + } else { + $sectionlabel = $chartlabels[$key]; + } + // If all questions of $section are unseen then don't show feedbackscores for this section. + if ($compare && !is_nan($scorepercent[$key])) { + $table = $table ?? new html_table(); + $table->data[] = array($sectionlabel, $scorepercent[$key] . '%' . $oppositescore, + $allscorepercent[$key] . '%' . $oppositeallscore); + } else if (isset($allscorepercent[$key]) && !is_nan($allscorepercent[$key])) { + $table = $table ?? new html_table(); + $table->data[] = array($sectionlabel, $allscorepercent[$key] . '%' . $oppositeallscore); + } } } } @@ -3522,9 +4032,19 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre } if ($usergraph && $this->survey->chart_type) { - $this->page->add_to_page('feedbackcharts', - draw_chart($feedbacktype = 'sections', $this->survey->chart_type, array_values($chartlabels), - array_values($scorepercent), array_values($allscorepercent), $sectionlabel, $groupname, $allresponses)); + $this->page->add_to_page( + 'feedbackcharts', + draw_chart( + 'sections', + array_values($chartlabels), + $groupname, + $allresponses, + $this->survey->chart_type, + array_values($scorepercent), + array_values($allscorepercent), + $sectionlabel + ) + ); } if ($this->survey->feedbackscores) { $this->page->add_to_page('feedbackscores', html_writer::table($table)); @@ -3533,4 +4053,127 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre return $feedbackmessages; } -} \ No newline at end of file + // Mobile support area. + + /** + * Save the data from the mobile app. + * @param int $userid + * @param int $sec + * @param bool $completed + * @param int $rid + * @param bool $submit + * @param string $action + * @param array $responses + * @return array + */ + public function save_mobile_data($userid, $sec, $completed, $rid, $submit, $action, array $responses) { + global $DB, $CFG; // Do not delete "$CFG". + + $ret = []; + $response = $this->build_response_from_appdata((object)$responses, $sec); + $response->sec = $sec; + $response->rid = $rid; + $response->id = $rid; + + if ($action == 'nextpage') { + $result = $this->next_page_action($response, $userid); + if (is_string($result)) { + $ret['warnings'] = $result; + } else { + $ret['nextpagenum'] = $result; + } + } else if ($action == 'previouspage') { + $ret['nextpagenum'] = $this->previous_page_action($response, $userid); + } else if (!$completed) { + // If reviewing a completed questionnaire, don't insert a response. + $msg = $this->response_check_format($response->sec, $response); + if (empty($msg)) { + $rid = $this->response_insert($response, $userid); + } else { + $ret['warnings'] = $msg; + $ret['response'] = $response; + } + } + + if ($submit && (!isset($ret['warnings']) || empty($ret['warnings']))) { + $this->commit_submission_response($rid, $userid); + } + return $ret; + } + + /** + * Get all of the areas that can have files. + * @return array + * @throws dml_exception + */ + public function get_all_file_areas() { + global $DB; + + $areas = []; + $areas['info'] = $this->sid; + $areas['thankbody'] = $this->sid; + + // Add question areas. + if (empty($this->questions)) { + $this->add_questions(); + } + $areas['question'] = []; + foreach ($this->questions as $question) { + $areas['question'][] = $question->id; + } + + // Add feedback areas. + $areas['feedbacknotes'] = $this->sid; + $fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->sid]); + if (!empty($fbsections)) { + $areas['sectionheading'] = []; + foreach ($fbsections as $section) { + $areas['sectionheading'][] = $section->id; + $feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $section->id]); + if (!empty($feedbacks)) { + $areas['feedback'] = []; + foreach ($feedbacks as $feedback) { + $areas['feedback'][] = $feedback->id; + } + } + } + } + + return $areas; + } + + /** + * Gets the identity fields. + * + * @param array $options + * @return array + */ + protected function get_identity_fields($options) { + $fields = !in_array('useridentityfields', $options) || $this->respondenttype == 'anonymous' ? [] : + \core_user\fields::get_identity_fields($this->context); + return $fields; + } + + /** + * Gets the identity fields values for a user. + * + * @param object $context + * @param int $userid + * @return array + */ + public static function get_user_identity_fields($context, $userid) { + global $DB; + + $fields = \core_user\fields::for_identity($context); + [ + 'selects' => $selects, + 'joins' => $joins, + 'params' => $params + ] = (array)$fields->get_sql('u', false, '', '', false); + $sql = "SELECT $selects + FROM {user} u $joins + WHERE u.id = ?"; + $row = $DB->get_record_sql($sql, array_merge($params, [$userid])); + return $row; + } +} diff --git a/questions.php b/questions.php index 1156aad2..6175512c 100644 --- a/questions.php +++ b/questions.php @@ -14,28 +14,36 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see