Commit ceb3fd15 authored by Mehmedovski's avatar Mehmedovski
Browse files

Initial commit: KNIGHT adaptivequiz for Moodle 5.0-5.2 (based on upstream...

Initial commit: KNIGHT adaptivequiz for Moodle 5.0-5.2 (based on upstream MOODLE_500, tested with 5.2, KNIGHT customizations)
parents
// 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/>.
/**
* Module to manage item bank.
*
* @module mod_adaptivequiz/item_bank
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ModalForm from 'core_form/modalform';
import {getString} from 'core/str';
/**
* DOM selectors.
*
* @constant
* @type Object
*/
const SELECTORS = {
showQuestionBanksDialog: '[data-action="show-question-banks-dialog"]',
};
/**
* Entry point of the module.
*/
export const init = () => {
document.querySelector(SELECTORS.showQuestionBanksDialog).addEventListener('click', (e) => {
const idFormArg = e.target.dataset.id;
const courseIdFormArg = e.target.dataset.courseId;
const form = new ModalForm({
formClass: "mod_adaptivequiz\\form\\assign_question_bank_form",
args: {
id: idFormArg,
course: courseIdFormArg,
},
modalConfig: {
title: getString('itembankeditqbanks', 'adaptivequiz'),
},
saveButtonText: getString('itembankaddqbankbn', 'adaptivequiz'),
});
form.addEventListener(form.events.FORM_SUBMITTED, () => window.location.reload());
form.show();
});
};
// 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/>.
/**
* Autocomplete data source for question bank selectors.
*
* @module mod_adaptivequiz/question_banks_datasource
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {call as fetchMany} from 'core/ajax';
import Notification from 'core/notification';
export default {
/**
* Source of data for Ajax element.
*
* @param {String} selector The selector of the auto complete element.
* @param {String} query The query string.
* @param {Function} callback A callback function receiving an array of results.
*/
transport: function(selector, query, callback) {
const element = document.querySelector(selector);
const contextId = element.dataset.contextid;
const inCourseId = element.dataset.incourseid;
const notInCourseId = element.dataset.notincourseid;
if (!contextId) {
throw new Error('The attribute data-contextid is required on ' + selector);
}
fetchMany([{
methodname: 'mod_adaptivequiz_search_question_banks',
args: {
contextid: contextId,
incourseid: inCourseId,
notincourseid: notInCourseId,
search: query,
},
}])[0]
.then(callback)
.catch(Notification.exception);
},
/**
* Process the results for auto complete elements.
*
* @param {String} selector The selector of the auto complete element.
* @param {Array} results An array or results.
* @return {Array} New array of results.
*/
processResults: (selector, results) => {
return results.questionbanks;
},
};
<?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/>.
/**
* Adaptive quiz attempt script.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
require_once($CFG->dirroot . '/tag/lib.php');
use mod_adaptivequiz\local\attempt;
use mod_adaptivequiz\local\catalgo;
use mod_adaptivequiz\local\fetchquestion;
use mod_adaptivequiz\output\attempt_debug_info;
$id = required_param('cmid', PARAM_INT); // Course module id.
$uniqueid = optional_param('uniqueid', 0, PARAM_INT); // Unique id of the attempt.
$difflevel = optional_param('dl', 0, PARAM_INT); // Difficulty level of question.
if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
throw new moodle_exception('coursemisconf');
}
global $USER, $DB, $SESSION;
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
$passwordattempt = false;
try {
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*', MUST_EXIST);
} catch (dml_exception $e) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
$debuginfo = '';
if (!empty($e->debuginfo)) {
$debuginfo = $e->debuginfo;
}
throw new moodle_exception('invalidmodule', 'error', $url, $e->getMessage(), $debuginfo);
}
// Setup page global for standard viewing.
$viewurl = new moodle_url('/mod/adaptivequiz/view.php', array('id' => $cm->id));
$PAGE->set_url('/mod/adaptivequiz/view.php', array('id' => $cm->id));
$PAGE->set_title(format_string($adaptivequiz->name));
$PAGE->set_context($context);
$PAGE->activityheader->disable();
$PAGE->add_body_class('limitedwidth');
// Check if the user has the attempt capability.
require_capability('mod/adaptivequiz:attempt', $context);
// Check if the user has any previous attempts at this activity.
$count = adaptivequiz_count_user_previous_attempts($adaptivequiz->id, $USER->id);
if (!adaptivequiz_allowed_attempt($adaptivequiz->attempts, $count)) {
throw new moodle_exception('noattemptsallowed', 'adaptivequiz');
}
// Create an instance of the module renderer class.
$output = $PAGE->get_renderer('mod_adaptivequiz');
// Setup password required form.
$mform = $output->display_password_form($cm->id);
// Check if a password is required.
if (!empty($adaptivequiz->password)) {
// Check if the user has alredy entered in their password.
$condition = adaptivequiz_user_entered_password($adaptivequiz->id);
if (empty($condition) && $mform->is_cancelled()) {
// Return user to landing page.
redirect($viewurl);
} else if (empty($condition) && $data = $mform->get_data()) {
$SESSION->passwordcheckedadpq = array();
if (0 == strcmp($data->quizpassword, $adaptivequiz->password)) {
$SESSION->passwordcheckedadpq[$adaptivequiz->id] = true;
} else {
$SESSION->passwordcheckedadpq[$adaptivequiz->id] = false;
$passwordattempt = true;
}
}
}
// Create an instance of the adaptiveattempt class.
$adaptiveattempt = new attempt($adaptivequiz, $USER->id);
$algo = new stdClass();
$nextdiff = null;
$standarderror = 0.0;
$message = '';
// KNIGHT (Feature 6): slot of the question just answered this request (0 = none), for immediate feedback.
$previousquestionslot = 0;
// If uniqueid is not empty the process respones.
if (!empty($uniqueid) && confirm_sesskey()) {
// Check if the uniqueid belongs to the same attempt record the user is currently using.
$attemptrec = $adaptiveattempt->get_attempt();
if (!adaptivequiz_uniqueid_part_of_attempt($uniqueid, $cm->instance, $USER->id)) {
throw new moodle_exception('uniquenotpartofattempt', 'adaptivequiz');
}
// Process student's responses.
try {
// Set a time stamp for the actions below.
$time = time();
// Load the user's current usage from the DB.
$quba = question_engine::load_questions_usage_by_activity((int) $uniqueid);
// Update the actions done to the question.
$quba->process_all_actions($time);
// Finish the grade attempt at the question.
$quba->finish_all_questions($time);
// Save the data about the usage to the DB.
question_engine::save_questions_usage_by_activity($quba);
// KNIGHT (Feature 6): remember the just-answered (now graded) slot so its immediate feedback
// can be rendered above the next question. Derive it from the usage itself - the last slot before
// the next question is fetched - rather than trusting the request, so a manipulated 'slots' value
// cannot reach the feedback rendering.
$answeredslots = $quba->get_slots();
$previousquestionslot = empty($answeredslots) ? 0 : (int) end($answeredslots);
if (!empty($difflevel)) {
// Check if the minimum number of attempts have been reached.
$minattemptreached = adaptivequiz_min_attempts_reached($uniqueid, $cm->instance, $USER->id);
// Create an instance of the CAT algo class.
$algo = new catalgo($quba, (int) $attemptrec->id, $minattemptreached, (int) $difflevel);
// Calculate the next difficulty level.
$nextdiff = $algo->perform_calculation_steps();
// Increment difficulty level for attempt.
$everythingokay = false;
$difflogit = $algo->get_levellogit();
$standarderror = $algo->get_standarderror();
$measure = $algo->get_measure();
$everythingokay = adaptivequiz_update_attempt_data($uniqueid, $cm->instance, $USER->id, $difflogit, $standarderror,
$measure);
// Something went wrong with updating the attempt. Print an error.
if (!$everythingokay) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
throw new moodle_exception('unableupdatediffsum', 'adaptivequiz', $url);
}
// Check whether the status property is empty.
$message = $algo->get_status();
if (!empty($message)) {
$standarderror = $algo->get_standarderror();
// Set the attempt to complete, update the standard error and attempt message, then redirect the user to the
// attempt finished page.
adaptivequiz_complete_attempt($uniqueid, $adaptivequiz, $context, $USER->id, $standarderror, $message);
$param = array('cmid' => $cm->id, 'id' => $cm->instance, 'uattid' => $uniqueid);
$url = new moodle_url('/mod/adaptivequiz/attemptfinished.php', $param);
redirect($url);
}
// Lastly decrement the sum of questions for the attempted difficulty level.
$fetchquestion = new fetchquestion($quba, $difflevel, $adaptivequiz->lowestlevel, $adaptivequiz->highestlevel);
$tagquestcount = $fetchquestion->get_tagquestsum();
$tagquestcount = $fetchquestion->decrement_question_sum_from_difficulty($tagquestcount, $difflevel);
$fetchquestion->set_tagquestsum($tagquestcount);
// Force the class to deconstruct the object and save the updated mapping to the session global.
unset($fetchquestion);
}
} catch (question_out_of_sequence_exception $e) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', $url);
} catch (Exception $e) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
$debuginfo = '';
if (!empty($e->debuginfo)) {
$debuginfo = $e->debuginfo;
}
throw new moodle_exception('errorprocessingresponses', 'question', $url, $e->getMessage(), $debuginfo);
}
}
$adaptivequiz->context = $context;
$adaptivequiz->cm = $cm;
// If value is null then set the difficulty level to the starting level for the attempt.
if (!is_null($nextdiff)) {
$adaptiveattempt->set_level((int) $nextdiff);
} else {
$adaptiveattempt->set_level((int) $adaptivequiz->startinglevel);
}
// If we have a previous difficulty level, pass that off to the attempt so that it
// can modify the next-question search process based on this level.
if (isset($difflevel) && !is_null($difflevel)) {
$adaptiveattempt->set_last_difficulty_level($difflevel);
}
$attemptstatus = $adaptiveattempt->start_attempt();
// Check if attempt status is set to ready.
if (empty($attemptstatus)) {
// Retrieve the most recent status message for the attempt.
$message = $adaptiveattempt->get_status();
// Set the attempt to complete, update the standard error and attempt message, then redirect the user to the attempt-finished
// page.
if ($algo instanceof catalgo) {
$standarderror = $algo->get_standarderror();
}
$noquestionsfetchedforattempt = $uniqueid == 0;
if ($noquestionsfetchedforattempt) {
// The script will try to complete an 'empty' attempt as it couldn't fetch the first question for some reason.
// This is an invalid behaviour, which could be caused by a misconfigured questions pool. Stop it here.
throw new moodle_exception('attemptnofirstquestion', 'adaptivequiz',
(new moodle_url('/mod/adaptivequiz/view.php', ['id' => $cm->id]))->out());
}
adaptivequiz_complete_attempt($uniqueid, $adaptivequiz, $context, $USER->id, $standarderror, $message);
// Redirect the user to the attemptfeedback page.
$param = array('cmid' => $cm->id, 'id' => $cm->instance, 'uattid' => $uniqueid);
$url = new moodle_url('/mod/adaptivequiz/attemptfinished.php', $param);
redirect($url);
}
// Retrieve the question slot id.
$slot = $adaptiveattempt->get_question_slot_number();
// Retrieve the question_usage_by_activity object.
$quba = $adaptiveattempt->get_quba();
// If $nextdiff is null then this is either a new attempt or a continuation of an previous attempt. Calculate the current
// difficulty level the attempt should be at.
if (is_null($nextdiff)) {
// Calculate the current difficulty level.
$adaptivequiz->lowestlevel = (int) $adaptivequiz->lowestlevel;
$adaptivequiz->highestlevel = (int) $adaptivequiz->highestlevel;
$adaptivequiz->startinglevel = (int) $adaptivequiz->startinglevel;
// Create an instance of the catalgo class, however constructor arguments are not important.
$algo = new catalgo($quba, 1, false, 1);
$level = $algo->get_current_diff_level($quba, $adaptivequiz->startinglevel, $adaptivequiz);
} else {
// Retrieve the currently set difficulty level.
$level = $adaptiveattempt->get_level();
}
$headtags = $output->init_metadata($quba, $slot);
$PAGE->requires->js_init_call('M.mod_adaptivequiz.init_attempt_form', array($viewurl->out(), $adaptivequiz->browsersecurity),
false, $output->adaptivequiz_get_js_module());
// Init secure window if enabled.
if (!empty($adaptivequiz->browsersecurity)) {
$PAGE->blocks->show_only_fake_blocks();
$output->init_browser_security();
} else {
$PAGE->set_heading(format_string($course->fullname));
}
echo $output->header();
// Check if the user entered a password.
$condition = adaptivequiz_user_entered_password($adaptivequiz->id);
if (!empty($adaptivequiz->password) && empty($condition)) {
if ($passwordattempt) {
$mform->set_data(array('message' => get_string('wrongpassword', 'adaptivequiz')));
}
$mform->display();
} else {
$attemptrecord = $adaptiveattempt->get_attempt();
if ($adaptivequiz->showattemptprogress) {
echo $output->container_start('attempt-progress-container');
echo $output->attempt_progress($attemptrecord->questionsattempted, $adaptivequiz->maximumquestions);
echo $output->container_end();
}
// KNIGHT (Feature 6): show immediate feedback for the just-answered question below the progress bar.
if (!empty($adaptivequiz->immediatefeedback) && !empty($previousquestionslot)) {
echo $output->question_feedback(
$quba,
$previousquestionslot,
!empty($adaptivequiz->immediatefeedbackshowsolution),
(float) $adaptivequiz->acceptancethreshold
);
}
echo $output->question_submit_form(
$id,
$quba,
$slot,
$level,
$attemptrecord->questionsattempted + 1,
$adaptivequiz->showquestiondifficultylevel
);
if ($adaptivequiz->debuginfoenable) {
echo $output->container_start();
echo $output->render(new attempt_debug_info($attemptrecord));
echo $output->container_end();
}
}
echo $output->print_footer();
<?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/>.
/**
* Adaptive quiz attempt script
*
* @package mod_adaptivequiz
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
$cmid = required_param('cmid', PARAM_INT); // Course module id.
$instance = required_param('id', PARAM_INT); // Activity instance id.
$uniqueid = required_param('uattid', PARAM_INT); // Attempt unique id.
if (!$cm = get_coursemodule_from_id('adaptivequiz', $cmid)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', ['id' => $cm->course])) {
throw new moodle_exception('coursemisconf');
}
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $cm->instance], '*', MUST_EXIST);
$attempt = $DB->get_record('adaptivequiz_attempt', ['uniqueid' => $uniqueid], '*', MUST_EXIST);
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
// TODO - check if user has capability to attempt.
// Check if this is the owner of the attempt.
$validattempt = adaptivequiz_uniqueid_part_of_attempt($uniqueid, $instance, $USER->id);
// Display an error message if this is not the owner of the attempt.
if (!$validattempt) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', ['cmid' => $cm->id]);
throw new moodle_exception('notyourattempt', 'adaptivequiz', $url);
}
$PAGE->set_url('/mod/adaptivequiz/view.php', ['id' => $cm->id]);
$PAGE->set_title(format_string($adaptivequiz->name));
$PAGE->set_context($context);
$PAGE->activityheader->disable();
$PAGE->add_body_class('limitedwidth');
$output = $PAGE->get_renderer('mod_adaptivequiz');
// KNIGHT (Feature 4): build the detailed competency feedback link once so it is offered in both the
// normal and the secure-window flow. Shown when the feature is enabled.
$feedbackbutton = '';
if (!empty($adaptivequiz->competencyfeedbackenable)) {
$feedbackurl = new moodle_url('/mod/adaptivequiz/feedback.php', ['id' => $cm->id, 'attempt' => $attempt->id]);
$feedbackbutton = html_writer::div(
html_writer::link($feedbackurl, get_string('detailedfeedback', 'adaptivequiz'), ['class' => 'btn btn-primary']),
'text-center mt-3'
);
}
// Display page as a 'secure' window if enabled.
if ($adaptivequiz->browsersecurity) {
$PAGE->blocks->show_only_fake_blocks();
$output->init_browser_security(false);
echo $output->header();
echo $output->attempt_finished_page($adaptivequiz, $cm, $attempt);
echo $feedbackbutton;
$PAGE->requires->js_init_call('M.mod_adaptivequiz.secure_window.init_close_button',
[new moodle_url('/mod/adaptivequiz/view.php', ['id' => $cm->id])], $output->adaptivequiz_get_js_module());
echo $output->footer();
exit;
}
$PAGE->set_heading(format_string($course->fullname));
echo $output->header();
echo $output->attempt_finished_page($adaptivequiz, $cm, $attempt);
echo $feedbackbutton;
echo $output->footer();
<?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/>.
/**
* Provides the steps to perform one complete backup of the adaptivequiz instance.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/mod/adaptivequiz/backup/moodle2/backup_adaptivequiz_stepslib.php');
class backup_adaptivequiz_activity_task extends backup_activity_task {
/**
* No specific settings for this activity
*/
protected function define_my_settings() {
}
/**
* Defines backup steps to store the instance data and required questions
*/
protected function define_my_steps() {
// Generate the adaptivequiz.xml file containing all the quiz information
// and annotating used questions.
$this->add_step(new backup_adaptivequiz_activity_structure_step('adaptivequiz_structure', 'adaptivequiz.xml'));
// Note: Following steps must be present
// in all the activities using question banks.
// Process all the annotated questions to calculate the question
// categories needing to be included in backup for this activity
// plus the categories belonging to the activity context itself.
$this->add_step(new backup_calculate_question_categories('activity_question_categories'));
// Clean backup_temp_ids table from questions. We already
// have used them to detect question_categories and aren't
// needed anymore.
$this->add_step(new backup_delete_temp_questions('clean_temp_questions'));
}
/**
* Encodes URLs to the index.php and view.php scripts
* @param string $content some HTML text that eventually contains URLs to the activity instance scripts
* @return string the content with the URLs encoded
*/
public static function encode_content_links($content) {
global $CFG;
$base = preg_quote($CFG->wwwroot, '/');
// Link to the list of adatpivequizzes.
$search = "/(".$base."\/mod\/adaptivequiz\/index.php\?id\=)([0-9]+)/";
$content = preg_replace($search, '$@ADAPTIVEQUIZINDEX*$2@$', $content);
// Link to adaptivequiz view by moduleid.
$search = "/(".$base."\/mod\/adaptivequiz\/view.php\?id\=)([0-9]+)/";
$content = preg_replace($search, '$@ADAPTIVEQUIZVIEWBYID*$2@$', $content);
// Link to adaptivequiz view by adaptivequizid.
$search = "/(".$base."\/mod\/adaptivequiz\/view.php\?q\=)([0-9]+)/";
$content = preg_replace($search, '$@ADAPTIVEQUIZVIEWBYQ*$2@$', $content);
return $content;
}
}
<?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/>.
/**
* Define all the backup steps that will be used by the backup_adaptivequiz_activity_task.
*
* @package mod_adaptivequiz
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_adaptivequiz_activity_structure_step extends backup_questions_activity_structure_step {
/**
* Define the backup structure.
*
* @return backup_nested_element The root element (adaptivequiz), wrapped into standard activity structure.
*/
protected function define_structure() {
// To know if we are including userinfo.
$userinfo = $this->get_setting_value('userinfo');
// Define each element separated.
$nodes = ['name', 'intro', 'introformat', 'attempts', 'password', 'browsersecurity', 'attemptfeedbackenable',
'attemptfeedback', 'attemptfeedbackformat', 'showabilitymeasure', 'showabilitymeasurefeedback',
'showabilitymeasuresummary', 'showattemptprogress', 'showownattemptresult', 'questionschecked',
'questionchecktrigger', 'competencyfeedbackenable', 'showquestiondifficultylevel', 'immediatefeedback',
'immediatefeedbackshowsolution', 'highestlevel', 'lowestlevel',
'acceptancethreshold',
'minimumquestions',
'maximumquestions', 'standarderror', 'startinglevel', 'timecreated', 'timemodified',
'completionattemptcompleted'];
$adaptivequiz = new backup_nested_element('adaptivequiz', ['id'], $nodes);
// Attempts.
$adaptiveattempts = new backup_nested_element('adaptiveattempts');
$nodes = ['userid', 'uniqueid', 'attemptstate', 'attemptstopcriteria', 'questionsattempted', 'difficultysum',
'standarderror', 'measure', 'timecreated', 'timemodified'];
$adaptiveattempt = new backup_nested_element('adaptiveattempt', ['id'], $nodes);
// This module is using questions, so produce the related question states and sessions.
// attaching them to the $attempt element based in 'uniqueid' matching.
$this->add_question_usages($adaptiveattempt, 'uniqueid');
// Activity to question categories reference.
$adaptivequestioncats = new backup_nested_element('adatpivequestioncats');
$adaptivequestioncat = new backup_nested_element('adatpivequestioncat', ['id'], ['questioncategory']);
// KNIGHT (Feature 4): per-level competency descriptions.
$competencydescs = new backup_nested_element('competencydescs');
$competencydesc = new backup_nested_element('competencydesc', ['id'], ['level', 'description']);
// Build the tree.
$adaptivequiz->add_child($adaptiveattempts);
$adaptiveattempts->add_child($adaptiveattempt);
$adaptivequiz->add_child($adaptivequestioncats);
$adaptivequestioncats->add_child($adaptivequestioncat);
$adaptivequiz->add_child($competencydescs);
$competencydescs->add_child($competencydesc);
// Define sources.
$adaptivequiz->set_source_table('adaptivequiz', ['id' => backup::VAR_ACTIVITYID]);
$adaptivequestioncat->set_source_table('adaptivequiz_question', ['instance' => backup::VAR_PARENTID]);
$competencydesc->set_source_table('adaptivequiz_competencydesc', ['adaptivequizid' => backup::VAR_PARENTID]);
// All the rest of elements only happen if we are including user info.
if ($userinfo) {
$sql = 'SELECT *
FROM {adaptivequiz_attempt}
WHERE instance = :instance';
$param = ['instance' => backup::VAR_PARENTID];
$adaptiveattempt->set_source_sql($sql, $param);
}
// Define id annotations.
$adaptivequestioncat->annotate_ids('question_categories', 'questioncategory');
$adaptiveattempt->annotate_ids('user', 'userid');
$adaptivequiz->annotate_files('mod_adaptivequiz', 'intro', null); // This file area hasn't itemid.
$adaptivequiz->annotate_files('mod_adaptivequiz', 'attemptfeedback', null);
return $this->prepare_activity_structure($adaptivequiz);
}
}
<?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/>.
/**
* Restore task that provides all the settings and steps to perform one complete restore of the activity.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/mod/adaptivequiz/backup/moodle2/restore_adaptivequiz_stepslib.php');
class restore_adaptivequiz_activity_task extends restore_activity_task {
/**
* Define (add) particular settings this activity can have
*/
protected function define_my_settings() {
// No particular settings for this activity.
}
/**
* Define (add) particular steps this activity can have
*/
protected function define_my_steps() {
// Adaptivequiz only has one structure step.
$this->add_step(new restore_adaptivequiz_activity_structure_step('adaptivequiz_structure', 'adaptivequiz.xml'));
}
/**
* Define the contents in the activity that must be
* processed by the link decoder
* @return array an array of restore_decode_content objects
*/
public static function define_decode_contents() {
$contents = array();
$contents[] = new restore_decode_content('adaptivequiz', array('intro'), 'adaptivequiz');
return $contents;
}
/**
* Define the decoding rules for links belonging
* to the activity to be executed by the link decoder
* @return array an array of restore_decode_rule objects
*/
public static function define_decode_rules() {
$rules = array();
$rules[] = new restore_decode_rule('ADAPTIVEQUIZVIEWBYID', '/mod/adaptivequiz/view.php?id=$1', 'course_module');
$rules[] = new restore_decode_rule('ADAPTIVEQUIZVIEWBYQ', '/mod/adaptivequiz/view.php?q=$1', 'adaptivequiz');
$rules[] = new restore_decode_rule('ADAPTIVEQUIZINDEX', '/mod/adaptivequiz/index.php?id=$1', 'course');
return $rules;
}
/**
* Define the restore log rules that will be applied
* by the {@link restore_logs_processor} when restoring
* adaptivequiz logs. It must return one array
* of {@link restore_log_rule} objects
* @return array an array of restore_log_rule objects
*/
public static function define_restore_log_rules() {
$rules = array();
// TODO update this method when logging statemtns have been added to the code.
return $rules;
}
/**
* Define the restore log rules that will be applied
* by the {@link restore_logs_processor} when restoring
* course logs. It must return one array
* of {@link restore_log_rule} objects
*
* Note this rules are applied when restoring course logs
* by the restore final task, but are defined here at
* activity level. All them are rules not linked to any module instance (cmid = 0)
* @return array an array of of restore_log_rule objects
*/
public static function define_restore_log_rules_for_course() {
$rules = array();
return $rules;
}
}
<?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/>.
/**
* Structure step to restore one adaptivequiz activity.
*
* @package mod_adaptivequiz
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_adaptivequiz_activity_structure_step extends restore_questions_activity_structure_step {
/**
* Define the structure for restoring the activity.
*
* @return backup_nested_element The $activitystructure wrapped by the common 'activity' element.
*/
protected function define_structure() {
$paths = array();
$userinfo = $this->get_setting_value('userinfo');
$adaptivequiz = new restore_path_element('adaptivequiz', '/activity/adaptivequiz');
$paths[] = $adaptivequiz;
$paths[] = new restore_path_element('adaptivequiz_question',
'/activity/adaptivequiz/adatpivequestioncats/adatpivequestioncat');
// KNIGHT (Feature 4): per-level competency descriptions.
$paths[] = new restore_path_element(
'adaptivequiz_competencydesc',
'/activity/adaptivequiz/competencydescs/competencydesc'
);
if ($userinfo) {
$attempt = new restore_path_element('adaptivequiz_attempt', '/activity/adaptivequiz/adaptiveattempts/adaptiveattempt');
$paths[] = $attempt;
// Add states and sessions.
$this->add_question_usages($attempt, $paths);
}
// Return the paths wrapped into standard activity structure.
return $this->prepare_activity_structure($paths);
}
/**
* Process the adaptivequiz element.
*
* @param stdClass An object whose properties are nodes in the adatpviequiz structure.
*/
protected function process_adaptivequiz($data) {
global $CFG, $DB;
$data = (object)$data;
$oldid = $data->id;
$data->course = $this->get_courseid();
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->timemodified = $this->apply_date_offset($data->timemodified);
// Insert the quiz record.
$newitemid = $DB->insert_record('adaptivequiz', $data);
// Immediately after inserting "activity" record, call this.
$this->apply_activity_instance($newitemid);
}
/**
* Process the activity instance to question categories relation structure.
*
* @param stdClass An object whose properties are nodes in the adatpviequiz_question structure.
*/
protected function process_adaptivequiz_question($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->instance = $this->get_new_parentid('adaptivequiz');
// Check if catid is not empty and update the record with the new category id.
$catid = $this->get_mappingid('question_category', $data->questioncategory);
if (!empty($catid)) {
$data->questioncategory = $catid;
}
$DB->insert_record('adaptivequiz_question', $data);
}
/**
* Process a KNIGHT competency description (Feature 4).
*
* @param stdClass An object whose properties are nodes in the competencydesc structure.
*/
protected function process_adaptivequiz_competencydesc($data) {
global $DB;
$data = (object)$data;
$data->adaptivequizid = $this->get_new_parentid('adaptivequiz');
$DB->insert_record('adaptivequiz_competencydesc', $data);
}
/**
* Process the activity instance to question categories relation structure.
*
* @param stdClass An object whose properties are nodes in the adatpviequiz_attempt structure.
*/
protected function process_adaptivequiz_attempt($data) {
$data = (object)$data;
$oldid = $data->id;
$data->instance = $this->get_new_parentid('adaptivequiz');
$data->userid = $this->get_mappingid('user', $data->userid);
$data->timemodified = $this->apply_date_offset($data->timemodified);
// The data is actually inserted into the database later in inform_new_usage_id.
$this->currentadatpivequizattempt = clone($data);
}
/**
* This function assigns the new question usage by activity id to the attempt.
*
* @param int $newusageid A new question usage by activity id.
*/
protected function inform_new_usage_id($newusageid) {
global $DB;
$data = $this->currentadatpivequizattempt;
$oldid = $data->id;
$data->uniqueid = $newusageid;
$newitemid = $DB->insert_record('adaptivequiz_attempt', $data);
// Save quiz_attempt->id mapping, because logs use it. (logs will be implemented latter).
$this->set_mapping('adaptivequiz_attempt', $oldid, $newitemid, false);
}
/**
* Overrides the parent's method.
*/
protected function after_execute() {
parent::after_execute();
$this->add_related_files('mod_adaptivequiz', 'intro', null);
$this->add_related_files('mod_adaptivequiz', 'attemptfeedback', 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/>.
namespace mod_adaptivequiz;
use coding_exception;
use core\persistent;
use dml_missing_record_exception;
use mod_adaptivequiz\local\attempt\attempt_state;
/**
* An implementation of Moodle's persistent class for attempts.
*
* IMPORTANT: currently, the class is used as a read model only. Its capabilities to store the modified attempt's properties
* are blocked as far as allowed by Moodle's persistent implementation.
*
* @package mod_adaptivequiz
* @copyright 2025 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt extends persistent {
/** @var string */
const TABLE = 'adaptivequiz_attempt';
/**
* Implements the parent's method.
*
* @return array
*/
protected static function define_properties() {
return [
'instance' => [
'type' => PARAM_INT,
],
'userid' => [
'type' => PARAM_INT,
],
'uniqueid' => [
'type' => PARAM_INT,
],
'attemptstate' => [
'type' => PARAM_ALPHANUMEXT,
'choices' => [
attempt_state::IN_PROGRESS,
attempt_state::COMPLETED,
],
],
'attemptstopcriteria' => [
'type' => PARAM_TEXT,
'null' => NULL_ALLOWED,
'default' => '',
],
'questionsattempted' => [
'type' => PARAM_INT,
],
'difficultysum' => [
'type' => PARAM_FLOAT,
],
'standarderror' => [
'type' => PARAM_FLOAT,
],
'measure' => [
'type' => PARAM_FLOAT,
],
];
}
/**
* A hook implementation.
*
* As soon as the class is used as a read model, this is used to prevent any changes in the attempt object stored.
*/
protected function before_create() {
throw new coding_exception('The attempt persistent class must not be used to store the attempt\'s state.');
}
/**
* A hook implementation.
*
* As soon as the class is used as a read model, this is used to prevent any changes in the attempt object stored.
*/
protected function before_update() {
throw new coding_exception('The attempt persistent class must not be used to store the attempt\'s state.');
}
/**
* A hook implementation.
*
* As soon as the class is used as a read model, this is used to prevent deletion of attempts.
*/
protected function before_delete() {
throw new coding_exception('The attempt persistent class must not be used to delete attempts.');
}
/**
* Returns an attempt with the highest score (ability measure value) for the user.
*
* The caller assumes the user has made at least one attempt, a DML exception will be thrown if no attempts found at all.
*
* @param int $userid
* @param int $adaptivequizid
*/
public static function get_with_highest_score_for_user(int $userid, int $adaptivequizid): self {
$attempts = self::get_records([
'userid' => $userid,
'instance' => $adaptivequizid,
'attemptstate' => attempt_state::COMPLETED,
], 'measure', 'DESC', 0, 1);
if (!$attempts) {
throw new dml_missing_record_exception(self::TABLE);
}
$keyfirst = array_key_first($attempts);
return $attempts[$keyfirst];
}
/**
* Returns the first attempt the user has made.
*
* The caller assumes the user has made at least one attempt, a DML exception will be thrown if no attempts found at all.
*
* @param int $userid
* @param int $adaptivequizid
*/
public static function get_first_for_user(int $userid, int $adaptivequizid): self {
$attempts = self::get_records([
'userid' => $userid,
'instance' => $adaptivequizid,
'attemptstate' => attempt_state::COMPLETED,
], 'timecreated', 'ASC', 0, 1);
if (!$attempts) {
throw new dml_missing_record_exception(self::TABLE);
}
$keyfirst = array_key_first($attempts);
return $attempts[$keyfirst];
}
/**
* Returns the last attempt the user has made.
*
* The caller assumes the user has made at least one attempt, a DML exception will be thrown if no attempts found at all.
*
* @param int $userid
* @param int $adaptivequizid
*/
public static function get_last_for_user(int $userid, int $adaptivequizid): self {
$attempts = self::get_records([
'userid' => $userid,
'instance' => $adaptivequizid,
'attemptstate' => attempt_state::COMPLETED,
], 'timecreated', 'DESC', 0, 1);
if (!$attempts) {
throw new dml_missing_record_exception(self::TABLE);
}
$keyfirst = array_key_first($attempts);
return $attempts[$keyfirst];
}
}
<?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;
/**
* Defines placeholders available in the custom attempt feedback text.
*
* @package mod_adaptivequiz
* @copyright 2025 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
enum attempt_feedback_placeholder_option: string implements editor_placeholder_option {
case ABILITY_MEASURE = 'abilitymeasure';
case QUESTION_LOWEST_LEVEL = 'qlowestlevel';
case QUESTION_HIGHEST_LEVEL = 'qhighestlevel';
/**
* Implements the interface.
*/
public function id(): string {
return $this->value;
}
/**
* Implements the interface.
*/
public function key(): string {
return '{{'. $this->value .'}}';
}
/**
* Implements the interface.
*/
public function description(): string {
return match ($this) {
self::ABILITY_MEASURE => get_string('attemptquestion_ability', 'adaptivequiz'),
self::QUESTION_LOWEST_LEVEL => get_string('lowestlevel', 'adaptivequiz'),
self::QUESTION_HIGHEST_LEVEL => get_string('highestlevel', 'adaptivequiz'),
};
}
}
<?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;
use mod_adaptivequiz\external\ability_measure_exporter;
/**
* A class to manage placeholders in custom attempt feedback's text editor.
*
* @package mod_adaptivequiz
* @copyright 2025 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_feedback_placeholders_helper {
/**
* @var attempt_feedback_placeholder_option[] $options Placeholders configuration.
*/
private array $options = [];
/**
* Empty and closed, the factory method must be used instead.
*/
public function __construct() {
}
/**
* Returns an instance of placeholders collection populated with the options.
*/
public function placeholder_options(): editor_placeholders {
$placeholders = new editor_placeholders();
foreach ($this->options as $option) {
$placeholders->add_option($option);
}
return $placeholders;
}
/**
* Searches for placeholders in the given attempt feedback text and replaces it with corresponding values.
*
* @param string $text Text with placeholder.
* @param ability_measure_exporter $abilitymeasureexporter Contains the placeholders' values.
* @return string The text with replaced placeholders.
*/
public function format_feedback_text(string $text, ability_measure_exporter $abilitymeasureexporter): string {
global $PAGE;
$output = $PAGE->get_renderer('core');
$abilitymeasuredata = $abilitymeasureexporter->export($output);
$abilitymeasuredatamap = [
attempt_feedback_placeholder_option::ABILITY_MEASURE->key() => $abilitymeasuredata->abilitymeasurevalue,
attempt_feedback_placeholder_option::QUESTION_LOWEST_LEVEL->key() => $abilitymeasuredata->lowestlevel,
attempt_feedback_placeholder_option::QUESTION_HIGHEST_LEVEL->key() => $abilitymeasuredata->highestlevel,
];
$toreplace = array_keys($abilitymeasuredatamap);
$replacewith = array_values($abilitymeasuredatamap);
return str_replace($toreplace, $replacewith, $text);
}
/**
* Contains the default instantiation of the placeholder options.
*/
public static function configured(): self {
$helper = new self();
$helper->options = [
attempt_feedback_placeholder_option::ABILITY_MEASURE,
attempt_feedback_placeholder_option::QUESTION_LOWEST_LEVEL,
attempt_feedback_placeholder_option::QUESTION_HIGHEST_LEVEL,
];
return $helper;
}
}
<?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/>.
/**
* Handles our own events to make some reactive changes, for example, update activity completion state (if completion is enabled).
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz;
use completion_info;
use core\event\base;
class attempt_state_change_observers {
public static function attempt_completed(base $event): void {
global $DB;
// Update completion state if enabled.
if (!$attempt = $event->get_record_snapshot('adaptivequiz_attempt', $event->objectid)) {
return;
}
if (!$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attempt->instance])) {
return;
}
if (!$course = $DB->get_record('course', ['id' => $adaptivequiz->course])) {
return;
}
$completion = new completion_info($course);
if (!$completion->is_enabled()) {
return;
}
if (!$adaptivequiz->completionattemptcompleted) {
return;
}
if (!$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $adaptivequiz->course)) {
return;
}
$completion->update_state($cm, COMPLETION_COMPLETE, $event->userid);
}
}
<?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/>.
/**
* Activity custom completion subclass for the adaptive quiz activity.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\completion;
use core_completion\activity_custom_completion;
use mod_adaptivequiz\local\attempt;
class custom_completion extends activity_custom_completion {
/**
* @inheritDoc
*/
public function get_state(string $rule): int {
$this->validate_rule($rule);
return attempt::user_has_completed_on_quiz($this->cm->instance, $this->userid)
? COMPLETION_COMPLETE
: COMPLETION_INCOMPLETE;
}
/**
* @inheritDoc
*/
public static function get_defined_custom_rules(): array {
return ['completionattemptcompleted'];
}
/**
* @inheritDoc
*/
public function get_custom_rule_descriptions(): array {
return ['completionattemptcompleted' => get_string('completionattemptcompletedcminfo', 'adaptivequiz')];
}
/**
* @inheritDoc
*/
public function get_sort_order(): array {
return ['completionview', 'completionusegrade', 'completionattemptcompleted'];
}
}
<?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;
/**
* Defines an interface for placeholder options.
*
* The purpose is to define a single interface for placeholders to be used with any text editor within the plugin.
*
* @package mod_adaptivequiz
* @copyright 2025 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface editor_placeholder_option {
/**
* Returns an option's string value.
*/
public function id(): string;
/**
* Returns an option's string value enclosed in special symbols to identify it as a placeholder in a text.
*/
public function key(): string;
/**
* Defines text for the given option to be used as an option's explanation on a page, in a form, etc.
*/
public function description(): string;
}
<?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;
/**
* A container for placeholder options to be used in a text editor.
*
* The class can be used for any text editor.
*
* @package mod_adaptivequiz
* @copyright 2025 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class editor_placeholders {
/**
* @var editor_placeholder_option[] $options.
*/
private array $options;
/**
* The constructor, intentionally empty.
*/
public function __construct() {
}
/**
* Adds an option to the container.
*
* @param editor_placeholder_option $option
* @return self To enable chaining.
*/
public function add_option(editor_placeholder_option $option): self {
$this->options[] = $option;
return $this;
}
/**
* Returns the contained options.
*
* @return editor_placeholder_option[]
*/
public function options(): array {
return $this->options;
}
}
<?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/>.
/**
* Event which is triggered when a user completes an attempt on adaptive quiz.
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\event;
use core\event\base;
use moodle_exception;
use moodle_url;
class attempt_completed extends base {
/**
* @inheritDoc
*/
public static function get_name() {
return get_string('eventattemptcompleted', 'adaptivequiz');
}
/**
* @inheritDoc
*/
public function get_description() {
return "The user with id '$this->userid' has completed the attempt with id '$this->objectid' for the " .
"adaptive quiz with course module id '$this->contextinstanceid'.";
}
/**
* Returns related URL where result of the event can be observed.
*
* @throws moodle_exception
* @return moodle_url
*/
public function get_url() {
return new moodle_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $this->objectid]);
}
/**
* @inheritDoc
*/
public static function get_objectid_mapping() {
return ['db' => 'adaptivequiz_attempt', 'restore' => 'adaptiveattempts'];
}
/**
* @inheritDoc
*/
protected function init() {
$this->data['objecttable'] = 'adaptivequiz_attempt';
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
}
<?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 mod_peerassess instance list viewed event.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\event;
class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
/**
* Create the event from course record.
*
* @param \stdClass $course
* @return course_module_instance_list_viewed
*/
public static function create_from_course(\stdClass $course) {
$params = array(
'context' => \context_course::instance($course->id)
);
$event = self::create($params);
$event->add_record_snapshot('course', $course);
return $event;
}
}
<?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/>.
/**
* Defines the course module viewed event.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz\event;
class course_module_viewed extends \core\event\course_module_viewed {
protected function init() {
$this->data['objecttable'] = 'adaptivequiz';
parent::init();
}
}
<?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\external;
use core\external\exporter;
use mod_adaptivequiz\local\catalgo;
use renderer_base;
/**
* Exporter class to format the ability measure value for output.
*
* Note, the output from this exporter is not intended for using in any further calculations. It's purely for output purposes:
* output templates, external functions, etc.
*
* @package mod_adaptivequiz
* @copyright 2025 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ability_measure_exporter extends exporter {
/**
* Defines the list of properties to export.
*
* @return array
*/
protected static function define_properties() {
return [
'highestlevel' => [
'type' => PARAM_INT,
],
'lowestlevel' => [
'type' => PARAM_INT,
],
];
}
/**
* Defines the list of extra properties to export.
*
* Implements the parent's abstract method.
*
* @return array
*/
protected static function define_other_properties() {
return [
'abilitymeasurevalue' => [
'type' => PARAM_FLOAT,
],
];
}
/**
* Returns a list of objects that are required to do the exporting.
*
* Overrides the parent's method.
*
* @return array
*/
protected static function define_related() {
return [
'attempt' => 'stdClass',
];
}
/**
* Get the additional values to inject while exporting.
*
* Overrides the parent's method.
*
* @param renderer_base $output
* @return array
*/
protected function get_other_values(renderer_base $output) {
$attempt = $this->related['attempt'];
$measure = round(catalgo::map_logit_to_scale($attempt->measure, $this->data['highestlevel'],
$this->data['lowestlevel']), 2);
return [
'abilitymeasurevalue' => $measure,
];
}
}
<?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\external;
use core\context;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_question\local\bank\question_bank_helper;
use mod_adaptivequiz\item_bank;
use stdClass;
/**
* Returns a list of filtered question banks.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class search_question_banks extends external_api {
/**
* @var int The maximum number of banks to return.
*/
const MAX_RESULTS = 20;
/**
* Return values definition.
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'questionbanks' => new external_multiple_structure(
new external_single_structure([
'value' => new external_value(PARAM_INT, 'ID of the qbank instance.'),
'label' => new external_value(PARAM_TEXT, 'Formatted bank name'),
]),
'List of question banks',
),
]);
}
/**
* Parameters definition.
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contextid' => new external_value(PARAM_INT, 'Context ID of the adaptive quiz module.'),
'incourseid' => new external_value(
PARAM_INT,
'Course ID to get question banks from.',
VALUE_DEFAULT,
default: null,
),
'notincourseid' => new external_value(
PARAM_INT,
'Course ID to exclude.',
VALUE_DEFAULT,
default: null,
),
'search' => new external_value(PARAM_TEXT, 'Search terms by which to filter the banks.', default: ''),
]);
}
/**
* Main.
*
* @param int $contextid Context ID of the adaptive quiz module.
* @param string $search String to filter results by question bank name.
* @param int|null $incourseid Specific course ID to get banks from.
* @param int|null $notincourseid Course ID to exclude.
*/
public static function execute(
int $contextid,
?int $incourseid = null,
?int $notincourseid = null,
string $search = ''
): array {
[
'contextid' => $contextid,
'incourseid' => $incourseid,
'notincourseid' => $notincourseid,
'search' => $search,
] = self::validate_parameters(self::execute_parameters(), [
'contextid' => $contextid,
'incourseid' => $incourseid,
'notincourseid' => $notincourseid,
'search' => $search,
]);
$context = context::instance_by_id($contextid);
self::validate_context($context);
$cm = get_coursemodule_from_id(modulename: 'adaptivequiz', cmid: $context->instanceid, strictness: MUST_EXIST);
$currentassignments = item_bank::get_question_banks_assigned_to_adaptivequiz(
$cm->instance,
'id',
$incourseid,
$notincourseid
);
$excludeqbankidlist = array_map(fn (stdClass $qbank) => $qbank->id, $currentassignments);
$qbanks = question_bank_helper::get_activity_instances_with_shareable_questions(
incourseids: $incourseid ? [$incourseid] : [],
notincourseids: $notincourseid ? [$notincourseid] : [],
filtercontext: $context,
search: $search,
limit: self::MAX_RESULTS + 1, // Return up to 1 extra result, so we know there are more.
);
$qbanks = array_filter($qbanks, function ($qbank) use ($excludeqbankidlist) {
return !in_array($qbank->cminfo->instance, $excludeqbankidlist);
});
$suggestions = array_map(function ($qbank) {
// The types returned by get_activity_instances_with_shareable_questions() are different across 5.x
// versions.
if ($qbank instanceof stdClass) {
return ['value' => $qbank->cminfo->instance, 'label' => $qbank->coursenamebankname];
}
/** @var \core_question\local\bank\formatted_bank $qbank */
return ['value' => $qbank->cminfo->instance, 'label' => $qbank->get_formatted()->coursenamebankname];
}, $qbanks);
sort($suggestions);
if (count($suggestions) > self::MAX_RESULTS) {
// If there are too many results, replace the last one with a placeholder.
$suggestions[array_key_last($suggestions)] = [
'value' => 0,
'label' => get_string('otherquestionbankstoomany', 'question', self::MAX_RESULTS),
];
}
return [
'questionbanks' => $suggestions,
];
}
}
Supports Markdown
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