From a3b11e784a45eccfad5e80d80bb09242b4937122 Mon Sep 17 00:00:00 2001 From: Lukas Celinak Date: 28.10.2018 18:00:41 MDL-63709 mod_feedback: Linear scale item Modified multianswer item and generated linear scale item. Radios stylled in style.css (difficulty: easy, requires teacher access to a course) Login as teacher or admin or manager TEST: add new answer to feedback, select Scale type TEST: select value from and value to, write left label and right label TEST: submit and save answer Login as student TEST: submit answer response Login as teacher TEST: show analyses, export TEST: show responses diff --git a/mod/feedback/item/scale/lib.php b/mod/feedback/item/scale/lib.php new file mode 100644 index 0000000..276a4aa --- /dev/null +++ b/mod/feedback/item/scale/lib.php @@ -0,0 +1,510 @@ +. + +/** + * Lib of functions for Scale item + * + * Edited multichoice answer as a new tzpe of answer, scale with value left label, + * radiobuttons with value above the button and righct label for value. + * + * @author Lukas Celinak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2018 Lukas Celinak + * @package mod_feedback + */ +defined('MOODLE_INTERNAL') OR die('not allowed'); +require_once($CFG->dirroot . '/mod/feedback/item/feedback_item_class.php'); + + +define('FEEDBACK_SCALE_START_SEP', '>>>>>'); +define('FEEDBACK_SCALE_VALUES_SEP', '|'); +define('FEEDBACK_SCALE_END_SEP', '<<<<<'); +define('FEEDBACK_SCALE_IGNOREEMPTY', 'i'); + +/** + * Main class for Scale item + * + * @author Lukas Celinak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2018 Lukas Celinak + * @package mod_feedback + */ +class feedback_item_scale extends feedback_item_base { + + /** + * Item type definition + * + * @var string $type + */ + protected $type = "scale"; + + /** + * Build form for add answer. + * + * @param stdClass $item + * @param stdClass $feedback + * @param stdClass $cm + */ + public function build_editform($item, $feedback, $cm) { + global $DB, $CFG; + require_once('scale_form.php'); + + // Get the lastposition number of the feedback_items. + $position = $item->position; + $lastposition = $DB->count_records('feedback_item', array('feedback' => $feedback->id)); + if ($position == -1) { + $itemfromlast = $lastposition + 1; + $itemformvalue = $lastposition + 1; + $item->position = $lastposition + 1; + } else { + $itemfromlast = $lastposition; + $itemformvalue = $item->position; + } + // The elements for position dropdownlist. + $positionlist = array_slice(range(0, $itemfromlast), 1, $itemfromlast, true); + + $item->presentation = empty($item->presentation) ? '' : $item->presentation; + $info = $this->get_info($item); + + $item->ignoreempty = $this->ignoreempty($item); + + // All items for dependitem. + $feedbackitems = feedback_get_depend_candidates_for_item($feedback, $item); + $commonparams = array('cmid' => $cm->id, + 'id' => isset($item->id) ? $item->id : null, + 'typ' => $item->typ, + 'items' => $feedbackitems, + 'feedback' => $feedback->id); + + // Build the form. + $customdata = array('item' => $item, + 'common' => $commonparams, + 'positionlist' => $positionlist, + 'position' => $position, + 'info' => $info); + + $this->item_form = new scaleform('edit_item.php', $customdata); + } + + /** + * Function for saving item to the database + * + * @return boolean + */ + public function save_item() { + global $DB; + + if (!$this->get_data()) { + return false; + } + $item = $this->item; + + if (isset($item->clone_item) AND $item->clone_item) { + $item->id = ''; + $item->position++; + } + + $this->set_ignoreempty($item, $item->ignoreempty); + + $item->hasvalue = $this->get_hasvalue(); + if (!$item->id) { + $item->id = $DB->insert_record('feedback_item', $item); + } else { + $DB->update_record('feedback_item', $item); + } + + return $DB->get_record('feedback_item', array('id' => $item->id)); + } + + /** + * Helper function for collected data, both for analysis page and export to excel + * + * @param stdClass $item the db-object from feedback_item + * @param int $groupid + * @param int $courseid + * @return array + */ + protected function get_analysed($item, $groupid = false, $courseid = false) { + $info = $this->get_info($item); + $analyseditem = array(); + $analyseditem[] = $item->typ; + $analyseditem[] = format_string($item->name); + + // Get the possible answers. + $answers = null; + + for ($index = $info->scalefrom; $index <= $info->scaleto; $index++) { + $answers[] = $index; + } + $answers[0] .= " ({$info->scalelabelfrom})"; + $answers[count($answers) - 1] .= " ({$info->scalelabelto})"; + + if (!is_array($answers)) { + return null; + } + + // Get the values. + $values = feedback_get_group_values($item, $groupid, $courseid, $this->ignoreempty($item)); + if (!$values) { + return null; + } + + // Get answertext, answercount and quotient for each answer. + $analysedanswer = array(); + + $sizeofanswers = count($answers); + for ($i = 1; $i <= $sizeofanswers; $i++) { + $ans = new stdClass(); + $ans->answertext = $answers[$i - 1]; + $ans->answercount = 0; + foreach ($values as $value) { + if ($value->value == $i) { + $ans->answercount++; + } + } + $ans->quotient = $ans->answercount / count($values); + $analysedanswer[] = $ans; + } + + $analyseditem[] = $analysedanswer; + return $analyseditem; + } + + /** + * Helper function for printing answer values + * + * @param stdSclass $item + * @param stdSclass $value + * @return string + */ + public function get_printval($item, $value) { + $info = $this->get_info($item); + + $printval = ''; + + if (!isset($value->value)) { + return $printval; + } + + for ($index = $info->scalefrom; $index <= $info->scaleto; $index++) { + $presentation[] = $index; + } + + $index = 1; + foreach ($presentation as $pres) { + if ($value->value == $index) { + $printval = format_string($pres); + break; + } + $index++; + } + + return $printval; + } + + /** + * Function for printing Scale item analysses + * + * @param stdSclass $item + * @param string $itemnr + * @param int $groupid + * @param int $courseid + */ + public function print_analysed($item, $itemnr = '', $groupid = false, $courseid = false) { + global $OUTPUT; + + $analyseditem = $this->get_analysed($item, $groupid, $courseid); + if ($analyseditem) { + $itemname = $analyseditem[1]; + echo "typ}\">"; + echo ''; + echo "
'; + echo $itemnr . ' '; + if (strval($item->label) !== '') { + echo '(' . format_string($item->label) . ') '; + } + echo format_string($itemname); + echo '
"; + $analysedvals = $analyseditem[2]; + $count = 0; + $data = []; + foreach ($analysedvals as $val) { + $quotient = format_float($val->quotient * 100, 2); + $strquotient = ''; + if ($val->quotient > 0) { + $strquotient = ' (' . $quotient . ' %)'; + } + $answertext = format_text(trim($val->answertext), FORMAT_HTML, array('noclean' => true, 'para' => false)); + + $data['labels'][$count] = $answertext; + $data['series'][$count] = $val->answercount; + $data['series_labels'][$count] = $val->answercount . $strquotient; + $count++; + } + $chart = new \core\chart_bar(); + $chart->set_horizontal(true); + $series = new \core\chart_series(format_string(get_string("responses", "feedback")), $data['series']); + $series->set_labels($data['series_labels']); + $chart->add_series($series); + $chart->set_labels($data['labels']); + + echo $OUTPUT->render($chart); + } + } + + /** + * Function for excel print + * + * @param stdSclass $worksheet + * @param int $rowoffset + * @param stdSclass $xlsformats + * @param stdSclass $item + * @param int $groupid + * @param int $courseid + * @return int + */ + public function excelprint_item(&$worksheet, $rowoffset, $xlsformats, $item, $groupid, $courseid = false) { + $analyseditem = $this->get_analysed($item, $groupid, $courseid); + $data = $analyseditem[2]; + $worksheet->write_string($rowoffset, 0, $item->label, $xlsformats->head2); + $worksheet->write_string($rowoffset, 1, $analyseditem[1], $xlsformats->head2); + if (is_array($data)) { + $sizeofdata = count($data); + for ($i = 0; $i < $sizeofdata; $i++) { + $analyseddata = $data[$i]; + $worksheet->write_string($rowoffset, $i + 2, trim($analyseddata->answertext), $xlsformats->head2); + $worksheet->write_number($rowoffset + 1, $i + 2, $analyseddata->answercount, $xlsformats->default); + $worksheet->write_number($rowoffset + 2, $i + 2, $analyseddata->quotient, $xlsformats->procent); + } + } + $rowoffset += 3; + return $rowoffset; + } + + /** + * Options for the scale element + * + * @param stdClass $item + * @return array + */ + protected function get_options($item) { + $info = $this->get_info($item); + $options = array(); + + for ($index = $info->scalefrom; $index <= $info->scaleto; $index++) { + $options[$index + 1] = format_text($index, FORMAT_HTML, array('noclean' => true, 'para' => false)); + } + + return $options; + } + + /** + * Adds an input element to the complete form + * + * @param stdClass $item + * @param mod_feedback_complete_form $form + */ + public function complete_form_element($item, $form) { + $info = $this->get_info($item); + $name = $this->get_display_name($item); + $class = 'scale'; + $inputname = $item->typ . '_' . $item->id; + $options = $this->get_options($item); + $separator = ' '; + $tmpvalue = $form->get_item_value($item); + // Display group or radio or checkbox elements. + $class .= ' scale-horizontal'; + $objs = []; + // Main div. + $objs[] = ['html', "
"]; + // Div with label left. + + $objs[] = ['html', "
"]; + $objs[] = ['html', "
"]; + $objs[] = ['html', "
"]; + $objs[] = ['html', "
" + . "
{$info->scalelabelfrom}" + . "
"]; + $objs[] = ['html', " $label) { + $objs[] = ['html', "
"]; + $objs[] = ['html', "
"]; + $objs[] = ['html', ""]; + $objs[] = ['html', "
"]; + $objs[] = ['html', "
"]; + $objs[] = ['radio', $inputname . '[0]', '', '', $idx]; + $objs[] = ['html', "
"]; + $objs[] = ['html', "
"]; + } + + // Div with label right. + $objs[] = ['html', "
"]; + $objs[] = ['html', "
"]; + $objs[] = ['html', "
"]; + $objs[] = ['html', "
" + . "
{$info->scalelabelto}
"]; + $objs[] = ['html', " 'feedback_item_' . $item->id])]; + $objs[] = ['html', "
"]; + $element = $form->add_form_group_element($item, 'group_' . $inputname, $name, $objs, $separator, $class); + $form->set_element_default($inputname . '[0]', $tmpvalue); + $form->set_element_type($inputname . '[0]', PARAM_INT); + + // Process 'required' rule. + if ($item->required) { + $elementname = $element->getName(); + $form->add_validation_rule(function($values, $files) use ($elementname, $item) { + $inputname = $item->typ . '_' . $item->id; + return empty($values[$inputname]) || !array_filter($values[$inputname]) ? + array($elementname => get_string('required')) : true; + }); + } + } + + /** + * Prepares value that user put in the form for storing in DB + * + * @param array $value + * @return string + */ + public function create_value($value) { + $value = array_unique(array_filter($value)); + return join(FEEDBACK_SCALE_VALUES_SEP, $value); + } + + /** + * Compares the dbvalue with the dependvalue + * + * @param stdClass $item + * @param string $dbvalue is the value input by user in the format as it is stored in the db + * @param string $dependvalue is the value that it needs to be compared against + */ + public function compare_value($item, $dbvalue, $dependvalue) { + if (is_array($dbvalue)) { + $dbvalues = $dbvalue; + } else { + $dbvalues = explode(FEEDBACK_SCALE_VALUES_SEP, $dbvalue); + } + + $info = $this->get_info($item); + + if ($info->presentation) { + $presentation = array(); + $start = explode(FEEDBACK_SCALE_START_SEP, $presentation); + $end = explode(FEEDBACK_SCALE_END_SEP, $start[1]); + $itemvalues = explode(FEEDBACK_SCALE_VALUES_SEP, $end[0]); + + $info->scalefrom = $itemvalues[0]; + $info->scaleto = $itemvalues[1]; + $info->scalelabelfrom = $start[0]; + $info->scalelabelto = $end[1]; + + for ($index1 = $info->scalefrom; $index1 < $info->scaleto; $index1++) { + $presentation[$index1] = $index1; + } + } + + $index = 1; + foreach ($presentation as $pres) { + foreach ($dbvalues as $dbval) { + if ($dbval == $index AND trim($pres) == $dependvalue) { + return true; + } + } + $index++; + } + return false; + } + + /** + * Function for generate object of scale item + * + * @param stdSclass $item + * @return \stdClass + */ + public function get_info($item) { + $presentation = empty($item->presentation) ? '' : $item->presentation; + $info = new stdClass(); + $info->presentation = ''; + if ($presentation) { + $start = explode(FEEDBACK_SCALE_START_SEP, $presentation); + $end = explode(FEEDBACK_SCALE_END_SEP, $start[1]); + $itemvalues = explode(FEEDBACK_SCALE_VALUES_SEP, $end[0]); + + $info->scalefrom = $itemvalues[0]; + $info->scaleto = $itemvalues[1]; + $info->scalelabelfrom = $start[0]; + $info->scalelabelto = $end[1]; + } + return $info; + } + + /** + * Function for set ignore empty submitted values + * + * @param stdSclass $item + * @param bool $ignoreempty + */ + public function set_ignoreempty($item, $ignoreempty = true) { + $item->options = str_replace(FEEDBACK_SCALE_IGNOREEMPTY, '', $item->options); + if ($ignoreempty) { + $item->options .= FEEDBACK_SCALE_IGNOREEMPTY; + } + } + + /** + * Function for check ignore empty submitted values + * + * @param stdSclass $item + * @return boolean + */ + public function ignoreempty($item) { + if (strstr($item->options, FEEDBACK_SCALE_IGNOREEMPTY)) { + return true; + } + return false; + } + + /** + * Return the analysis data ready for external functions. + * + * @param stdClass $item the item (question) information + * @param int $groupid the group id to filter data (optional) + * @param int $courseid the course id (optional) + * @return array an array of data with non scalar types json encoded + * @since Moodle 3.3 + */ + public function get_analysed_for_external($item, $groupid = false, $courseid = false) { + + $externaldata = array(); + $data = $this->get_analysed($item, $groupid, $courseid); + + if (!empty($data[2]) && is_array($data[2])) { + foreach ($data[2] as $d) { + $externaldata[] = json_encode($d); + } + } + return $externaldata; + } + +} diff --git a/mod/feedback/item/scale/scale_form.php b/mod/feedback/item/scale/scale_form.php new file mode 100644 index 0000000..ef056bd --- /dev/null +++ b/mod/feedback/item/scale/scale_form.php @@ -0,0 +1,123 @@ +. + +/** + * Form for adding a Scale item to feedback + * + * Edited multichoice answer as a new tzpe of answer, scale with value left label, + * radiobuttons with value above the button and righct label for value. + * + * @author Lukas Celinak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2018 Lukas Celinak + * @package mod_feedback + */ +defined('MOODLE_INTERNAL') OR die('not allowed'); +require_once($CFG->dirroot . '/mod/feedback/item/feedback_item_form_class.php'); + +/** + * Form class for adding a new scale answer to feedback + * + * + * @author Lukas Celinak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2018 Lukas Celinak + * @package mod_feedback + */ +class scaleform extends feedback_item_form { + + /** + * Item type definition + * + * @var string $type + */ + protected $type = "scale"; + + /** + * Add sclae answer type form definition + */ + public function definition() { + $item = $this->_customdata['item']; + $common = $this->_customdata['common']; + $positionlist = $this->_customdata['positionlist']; + $position = $this->_customdata['position']; + + $mform = & $this->_form; + $mform->addElement('header', 'general', get_string($this->type, 'feedback')); + $mform->addElement('advcheckbox', 'required', get_string('required', 'feedback'), '', null, array(0, 1)); + $mform->addElement('text', 'name', get_string('item_name', 'feedback'), array('size' => FEEDBACK_ITEM_NAME_TEXTBOX_SIZE, + 'maxlength' => 255)); + $mform->addElement('text', 'label', get_string('item_label', 'feedback'), array('size' => FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, + 'maxlength' => 255)); + $mform->addElement('selectyesno', 'ignoreempty', get_string('do_not_analyse_empty_submits', 'feedback')); + $arrayfrom = array('0' => '0', '1' => '1'); + $mform->addElement('select', 'scalefrom', get_string('scalefrom', 'feedback'), $arrayfrom); + + $arrayto = array(); + for ($index = 1; $index <= 10; $index++) { + $arrayto[$index] = $index; + } + + $mform->addElement('select', 'scaleto', get_string('scaleto', 'feedback'), $arrayto); + $mform->addElement('text', 'scalelabelfrom', get_string('scalelabelfrom', 'feedback')); + $mform->setType('scalelabelfrom', PARAM_RAW); + $mform->addElement('text', 'scalelabelto', get_string('scalelabelto', 'feedback')); + $mform->setType('scalelabelto', PARAM_RAW); + + parent::definition(); + $this->set_data($item); + } + + /** + * Function for set data to form + * + * @param stdClass $item + * @return stdClass + */ + public function set_data($item) { + $info = $this->_customdata['info']; + if (isset($info->scalefrom)) { + $item->scalefrom = $info->scalefrom; + $item->scaleto = $info->scaleto; + $item->scalelabelfrom = $info->scalelabelfrom; + $item->scalelabelto = $info->scalelabelto; + } + return parent::set_data($item); + } + + /** + * Get data from value saved in scale item + * + * @return boolean + */ + public function get_data() { + if (!$item = parent::get_data()) { + return false; + } + + $item->presentation = $item->scalelabelfrom . FEEDBACK_SCALE_START_SEP; + $item->presentation .= $item->scalefrom . FEEDBACK_SCALE_VALUES_SEP; + $item->presentation .= $item->scaleto . FEEDBACK_SCALE_END_SEP; + $item->presentation .= $item->scalelabelto; + + if (!isset($item->ignoreempty)) { + $item->ignoreempty = 0; + } + + return $item; + } + +} \ No newline at end of file diff --git a/mod/feedback/styles.css b/mod/feedback/styles.css index 3857508..18b001e 100644 --- a/mod/feedback/styles.css +++ b/mod/feedback/styles.css @@ -116,3 +116,62 @@ .path-mod-feedback .response_navigation .prev_response { text-align: left; } + +.path-mod-feedback .scale-item { + display: flex; + justify-content: center; + min-height: 3em; + min-width: 30%; +} + +.path-mod-feedback .scale-item-label-container { + flex-direction: column; + text-align: center; + max-width: 20%; + display: flex; + flex-grow: 1; +} + +.path-mod-feedback .scale-item-label-box { + display: flex; + justify-content: center; + min-height: 3em; + align-items: center; +} + +.path-mod-feedback .scale-item-label-text { + line-height: 135%; + min-width: 0%; + padding: 0 5px; + font-weight: bold; + +} + +.path-mod-feedback .scale-item-radio-container { + display: flex; + flex-direction: column; + text-align: center; + flex-grow: 1; + align-items: stretch; +} + +.path-mod-feedback .scale-item-radio-box { + justify-content: center; + min-height: 3em; + display: flex; +} + +.path-mod-feedback .scale-item-radio-box-input { + background-color: #fafafa; + justify-content: center; + min-height: 3em; + display: flex; +} + +.path-mod-feedback .scale-item .scale-item-radio-box-input input { + margin-right: 0; + +} +.path-mod-feedback .scale-item .scale-item-radio-box-input label { + margin-right: 0; +} \ No newline at end of file