diff --git a/classes/edit_question_form.php b/classes/edit_question_form.php index 6e10d60a..92098e84 100644 --- a/classes/edit_question_form.php +++ b/classes/edit_question_form.php @@ -90,6 +90,36 @@ public function validation($data, $files) { } } + // If this is a slider question. + if ($data['type_id'] == QUESSLIDER) { + if (isset($data['minrange']) && isset($data['maxrange']) && isset($data['startingvalue']) && + isset($data['stepvalue'])) { + if ($data['minrange'] >= $data['maxrange']) { + $errors['maxrange'] = get_string('invalidrange', 'questionnaire'); + } + + if (($data['startingvalue'] > $data['maxrange']) || ($data['startingvalue'] < $data['minrange'])) { + $errors['startingvalue'] = get_string('invalidstartingvalue', 'questionnaire'); + } + + if ($data['startingvalue'] > 100 || $data['startingvalue'] < -100) { + $errors['startingvalue'] = get_string('invalidstartingvalue', 'questionnaire'); + } + + if (($data['stepvalue'] > $data['maxrange']) || $data['stepvalue'] < 1) { + $errors['stepvalue'] = get_string('invalidincrement', 'questionnaire'); + } + + if ($data['minrange'] < -100) { + $errors['minrange'] = get_string('invalidminmaxrange', 'questionnaire'); + } + + if ($data['maxrange'] > 100) { + $errors['maxrange'] = get_string('invalidminmaxrange', 'questionnaire'); + } + } + } + return $errors; } diff --git a/classes/output/mobile.php b/classes/output/mobile.php index 0dd58aa4..74d3f549 100644 --- a/classes/output/mobile.php +++ b/classes/output/mobile.php @@ -258,6 +258,10 @@ protected static function add_pagequestion_data($questionnaire, $pagenum, $respo $question = $questionnaire->questions[$questionid]; if ($question->supports_mobile()) { $pagequestions[] = $question->mobile_question_display($qnum, $questionnaire->autonum); + $mobileotherdata = $question->mobile_otherdata(); + if (!empty($mobileotherdata)) { + $responses = array_merge($responses, $mobileotherdata); + } if (($response !== null) && isset($response->answers[$questionid])) { $responses = array_merge($responses, $question->get_mobile_response_data($response)); } diff --git a/classes/question/question.php b/classes/question/question.php index b05eab7c..1b649598 100644 --- a/classes/question/question.php +++ b/classes/question/question.php @@ -41,6 +41,7 @@ define('QUESRATE', 8); define('QUESDATE', 9); define('QUESNUMERIC', 10); +define('QUESSLIDER', 11); define('QUESPAGEBREAK', 99); define('QUESSECTIONTEXT', 100); @@ -117,7 +118,8 @@ abstract class question { QUESDATE => 'date', QUESNUMERIC => 'numerical', QUESPAGEBREAK => 'pagebreak', - QUESSECTIONTEXT => 'sectiontext' + QUESSECTIONTEXT => 'sectiontext', + QUESSLIDER => 'slider', ]; /** @var array $notifications Array of extra messages for display purposes. */ @@ -1563,6 +1565,9 @@ public function mobile_question_display($qnum, $autonum = false) { ]; $mobiledata->choices = $this->mobile_question_choices_display(); + if ($this->mobile_question_extradata_display()) { + $mobiledata->extradata = json_decode($this->extradata); + } if ($autonum) { $mobiledata->content = $qnum . '. ' . $mobiledata->content; $mobiledata->content_stripped = $qnum . '. ' . $mobiledata->content_stripped; @@ -1617,4 +1622,22 @@ public function get_mobile_response_data($response) { return $resultdata; } + + /** + * True if question need extradata for mobile app. + * + * @return bool + */ + public function mobile_question_extradata_display() { + return false; + } + + /** + * Return the otherdata to be used by the mobile app. + * + * @return array + */ + public function mobile_otherdata() { + return []; + } } diff --git a/classes/question/slider.php b/classes/question/slider.php new file mode 100644 index 00000000..37c284ed --- /dev/null +++ b/classes/question/slider.php @@ -0,0 +1,304 @@ +. + +namespace mod_questionnaire\question; + +/** + * This file contains the parent class for slider question types. + * + * @author Hieu Vu Van + * @copyright 2022 The Open University. + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class slider extends question { + + /** + * Return the responseclass used. + * @return string + */ + protected function responseclass() { + return '\\mod_questionnaire\\responsetype\\slider'; + } + + /** + * Return the help name. + * @return string + */ + public function helpname() { + return 'slider'; + } + + /** + * Return true if the question has choices. + */ + public function has_choices() { + return false; + } + + /** + * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. + * + * @return boolean | string + */ + public function question_template() { + return 'mod_questionnaire/question_slider'; + } + + /** + * Override and return a response template if provided. Output of response_survey_display is iterpreted based on this. + * + * @return boolean | string + */ + public function response_template() { + return 'mod_questionnaire/response_slider'; + } + + /** + * Return the context tags for the check question template. + * + * @param \mod_questionnaire\responsetype\response\response $response + * @param array $dependants Array of all questions/choices depending on this question. + * @param boolean $blankquestionnaire + * @return object The check question context tags. + * + */ + protected function question_survey_display($response, $dependants = [], $blankquestionnaire = false) { + global $PAGE; + $PAGE->requires->js_init_call('M.mod_questionnaire.init_slider', null, false, questionnaire_get_js_module()); + $extradata = json_decode($this->extradata); + $questiontags = new \stdClass(); + if (isset($response->answers[$this->id][0])) { + $extradata->startingvalue = $response->answers[$this->id][0]->value; + } + $extradata->name = 'q' . $this->id; + $extradata->id = self::qtypename($this->type_id) . $this->id; + $questiontags->qelements = new \stdClass(); + $questiontags->qelements->extradata = $extradata; + return $questiontags; + } + + /** + * Return the context tags for the slider response template. + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass The check question response context tags. + */ + protected function response_survey_display($response) { + global $PAGE; + $PAGE->requires->js_init_call('M.mod_questionnaire.init_slider', null, false, questionnaire_get_js_module()); + + $resptags = new \stdClass(); + if (isset($response->answers[$this->id])) { + $answer = reset($response->answers[$this->id]); + $resptags->content = format_text($answer->value, FORMAT_HTML); + if (!empty($response->answers[$this->id]['extradata'])) { + $resptags->extradata = $response->answers[$this->id]['extradata']; + } else { + $extradata = json_decode($this->extradata); + $resptags->extradata = $extradata; + } + } + return $resptags; + } + + /** + * Add the form required field. + * @param \MoodleQuickForm $mform + * @return \MoodleQuickForm + */ + protected function form_required(\MoodleQuickForm $mform) { + return $mform; + } + + /** + * Return the form precision. + * @param \MoodleQuickForm $mform + * @param string $helptext + * @return \MoodleQuickForm|void + */ + protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { + return question::form_precise_hidden($mform); + } + + /** + * Return the form length. + * @param \MoodleQuickForm $mform + * @param string $helptext + * @return \MoodleQuickForm|void + */ + protected function form_length(\MoodleQuickForm $mform, $helptext = '') { + return question::form_length_hidden($mform); + } + + /** + * Override if the question uses the extradata field. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm + */ + protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') { + $minelementname = 'minrange'; + $maxelementname = 'maxrange'; + $startingvalue = 'startingvalue'; + $stepvalue = 'stepvalue'; + + $ranges = []; + if (!empty($this->extradata)) { + $ranges = json_decode($this->extradata); + } + $mform->addElement('text', 'leftlabel', get_string('leftlabel', 'questionnaire')); + $mform->setType('leftlabel', PARAM_RAW); + if (isset($ranges->leftlabel)) { + $mform->setDefault('leftlabel', $ranges->leftlabel); + } + $mform->addElement('text', 'centerlabel', get_string('centerlabel', 'questionnaire')); + $mform->setType('centerlabel', PARAM_RAW); + if (isset($ranges->centerlabel)) { + $mform->setDefault('centerlabel', $ranges->centerlabel); + } + $mform->addElement('text', 'rightlabel', get_string('rightlabel', 'questionnaire')); + $mform->setType('rightlabel', PARAM_RAW); + if (isset($ranges->rightlabel)) { + $mform->setDefault('rightlabel', $ranges->rightlabel); + } + + $patterint = '/^-?\d+$/'; + $mform->addElement('text', $minelementname, get_string($minelementname, 'questionnaire'), ['size' => '3']); + $mform->setType($minelementname, PARAM_RAW); + $mform->addRule($minelementname, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($minelementname, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($minelementname, get_string('err_numeric', 'form'), 'regex', $patterint, 'client'); + $mform->addHelpButton($minelementname, $minelementname, 'questionnaire'); + if (isset($ranges->minrange)) { + $mform->setDefault($minelementname, $ranges->minrange); + } else { + $mform->setDefault($minelementname, 1); + } + + $mform->addElement('text', $maxelementname, get_string($maxelementname, 'questionnaire'), ['size' => '3']); + $mform->setType($maxelementname, PARAM_RAW); + $mform->addHelpButton($maxelementname, $maxelementname, 'questionnaire'); + $mform->addRule($maxelementname, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($maxelementname, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($maxelementname, get_string('err_numeric', 'form'), 'regex', $patterint, 'client'); + if (isset($ranges->maxrange)) { + $mform->setDefault($maxelementname, $ranges->maxrange); + } else { + $mform->setDefault($maxelementname, 10); + } + + $mform->addElement('text', $startingvalue, get_string($startingvalue, 'questionnaire'), ['size' => '3']); + $mform->setType($startingvalue, PARAM_RAW); + $mform->addHelpButton($startingvalue, $startingvalue, 'questionnaire'); + $mform->addRule($startingvalue, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($startingvalue, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($startingvalue, get_string('err_numeric', 'form'), 'regex', $patterint, 'client'); + if (isset($ranges->startingvalue)) { + $mform->setDefault($startingvalue, $ranges->startingvalue); + } else { + $mform->setDefault($startingvalue, 5); + } + + $mform->addElement('text', $stepvalue, get_string($stepvalue, 'questionnaire'), ['size' => '3']); + $mform->setType($stepvalue, PARAM_RAW); + $mform->addHelpButton($stepvalue, $stepvalue, 'questionnaire'); + $mform->addRule($stepvalue, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($stepvalue, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($stepvalue, get_string('err_numeric', 'form'), 'regex', '/^-?\d+$/', 'client'); + + if (isset($ranges->stepvalue)) { + $mform->setDefault($stepvalue, $ranges->stepvalue); + } else { + $mform->setDefault($stepvalue, 1); + } + return $mform; + } + + /** + * Any preprocessing of general data. + * @param \stdClass $formdata + * @return bool + */ + protected function form_preprocess_data($formdata) { + $ranges = []; + if (isset($formdata->minrange)) { + $ranges['minrange'] = $formdata->minrange; + } + if (isset($formdata->maxrange)) { + $ranges['maxrange'] = $formdata->maxrange; + } + if (isset($formdata->startingvalue)) { + $ranges['startingvalue'] = $formdata->startingvalue; + } + if (isset($formdata->stepvalue)) { + $ranges['stepvalue'] = $formdata->stepvalue; + } + if (isset($formdata->leftlabel)) { + $ranges['leftlabel'] = $formdata->leftlabel; + } + if (isset($formdata->rightlabel)) { + $ranges['rightlabel'] = $formdata->rightlabel; + } + if (isset($formdata->centerlabel)) { + $ranges['centerlabel'] = $formdata->centerlabel; + } + + // Now store the new named degrees in extradata. + $formdata->extradata = json_encode($ranges); + return parent::form_preprocess_data($formdata); + } + + /** + * True if question provides mobile support. + * + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * True if question need extradata for mobile app. + * + * @return bool + */ + public function mobile_question_extradata_display() { + return true; + } + + /** + * Return the mobile question display. + * + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->isslider = true; + return $mobiledata; + } + + /** + * Return the otherdata to be used by the mobile app. + * + * @return array + */ + public function mobile_otherdata() { + $extradata = json_decode($this->extradata); + return [$this->mobile_fieldkey() => $extradata->startingvalue]; + } +} diff --git a/classes/questions_form.php b/classes/questions_form.php index 40966eb2..769cbe86 100644 --- a/classes/questions_form.php +++ b/classes/questions_form.php @@ -256,7 +256,7 @@ public function definition() { $manageqgroup[] =& $mform->createElement('image', 'editbutton['.$question->id.']', $esrc, $eextra); $manageqgroup[] =& $mform->createElement('image', 'removebutton['.$question->id.']', $rsrc, $rextra); - if ($tid != QUESPAGEBREAK && $tid != QUESSECTIONTEXT) { + if ($tid != QUESPAGEBREAK && $tid != QUESSECTIONTEXT && $tid != QUESSLIDER) { if ($required == 'y') { $reqsrc = $questionnaire->renderer->image_url('t/stop'); $strrequired = get_string('required', 'questionnaire'); diff --git a/classes/responsetype/slider.php b/classes/responsetype/slider.php new file mode 100644 index 00000000..5055c3d1 --- /dev/null +++ b/classes/responsetype/slider.php @@ -0,0 +1,54 @@ +. + +namespace mod_questionnaire\responsetype; + +/** + * Class for slider text response types. + * + * @author Hieu Vu Van + * @copyright 2022 The Open University. + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class slider extends numericaltext { + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + * @throws \dml_exception + */ + public static function response_answers_by_question($rid) { + global $DB; + + $answers = []; + $sql = 'SELECT qs.id, qs.response_id as responseid, qs.question_id as questionid, + 0 as choiceid, qs.response as value, qq.extradata ' . + 'FROM {' . static::response_table() . '} qs ' . + 'INNER JOIN {questionnaire_question} qq ON qq.id = qs.question_id ' . + 'WHERE response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $record) { + $answers[$record->questionid][] = answer\answer::create_from_data($record); + if (!empty($record->extradata)) { + $answers[$record->questionid]['extradata'] = json_decode($record->extradata); + } + } + return $answers; + } +} diff --git a/db/install.php b/db/install.php index 2e630363..55b0eda4 100644 --- a/db/install.php +++ b/db/install.php @@ -93,6 +93,13 @@ function xmldb_questionnaire_install() { $questiontype->response_table = 'response_text'; $id = $DB->insert_record('questionnaire_question_type', $questiontype); + $questiontype = new stdClass(); + $questiontype->typeid = 11; + $questiontype->type = 'Slider'; + $questiontype->has_choices = 'n'; + $questiontype->response_table = 'response_text'; + $id = $DB->insert_record('questionnaire_question_type', $questiontype); + $questiontype = new stdClass(); $questiontype->typeid = 99; $questiontype->type = 'Page Break'; diff --git a/db/mobile.php b/db/mobile.php index cbc7dcef..01bec8fb 100644 --- a/db/mobile.php +++ b/db/mobile.php @@ -36,7 +36,7 @@ 'method' => 'mobile_view_activity', 'styles' => [ 'url' => $CFG->wwwroot . '/mod/questionnaire/styles_app.css', - 'version' => '1.4' + 'version' => '1.5' ] ] ], diff --git a/db/upgrade.php b/db/upgrade.php index daf754d2..3861f365 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -983,6 +983,20 @@ function xmldb_questionnaire_upgrade($oldversion=0) { upgrade_mod_savepoint(true, 2020062301, 'questionnaire'); } + if ($oldversion < 2022092200) { + // Add new slider question type. + $exist = $DB->record_exists('questionnaire_question_type', ['typeid' => 11]); + if (!$exist) { + $questiontype = new stdClass(); + $questiontype->typeid = 11; + $questiontype->type = 'Slider'; + $questiontype->has_choices = 'n'; + $questiontype->response_table = 'response_text'; + $DB->insert_record('questionnaire_question_type', $questiontype); + } + upgrade_mod_savepoint(true, 2022092200, 'questionnaire'); + } + return $result; } diff --git a/lang/en/questionnaire.php b/lang/en/questionnaire.php index d904e11b..dce6e767 100644 --- a/lang/en/questionnaire.php +++ b/lang/en/questionnaire.php @@ -117,6 +117,7 @@ $string['createcontent_help'] = 'Select one of the radio button options. \'Create new\' is the default.'; $string['createcontent_link'] = 'mod/questionnaire/mod#Content_Options'; $string['createnew'] = 'Create new'; +$string['centerlabel'] = 'Centre label'; $string['date'] = 'Date'; $string['date_help'] = 'Use this question type if you expect the response to be a correctly formatted date.'; $string['date_link'] = 'mod/questionnaire/questions#Date'; @@ -263,12 +264,17 @@ $string['invalidresponserecord'] = 'Invalid response record specified.'; $string['invalidsurveyid'] = 'Invalid questionnaire ID.'; $string['invalidsectionid'] = 'Invalid feedback section specified.'; +$string['invalidrange'] = 'The maximum slider value must be greater than the minimum slider value.'; +$string['invalidstartingvalue'] = 'The starting value must be equal to or between the minimum and maximum values. For example, if using a scale of 1-10, the starting value could be 5.'; +$string['invalidminmaxrange'] = 'This question type supports an absolute maximum range of -100 to +100. We expect the vast majority of questionnaire designs to use a range of 1-10 or -10 to +10.'; +$string['invalidincrement'] = 'Note that the value increments must be lower than the maximum value. For example, if a scale of 1-10, the increment value would probably be 1.'; $string['indirectwarnings'] = 'This list shows the indirect dependent questions and the remaining dependencies for direct dependent questions:'; $string['kindofratescale'] = 'Type of rate scale'; $string['kindofratescale_help'] = 'Right-click on the More Help link below.'; $string['kindofratescale_link'] = 'mod/questionnaire/questions#Type_of_rate_scale'; $string['lastrespondent'] = 'Last Respondent'; $string['length'] = 'Length'; +$string['leftlabel'] = 'Left label'; $string['managequestions'] = 'Manage questions'; $string['managequestions_help'] = 'In the Manage questions section of the Edit Questions page, you can conduct a number of operations on a Questionnaire\'s questions.'; $string['managequestions_link'] = 'mod/questionnaire/questions#Manage_questions'; @@ -305,6 +311,10 @@ $string['myresponses'] = 'All your responses'; $string['myresponsetitle'] = 'Your {$a} response(s)'; $string['myresults'] = 'Your Results'; +$string['minrange'] = 'Minimum slider range (left)'; +$string['minrange_help'] = 'Set the minimum value of the range on the left-hand side. It defaults to 1, but can set as low as -100. If you use a negative number (-100 to -1), the right-hand maximum will be expressed with a positive (+) sign.'; +$string['maxrange'] = 'Maximum slider range (right)'; +$string['maxrange_help'] = 'Set the maximum value of the range on the right-hand side. It defaults to 100, but it could be any number between 1-100. If the minimum value for the left-hand is a negative value, the maximum range will be expressed with a positive (+) sign.'; $string['name'] = 'Name'; $string['navigate'] = 'Allow branching questions'; $string['navigate_help'] = 'Enable Yes/No and Radio Buttons questions to have Child questions dependent on their choices in your questionnaire.'; @@ -548,6 +558,7 @@ $string['resume_link'] = 'mod/questionnaire/mod#Save/Resume_answers'; $string['resumesurvey'] = 'Resume questionnaire'; $string['return'] = 'Return'; +$string['rightlabel'] = 'Right label'; $string['save'] = 'Save'; $string['save_and_exit'] = 'Save and exit'; $string['saveasnew'] = 'Save as New Question'; @@ -596,6 +607,12 @@ $string['surveynotexists'] = 'questionnaire does not exist.'; $string['surveyowner'] = 'You must be a questionnaire owner to perform this operation.'; $string['surveyresponse'] = 'Response from questionnaire'; +$string['slider'] = 'Slider'; +$string['slider_help'] = 'The slider question allows respondents to select a value from a continuous range by dragging a slider between two extremes. A centre value can also be set.'; +$string['startingvalue'] = 'Slider starting value'; +$string['startingvalue_help'] = 'The slider starting value specifies where the slider should first appear for respondents. It defaults to 1 because the range is unknown. You may wish to start it in the centre of the range by giving a central value (a range of 1-100 has a centre value of 50).'; +$string['stepvalue'] = 'Slider increment value'; +$string['stepvalue_help'] = 'The slider increment value specifies how finely you wish respondents to indicate their response in the range. The question defaults to a range of 1-100 with an increment of one, allowing respondents to give values of 70, 71, 72, 73, 74 etc. But you could instead set increments of five, allowing respondents to give values of 60, 65, 70, 75, 80 etc., or even just a range of 1-10 with increments of 1.'; $string['template'] = 'Template'; $string['templatenotviewable'] = 'Template questionnaires are not viewable.'; $string['text'] = 'Question Text'; diff --git a/locallib.php b/locallib.php index f4f7b932..8875a2ce 100644 --- a/locallib.php +++ b/locallib.php @@ -487,6 +487,8 @@ function questionnaire_get_type ($id) { return get_string('date', 'questionnaire'); case 10: return get_string('numeric', 'questionnaire'); + case 11: + return get_string('slider', 'questionnaire'); case 100: return get_string('sectiontext', 'questionnaire'); case 99: diff --git a/module.js b/module.js index 28d7b628..f3eb8d60 100644 --- a/module.js +++ b/module.js @@ -241,4 +241,35 @@ M.mod_questionnaire.init_sendmessage = function(Y) { }); }, '#checkstarted'); -}; \ No newline at end of file +}; +M.mod_questionnaire.init_slider = function(Y) { + const allRanges = document.querySelectorAll(".slider"); + allRanges.forEach(wrap => { + const range = wrap.querySelector("input.questionnaire-slider"); + const bubble = wrap.querySelector(".bubble"); + + range.addEventListener("input", () => { + setBubble(range, bubble); + }); + setBubble(range, bubble); + }); + + function setBubble(range, bubble) { + const val = range.value; + const min = range.min ? range.min : 0; + const max = range.max ? range.max : 100; + var newVal = Number(((val - min) * 100) / (max - min)); + var positiveVal = ''; + if (range.min && range.min < 0) { + if (range.max && range.max > 0) { + if (val > 0) { + positiveVal = '+'; + } + } + } + bubble.innerHTML = positiveVal + val; + + // Sorta magic numbers based on size of the native UI thumb + bubble.style.left = `calc(${newVal}% + (${8 - newVal * 0.15}px))`; + } +}; diff --git a/questionnaire.class.php b/questionnaire.class.php index ef599ca4..e677636d 100644 --- a/questionnaire.class.php +++ b/questionnaire.class.php @@ -3245,7 +3245,8 @@ public function generate_csv($currentgroupid, $rid='', $userid='', $choicecodes= '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))) { @@ -4036,7 +4037,7 @@ public function save_mobile_data($userid, $sec, $completed, $rid, $submit, $acti global $DB, $CFG; // Do not delete "$CFG". $ret = []; - $response = $this->build_response_from_appdata($responses, $sec); + $response = $this->build_response_from_appdata((object)$responses, $sec); $response->sec = $sec; $response->rid = $rid; $response->id = $rid; diff --git a/styles.css b/styles.css index 28f45a3c..41dcd324 100644 --- a/styles.css +++ b/styles.css @@ -453,4 +453,93 @@ td.selected { #page-mod-questionnaire-questions #fitem_id_allchoices #id_allchoices, #page-mod-questionnaire-questions #fitem_id_allnameddegrees #id_allnameddegrees { resize: both; -} \ No newline at end of file +} + +.path-mod-questionnaire .slidecontainer { + width: 100%; +} + +.path-mod-questionnaire .slider { + -webkit-appearance: none; + width: 100%; + outline: none; + opacity: 0.7; + -webkit-transition: .2s; + transition: opacity .2s; + float: left; + margin-top: 40px; +} +.path-mod-questionnaire .slider input { + width: 100%; +} + +.path-mod-questionnaire .slider:hover { + opacity: 1; +} + +.path-mod-questionnaire .slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 25px; + background: #04aa6d; + cursor: pointer; + border-radius: 50%; +} + +.path-mod-questionnaire .slider::-moz-range-thumb { + width: 25px; + height: 25px; + background: #04aa6d; + cursor: pointer; +} + +.path-mod-questionnaire .question-slider { + display: flex; + align-items: baseline; +} + +.path-mod-questionnaire .left-side-label { + text-align: right; + padding-right: 20px; + margin-top: 40px; + flex-grow: 1; +} + +.path-mod-questionnaire .right-side-label { + text-align: left; + padding-left: 20px; + margin-top: 40px; + flex-grow: 1; +} + +.path-mod-questionnaire .middle-side-content { + flex-grow: 8; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.path-mod-questionnaire .middle-side-label { + text-align: center; +} + +.path-mod-questionnaire .bubble { + background: #000; + color: white; + padding: 3px; + border-radius: 10px; + left: 50%; + transform: translate(-52%, -50px); + position: relative; + text-align: center; + width: 40px; +} +.path-mod-questionnaire .bubble::after { + content: ""; + position: absolute; + width: 2px; + height: 2px; + left: 50%; +} diff --git a/styles_app.css b/styles_app.css index 925f2ed4..e1b368db 100644 --- a/styles_app.css +++ b/styles_app.css @@ -1,4 +1,17 @@ span.mobileratequestion { padding-left: 2em; padding-right: 2em; +} + +.mod_questionnaire_slider .range-has-pin .range-pin { + -webkit-transform: translate3d(0, 0, 0) scale(1); + transform: translate3d(0, 0, 0) scale(1); +} +.mod_questionnaire_slider .range-has-pin::part(pin){ + -webkit-transform: translate3d(0, -24px, 0) scale(1); + transform: translate3d(0, -24px, 0) scale(1); +} + +ion-label.disabled { + opacity: 0.8 !important; } \ No newline at end of file diff --git a/templates/local/mobile/ionic3/slider_question.mustache b/templates/local/mobile/ionic3/slider_question.mustache new file mode 100644 index 00000000..64aef335 --- /dev/null +++ b/templates/local/mobile/ionic3/slider_question.mustache @@ -0,0 +1,55 @@ +{{! + 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_questionnaire/mobile_slider_question + + Template which defines a slider question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "extradata": { + "minrange" : 1 + "maxrange" : 10 + "startingvalue" : 5 + "stepvalue" : 1 + "leftlabel" : "left label" + "rightlabel" : "right label" + "centerlabel": "center label" + }, + completed: 0 + + } + +}} +{{=<% %>=}} +<%#extradata%> + + disabled="true"<%/completed%> + min="<%extradata.minrange%>" max="<%extradata.maxrange%>" + pin="true" step="<%extradata.stepvalue%>" + [(ngModel)]="CONTENT_OTHERDATA.<%fieldkey%>"> + <%extradata.leftlabel%> + <%extradata.rightlabel%> + + +<%extradata.centerlabel%> +<%/extradata%> diff --git a/templates/local/mobile/ionic3/view_activity_page.mustache b/templates/local/mobile/ionic3/view_activity_page.mustache index d48ddd29..62e9ae53 100644 --- a/templates/local/mobile/ionic3/view_activity_page.mustache +++ b/templates/local/mobile/ionic3/view_activity_page.mustache @@ -121,6 +121,9 @@ <%#israte%> <%> mod_questionnaire/local/mobile/ionic3/rate_question %> <%/israte%> + <%#isslider%> + <%> mod_questionnaire/local/mobile/ionic3/slider_question %> + <%/isslider%> <%/pagequestions%> <%^pagequestions%> diff --git a/templates/local/mobile/latest/slider_question.mustache b/templates/local/mobile/latest/slider_question.mustache new file mode 100644 index 00000000..80623356 --- /dev/null +++ b/templates/local/mobile/latest/slider_question.mustache @@ -0,0 +1,57 @@ +{{! + 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_questionnaire/mobile_slider_question + + Template which defines a slider question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "extradata": { + "minrange" : 1 + "maxrange" : 10 + "startingvalue" : 5 + "stepvalue" : 1 + "leftlabel" : "left label" + "rightlabel" : "right label" + "centerlabel": "center label" + }, + "completed": 0 + } + +}} +{{=<% %>=}} +<%#extradata%> + + disabled="true"<%/completed%> + min="<%extradata.minrange%>" max="<%extradata.maxrange%>" + pin="true" step="<%extradata.stepvalue%>" + [(ngModel)]="CONTENT_OTHERDATA.<%fieldkey%>"> + <%extradata.leftlabel%> + <%extradata.rightlabel%> + + + class="disabled"<%/completed%>> + + +<%/extradata%> diff --git a/templates/local/mobile/latest/view_activity_page.mustache b/templates/local/mobile/latest/view_activity_page.mustache index c0d01651..54a1bae8 100644 --- a/templates/local/mobile/latest/view_activity_page.mustache +++ b/templates/local/mobile/latest/view_activity_page.mustache @@ -124,6 +124,9 @@ <%#israte%> <%> mod_questionnaire/local/mobile/latest/rate_question %> <%/israte%> + <%#isslider%> + <%> mod_questionnaire/local/mobile/latest/slider_question %> + <%/isslider%> <%/pagequestions%> <%^pagequestions%> diff --git a/templates/question_slider.mustache b/templates/question_slider.mustache new file mode 100644 index 00000000..9d4099db --- /dev/null +++ b/templates/question_slider.mustache @@ -0,0 +1,73 @@ +{{! + 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_questionnaire/question_yesno + + Template which defines a yes/no type question survey display. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "qelements": { + "choice": [ + { + "id": "choice1", + "value": "y", + "name": "q23", + "checked": 1, + "disabled": "", + "onclick": "dosomething()", + "label": "Yes" + }, + { + "id": "choice2", + "value": "n", + "name": "q23", + "checked": 0, + "disabled": "", + "onclick": "dosomething()", + "label": "No" + } + ] + } + } + }} + +
+ {{#qelements}} + {{#extradata}} +
{{extradata.leftlabel}}
+
+
+ + +
+
{{extradata.centerlabel}}
+
+
{{extradata.rightlabel}}
+ {{/extradata}} + {{/qelements}} +
+ diff --git a/templates/response_slider.mustache b/templates/response_slider.mustache new file mode 100644 index 00000000..9a6532f5 --- /dev/null +++ b/templates/response_slider.mustache @@ -0,0 +1,52 @@ +{{! + 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_questionnaire/response_text + + Template which defines a text type question survey display. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "content": "HTML for numeric" + } + }} + +
+
+ {{#extradata}} +
{{extradata.leftlabel}}
+
+
+ + +
+
{{extradata.centerlabel}}
+
+
{{extradata.rightlabel}}
+ {{/extradata}} +
+
+ diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php index 850812c5..b0d67965 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -118,7 +118,8 @@ public function i_add_a_question_and_i_fill_the_form_with($questiontype, TableNo 'Radio Buttons', 'Rate (scale 1..5)', 'Text Box', - 'Yes/No'); + 'Yes/No', + 'Slider'); if (!in_array($questiontype, $validtypes)) { throw new ExpectationException('Invalid question type specified.', $this->getSession()); diff --git a/tests/behat/slider_question.feature b/tests/behat/slider_question.feature new file mode 100644 index 00000000..7e134d34 --- /dev/null +++ b/tests/behat/slider_question.feature @@ -0,0 +1,87 @@ +@mod @mod_questionnaire +Feature: Slider questions can add slider with range for users to choose + In order to setup a slider question + As a teacher + I need to specify the range. + + Background: Add a slider question to a questionnaire. + 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 | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I add a "Slider" question and I fill the form with: + | Question Name | Q1 | + | Question Text | Slider quesrion test | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | 5 | + | Maximum slider range (right) | 100 | + | Slider starting value | 5 | + | Slider increment value | 5 | + Then I should see "position 1" + And I should see " [Slider] (Q1)" + And I should see "Slider quesrion test" + And I log out + + @javascript + Scenario: Student use slider questionnaire. + And I log in as "student1" + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Answer the questions..." in current page administration + Then I should see "Slider quesrion test" + And I should see "Left" + And I should see "Right" + And I should see "Center" + 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)" + + @javascript + Scenario: Teacher use slider questionnaire with invalid setting. + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I add a "Slider" question and I fill the form with: + | Question Name | Q1 | + | Question Text | Slider quesrion test | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | 10 | + | Maximum slider range (right) | 5 | + | Slider starting value | 10 | + | Slider increment value | 15 | + And I should see "The maximum slider value must be greater than the minimum slider value." + And I should see "Note that the value increments must be lower than the maximum value. For example, if a scale of 1-10, the increment value would probably be 1." + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I add a "Slider" question and I fill the form with: + | Question Name | Q1 | + | Question Text | Slider quesrion test | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | -999 | + | Maximum slider range (right) | 999 | + | Slider starting value | 10 | + | Slider increment value | 15 | + And I should see "This question type supports an absolute maximum range of -100 to +100. We expect the vast majority of questionnaire designs to use a range of 1-10 or -10 to +10." diff --git a/tests/csvexport_test.php b/tests/csvexport_test.php index dd164e71..04f3687c 100644 --- a/tests/csvexport_test.php +++ b/tests/csvexport_test.php @@ -91,15 +91,15 @@ private function expected_complete_output() { "Q07_Check Boxes 1012->twelve Q07_Check Boxes 1012->thirteen Q08_Rate Scale 1014->fourteen " . "Q08_Rate Scale 1014->fifteen Q08_Rate Scale 1014->sixteen Q08_Rate Scale 1014->seventeen " . "Q08_Rate Scale 1014->eighteen Q08_Rate Scale 1014->nineteen Q08_Rate Scale 1014->twenty " . - "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous", + "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous Q09_Slider 1016", " Test course 1 Testy Lastname1 username1 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname2 username2 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname3 username3 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname4 username4 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 "]; + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5"]; } /** @@ -115,16 +115,16 @@ private function expected_incomplete_output() { "Q07_Check Boxes 1012->twelve Q07_Check Boxes 1012->thirteen Q08_Rate Scale 1014->fourteen " . "Q08_Rate Scale 1014->fifteen Q08_Rate Scale 1014->sixteen Q08_Rate Scale 1014->seventeen " . "Q08_Rate Scale 1014->eighteen Q08_Rate Scale 1014->nineteen Q08_Rate Scale 1014->twenty " . - "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous", + "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous Q09_Slider 1016", " Test course 1 Testy Lastname1 username1 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname2 username2 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname3 username3 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname4 username4 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname5 username5 n Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 "]; + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5"]; } } diff --git a/tests/generator/lib.php b/tests/generator/lib.php index fa0d52bd..7f6c6adc 100644 --- a/tests/generator/lib.php +++ b/tests/generator/lib.php @@ -386,6 +386,10 @@ public function type_str($qtypeid) { break; case QUESPAGEBREAK: $qtype = 'sectionbreak'; + break; + case QUESSLIDER: + $qtype = 'Slider'; + break; } return $qtype; } @@ -429,6 +433,10 @@ public function type_name($qtypeid) { break; case QUESPAGEBREAK: $qtype = 'Section Break'; + break; + case QUESSLIDER: + $qtype = 'Slider'; + break; } return $qtype; } @@ -651,6 +659,9 @@ public function generate_response($questionnaire, $questions, $userid, $complete } $responses[] = new question_response($question->id, $answers); break; + case QUESSLIDER : + $responses[] = new question_response($question->id, 5); + break; } } @@ -671,7 +682,7 @@ public function create_and_fully_populate($coursecount = 4, $studentcount = 20, $dg = $this->datagenerator; $qdg = $this; - $questiontypes = [QUESTEXT, QUESESSAY, QUESNUMERIC, QUESDATE, QUESRADIO, QUESDROP, QUESCHECK, QUESRATE]; + $questiontypes = [QUESTEXT, QUESESSAY, QUESNUMERIC, QUESDATE, QUESRADIO, QUESDROP, QUESCHECK, QUESRATE, QUESSLIDER]; $students = []; $courses = []; $questionnaires = []; diff --git a/tests/questiontypes_test.php b/tests/questiontypes_test.php index be7aff5e..3d36aada 100644 --- a/tests/questiontypes_test.php +++ b/tests/questiontypes_test.php @@ -86,6 +86,12 @@ public function test_create_question_textbox() { $this->create_test_question(QUESTEXT, '\\mod_questionnaire\\question\\text', $questiondata); } + public function test_create_question_slider() { + $questiondata = array( + 'content' => 'Enter a number'); + $this->create_test_question(QUESSLIDER, '\\mod_questionnaire\\question\\slider', $questiondata); + } + public function test_create_question_yesno() { $this->create_test_question(QUESYESNO, '\\mod_questionnaire\\question\\yesno', array('content' => 'Enter yes or no')); } diff --git a/tests/responsetypes_test.php b/tests/responsetypes_test.php index 9c48b01c..79bf4c1c 100644 --- a/tests/responsetypes_test.php +++ b/tests/responsetypes_test.php @@ -90,6 +90,33 @@ public function test_create_response_text() { $this->assertEquals('This is my essay.', $textresponse->response); } + public function test_create_response_slider() { + global $DB; + + $this->resetAfterTest(); + + // Some common variables used below. + $userid = 1; + + // Set up a questionnaire with one text response question. + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); + $questiondata = ['content' => 'Enter some text']; + $questionnaire = $generator->create_test_questionnaire($course, QUESSLIDER, $questiondata); + $question = reset($questionnaire->questions); + $response = $generator->create_question_response($questionnaire, $question, 5, $userid); + + // Test the responses for this questionnaire. + $this->response_tests($questionnaire->id, $response->id, $userid); + + // Retrieve the specific text response. + $textresponses = $DB->get_records('questionnaire_response_text', ['response_id' => $response->id]); + $this->assertEquals(1, count($textresponses)); + $textresponse = reset($textresponses); + $this->assertEquals($question->id, $textresponse->question_id); + $this->assertEquals(5, $textresponse->response); + } + public function test_create_response_date() { global $DB; diff --git a/version.php b/version.php index cf3c5b81..2240b82a 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022030301; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022092200; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2022030300; // Moodle version (4.0). $plugin->component = 'mod_questionnaire';