Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

<?php
// 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 <http://www.gnu.org/licenses/>.

defined('MOODLE_INTERNAL') || die();

use mod_questionnaire\generator\question_response,
    mod_questionnaire\generator\question_response_rank,
    mod_questionnaire\question\question;

global $CFG;
require_once($CFG->dirroot.'/mod/questionnaire/locallib.php');
require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php');

/**
 * The mod_questionnaire data generator.
 *
 * @package    mod_questionnaire
 * @copyright  2015 Mike Churchward (mike@churchward.ca)
 * @author     Mike Churchward
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class mod_questionnaire_generator extends testing_module_generator {

    /**
     * @var int Current position of assigned options.
     */
    protected $curpos = 0;

    /**
     * @var int keep track of how many questions have been created.
     */
    protected $questioncount = 0;

    /**
     * @var int
     */
    protected $responsecount = 0;

    /**
     * @var questionnaire[]
     */
    protected $questionnaires = [];

    /**
     * To be called from data reset code only, do not use in tests.
     * @return void
     */
    public function reset() {
        $this->questioncount = 0;
        $this->responsecount = 0;
        $this->questionnaires = [];
        parent::reset();
    }

    /**
     * Acessor for questionnaires.
     * @return array
     */
    public function questionnaires() {
        return $this->questionnaires;
    }

    /**
     * Create a questionnaire activity.
     * @param array $record Will be changed in this function.
     * @param array $options
     * @return questionnaire
     */
    public function create_instance($record = null, array $options = null) {
        $record = (object)(array)$record;

        $defaultquestionnairesettings = array(
            'qtype' => 0,
            'respondenttype' => 'fullname',
            'resp_eligible' => 'all',
            'resp_view' => 0,
            'opendate' => 0,
            'closedate' => 0,
            'resume' => 0,
            'navigate' => 0,
            'grade' => 0,
            'sid' => 0,
            'timemodified' => time(),
            'completionsubmit' => 0,
            'autonum' => 3,
            'create' => 'new-0', // Used in form only to indicate a new, empty instance.
        );

        foreach ($defaultquestionnairesettings as $name => $value) {
            if (!isset($record->{$name})) {
                $record->{$name} = $value;
            }
        }

        $instance = parent::create_instance($record, (array)$options);
        $cm = get_coursemodule_from_instance('questionnaire', $instance->id);
        $course = get_course($cm->course);
        $questionnaire = new questionnaire($course, $cm, 0, $instance, false);

        $this->questionnaires[$instance->id] = $questionnaire;

        return $questionnaire;
    }

    /**
     * Create a survey instance with data from an existing questionnaire object.
     * @param questionnaire $questionnaire
     * @param array $record
     * @return bool|int
     */
    public function create_content($questionnaire, $record = array()) {
        global $DB;

        $survey = $DB->get_record('questionnaire_survey', array('id' => $questionnaire->sid), '*', MUST_EXIST);
        foreach ($record as $name => $value) {
            $survey->{$name} = $value;
        }
        return $questionnaire->survey_update($survey);
    }

    /**
     * Function to create a question.
     *
     * @param questionnaire $questionnaire
     * @param array|stdClass $record
     * @param array|stdClass $data - accompanying data for question - e.g. choices
     * @return \mod_questionnaire\question\question the question object
     */
    public function create_question(questionnaire $questionnaire, $record = null, $data = null) {
        global $DB;

        // Increment the question count.
        $this->questioncount++;

        $record = (array)$record;

        $record['position'] = count($questionnaire->questions);

        if (!isset($record['surveyid'])) {
            throw new coding_exception('surveyid must be present in phpunit_util::create_question() $record');
        }

        if (!isset($record['name'])) {
            throw new coding_exception('name must be present in phpunit_util::create_question() $record');
        }

        if (!isset($record['type_id'])) {
            throw new coding_exception('typeid must be present in phpunit_util::create_question() $record');
        }

        if (!isset($record['content'])) {
            $record['content'] = 'Random '.$this->type_str($record['type_id']).' '.uniqid();
        }

        // Get question type.
        $typeid = $record['type_id'];

        if ($typeid === QUESRATE && !isset($record['length'])) {
            $record['length'] = 5;
        }

        if ($typeid !== QUESPAGEBREAK && $typeid !== QUESSECTIONTEXT) {
            $qtype = $DB->get_record('questionnaire_question_type', ['id' => $typeid]);
            if (!$qtype) {
                throw new coding_exception('Could not find question type with id ' . $typeid);
            }
            // Throw an error if this requires choices and it hasn't got them.
            $this->validate_question($qtype->typeid, $data);
        }

        $record = (object)$record;

        // Add the question.
        $record->id = $DB->insert_record('questionnaire_question', $record);

        $question = \mod_questionnaire\question\question::question_builder($record->type_id, $record->id, $record);

        // Add the question choices if required.
        if ($typeid !== QUESPAGEBREAK && $typeid !== QUESSECTIONTEXT) {
            if ($question->has_choices()) {
                $this->add_question_choices($question, $data);
                $record->opts = $data;
            }
        }

        // Update questionnaire.
        $questionnaire->add_questions();

        return $question;
    }

    /**
     * Create a questionnaire with questions and response data for use in other tests.
     * @param stdClass $course
     * @param null|int $qtype
     * @param array $questiondata
     * @param null|array|stdClass $choicedata
     * @return questionnaire
     */
    public function create_test_questionnaire($course, $qtype = null, $questiondata = array(), $choicedata = null) {
        $questionnaire = $this->create_instance(array('course' => $course->id));
        $cm = get_coursemodule_from_instance('questionnaire', $questionnaire->id);
        if ($qtype !== null) {
            $questiondata['type_id'] = $qtype;
            $questiondata['surveyid'] = $questionnaire->sid;
            $questiondata['name'] = isset($questiondata['name']) ? $questiondata['name'] : 'Q1';
            $questiondata['content'] = isset($questiondata['content']) ? $questiondata['content'] : 'Test content';
            $this->create_question($questionnaire, $questiondata, $choicedata);
        }
        $questionnaire = new questionnaire($course, $cm, $questionnaire->id, null, true);
        return $questionnaire;
    }

    /**
     * Create a reponse to the supplied question.
     * @param questionnaire $questionnaire
     * @param question $question
     * @param int|array $respval
     * @param int $userid
     * @param int $section
     * @return false|mixed|stdClass
     */
    public function create_question_response($questionnaire, $question, $respval, $userid = 1, $section = 1) {
        global $DB;
        $currentrid = 0;
        if (!is_array($respval)) {
            $respval = ['q'.$question->id => $respval];
        }
        $respdata = (object)(array_merge(['sec' => $section, 'rid' => $currentrid, 'a' => $questionnaire->id], $respval));
        $responseid = $questionnaire->response_insert($respdata, $userid);
        $this->response_commit($questionnaire, $responseid);
        return $DB->get_record('questionnaire_response', array('id' => $responseid));
    }

    /**
     * Need to create a method to access a private questionnaire method.
     * TO DO - may not need this with above "TO DO".
     * @param questionnaire $questionnaire
     * @param int $responseid
     * @return mixed
     */
    private function response_commit($questionnaire, $responseid) {
        $method = new ReflectionMethod('questionnaire', 'response_commit');
        $method->setAccessible(true);
        return $method->invoke($questionnaire, $responseid);
    }

    /**
     * Validate choice question type
     * @param array $data
     * @throws coding_exception
     */
    protected function validate_question_choice($data) {
        if (empty($data)) {
            throw new coding_exception('You must pass in an array of choices for the choice question type');
        }
    }

    /**
     * Validate radio question type
     * @param array $data
     * @throws coding_exception
     */
    protected function validate_question_radio($data) {
        if (empty($data)) {
            throw new coding_exception('You must pass in an array of choices for the radio question type');
        }
    }

    /**
     * Validate checkbox question type
     * @param array $data
     * @throws coding_exception
     */
    protected function validate_question_check($data) {
        if (empty($data)) {
            throw new coding_exception('You must pass in an array of choices for the checkbox question type');
        }
    }

    /**
     * Validate rating question type
     * @param array $data
     * @throws coding_exception
     */
    protected function validate_question_rate($data) {
        if (empty($data)) {
            throw new coding_exception('You must pass in an array of choices for the rate question type');
        }
    }

    /**
     * Thrown an error if the question isn't receiving the data it should receive.
     * @param string $typeid
     * @param array $data
     */
    protected function validate_question($typeid, $data) {
        if ($typeid == QUESCHOOSE) {
            $this->validate_question_choice($data);
        } else if ($typeid === QUESRADIO) {
            $this->validate_question_radio($data);
        } else if ($typeid === QUESCHECK) {
            $this->validate_question_check($data);
        } else if ($typeid === QUESRATE) {
            $this->validate_question_rate($data);
        }
    }

    /**
     * Add choices to question.
     *
     * @param \mod_questionnaire\question\question $question
     * @param array $data
     */
    protected function add_question_choices($question, $data) {
        foreach ($data as $content) {
            if (!is_object($content)) {
                $content = (object)[
                    'content' => $content,
                    'value' => $content
                ];
            }
            $record = (object)[
                'question_id' => $question->id,
                'content' => $content->content,
                'value' => $content->value
            ];
            $question->add_choice($record);
        }
    }

    /**
     * Does this question have choices.
     * TODO - use question object
     * @param int $typeid
     * @return bool
     */
    public function question_has_choices($typeid) {
        $choicequestions = [QUESCHOOSE, QUESRADIO, QUESCHECK, QUESDROP, QUESRATE];
        return in_array($typeid, $choicequestions);
    }

    /**
     * Return a string value for the int id.
     * @param int $qtypeid
     * @return string
     */
    public function type_str($qtypeid) {
        switch ($qtypeid) {
            case QUESYESNO:
                $qtype = 'yesno';
                break;
            case QUESTEXT:
                $qtype = 'textbox';
                break;
            case QUESESSAY:
                $qtype = 'essaybox';
                break;
            case QUESRADIO:
                $qtype = 'radiobuttons';
                break;
            case QUESCHECK:
                $qtype = 'checkboxes';
                break;
            case QUESDROP:
                $qtype = 'dropdown';
                break;
            case QUESRATE:
                $qtype = 'ratescale';
                break;
            case QUESDATE:
                $qtype = 'date';
                break;
            case QUESNUMERIC:
                $qtype = 'numeric';
                break;
            case QUESSECTIONTEXT:
                $qtype = 'sectiontext';
                break;
            case QUESPAGEBREAK:
                $qtype = 'sectionbreak';
                break;
            case QUESSLIDER:
                $qtype = 'Slider';
                break;
        }
        return $qtype;
    }

    /**
     * Return a display string for the int id.
     * @param int $qtypeid
     * @return string
     */
    public function type_name($qtypeid) {
        switch ($qtypeid) {
            case QUESYESNO:
                $qtype = 'Yes / No';
                break;
            case QUESTEXT:
                $qtype = 'Text Box';
                break;
            case QUESESSAY:
                $qtype = 'Essay Box';
                break;
            case QUESRADIO:
                $qtype = 'Radio Buttons';
                break;
            case QUESCHECK:
                $qtype = 'Check Boxes';
                break;
            case QUESDROP:
                $qtype = 'Drop Down';
                break;
            case QUESRATE:
                $qtype = 'Rate Scale';
                break;
            case QUESDATE:
                $qtype = 'Date';
                break;
            case QUESNUMERIC:
                $qtype = 'Numeric';
                break;
            case QUESSECTIONTEXT:
                $qtype = 'Section Text';
                break;
            case QUESPAGEBREAK:
                $qtype = 'Section Break';
                break;
            case QUESSLIDER:
                $qtype = 'Slider';
                break;
        }
        return $qtype;
    }

    /**
     * Add the response choice.
     * @param \mod_questionnaire\responsetype\response\response $questionresponse
     * @param int $responseid
     */
    protected function add_response_choice($questionresponse, $responseid) {
        global $DB;

        $question = $DB->get_record('questionnaire_question', ['id' => $questionresponse->questionid]);
        $qtype = intval($question->type_id);

        if (is_array($questionresponse->response)) {
            foreach ($questionresponse->response as $choice) {
                $newresponse = clone($questionresponse);
                $newresponse->response = $choice;
                $this->add_response_choice($newresponse, $responseid);
            }
            return;
        }

        if ($qtype === QUESCHOOSE || $qtype === QUESRADIO || $qtype === QUESDROP || $qtype === QUESCHECK || $qtype === QUESRATE) {
            if (is_int($questionresponse->response)) {
                $choiceid = $questionresponse->response;
            } else {
                if ($qtype === QUESRATE) {
                    if (!$questionresponse->response instanceof question_response_rank) {
                        throw new coding_exception('Question response for ranked choice should be of type question_response_rank');
                    }
                    $choiceval = $questionresponse->response->choice->content;
                } else {
                    if (!is_object($questionresponse->response)) {
                        $choiceval = $questionresponse->response;
                    } else {
                        if ($questionresponse->response->content.'' === '') {
                            throw new coding_exception('Question response cannot be null for question type '.$qtype);
                        }
                        $choiceval = $questionresponse->response->content;
                    }

                }

                // Lookup the choice id.
                $comptext = $DB->sql_compare_text('content');
                $select = 'WHERE question_id = ? AND '.$comptext.' = ?';

                $params = [intval($question->id), $choiceval];
                $rs = $DB->get_records_sql("SELECT * FROM {questionnaire_quest_choice} $select", $params, 0, 1);
                $choice = reset($rs);
                if (!$choice) {
                    throw new coding_exception('Could not find choice for "'.$choiceval.
                        '" (question_id = '.$question->id.')', var_export($choiceval, true));
                }
                $choiceid = $choice->id;

            }
            if ($qtype == QUESRATE) {
                $DB->insert_record('questionnaire_response_rank', [
                        'response_id' => $responseid,
                        'question_id' => $questionresponse->questionid,
                        'choice_id' => $choiceid,
                        'rankvalue' => $questionresponse->response->rankvalue
                    ]
                );
            } else {
                if ($qtype === QUESCHOOSE || $qtype === QUESRADIO || $qtype === QUESDROP) {
                    $instable = 'questionnaire_resp_single';
                } else if ($qtype === QUESCHECK) {
                    $instable = 'questionnaire_resp_multiple';
                }
                $DB->insert_record($instable, [
                        'response_id' => $responseid,
                        'question_id' => $questionresponse->questionid,
                        'choice_id' => $choiceid
                    ]
                );
            }
        } else {
            $DB->insert_record('questionnaire_response_text', [
                    'response_id' => $responseid,
                    'question_id' => $questionresponse->questionid,
                    'response' => $questionresponse->response
                ]
            );
        }
    }

    /**
     * Create response to questionnaire.
     *
     * @param array $questionresponses
     * @param array|stdClass $record
     * @param boolean $complete Whether the response is complete or not.
     * @return stdClass the discussion object
     */
    public function create_response($questionresponses, $record = null, $complete = true) {
        global $DB;

        // Increment the response count.
        $this->responsecount++;

        $record = (array)$record;

        if (!isset($record['questionnaireid'])) {
            throw new coding_exception('questionnaireid must be present in phpunit_util::create_response() $record');
        }

        if (!isset($record['userid'])) {
            throw new coding_exception('userid must be present in phpunit_util::create_response() $record');
        }

        $record['submitted'] = time() + $this->responsecount;

        // Add the response.
        $record['id'] = $DB->insert_record('questionnaire_response', $record);
        $responseid = $record['id'];

        foreach ($questionresponses as $questionresponse) {
            if (!$questionresponse instanceof question_response) {
                throw new coding_exception('Question responses must have an instance of question_response'.
                    var_export($questionresponse, true));
            }
            $this->add_response_choice($questionresponse, $responseid);
        }

        // Mark response as complete.
        $record['complete'] = ($complete) ? 'y' : 'n';
        $DB->update_record('questionnaire_response', $record);

        return $record;
    }


    /**
     * Generate an array of assigned options;
     * @param int $number
     */
    public function assign_opts($number = 5) {
        $opts = 'blue, red, yellow, orange, green, purple, white, black, earth, wind, fire, space, car, truck, train' .
            ', van, tram, one, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve, thirteen' .
            ', fourteen, fifteen, sixteen, seventeen, eighteen, nineteen, twenty, happy, sad, jealous, angry';
        $opts = explode (', ', $opts);
        $numopts = count($opts);

        if ($number > (count($opts) / 2)) {
            throw new coding_exception('Maxiumum number of options is '.(count($opts) / 2));
        }

        $retopts = [];
        while (count($retopts) < $number) {
            $retopts[] = $opts[$this->curpos];
            $retopts = array_unique($retopts);
            if (++$this->curpos == $numopts) {
                $this->curpos = 0;
            }
        }
        // Return re-indexed version of array (otherwise you can get a weird index of 1,2,5,9, etc).
        return array_values($retopts);
    }

    /**
     * Generate a response.
     * @param questionnaire $questionnaire
     * @param \mod_questionnaire\question\question[] $questions
     * @param int $userid
     * @param bool $complete
     * @return stdClass
     */
    public function generate_response($questionnaire, $questions, $userid, $complete = true) {
        $responses = [];
        foreach ($questions as $question) {

            $choices = [];
            if ($question->has_choices()) {
                $choices = array_values($question->choices);
            }

            switch ($question->type_id) {
                case QUESTEXT :
                    $responses[] = new question_response($question->id, 'Test answer');
                    break;
                case QUESESSAY :
                    $resptext = '<h1>Some header text</h1><p>Some paragraph text</p>';
                    $responses[] = new question_response($question->id, $resptext);
                    break;
                case QUESNUMERIC :
                    $responses[] = new question_response($question->id, 83);
                    break;
                case QUESDATE :
                    $date = mktime(0, 0, 0, 12, 28, 2017);
                    $dateformat = get_string('strfdate', 'questionnaire');
                    $datestr = userdate ($date, $dateformat, '1', false);
                    $responses[] = new question_response($question->id, $datestr);
                    break;
                case QUESRADIO :
                case QUESDROP :
                    $optidx = count($choices) - 1;
                    $responses[] = new question_response($question->id, $choices[$optidx]);
                    break;
                case QUESCHECK :
                    $answers = [];
                    for ($a = 0; $a < count($choices) - 1; $a++) {
                        $optidx = count($choices) - 1;
                        $answers[] = $choices[$optidx]->content;
                    }

                    $answers = array_unique($answers);

                    $responses[] = new question_response($question->id, $answers);
                    break;
                case QUESRATE :
                    $answers = [];
                    for ($a = 0; $a < count($choices) - 1; $a++) {
                        $answers[] = new question_response_rank($choices[$a], (($a % 5) + 1));
                    }
                    $responses[] = new question_response($question->id, $answers);
                    break;
                case QUESSLIDER :
                    $responses[] = new question_response($question->id, 5);
                    break;
            }

        }
        return $this->create_response($responses, ['questionnaireid' => $questionnaire->id, 'userid' => $userid], $complete);
    }

    /**
     * Create fully defined questionnaires into the test database.
     * @param int $coursecount
     * @param int $studentcount
     * @param int $questionnairecount
     * @param int $questionspertype
     */
    public function create_and_fully_populate($coursecount = 4, $studentcount = 20, $questionnairecount = 2,
                                              $questionspertype = 5) {
        global $DB;

        $dg = $this->datagenerator;
        $qdg = $this;

        $this->curpos = 0;
        $questiontypes = [QUESTEXT, QUESESSAY, QUESNUMERIC, QUESDATE, QUESRADIO, QUESDROP, QUESCHECK, QUESRATE, QUESSLIDER];
        $students = [];
        $courses = [];
        $questionnaires = [];

        for ($u = 0; $u < $studentcount; $u++) {
            $students[] = $dg->create_user(['firstname' => 'Testy']);
        }

        $manplugin = enrol_get_plugin('manual');

        // Create courses.
        for ($c = 0; $c < $coursecount; $c++) {
            $course = $dg->create_course();
            $courses[] = $course;

            // Enrol students on course.
            $manualenrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
            foreach ($students as $student) {
                $studentrole = $DB->get_record('role', array('shortname' => 'student'));
                $manplugin->enrol_user($manualenrol, $student->id, $studentrole->id);
            }
        }

        // Create questionnaires in each course.
        $qname = 1000;
        for ($q = 0; $q < $questionnairecount; $q++) {
            foreach ($courses as $course) {
                $questionnaire = $qdg->create_instance(['course' => $course->id]);
                $questionnaires[] = $questionnaire;
                $questions = [];
                foreach ($questiontypes as $questiontype) {
                    // Add section text for this question.
                    $qdg->create_question(
                        $questionnaire,
                        [
                            'surveyid' => $questionnaire->sid,
                            'name' => $qdg->type_name($questiontype),
                            'type_id' => QUESSECTIONTEXT
                        ]
                    );
                    // Create questions.
                    for ($qpt = 0; $qpt < $questionspertype; $qpt++) {
                        $opts = null;
                        if ($qdg->question_has_choices($questiontype)) {
                            $opts = $qdg->assign_opts(10);
                        }
                        $questions[] = $qdg->create_question(
                            $questionnaire,
                            [
                                'surveyid' => $questionnaire->sid,
                                'name' => $qdg->type_name($questiontype).' '.$qname++,
                                'type_id' => $questiontype
                            ],
                            $opts
                        );
                    }
                    // Add page break.
                    $qdg->create_question(
                        $questionnaire,
                        [
                            'surveyid' => $questionnaire->sid,
                            'name' => 'pagebreak '.$qname++,
                            'type_id' => QUESPAGEBREAK
                        ]
                    );
                }

                // Create responses.
                $count = 1;
                foreach ($students as $student) {
                    // Make the last response an "incomplete" response.
                    if ($count < $studentcount) {
                        $qdg->generate_response($questionnaire, $questions, $student->id);
                    } else {
                        $qdg->generate_response($questionnaire, $questions, $student->id, false);
                    }
                    $count++;
                }
            }
        }
    }
}