diff --git a/classes/feedback_form.php b/classes/feedback_form.php index 25260847..37401a00 100644 --- a/classes/feedback_form.php +++ b/classes/feedback_form.php @@ -41,11 +41,20 @@ public function definition() { // Questionnaire Feedback Sections and Messages. $mform->addElement('header', 'submithdr', get_string('feedbackoptions', 'questionnaire')); + // Do not display feedbacknone and feedbackglobal options if this is a questionnaire with keywords. + $questionnairehaskeywords = false; + foreach ($questionnaire->questions as $question) { + if ($question->has_keywords()) { + $questionnairehaskeywords = true; + break; + } + } $feedbackoptions = []; - $feedbackoptions[0] = get_string('feedbacknone', 'questionnaire'); - $feedbackoptions[1] = get_string('feedbackglobal', 'questionnaire'); + if (!$questionnairehaskeywords) { + $feedbackoptions[0] = get_string('feedbacknone', 'questionnaire'); + $feedbackoptions[1] = get_string('feedbackglobal', 'questionnaire'); + } $feedbackoptions[2] = get_string('feedbacksections', 'questionnaire'); - $mform->addElement('select', 'feedbacksections', get_string('feedbackoptions', 'questionnaire'), $feedbackoptions); $mform->setDefault('feedbacksections', $questionnaire->survey->feedbacksections); $mform->addHelpButton('feedbacksections', 'feedbackoptions', 'questionnaire'); diff --git a/classes/feedback_section_form.php b/classes/feedback_section_form.php index 740d5604..3f4261ff 100644 --- a/classes/feedback_section_form.php +++ b/classes/feedback_section_form.php @@ -43,6 +43,7 @@ public function definition() { $feedbacksection = $this->_customdata->feedbacksection; $validquestions = $this->_customdata->validquestions; $survey = $this->_customdata->survey; + $canselectquestions = $this->_customdata->canselectquestions; $feedbacksections = $questionnaire->survey->feedbacksections; $this->_feedbacks = $feedbacksection->sectionfeedback; $this->context = $questionnaire->context; @@ -89,7 +90,7 @@ public function definition() { $mform->setDefault('feedbacknotes', $questionnaire->survey->feedbacknotes); $mform->addHelpButton('sectionheading', 'feedbackheading', 'questionnaire'); - if ($questionnaire->survey->feedbacksections > 0) { + if ($questionnaire->survey->feedbacksections > 0 && $canselectquestions) { // Sections. if ($survey->feedbacksections > 1) { $mform->addElement('header', 'fbsection_' . $feedbacksection->id, diff --git a/classes/question/question.php b/classes/question/question.php index faa4ec80..1e4e46e3 100644 --- a/classes/question/question.php +++ b/classes/question/question.php @@ -501,7 +501,7 @@ public function supports_feedback_scores() { public function valid_feedback() { if ($this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name)) { foreach ($this->choices as $choice) { - if ($choice->value != null) { + if ($choice->value != null && $choice->value !== '!other') { return true; } } @@ -509,6 +509,26 @@ public function valid_feedback() { return false; } + /** + * True if the question supports feedback and has keywords instead of score value (for DISC personality test). + * Override if the default logic is not enough. + * @return bool + */ + public function has_keywords() { + if ($this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name)) { + foreach ($this->choices as $choice) { + if ($choice->value !== null) { + // D param means no digits. + $r = preg_match_all("/(\D+)/", $choice->value, $matches); + if ($r && $matches[0][0] !== '!other') { + return true; + } + } + } + } + return false; + } + /** * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. * @param array $rids @@ -531,7 +551,7 @@ public function get_feedback_maxscore() { if ($this->valid_feedback()) { $maxscore = 0; foreach ($this->choices as $choice) { - if (isset($choice->value) && ($choice->value != null)) { + if (isset($choice->value) && ($choice->value != null) && is_numeric($choice->value)) { if ($choice->value > $maxscore) { $maxscore = $choice->value; } @@ -1368,12 +1388,12 @@ public function form_update($formdata, $questionnaire) { $choicerecord->id = $ekey; $choicerecord->question_id = $this->qid; $choicerecord->content = trim($newchoices[$nidx]); - $r = preg_match_all("/^(\d{1,2})(=.*)$/", $newchoices[$nidx], $matches); - // This choice has been attributed a "score value" OR this is a rate question type. + $r = preg_match_all("/^(\d{1,2}|\D.*)=(.*)$/", $newchoices[$nidx], $matches); + // This choice has been attributed a "score value" or a DISC keyword OR this is a rate question type. if ($r) { $newscore = $matches[1][0]; $choicerecord->value = $newscore; - } else { // No score value for this choice. + } else { // No score value for this choice. $choicerecord->value = null; } $this->update_choice($choicerecord); @@ -1389,10 +1409,13 @@ public function form_update($formdata, $questionnaire) { $choicerecord = new \stdClass(); $choicerecord->question_id = $this->qid; $choicerecord->content = trim($newchoices[$nidx]); - $r = preg_match_all("/^(\d{1,2})(=.*)$/", $choicerecord->content, $matches); - // This choice has been attributed a "score value" OR this is a rate question type. + $r = preg_match_all("/^(\d{1,2}|\D.*)=(.*)$/", $choicerecord->content, $matches); + // This choice has been attributed a "score value" or a DISC keyword OR this is a rate question type. if ($r) { - $choicerecord->value = $matches[1][0]; + $newscore = $matches[1][0]; + $choicerecord->value = $newscore; + } else { // No score value for this choice. + $choicerecord->value = null; } $this->add_choice($choicerecord); $nidx++; diff --git a/fbsections.php b/fbsections.php index 13f3a8e7..b90ec17d 100644 --- a/fbsections.php +++ b/fbsections.php @@ -87,6 +87,7 @@ } } + // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\feedbackpage()); @@ -111,9 +112,19 @@ } } +// Do not display Feedback questions option if this is a DISC questionnaire. +$canselectquestions = true; +foreach ($questionnaire->questions as $question) { + if ($question->has_keywords()) { + $canselectquestions = false; + break; + } +} + $customdata = new stdClass(); $customdata->feedbacksection = $feedbacksection; $customdata->validquestions = $validquestions; +$customdata->canselectquestions = $canselectquestions; $customdata->survey = $questionnaire->survey; $customdata->sectionselect = $DB->get_records_menu('questionnaire_fb_sections', ['surveyid' => $questionnaire->survey->id], 'section', 'id,sectionlabel'); diff --git a/locallib.php b/locallib.php index f58a5d49..4e308935 100644 --- a/locallib.php +++ b/locallib.php @@ -114,7 +114,7 @@ function questionnaire_choice_values($content) { } // Check for score value first (used e.g. by personality test feature). - $r = preg_match_all("/^(\d{1,2}=)(.*)$/", $content, $matches); + $r = preg_match_all("/^(\d{1,2}|\w*)=(.*)$/", $content, $matches); if ($r) { $content = $matches[2][0]; } diff --git a/questionnaire.class.php b/questionnaire.class.php index 901b07dd..8ab43642 100644 --- a/questionnaire.class.php +++ b/questionnaire.class.php @@ -3736,19 +3736,55 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre // Calculate max score per question in questionnaire. $qmax = []; $maxtotalscore = 0; + $nbquestionswithkeywords = 0; + // Calculate max score per questionnaire by adding nb of questions with keywords. + $thisquestionnairehaskeywords = false; foreach ($this->questions as $question) { - $qid = $question->id; - if ($question->valid_feedback()) { + if ($question->has_keywords()) { + $thisquestionnairehaskeywords = true; + $maxtotalscore++; + $nbquestionswithkeywords++; + } + } + if (!$thisquestionnairehaskeywords) { + foreach ($this->questions as $question) { + $qid = $question->id; $qmax[$qid] = $question->get_feedback_maxscore(); $maxtotalscore += $qmax[$qid]; // Get all the feedback scores for this question. $responsescores[$qid] = $question->get_feedback_scores($rids); } + } else { + foreach ($this->questions as $question) { + $qid = $question->id; + if ($question->has_keywords() ) { + // Get all the feedback scores (actually keywords) for this question. + $responsescores[$qid] = $question->get_feedback_scores($rids); + } + } } - // Just in case no values have been entered in the various questions possible answers field. + + /** + * Returns the number of responses containing the keyword in specified response. + * @param array $responsescores The array of response scores. + * @param string $keyword + * @param int $rid + * @return number + */ + function countscore($responsescores, $keyword, $rid) { + $count = 0; + foreach ($responsescores as $subarray) { + if (isset($subarray[$rid]) && $subarray[$rid]->score === $keyword) { + $count++; + } + } + return $count; + } + if ($maxtotalscore === 0) { return ''; } + $feedbackmessages = []; // Get individual scores for each question in this responses set. @@ -3759,32 +3795,40 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $nbparticipants = max(1, $nbparticipants - !$isgroupmember); } foreach ($responsescores as $qid => $responsescore) { - if (!empty($responsescore)) { - foreach ($responsescore as $rrid => $response) { - // If this is current user's response OR if current user is viewing another group's results. - if ($rrid == $rid || $allresponses) { - if (!isset($qscore[$qid])) { - $qscore[$qid] = 0; - } - $qscore[$qid] = $response->score; + foreach ($responsescore as $rrid => $response) { + // If this is current user's response OR if current user is viewing another group's results. + if ($rrid == $rid || $allresponses) { + if (!isset($qscore[$qid])) { + $qscore[$qid] = 0; } - // Course score. - if (!isset($allqscore[$qid])) { - $allqscore[$qid] = 0; + if (!empty($responsescore)) { + if (!$thisquestionnairehaskeywords) { + $qscore[$qid] = $response->score; + } } - // Only add current score if conditions below are met. - if ($groupmode == 0 || $isgroupmember || (!$isgroupmember && $rrid != $rid) || $allresponses) { - $allqscore[$qid] += $response->score; + } + // Course score. + if (!isset($allqscore[$qid])) { + $allqscore[$qid] = 0; + } + + // Only add current score if conditions below are met. + if ($groupmode == 0 || $isgroupmember || (!$isgroupmember && $rrid != $rid) || $allresponses) { + if (!empty($responsescore)) { + if (!$thisquestionnairehaskeywords) { + $allqscore[$qid] += $response->score; + } } } } } + $totalscore = array_sum($qscore); $scorepercent = round($totalscore / $maxtotalscore * 100); $oppositescorepercent = 100 - $scorepercent; $alltotalscore = array_sum($allqscore); - $allscorepercent = round($alltotalscore / $nbparticipants / $maxtotalscore * 100); + $allscorepercent = round($alltotalscore / $nbparticipants / $maxtotalscore * 100); // No need to go further if feedback is global, i.e. only relying on total score. if ($this->survey->feedbacksections == 1) { $sectionid = $fbsectionsnb[0]; @@ -3872,7 +3916,6 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $oppositescorepercent = array(); $alloppositescorepercent = array(); $chartlabels = array(); - // Sections where all questions are unseen because of the $advdependencies. $nanscores = array(); for ($i = 1; $i <= $numsections; $i++) { @@ -3887,27 +3930,52 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre if (($filteredsections != null) && !in_array($section, $filteredsections)) { continue; } - foreach ($fbsections as $key => $fbsection) { - if ($fbsection->section == $section) { - $feedbacksectionid = $key; - $scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation); - if (empty($scorecalculation) && !is_array($scorecalculation)) { - $scorecalculation = []; + if (!$thisquestionnairehaskeywords) { + foreach ($fbsections as $key => $fbsection) { + if ($fbsection->section == $section) { + $feedbacksectionid = $key; + $scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation); + if (empty($scorecalculation) && !is_array($scorecalculation)) { + $scorecalculation = []; + } + $sectionheading = $fbsection->sectionheading; + $imageid = $fbsection->id; + $chartlabels[$section] = $fbsection->sectionlabel; } - $sectionheading = $fbsection->sectionheading; - $imageid = $fbsection->id; - $chartlabels[$section] = $fbsection->sectionlabel; } + foreach ($scorecalculation as $qid => $key) { + // Just in case a question pertaining to a section has been deleted or made not required + // after being included in scorecalculation. + if (isset($qscore[$qid])) { + $key = ($key == 0) ? 1 : $key; + $score[$section] += round($qscore[$qid] * $key); + $maxscore[$section] += round($qmax[$qid] * $key); + if ($compare || $allresponses) { + $allscore[$section] += round($allqscore[$qid] * $key); + } + } + } + } else { + foreach ($fbsections as $key => $fbsection) { + if ($fbsection->section == $section) { + $feedbacksectionid = $key; + $sectionheading = $fbsection->sectionheading; + $imageid = $fbsection->id; + $chartlabels[$section] = $fbsection->sectionlabel; + $score[$section] = countscore($responsescores, $fbsection->sectionlabel, $rid); + } + } + // Set maxscore for all sections to nb of questions with keywords. + $maxscore[$section] = $nbquestionswithkeywords; } - foreach ($scorecalculation as $qid => $key) { - // Just in case a question pertaining to a section has been deleted or made not required - // after being included in scorecalculation. - if (isset($qscore[$qid])) { - $key = ($key == 0) ? 1 : $key; - $score[$section] += round($qscore[$qid] * $key); - $maxscore[$section] += round($qmax[$qid] * $key); - if ($compare || $allresponses) { - $allscore[$section] += round($allqscore[$qid] * $key); + + if ($thisquestionnairehaskeywords && ($compare || $allresponses)) { + foreach ($rids as $key => $rid) { + foreach ($fbsections as $key => $fbsection) { + if ($fbsection->section == $section) { + $keyword = $fbsection->sectionlabel; + $allscore[$section] += countscore($responsescores, $keyword, $rid); + } } } } @@ -3915,7 +3983,6 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre if ($maxscore[$section] == 0) { array_push($nanscores, $section); } - $scorepercent[$section] = ($maxscore[$section] > 0) ? (round($score[$section] / $maxscore[$section] * 100)) : 0; $oppositescorepercent[$section] = 100 - $scorepercent[$section]; @@ -3924,7 +3991,6 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $maxscore[$section] * 100)) : 0; $alloppositescorepercent[$section] = 100 - $allscorepercent[$section]; } - if (!$allresponses) { if (is_nan($scorepercent[$section])) { // Info: all questions of $section are unseen @@ -4021,8 +4087,7 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $allresponses, $this->survey->chart_type, array_values($scorepercent), - array_values($allscorepercent), - $sectionlabel + array_values($allscorepercent) ) ); } diff --git a/tests/behat/add_multi_feedback_with_sections_in_keywords_questionnaire.feature b/tests/behat/add_multi_feedback_with_sections_in_keywords_questionnaire.feature new file mode 100644 index 00000000..eb5f6c12 --- /dev/null +++ b/tests/behat/add_multi_feedback_with_sections_in_keywords_questionnaire.feature @@ -0,0 +1,147 @@ +@mod @mod_questionnaire +Feature: In questionnaire, personality tests of the DISC assessment type can be constructed using feedback on specific question responses. + In order to define a feedback questionnaire of the DISC assessment Test type + As a teacher + I must add questions of the required question type i.e. radio buttons with a string keyword instead of a value for each possible answer + and complete the feedback options by creating sections with labels corresponding exactly to the keywords used in the radio buttons possible answers. + + @javascript + Scenario: Create a questionnaire with radio buttons questions with keywords and the relevant feedback sections. + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | resume | navigate | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | + And I am on the "Test questionnaire" "mod_questionnaire > questions" page logged in as "teacher1" + Then I should see "Add questions" + And I add a "Radio Buttons" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | When faced with a challenge, how do you react? | + | Possible answers | Dominant=Take charge immediately.,Conscientious=Carefully assess the situation before acting.,Steady=Seek guidance and support from others.,Influential=Avoid confrontation and hope the issue resolves itself.| + Then I should see "[Radio Buttons] (Q1)" + And I add a "Radio Buttons" question and I fill the form with: + | Question Name | Q2 | + | Yes | y | + | Question Text | How do you approach social situations? | + | Possible answers | Influential=Eagerly initiate conversations and take the lead.,Conscientious=Observe and listen before contributing.,Steady=Form close connections with a few individuals.,Dominant=Prefer to spend time alone or with a small group of close friends. | + Then I should see "[Radio Buttons] (Q2)" + And I add a "Radio Buttons" question and I fill the form with: + | Question Name | Q3 | + | Yes | y | + | Question Text | How do you handle unexpected changes to your plans? | + | Possible answers | Conscientious=Feel uneasy and take time to adjust.,Steady=Seek support and guidance from others.,Influential=Quickly adapt and find alternative solutions.,Dominant=Stick to the original plan and hope for the best.| + Then I should see "[Radio Buttons] (Q3" + And I add a "Radio Buttons" question and I fill the form with: + | Question Name | Q4 | + | Yes | y | + | Question Text | How do you express your emotions? | + | Possible answers | Steady=Carefully and considerately.,Dominant=Privately and discreetly.,Influential=Openly and passionately.,Conscientious=Thoughtfully and logically.| + Then I should see "[Radio Buttons] (Q4)" + And I follow "Feedback" + And I should see "Feedback options" + # The field "id_feedbacksections" is default set to "Feedback sections" + And I set the field "id_feedbackscores" to "Yes" + And I set the field "id_feedbacknotes" to "These are the main Feedback notes" + And I press "Save settings and edit Feedback Sections" + Then I should see "[New section]" + And I set the field "id_sectionlabel" to "Conscientious" + And I set the field "id_sectionheading" to "Conscientious heading" + And I press "Save changes" + And I follow "Conscientious section messages" + And I set the field "id_feedbacktext_0" to "You seem to have a conscientious style" + And I set the field "id_feedbackboundaries_0" to "50" + And I set the field "id_feedbacktext_1" to "You seem to have an average conscientious style" + And I set the field "id_feedbackboundaries_1" to "20" + And I set the field "id_feedbacktext_2" to "You seem to have a less conscientious style" + And I press "Save changes" + And I set the field "id_newsectionlabel" to "Dominant" + And I press "Add new section" + And I set the field "id_sectionheading" to "Dominant heading" + And I follow "Dominant section messages" + And I set the field "id_feedbacktext_0" to "You seem to have a dominance style" + And I set the field "id_feedbackboundaries_0" to "50" + And I set the field "id_feedbacktext_1" to "You seem to have an average dominance style" + And I set the field "id_feedbackboundaries_1" to "20" + And I set the field "id_feedbacktext_2" to "You seem to have a less dominance style" + And I press "Save changes" + And I set the field "id_newsectionlabel" to "Influential" + And I press "Add new section" + And I set the field "id_sectionheading" to "Influential heading" + And I follow "Influential section messages" + And I set the field "id_feedbacktext_0" to "You seem to have an influential style" + And I set the field "id_feedbackboundaries_0" to "50" + And I set the field "id_feedbacktext_1" to "You seem to have an average influential style" + And I set the field "id_feedbackboundaries_1" to "20" + And I set the field "id_feedbacktext_2" to "You seem to have a less influential style" + And I press "Save changes" + And I set the field "id_newsectionlabel" to "Steady" + And I press "Add new section" + And I set the field "id_sectionheading" to "Steady heading" + And I follow "Steady section messages" + And I set the field "id_feedbacktext_0" to "You seem to have a steady style" + And I set the field "id_feedbackboundaries_0" to "50" + And I set the field "id_feedbacktext_1" to "You seem to have an average steady style" + And I set the field "id_feedbackboundaries_1" to "20" + And I set the field "id_feedbacktext_2" to "You seem to have a less steady style" + And I press "Save changes" + And I log out + +# Scenario: Student completes feedback questions. + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + #Q1 "When faced with a challenge, how do you react?" + And I click on "Take charge immediately." "radio" + #Q2 "How do you approach social situations?" + And I click on "Eagerly initiate conversations and take the lead." "radio" + #Q3 "How do you handle unexpected changes to your plans?" + And I click on "Stick to the original plan and hope for the best." "radio" + #Q4 "How do you express your emotions?" + And I click on "Thoughtfully and logically." "radio" + And I press "Submit questionnaire" + Then I should see "Thank you for completing this Questionnaire." + And I press "Continue" + Then I should see "View your response(s)" + And I should see "Feedback Report" + And I should see "You seem to have an average conscientious style" + And I should see "You seem to have a dominance style" + And I should see "You seem to have an average influential style" + And I should see "You seem to have a less steady style" + And I should see "These are the main Feedback notes" + And I log out + +# Scenario: Another student completes feedback questions differently. + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + #Q1 "When faced with a challenge, how do you react?" + And I click on "Carefully assess the situation before acting." "radio" + #Q2 "How do you approach social situations?" + And I click on "Observe and listen before contributing." "radio" + #Q3 "How do you handle unexpected changes to your plans?" + And I click on "Seek support and guidance from others." "radio" + #Q4 "How do you express your emotions?" + And I click on "Privately and discreetly." "radio" + And I press "Submit questionnaire" + Then I should see "Thank you for completing this Questionnaire." + And I press "Continue" + Then I should see "View your response(s)" + And I should see "Feedback Report" + And I should see "You seem to have a conscientious style" + And I should see "You seem to have an average dominance style" + And I should see "You seem to have a less influential style" + And I should see "You seem to have an average steady style" + And I should see "These are the main Feedback notes" + And I log out