Commit 25215446 authored by 0815-xyz's avatar 0815-xyz
Browse files

Initial commit

parents
<?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/>.
/**
* This interface defines the methods required for pluggable statistic-results that may be added to the question analysis.
*
* @copyright 2013 Middlebury College {@link http://www.middlebury.edu/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\questionanalysis\statistics;
interface question_statistic_result {
/**
* A sortable version of the result.
*
* @return mixed string or numeric
*/
public function sortable ();
/**
* A printable version of the result.
*
* @param numeric $result
* @return mixed string or numeric
*/
public function printable ();
}
<?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/>.
/**
* This interface defines the methods required for pluggable statistics that may be added to the question analysis.
*
* @copyright 2013 Middlebury College {@link http://www.middlebury.edu/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\questionanalysis\statistics;
use mod_adaptivequiz\local\questionanalysis\question_analyser;
class times_used_statistic implements question_statistic {
/**
* Answer a display-name for this statistic.
*
* @return string
*/
public function get_display_name () {
return get_string('times_used_display_name', 'adaptivequiz');
}
/**
* Calculate this statistic for a question's results
*
* @param question_analyser $analyser
* @return question_statistic_result
*/
public function calculate (question_analyser $analyser) {
return new times_used_statistic_result (count($analyser->get_results()));
}
}
<?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/>.
/**
* This interface defines the methods required for pluggable statistic-results that may be added to the question analysis.
*
* @copyright 2013 Middlebury College {@link http://www.middlebury.edu/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\questionanalysis\statistics;
class times_used_statistic_result implements question_statistic_result {
/** @var int $count */
protected $count = null;
/**
* Constructor
*
* @param int $count
* @return void
*/
public function __construct ($count) {
$this->count = $count;
}
/**
* A sortable version of the result.
*
* @return mixed string or numeric
*/
public function sortable () {
return $this->count;
}
/**
* A printable version of the result.
*
* @param numeric $result
* @return mixed string or numeric
*/
public function printable () {
return $this->count;
}
}
<?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/>.
namespace mod_adaptivequiz\local\report;
use core_tag_tag;
use mod_adaptivequiz\local\catalgo;
use question_engine;
use question_usage_by_activity;
use stdClass;
/**
* Provides data for attempt reports.
*
* The class is intended to be used in exporting of renderable objects only.
*
* @package mod_adaptivequiz
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_report_helper {
/**
* Returns data to build the answers distribution report for the given attempt.
*
* The returned data has the following format:
* array(
* int => (object) array(
* 'numcorrect' => int,
* 'numwrong' => int,
* )
* )
* where the int key is a question difficulty level, 'numcorrect' and 'numwrong' are the numbers of correct and wrong answers
* for the key level respectively.
*
* @param int $attemptid
* @return stdClass[]
*/
public static function prepare_answers_distribution_data(int $attemptid): array {
global $DB;
$attemptrecord = $DB->get_record('adaptivequiz_attempt', ['id' => $attemptid], '*', MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attemptrecord->instance], '*', MUST_EXIST);
$quba = question_engine::load_questions_usage_by_activity($attemptrecord->uniqueid);
$data = [];
// This step is required to ensure we have an entry for each difficulty level, even when no questions of
// such level were administered.
for ($i = $adaptivequiz->lowestlevel; $i <= $adaptivequiz->highestlevel; $i++) {
$dataitem = new stdClass();
$dataitem->numcorrect = 0;
$dataitem->numwrong = 0;
$data[$i] = $dataitem;
}
foreach ($quba->get_slots() as $slot) {
$question = $quba->get_question($slot);
$tags = core_tag_tag::get_item_tags_array('core_question', 'question', $question->id);
$difficulty = adaptivequiz_get_difficulty_from_tags($tags);
// KNIGHT: Questions are considered correct if the marked fraction is at least at the acceptance threshold determined by the plugin settings
$answeredcorrectly = ($quba->get_question_fraction($slot) >= $adaptivequiz->acceptancethreshold);
$answeredcorrectly ? $data[$difficulty]->numcorrect++ : $data[$difficulty]->numwrong++;
}
return $data;
}
/**
* Returns data to build the questions administration report for the given attempt.
*
* The returned data has the following format:
* array(
* int => (object) array(
* 'targetdifficulty' => int,
* 'administereddifficulty' => int,
* 'abilitymeasure' => float,
* 'standarderrormax' => float,
* 'standarderrormin' => float,
* 'standarderror' => float,
* 'answeredcorrectly' => bool,
* )
* )
* where the int key is a question slot number (sequence number), the properties have the following meaning:
* targetdifficulty - the difficulty level the algorithm has suggested for the administered question
* administereddifficulty - the difficulty level the actually administered question had
* standarderrormax - the maximum possible value of ability measure given the current standard error value
* standarderrormin - the opposite to the above
* standarderror - the percentage value
* answeredcorrectly - whether the administered question was answered correctly
*
* @param int $attemptid
* @return stdClass[]
*/
public static function prepare_administration_data(int $attemptid): array {
global $DB;
$attemptrecord = $DB->get_record('adaptivequiz_attempt', ['id' => $attemptid], '*', MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attemptrecord->instance], '*', MUST_EXIST);
$quba = question_engine::load_questions_usage_by_activity($attemptrecord->uniqueid);
$data = [];
$numattempted = 0;
$difficultysum = 0;
$sumcorrect = 0;
$sumincorrect = 0;
foreach ($quba->get_slots() as $i => $slot) {
$targetlevel = ($i > 0)
? self::compute_target_difficulty_level($quba, $adaptivequiz, $slot, $numattempted)
: $adaptivequiz->startinglevel;
$question = $quba->get_question($slot);
$tags = core_tag_tag::get_item_tags_array('core_question', 'question', $question->id);
$difficulty = adaptivequiz_get_difficulty_from_tags($tags);
// KNIGHT: Questions are considered correct if the marked fraction is at least at the acceptance threshold determined by the plugin settings
$answeredcorrectly = ($quba->get_question_fraction($slot) >= $adaptivequiz->acceptancethreshold);
$answeredcorrectly ? $sumcorrect++ : $sumincorrect++;
$qdifficultylogits = catalgo::convert_linear_to_logit($difficulty, $adaptivequiz->lowestlevel,
$adaptivequiz->highestlevel);
$difficultysum = $difficultysum + $qdifficultylogits;
$numattempted++;
$abilitylogits = catalgo::estimate_measure($difficultysum, $numattempted, $sumcorrect,
$sumincorrect);
$abilityfraction = 1 / ( 1 + exp( (-1 * $abilitylogits) ) );
$ability = (($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel) * $abilityfraction) + $adaptivequiz->lowestlevel;
$stderrlogits = catalgo::estimate_standard_error($numattempted, $sumcorrect, $sumincorrect);
$stderr = catalgo::convert_logit_to_percent($stderrlogits);
$errormax = min($adaptivequiz->highestlevel,
$ability + ($stderr * ($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel)));
$errormin = max($adaptivequiz->lowestlevel,
$ability - ($stderr * ($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel)));
$dataitem = new stdClass();
$dataitem->targetdifficulty = $targetlevel;
$dataitem->administereddifficulty = $difficulty;
$dataitem->abilitymeasure = $ability;
$dataitem->standarderrormax = $errormax;
$dataitem->standarderrormin = $errormin;
$dataitem->standarderror = $stderr;
$dataitem->answeredcorrectly = $answeredcorrectly;
$data[$i+1] = $dataitem;
}
return $data;
}
/**
* Computes the target difficulty level based on the answer to previous question.
*
* @param question_usage_by_activity $quba
* @param stdClass $adaptivequiz A record from {adaptivequiz}.
* @param int $slot Slot of the question to be administered.
* @param int $numattempted Number of questions already attempted.
* @return int
*/
private static function compute_target_difficulty_level(
question_usage_by_activity $quba,
stdClass $adaptivequiz,
int $slot,
int $numattempted
): int {
$previousslot = $slot - 1;
$previousquestion = $quba->get_question($previousslot);
$previousqtags = core_tag_tag::get_item_tags_array('core_question', 'question', $previousquestion->id);
$previousdifficulty = adaptivequiz_get_difficulty_from_tags($previousqtags);
$difficultylogits = catalgo::convert_linear_to_logit($previousdifficulty, $adaptivequiz->lowestlevel,
$adaptivequiz->highestlevel);
// KNIGHT: Questions are considered correct if the marked fraction is at least at the acceptance threshold determined by the plugin settings
$answeredcorrectly = ($quba->get_question_fraction($previousslot) >= $adaptivequiz->acceptancethreshold);
if ($answeredcorrectly) {
$targetlevel = round(catalgo::map_logit_to_scale($difficultylogits + 2 / $numattempted,
$adaptivequiz->highestlevel, $adaptivequiz->lowestlevel));
if ($targetlevel == $previousdifficulty && $targetlevel < $adaptivequiz->highestlevel) {
$targetlevel++;
}
return $targetlevel;
}
$targetlevel = round(catalgo::map_logit_to_scale($difficultylogits - 2 / $numattempted,
$adaptivequiz->highestlevel, $adaptivequiz->lowestlevel));
if ($targetlevel == $previousdifficulty && $targetlevel > $adaptivequiz->lowestlevel) {
$targetlevel--;
}
return $targetlevel;
}
}
<?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/>.
/**
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\individual_user_attempts;
final class filter {
/**
* @var int $adaptivequizid
*/
public $adaptivequizid;
/**
* @var int $userid
*/
public $userid;
public static function from_vars(int $adaptivequizid, int $userid): self {
$return = new self();
$return->adaptivequizid = $adaptivequizid;
$return->userid = $userid;
return $return;
}
}
<?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/>.
/**
* Contains definition of the table class for the user attempts report.
*
* @package mod_adaptivequiz
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\individual_user_attempts;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/tablelib.php');
use mod_adaptivequiz\local\attempt\attempt_state;
use mod_adaptivequiz\local\report\questions_difficulty_range;
use mod_adaptivequiz_renderer;
use moodle_url;
use stdClass;
use table_sql;
/**
* Definition of the table class for the user attempts report.
*
* @package mod_adaptivequiz
*/
final class table extends table_sql {
/**
* @var mod_adaptivequiz_renderer $renderer
*/
private $renderer;
/**
* @var filter $filter
*/
private $filter;
/**
* @var questions_difficulty_range $questionsdifficultyrange
*/
private $questionsdifficultyrange;
/**
* @var int $cmid
*/
private $cmid;
public function __construct(
mod_adaptivequiz_renderer $renderer,
filter $filter,
moodle_url $baseurl,
questions_difficulty_range $questionsdifficultyrange,
int $cmid,
//KNIGHT: Modifications with the purpose of hiding the column "reason for stopping the attempt" in the student review
bool $showattemptstopcriteria
) {
parent::__construct('individualuserattemptstable');
$this->renderer = $renderer;
$this->filter = $filter;
$this->questionsdifficultyrange = $questionsdifficultyrange;
//KNIGHT
$this->showattemptstopcriteria = $showattemptstopcriteria;
$this->cmid = $cmid;
$this->init($baseurl);
}
public function get_sql_sort() {
return 'timemodified DESC';
}
protected function col_attemptstate(stdClass $row): string {
if (0 == strcmp(attempt_state::IN_PROGRESS, $row->attemptstate)) {
return get_string('recentinprogress', 'adaptivequiz');
}
return get_string('recentcomplete', 'adaptivequiz');
}
protected function col_score(stdClass $row): string {
if ($row->measure === null || $row->stderror === null || $row->stderror == 0.0) {
return 'n/a';
}
$formatmeasureparams = new stdClass();
$formatmeasureparams->measure = $row->measure;
$formatmeasureparams->highestlevel = $this->questionsdifficultyrange->highest_level();
$formatmeasureparams->lowestlevel = $this->questionsdifficultyrange->lowest_level();
return $this->renderer->format_measure($formatmeasureparams) .
' ' . $this->renderer->format_standard_error($row);
}
protected function col_timecreated(stdClass $row): string {
return userdate($row->timecreated);
}
protected function col_timemodified(stdClass $row): string {
return userdate($row->timemodified);
}
/**
* A hook to format the output of actions column for an attempt row.
*
* @param stdClass $row A row from the {adaptivequiz_attempt}.
*/
protected function col_actions(stdClass $row): string {
return $this->renderer->individual_user_attempt_actions($row, $this->showattemptstopcriteria);
}
private function init(moodle_url $baseurl): void {
$columns = ['attemptstate', 'questionsattempted', 'score', 'timecreated', 'timemodified', 'actions'];
$headers = [
get_string('attemptstate', 'adaptivequiz'),
get_string('questionsattempted', 'adaptivequiz'),
get_string('score', 'adaptivequiz'),
get_string('attemptstarttime', 'adaptivequiz'),
get_string('attemptfinishedtimestamp', 'adaptivequiz'),
'',
];
$selectionstring = 'id, userid, uniqueid, attemptstate, questionsattempted, timemodified, timecreated, measure, standarderror AS stderror';
//KNIGHT
if($this->showattemptstopcriteria){
array_splice($columns, 1, 0, array('attemptstopcriteria'));
array_splice($headers, 1, 0, array(get_string('attemptstopcriteria', 'adaptivequiz')));
$selectionstring = 'id, userid, uniqueid, attemptstopcriteria, attemptstate, questionsattempted, timemodified, timecreated, measure, standarderror AS stderror';
}
$this->define_columns($columns);
$this->define_headers($headers);
$this->set_content_alignment_in_columns();
$this->define_baseurl($baseurl);
$this->set_attribute('class', $this->attributes['class'] . ' ' . $this->uniqueid);
$this->is_downloadable(false);
$this->collapsible(false);
$this->sortable(false);
$this->set_sql(
$selectionstring,
'{adaptivequiz_attempt}',
'instance = :adaptivequiz AND userid = :userid',
['adaptivequiz' => $this->filter->adaptivequizid, 'userid' => $this->filter->userid]
);
}
private function set_content_alignment_in_columns(): void {
foreach (array_keys($this->columns) as $column) {
$this->column_class[$column] .= ' text-center';
}
}
}
<?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/>.
/**
* The class defines a configuration object with range of questions difficulty. Acts as a container for related pieces
* of data - a value object. Normally is set from a corresponding activity record's values, thus, it doesn't perform
* any validation of the parameters when instantiated.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report;
use stdClass;
final class questions_difficulty_range {
/**
* @var int $lowestlevel
*/
private $lowestlevel;
/**
* @var int $highestlevel
*/
private $highestlevel;
private function __construct(int $lowestlevel, int $highestlevel) {
$this->lowestlevel = $lowestlevel;
$this->highestlevel = $highestlevel;
}
public function lowest_level(): int {
return $this->lowestlevel;
}
public function highest_level(): int {
return $this->highestlevel;
}
/**
* @param stdClass $instance A record from {adaptivequiz}.
*/
public static function from_activity_instance(stdClass $instance): self {
return new self($instance->lowestlevel, $instance->highestlevel);
}
}
<?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/>.
/**
* A class containing the data to filter the list of users with attempts by.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\filter;
use mod_adaptivequiz\local\report\users_attempts\user_preferences\filter_user_preferences;
final class filter {
/**
* @var int $adaptivequizid
*/
public $adaptivequizid;
/**
* @var int $groupid
*/
public $groupid;
/**
* @var int $users
*/
public $users;
/**
* @var int $includeinactiveenrolments Represents a bool value, as bool values normally come as int (0 or 1)
* from a request.
*/
public $includeinactiveenrolments;
public function fill_from_array(array $request): void {
foreach ($request as $propertyname => $propertyvalue) {
if (property_exists($this, $propertyname)) {
$this->$propertyname = $propertyvalue;
}
}
}
public function fill_from_preference(filter_user_preferences $filter): void {
$this->users = $filter->users();
$this->includeinactiveenrolments = $filter->include_inactive_enrolments();
}
public static function from_vars(int $adaptivequizid, int $groupid): self {
$return = new self();
$return->adaptivequizid = $adaptivequizid;
$return->groupid = $groupid;
return $return;
}
}
<?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/>.
/**
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\filter;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
use html_writer;
use moodle_url;
use moodleform;
final class filter_form extends moodleform {
/**
* Overrides the parent method to remove mandatory closing of fieldset before the submit button.
* Ignores the arguments, as it contains its own logic to display the button.
*
* @inheritDoc
*/
public function add_action_buttons($cancel = true, $submitlabel=null) {
$form =& $this->_form;
$form->addElement('submit', 'prefssubmit', get_string('reportattemptsfilterformsubmit', 'adaptivequiz'));
}
protected function definition() {
$form = $this->_form;
$form->addElement('header', 'filterheader', get_string('reportattemptsfilterformheader', 'adaptivequiz'));
$enrolmentoptions = [
filter_options::ENROLLED_USERS_WITH_NO_ATTEMPTS
=> get_string('reportattemptsenrolledwithnoattempts', 'adaptivequiz'),
filter_options::ENROLLED_USERS_WITH_ATTEMPTS
=> get_string('reportattemptsenrolledwithattempts', 'adaptivequiz'),
filter_options::BOTH_ENROLLED_AND_NOT_ENROLLED_USERS_WITH_ATTEMPTS
=> get_string('reportattemptsbothenrolledandnotenrolled', 'adaptivequiz'),
filter_options::NOT_ENROLLED_USERS_WITH_ATTEMPTS
=> get_string('reportattemptsnotenrolled', 'adaptivequiz')
];
$form->addElement('select', 'users', get_string('reportattemptsfilterusers', 'adaptivequiz'),
$enrolmentoptions);
$form->setDefault('users', filter_options::users_option_default());
$form->addElement('advcheckbox', 'includeinactiveenrolments',
get_string('reportattemptsfilterincludeinactiveenrolments', 'adaptivequiz'), '&nbsp;', null, [0, 1]);
$form->setDefault('includeinactiveenrolments', filter_options::INCLUDE_INACTIVE_ENROLMENTS_DEFAULT);
$form->addHelpButton('includeinactiveenrolments', 'reportattemptsfilterincludeinactiveenrolments',
'adaptivequiz');
$form->disabledIf('includeinactiveenrolments', 'users', 'eq',
filter_options::BOTH_ENROLLED_AND_NOT_ENROLLED_USERS_WITH_ATTEMPTS);
$this->add_action_buttons();
}
}
<?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/>.
/**
* Emulates an enum type to keep available filtering options. Defines default values for the options as well.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\filter;
final class filter_options {
public const ENROLLED_USERS_WITH_NO_ATTEMPTS = 1;
public const ENROLLED_USERS_WITH_ATTEMPTS = 2;
public const BOTH_ENROLLED_AND_NOT_ENROLLED_USERS_WITH_ATTEMPTS = 3;
public const NOT_ENROLLED_USERS_WITH_ATTEMPTS = 4;
public const INCLUDE_INACTIVE_ENROLMENTS_DEFAULT = 1;
public static function users_option_default(): int {
return self::BOTH_ENROLLED_AND_NOT_ENROLLED_USERS_WITH_ATTEMPTS;
}
public static function users_option_exists(int $option): bool {
return in_array($option, [
self::ENROLLED_USERS_WITH_NO_ATTEMPTS,
self::ENROLLED_USERS_WITH_ATTEMPTS,
self::BOTH_ENROLLED_AND_NOT_ENROLLED_USERS_WITH_ATTEMPTS,
self::NOT_ENROLLED_USERS_WITH_ATTEMPTS
]);
}
}
<?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/>.
/**
* The class contains all possible sql options needed to build the users' attempts table.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\sql;
use core\dml\sql_join;
use core_user\fields;
use mod_adaptivequiz\local\attempt\attempt_state;
final class sql_and_params {
/**
* @var string $fields
*/
private $fields;
/**
* @var string $from
*/
private $from;
/**
* @var string $where
*/
private $where;
/**
* @var string|null $where
*/
private $groupby;
/**
* @var array $params Normal array with query parameters as in {@link \moodle_database::get_records_sql()},
* for instance.
*/
private $params;
/**
* @var string $countsql Complete sql statement to pass to {@link \table_sql::set_count_sql()}.
*/
private $countsql;
/**
* @var array $params Same format as for {@link self::$params} above.
*/
private $countsqlparams;
private function __construct(
string $fields,
string $from,
string $where,
?string $groupby,
array $params,
?string $countsql,
?array $countsqlparams
) {
$this->fields = $fields;
$this->from = $from;
$this->where = $where;
$this->params = $params;
$this->groupby = $groupby;
$this->countsql = $countsql;
$this->countsqlparams = $countsqlparams;
}
public function fields(): string {
return $this->fields;
}
public function from(): string {
return $this->from;
}
public function where(): string {
return $this->where;
}
public function group_by(): ?string {
return $this->groupby;
}
public function params(): array {
return $this->params;
}
public function count_sql(): ?string {
return $this->countsql;
}
public function count_sql_params(): ?array {
return $this->countsqlparams;
}
public function with_group_filtering(int $groupid): self {
$from = $this->from . ' INNER JOIN {groups_members} gm ON u.id = gm.userid';
$where = $this->where . ' AND gm.groupid = :groupid';
$params = array_merge(['groupid' => $groupid], $this->params);
return new self($this->fields, $from, $where, $this->groupby, $params, $this->countsql, $this->countsqlparams);
}
public static function default(int $adaptivequizid): self {
list ($attemptsql, $params) = self::attempt_sql_and_params();
$fields = fields::for_name()
->including('id', 'email')
->get_sql('u', false, '', '', false)->selects
. ', ' . $attemptsql;
$from = '{adaptivequiz_attempt} aa JOIN {user} u ON u.id = aa.userid';
$where = self::base_where_sql() . ' AND aa.instance = :instance';
$params = array_merge($params, ['instance' => $adaptivequizid]);
$sqlcount = "SELECT COUNT(DISTINCT u.id) FROM $from WHERE $where";
return new self($fields, $from, $where, self::group_by_sql(), $params, $sqlcount, $params);
}
public static function for_enrolled_with_no_attempts(int $adaptivequizid, sql_join $enrolledjoin): self {
$fields = 'DISTINCT u.id' . fields::for_name()->including('email')->get_sql('u')->selects
. ', NULL as attemptsnum, NULL AS uniqueid, NULL AS attempttimefinished, NULL AS measure, NULL AS stderror';
$from = "
{user} u
$enrolledjoin->joins
LEFT JOIN {adaptivequiz_attempt} aa ON (aa.userid = u.id AND aa.instance = :instance)
";
$where = $enrolledjoin->wheres . ' AND aa.id IS NULL';
$params = array_merge(['instance' => $adaptivequizid], $enrolledjoin->params);
$sqlcount = "SELECT COUNT(DISTINCT u.id) FROM $from WHERE $where";
return new self($fields, $from, $where, null, $params, $sqlcount, $params);
}
public static function for_enrolled_with_attempts(int $adaptivequizid, sql_join $enrolledjoin): self {
list ($attemptsql, $params) = self::attempt_sql_and_params();
$fields = 'DISTINCT u.id' . fields::for_name()->including('email')->get_sql('u')->selects
. ', ' . $attemptsql;
$from = "
{user} u
$enrolledjoin->joins
JOIN {adaptivequiz_attempt} aa ON (aa.userid = u.id AND aa.instance = :instance)
";
$where = $enrolledjoin->wheres;
$params = array_merge($params, ['instance' => $adaptivequizid], $enrolledjoin->params);
$sqlcount = "SELECT COUNT(DISTINCT u.id) FROM $from WHERE $where";
return new self($fields, $from, $where, self::group_by_sql(), $params, $sqlcount, $params);
}
public static function for_not_enrolled_with_attempts(int $adaptivequizid, sql_join $enrolledjoin): self {
list ($attemptsql, $params) = self::attempt_sql_and_params();
$fields = 'DISTINCT u.id' . fields::for_name()->including('email')->get_sql('u')->selects
. ', ' . $attemptsql;
$from = '{adaptivequiz_attempt} aa JOIN {user} u ON u.id = aa.userid';
$where = self::base_where_sql() . "
AND aa.instance = :instance AND NOT EXISTS (
SELECT DISTINCT u.id FROM {user} u
$enrolledjoin->joins
WHERE u.id = aa.userid AND $enrolledjoin->wheres
)
";
$params = array_merge($params, ['instance' => $adaptivequizid], $enrolledjoin->params);
$sqlcount = "SELECT COUNT(DISTINCT u.id) FROM $from WHERE $where";
return new self($fields, $from, $where, self::group_by_sql(), $params, $sqlcount, $params);
}
private static function attempt_sql_and_params(): array {
return [
'(
SELECT COUNT(*) FROM {adaptivequiz_attempt} caa
WHERE caa.userid = u.id AND caa.instance = aa.instance
) AS attemptsnum,
(
SELECT maa.measure FROM {adaptivequiz_attempt} maa
WHERE maa.instance = aa.instance AND maa.userid = u.id AND maa.attemptstate = :attemptstate1
AND maa.standarderror > 0.0
ORDER BY measure DESC
LIMIT 1
) AS measure,
(
SELECT saa.standarderror FROM {adaptivequiz_attempt} saa
WHERE saa.instance = aa.instance AND saa.userid = u.id AND saa.attemptstate = :attemptstate2
AND saa.standarderror > 0.0
ORDER BY measure DESC
LIMIT 1
) AS stderror,
(
SELECT taa.timemodified FROM {adaptivequiz_attempt} taa
WHERE taa.instance = aa.instance AND taa.userid = u.id AND taa.attemptstate = :attemptstate3
AND taa.standarderror > 0.0
ORDER BY measure DESC
LIMIT 1
) AS attempttimefinished,
(
SELECT iaa.id FROM {adaptivequiz_attempt} iaa
WHERE iaa.instance = aa.instance AND iaa.userid = u.id AND iaa.attemptstate = :attemptstate4
AND iaa.standarderror > 0.0
ORDER BY measure DESC
LIMIT 1
) AS attemptid'
,
[
'attemptstate1' => attempt_state::COMPLETED,
'attemptstate2' => attempt_state::COMPLETED,
'attemptstate3' => attempt_state::COMPLETED,
'attemptstate4' => attempt_state::COMPLETED,
]
];
}
private static function group_by_sql(): string {
return 'u.id, aa.instance';
}
private static function base_where_sql(): string {
return 'u.deleted = 0';
}
}
<?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/>.
/**
* A class to keep the logic of resolving what sql with its parameters should be used for the report
* depending on filtering requested, etc.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\sql;
use context;
use mod_adaptivequiz\local\report\users_attempts\filter\filter;
use mod_adaptivequiz\local\report\users_attempts\filter\filter_options;
final class sql_resolver {
public static function sql_and_params(filter $filter, context $context): sql_and_params {
if (!$filter->users || $filter->users == filter_options::BOTH_ENROLLED_AND_NOT_ENROLLED_USERS_WITH_ATTEMPTS) {
$sqlandparams = sql_and_params::default($filter->adaptivequizid);
if ($filter->groupid) {
$sqlandparams = $sqlandparams->with_group_filtering($filter->groupid);
}
return $sqlandparams;
}
$enrolledjoin = get_enrolled_with_capabilities_join($context, '', 'mod/adaptivequiz:attempt',
$filter->groupid, self::resolve_active_enrolment_flag($filter->users, $filter->includeinactiveenrolments));
if ($filter->users == filter_options::ENROLLED_USERS_WITH_NO_ATTEMPTS) {
$sqlandparams = sql_and_params::for_enrolled_with_no_attempts($filter->adaptivequizid, $enrolledjoin);
}
if ($filter->users == filter_options::ENROLLED_USERS_WITH_ATTEMPTS) {
$sqlandparams = sql_and_params::for_enrolled_with_attempts($filter->adaptivequizid, $enrolledjoin);
}
if ($filter->users == filter_options::NOT_ENROLLED_USERS_WITH_ATTEMPTS) {
$sqlandparams = sql_and_params::for_not_enrolled_with_attempts($filter->adaptivequizid, $enrolledjoin);
}
if ($filter->groupid) {
$sqlandparams = $sqlandparams->with_group_filtering($filter->groupid);
}
return $sqlandparams;
}
private static function resolve_active_enrolment_flag(
int $usersoption,
int $includeinactiveenrolmentsoption
): bool {
if ($usersoption == filter_options::ENROLLED_USERS_WITH_NO_ATTEMPTS
|| $usersoption == filter_options::ENROLLED_USERS_WITH_ATTEMPTS) {
return !$includeinactiveenrolmentsoption;
}
return $includeinactiveenrolmentsoption;
}
}
<?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/>.
/**
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\user_preferences;
use mod_adaptivequiz\local\report\users_attempts\filter\filter_options;
final class filter_user_preferences {
/**
* @var int $users
*/
private $users;
/**
* @var int $includeinactiveenrolments
*/
private $includeinactiveenrolments;
private function __construct(int $users, int $includeinactiveenrolments) {
$this->users = filter_options::users_option_exists($users) ? $users : filter_options::users_option_default();
$this->includeinactiveenrolments = in_array($includeinactiveenrolments, [0, 1])
? $includeinactiveenrolments
: filter_options::INCLUDE_INACTIVE_ENROLMENTS_DEFAULT;
}
public function users(): int {
return $this->users;
}
public function include_inactive_enrolments(): int {
return $this->includeinactiveenrolments;
}
public function as_array(): array {
return ['users' => $this->users, 'includeinactiveenrolments' => $this->includeinactiveenrolments];
}
public static function from_array(array $filter): self {
return new self(
array_key_exists('users', $filter) ? $filter['users'] : filter_options::users_option_default(),
array_key_exists('includeinactiveenrolments', $filter)
? $filter['includeinactiveenrolments']
: filter_options::INCLUDE_INACTIVE_ENROLMENTS_DEFAULT
);
}
}
<?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/>.
/**
* A value object encapsulating user preferences to set up the report table.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\user_preferences;
use stdClass;
final class user_preferences {
public const PER_PAGE_OPTIONS = [5, 10, 15, 20, 25, 50];
public const PER_PAGE_DEFAULT = 15;
public const SHOW_INITIALS_BAR_DEFAULT = 1;
public const PERSISTENT_FILTER_DEFAULT = 0;
/**
* @var int $perpage
*/
private $perpage;
/**
* @var int $showinitialsbar Represents a boolean value, but defined as an int, as it's stored as an int.
*/
private $showinitialsbar;
/**
* @var int $persistentfilter Same as for $showinitialsbar above.
*/
private $persistentfilter;
/**
* @var filter_user_preferences|null $filter
*/
private $filter;
private function __construct(
int $perpage,
int $showinitialsbar,
int $persistentfilter,
?filter_user_preferences $filter
) {
$this->perpage = in_array($perpage, self::PER_PAGE_OPTIONS) ? $perpage : self::PER_PAGE_DEFAULT;
$this->showinitialsbar = in_array($showinitialsbar, [0, 1])
? $showinitialsbar
: self::SHOW_INITIALS_BAR_DEFAULT;
$this->persistentfilter = in_array($persistentfilter, [0, 1])
? $persistentfilter
: self::PERSISTENT_FILTER_DEFAULT;
$this->filter = $filter;
}
public function rows_per_page(): int {
return $this->perpage;
}
public function show_initials_bar(): bool {
return (bool) $this->showinitialsbar;
}
public function persistent_filter(): bool {
return (bool) $this->persistentfilter;
}
public function filter(): ?filter_user_preferences {
return $this->filter;
}
public function has_filter_preference(): bool {
return $this->filter !== null;
}
public function with_filter_preference(filter_user_preferences $preference): self {
return new self($this->perpage, $this->showinitialsbar, $this->persistentfilter, $preference);
}
public function without_filter_preference(): self {
return new self($this->perpage, $this->showinitialsbar, $this->persistentfilter, null);
}
public function as_array(): array {
$return = ['perpage' => $this->perpage, 'showinitialsbar' => $this->showinitialsbar,
'persistentfilter' => $this->persistentfilter];
$return['filter'] = ($this->filter === null) ? null : $this->filter->as_array();
return $return;
}
public static function from_array(array $prefs): self {
$filter = array_key_exists('filter', $prefs) ? $prefs['filter'] : null;
return new self(
array_key_exists('perpage', $prefs) ? $prefs['perpage'] : self::PER_PAGE_DEFAULT,
array_key_exists('showinitialsbar', $prefs) ? $prefs['showinitialsbar'] : self::SHOW_INITIALS_BAR_DEFAULT,
array_key_exists('persistentfilter', $prefs) ? $prefs['persistentfilter'] : self::PERSISTENT_FILTER_DEFAULT,
($filter === null) ? null : filter_user_preferences::from_array($filter)
);
}
public static function from_plain_object(stdClass $object): self {
return self::from_array((array) $object);
}
public static function defaults(): self {
return new self(self::PER_PAGE_DEFAULT, self::SHOW_INITIALS_BAR_DEFAULT, self::PERSISTENT_FILTER_DEFAULT, null);
}
}
<?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/>.
/**
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts\user_preferences;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
use moodleform;
class user_preferences_form extends moodleform {
/**
* Overrides the parent method to remove mandatory closing of fieldset before the submit button.
* Ignores the arguments, as it contains its own logic to display the button.
*
* @inheritDoc
*/
public function add_action_buttons($cancel = true, $submitlabel=null) {
$form =& $this->_form;
$form->addElement('submit', 'prefssubmit', get_string('reportattemptsprefsformsubmit', 'adaptivequiz'));
}
protected function definition() {
$form = $this->_form;
$form->addElement('header', 'prefsheader', get_string('reportattemptsprefsformheader', 'adaptivequiz'));
$form->addElement('select', 'perpage', get_string('reportattemptsusersperpage', 'adaptivequiz'),
array_combine(user_preferences::PER_PAGE_OPTIONS, user_preferences::PER_PAGE_OPTIONS));
$form->setDefault('perpage', user_preferences::PER_PAGE_DEFAULT);
$form->addElement('advcheckbox', 'showinitialsbar',
get_string('reportattemptsshowinitialbars', 'adaptivequiz'), '&nbsp;', null, [0, 1]);
$form->setDefault('showinitialsbar', user_preferences::SHOW_INITIALS_BAR_DEFAULT);
$form->addElement('advcheckbox', 'persistentfilter',
get_string('reportattemptspersistentfilter', 'adaptivequiz'), '&nbsp;', null, [0, 1]);
$form->setDefault('persistentfilter', user_preferences::PERSISTENT_FILTER_DEFAULT);
$form->addHelpButton('persistentfilter', 'reportattemptspersistentfilter', 'adaptivequiz');
$this->add_action_buttons();
}
}
<?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/>.
namespace mod_adaptivequiz\local\report\users_attempts\user_preferences;
/**
* Provides methods for storing and fetching of the report user preferences from the storage.
*
* Operates on user session as well to avoid unnecessary database queries.
*
* @package mod_adaptivequiz
* @copyright 2022 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class user_preferences_repository {
/**
* @var string Name of the preference.
*/
private const PREFERENCE_NAME = 'adaptivequiz_users_attempts_report';
/**
* Stores the given preferences object in available storages.
*
* @param user_preferences $prefs
*/
public static function save(user_preferences $prefs): void {
global $SESSION;
$prefsarr = $prefs->as_array();
$SESSION->flextableextra[self::PREFERENCE_NAME] = $prefsarr;
set_user_preference(self::PREFERENCE_NAME, json_encode($prefsarr));
}
/**
* Instantiates user preferences from available storages or default.
*
* @return user_preferences
*/
public static function get(): user_preferences {
global $SESSION;
if (!empty($SESSION->flextableextra[self::PREFERENCE_NAME])) {
return user_preferences::from_array($SESSION->flextableextra[self::PREFERENCE_NAME]);
}
if (!$json = get_user_preferences(self::PREFERENCE_NAME)) {
return user_preferences::defaults();
}
return user_preferences::from_array(json_decode($json, true));
}
}
<?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/>.
/**
* A class to display a table with users either with attempts or without them.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\report\users_attempts;
use coding_exception;
use context;
use dml_exception;
use html_writer;
use mod_adaptivequiz\local\report\questions_difficulty_range;
use mod_adaptivequiz\local\report\users_attempts\filter\filter;
use mod_adaptivequiz\local\report\users_attempts\sql\sql_resolver;
use mod_adaptivequiz_renderer;
use moodle_exception;
use moodle_url;
use stdClass;
use table_sql;
final class users_attempts_table extends table_sql {
private const UNIQUE_ID = 'usersattemptstable';
/**
* @var mod_adaptivequiz_renderer $renderer
*/
private $renderer;
/**
* @var int $cmid
*/
private $cmid;
/**
* @var questions_difficulty_range $questionsdifficultyrange
*/
private $questionsdifficultyrange;
/**
* @throws coding_exception
*/
public function __construct(
mod_adaptivequiz_renderer $renderer,
int $cmid,
questions_difficulty_range $questionsdifficultyrange,
moodle_url $baseurl,
context $context,
filter $filter
) {
parent::__construct(self::UNIQUE_ID);
$this->renderer = $renderer;
$this->cmid = $cmid;
$this->questionsdifficultyrange = $questionsdifficultyrange;
$this->init($baseurl, $context, $filter);
}
/**
* {@inheritdoc}
* @throws dml_exception
*/
public function query_db($pagesize, $useinitialsbar = true): void {
global $DB;
if (!$this->is_downloading()) {
if ($this->countsql === null) {
$this->countsql = 'SELECT COUNT(1) FROM '.$this->sql->from.' WHERE '.$this->sql->where;
$this->countparams = $this->sql->params;
}
$grandtotal = $DB->count_records_sql($this->countsql, $this->countparams);
if ($useinitialsbar && !$this->is_downloading()) {
$this->initialbars(true);
}
list($wsql, $wparams) = $this->get_sql_where();
if ($wsql) {
$this->countsql .= ' AND ' . $wsql;
$this->countparams = array_merge($this->countparams, $wparams);
$this->sql->where .= ' AND ' . $wsql;
$this->sql->params = array_merge($this->sql->params, $wparams);
$total = $DB->count_records_sql($this->countsql, $this->countparams);
} else {
$total = $grandtotal;
}
$this->pagesize($pagesize, $total);
}
$sort = $this->get_sql_sort();
if ($sort) {
$sort = "ORDER BY $sort";
}
$groupby = $this->sql->groupby ?? '';
if ($groupby) {
$groupby = "GROUP BY $groupby";
}
$sql = "SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where} {$groupby} {$sort}";
if (!$this->is_downloading()) {
$this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(),
$this->get_page_size());
} else {
$this->rawdata = $DB->get_records_sql($sql, $this->sql->params);
}
}
protected function col_attemptsnum(stdClass $row): string {
if (!$row->attemptsnum) {
return '-';
}
if (!$this->is_downloading()) {
return html_writer::link(
new moodle_url(
'/mod/adaptivequiz/viewattemptreport.php',
['userid' => $row->id, 'cmid' => $this->cmid]
),
$row->attemptsnum
);
}
return $row->attemptsnum;
}
/**
* @throws moodle_exception
*/
protected function col_measure(stdClass $row): string {
$formatmeasureparams = new stdClass();
$formatmeasureparams->measure = $row->measure;
$formatmeasureparams->highestlevel = $this->questionsdifficultyrange->highest_level();
$formatmeasureparams->lowestlevel = $this->questionsdifficultyrange->lowest_level();
$measure = $this->renderer->format_measure($formatmeasureparams);
if (!$row->attemptid) {
return $measure;
}
if (!$this->is_downloading()) {
return html_writer::link(
new moodle_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $row->attemptid]),
$measure
);
}
return $measure;
}
protected function col_stderror(stdClass $row): string {
$rendered = $this->renderer->format_standard_error($row);
if (!$this->is_downloading()) {
return $rendered;
}
return html_entity_decode($rendered, ENT_QUOTES, 'UTF-8');
}
/**
* @throws coding_exception
*/
protected function col_attempttimefinished(stdClass $row): string {
return intval($row->attempttimefinished)
? userdate($row->attempttimefinished)
: get_string('na', 'adaptivequiz');
}
/**
* A convenience method to call a bunch of init methods.
*
* @param moodle_url $baseurl
* @throws coding_exception
*/
private function init(moodle_url $baseurl, context $context, filter $filter): void {
$this->define_columns([
'fullname', 'email', 'attemptsnum', 'measure', 'stderror', 'attempttimefinished',
]);
$this->define_headers([
get_string('fullname'),
get_string('email'),
get_string('numofattemptshdr', 'adaptivequiz'),
get_string('bestscore', 'adaptivequiz'),
get_string('bestscorestderror', 'adaptivequiz'),
get_string('attemptfinishedtimestamp', 'adaptivequiz'),
]);
$this->define_baseurl($baseurl);
$this->set_attribute('class', $this->attributes['class'] . ' usersattemptstable');
$this->set_content_alignment_in_columns();
$this->collapsible(false);
$this->sortable(true, 'lastname');
$this->is_downloadable(true);
$sqlandparams = sql_resolver::sql_and_params($filter, $context);
$this->set_sql($sqlandparams->fields(), $sqlandparams->from(), $sqlandparams->where(), $sqlandparams->params());
$this->set_group_by_sql($sqlandparams->group_by());
$this->set_count_sql($sqlandparams->count_sql(), $sqlandparams->count_sql_params());
}
private function set_group_by_sql(?string $clause): void {
$this->sql->groupby = $clause;
}
private function set_content_alignment_in_columns(): void {
$this->column_class['attemptsnum'] .= ' text-center';
$this->column_class['measure'] .= ' text-center';
$this->column_class['stderror'] .= ' text-center';
}
}
<?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/>.
/**
* The class represents questions number per each difficulty, this is what
* {@link questions_repository::count_questions_number_per_difficulty()} returns.
* The purpose of this class is keeping the related pieces of data together, as the client code normally requires both
* difficulty level and number of questions for this difficulty set to perform its task.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\repository;
final class questions_number_per_difficulty {
/**
* @var int $difficulty
*/
private $difficulty;
/**
* @var int $questionsnumber
*/
private $questionsnumber;
public function __construct(int $difficulty, int $questionsnumber) {
$this->difficulty = $difficulty;
$this->questionsnumber = $questionsnumber;
}
public function difficulty(): int {
return $this->difficulty;
}
public function questions_number(): int {
return $this->questionsnumber;
}
}
<?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/>.
/**
* A class to wrap all database queries which are specific to questions and their related data. Normally should contain
* only static methods to call.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\repository;
use coding_exception;
use core_question\local\bank\question_version_status;
use core_tag_tag;
use dml_exception;
use question_finder;
use stdClass;
final class questions_repository {
/**
* Counts all questions in the pool tagged as 'adaptive' with a certain difficulty level.
*
* @param int[] $qcategoryidlist A list of id of questions categories.
* @param int $level Question difficulty which is contained in the question's tag.
*/
public static function count_adaptive_questions_in_pool_with_level(array $qcategoryidlist, int $level): int {
if (!$raw = question_finder::get_instance()->get_questions_from_categories($qcategoryidlist, '')) {
return 0;
}
$questionstags = core_tag_tag::get_items_tags('core_question', 'question', array_keys($raw));
// Filter 'non-adaptive' and level mismatching tags out.
$questionstags = array_map(function(array $tags) use ($level) {
return array_filter($tags, function(core_tag_tag $tag) use ($level) {
return substr($tag->name, strlen(ADAPTIVEQUIZ_QUESTION_TAG)) === (string)$level;
});
}, $questionstags);
// Filter empty tags arrays out.
$questionstags = array_filter($questionstags, function(array $tags) {
return !empty($tags);
});
return count($questionstags);
}
/**
* @param int[] $tagidlist
* @param int[] $categoryidlist
* @return questions_number_per_difficulty[]
* @throws coding_exception
* @throws dml_exception
*/
public static function count_questions_number_per_difficulty(array $tagidlist, array $categoryidlist): array {
global $DB;
if (empty($tagidlist) || empty($categoryidlist)) {
return [];
}
list($tagidlistsql, $tagidlistparam) = $DB->get_in_or_equal($tagidlist);
list($categoryidlistsql, $categoryidlistparam) = $DB->get_in_or_equal($categoryidlist);
$difficultyselect = $DB->sql_substr('t.name', strlen(ADAPTIVEQUIZ_QUESTION_TAG) + 1);
$sql = "SELECT {$difficultyselect} AS difficultylevel, COUNT(*) AS questionsnumber
FROM {tag} t
JOIN {tag_instance} ti ON t.id = ti.tagid
JOIN {question} q ON q.id = ti.itemid
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN (
SELECT questionbankentryid, MAX(version)
FROM {question_versions}
WHERE status = ?
GROUP BY questionbankentryid
) questionlatestversion ON questionlatestversion.questionbankentryid = qv.questionbankentryid
JOIN {question_bank_entries} qbe ON qbe.id = questionlatestversion.questionbankentryid
WHERE ti.itemtype = ?
AND ti.tagid {$tagidlistsql}
AND qbe.questioncategoryid {$categoryidlistsql}
GROUP BY t.name";
$params = array_merge([question_version_status::QUESTION_STATUS_READY, 'question'], $tagidlistparam,
$categoryidlistparam);
$records = $DB->get_records_sql($sql, $params);
if (empty($records)) {
return [];
}
$return = [];
foreach ($records as $record) {
$return[] = new questions_number_per_difficulty($record->difficultylevel, $record->questionsnumber);
}
return $return;
}
/**
* @param int[] $tagidlist
* @param int[] $categoryidlist
* @param int[] $excludequestionidlist
* @return stdClass[] A list of records from {question} table, the fields are id, name.
* @throws coding_exception
* @throws dml_exception
*/
public static function find_questions_with_tags(
array $tagidlist,
array $categoryidlist,
array $excludequestionidlist
): array {
global $DB;
if (empty($tagidlist) || empty($categoryidlist)) {
return [];
}
$params = [];
list($tagswhere, $tempparam) = $DB->get_in_or_equal($tagidlist, SQL_PARAMS_NAMED, 'tagids');
$params += $tempparam;
list($categorywhere, $tempparam) = $DB->get_in_or_equal($categoryidlist, SQL_PARAMS_NAMED, 'qcatids');
$params += $tempparam;
$excludequestionsclause = '';
if (!empty($excludequestionidlist)) {
list($excludequestionssql, $tempparam) = $DB->get_in_or_equal($excludequestionidlist, SQL_PARAMS_NAMED, 'excqids',
false);
$excludequestionsclause = "AND q.id {$excludequestionssql}";
$params += $tempparam;
}
$sql = "SELECT q.id, q.name
FROM {question} q
JOIN {tag_instance} ti ON q.id = ti.itemid
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
JOIN (
SELECT qv.questionid
FROM {question_versions} qv
JOIN (
SELECT questionbankentryid, MAX(version) latestversion
FROM {question_versions}
WHERE status = :questionstatus
GROUP BY questionbankentryid
) qbelv ON qv.version = qbelv.latestversion AND qv.questionbankentryid = qbelv.questionbankentryid
) qlv ON q.id = qlv.questionid
WHERE ti.itemtype = :itemtype
AND ti.tagid {$tagswhere}
AND qbe.questioncategoryid {$categorywhere}
{$excludequestionsclause}
ORDER BY q.id ASC";
$params += ['questionstatus' => question_version_status::QUESTION_STATUS_READY, 'itemtype' => 'question'];
if (!$records = $DB->get_records_sql($sql, $params)) {
return [];
}
return $records;
}
}
<?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/>.
/**
* A class to wrap all database queries which are specific to tags and their related data. Normally should contain
* only static methods to call.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\local\repository;
use coding_exception;
use dml_exception;
use stdClass;
final class tags_repository {
/**
* @param string[] $tagnames
* @return array Map of question difficulty level and tag id, same as what
* {@link moodle_database::get_records_menu()} would return.
* @throws dml_exception
* @throws coding_exception
*/
public static function get_question_level_to_tag_id_mapping_by_tag_names(array $tagnames): array {
global $DB;
list($tagnameselect, $tagnameparams) = $DB->get_in_or_equal($tagnames);
$sql = 'SELECT t.id, ' . $DB->sql_substr('t.name', strlen(ADAPTIVEQUIZ_QUESTION_TAG) + 1) . ' AS level
FROM {tag} t
JOIN {tag_instance} ti ON t.id = ti.tagid AND ti.itemtype = ?
WHERE t.name ' . $tagnameselect . '
GROUP BY t.id';
$params = array_merge(['question'], $tagnameparams);
if (!$records = $DB->get_records_sql($sql, $params)) {
return [];
}
return array_flip(
array_map(function(stdClass $record): int {
return $record->level;
}, $records)
);
}
/**
* @param string[] Array of tag names.
* @return int[] Tag id list.
* @throws dml_exception
* @throws coding_exception
*/
public static function get_tag_id_list_by_tag_names(array $tagnames): array {
global $DB;
list($tagnameselect, $tagnameparams) = $DB->get_in_or_equal($tagnames);
$sql = 'SELECT t.id
FROM {tag} t
JOIN {tag_instance} ti ON t.id = ti.tagid AND ti.itemtype = ?
WHERE t.name ' . $tagnameselect . '
GROUP BY t.id
ORDER BY t.id';
$params = array_merge(['question'], $tagnameparams);
return ($fieldset = $DB->get_fieldset_sql($sql, $params)) ? $fieldset : [];
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment