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
<?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\output;
use help_icon;
use html_writer;
use mod_adaptivequiz\local\attempt\attempt_state;
use mod_adaptivequiz_renderer;
use moodle_url;
use stdClass;
use table_sql;
/**
* Displays a list of user's own attempts on the view page.
*
* @package mod_adaptivequiz
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_attempts_overview extends table_sql {
/**
* @var mod_adaptivequiz_renderer $renderer
*/
private $renderer;
/**
* @var bool $showfeedback Whether the score links to the competency feedback (Feature 4).
*/
private $showfeedback = false;
/**
* The constructor.
*
* @param mod_adaptivequiz_renderer $renderer
*/
public function __construct(mod_adaptivequiz_renderer $renderer) {
parent::__construct('userattemptstable');
$this->renderer = $renderer;
}
/**
* A convenience function to call a bunch of init methods.
*
* @param moodle_url $baseurl
* @param stdClass $adaptivequiz A record form {adaptivequiz}.
* @param int $userid
*/
public function init(moodle_url $baseurl, stdClass $adaptivequiz, int $userid): void {
$columns = ['state', 'timefinished'];
$headers = [
get_string('attempt_state', 'adaptivequiz'),
get_string('attemptfinishedtimestamp', 'adaptivequiz'),
];
$help = [];
if ($adaptivequiz->showabilitymeasuresummary) {
$columns[] = 'measure';
$headers[] = get_string('abilityestimated', 'adaptivequiz') . ' / ' .
$adaptivequiz->lowestlevel . ' - ' . $adaptivequiz->highestlevel;
$help[count($columns) - 1] = new help_icon('abilityestimated', 'adaptivequiz');
}
// KNIGHT (Feature 2): offer a review icon when students may review their own attempts.
if (!empty($adaptivequiz->showownattemptresult)) {
$columns[] = 'review';
$headers[] = get_string('reviewattempt', 'adaptivequiz');
}
// KNIGHT (Feature 4): the score links to the competency feedback when the feature is enabled.
$this->showfeedback = !empty($adaptivequiz->competencyfeedbackenable);
$this->define_columns($columns);
$this->define_headers($headers);
$this->set_attribute('class', 'generaltable userattemptstable');
$this->is_downloadable(false);
$this->collapsible(false);
$this->sortable(false, 'timefinished', SORT_DESC);
$this->define_help_for_headers($help);
$this->set_column_css_classes();
$this->set_content_alignment_in_columns();
$this->define_baseurl($baseurl);
$this->set_sql('a.id, a.attemptstate AS state, a.timemodified AS timefinished, a.measure, q.highestlevel, ' .
'q.lowestlevel', '{adaptivequiz_attempt} a, {adaptivequiz} q', 'a.instance = q.id AND q.id = ? ' .
'AND userid = ?', [$adaptivequiz->id, $userid]);
}
/**
* A column formatter.
*
* @param stdClass $row
*/
protected function col_state(stdClass $row): string {
return get_string('recent' . $row->state, 'adaptivequiz');
}
/**
* A column formatter.
*
* @param stdClass $row
*/
protected function col_timefinished(stdClass $row): string {
if ($row->state != attempt_state::COMPLETED) {
return '';
}
return userdate($row->timefinished);
}
/**
* A column formatter.
*
* @param stdClass $row
*/
protected function col_measure(stdClass $row): string {
$measure = $this->renderer->format_measure($row);
if (!$this->showfeedback || $row->state != attempt_state::COMPLETED) {
return $measure;
}
// KNIGHT (Feature 4): the score links to the competency feedback for this attempt.
$url = new moodle_url(
'/mod/adaptivequiz/feedback.php',
['id' => $this->baseurl->param('id'), 'attempt' => $row->id]
);
return html_writer::link($url, $measure);
}
/**
* A column formatter: a link to review the student's own (completed) attempt.
*
* @param stdClass $row
*/
protected function col_review(stdClass $row): string {
if ($row->state != attempt_state::COMPLETED) {
return '';
}
$url = new moodle_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $row->id]);
return html_writer::link($url, $this->renderer->pix_icon('i/search', get_string('reviewattempt', 'adaptivequiz')));
}
/**
* Wraps setting alignment in columns.
*/
private function set_content_alignment_in_columns(): void {
foreach (array_keys($this->columns) as $columnname) {
$this->column_class[$columnname] .= ' text-center';
}
}
/**
* Wraps setting CSS classes for columns.
*/
private function set_column_css_classes(): void {
$this->column_class['state'] .= ' statecol';
if (array_key_exists('measure', $this->columns)) {
$this->column_class['measure'] .= ' abilitymeasurecol';
}
}
}
<?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/>.
/**
* Confirmation page to close a student attempt.
*
* @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');
use mod_adaptivequiz\local\attempt\attempt_state;
$attemptid = required_param('attempt', PARAM_INT);
$confirm = optional_param('confirm', 0, PARAM_INT);
$attempt = $DB->get_record('adaptivequiz_attempt', ['id' => $attemptid], '*', MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attempt->instance], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $adaptivequiz->course, false, MUST_EXIST);
$course = $DB->get_record('course', ['id' => $adaptivequiz->course], '*', MUST_EXIST);
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
// KNIGHT: closing an attempt is a managing action, not a viewing one. Require the manage capability
// so a view-only role (e.g. non-editing teacher with viewreport) cannot close attempts.
require_capability('mod/adaptivequiz:manage', $context);
$returnurl = new moodle_url('/mod/adaptivequiz/viewattemptreport.php', ['cmid' => $cm->id, 'userid' => $attempt->userid]);
if ($attempt->attemptstate == attempt_state::COMPLETED) {
throw new moodle_exception('errorclosingattempt_alreadycomplete', 'adaptivequiz', $returnurl);
}
$user = $DB->get_record('user', ['id' => $attempt->userid], '*', MUST_EXIST);
$PAGE->set_url('/mod/adaptivequiz/closeattempt.php', ['attempt' => $attempt->id]);
$PAGE->set_title(format_string($adaptivequiz->name));
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$renderer = $PAGE->get_renderer('mod_adaptivequiz');
$performancecalculation = new stdClass();
$performancecalculation->measure = $attempt->measure;
$performancecalculation->stderror = $attempt->standarderror;
$performancecalculation->lowestlevel = $adaptivequiz->lowestlevel;
$performancecalculation->highestlevel = $adaptivequiz->highestlevel;
$a = new stdClass();
$a->name = fullname($user);
$a->started = userdate($attempt->timecreated);
$a->modified = userdate($attempt->timemodified);
$a->num_questions = format_string($attempt->questionsattempted);
$a->measure = $renderer->format_measure($performancecalculation);
$a->standarderror = $renderer->format_standard_error($performancecalculation);
$a->current_user_name = fullname($USER);
$a->current_user_id = format_string($USER->id);
$a->now = userdate(time());
if ($confirm) {
$statusmessage = get_string('attemptclosedstatus', 'adaptivequiz', $a);
$closemessage = get_string('attemptclosed', 'adaptivequiz', $a);
adaptivequiz_complete_attempt($attempt->uniqueid, $adaptivequiz, $context, $attempt->userid, $attempt->standarderror,
$statusmessage);
redirect($returnurl, $closemessage, 4);
}
$message = html_writer::tag('p', get_string('confirmcloseattempt', 'adaptivequiz', $a)) .
html_writer::tag('p', get_string('confirmcloseattemptstats', 'adaptivequiz', $a)) .
html_writer::tag('p', get_string('confirmcloseattemptscore', 'adaptivequiz', $a));
$confirm = new moodle_url('/mod/adaptivequiz/closeattempt.php', ['attempt' => $attempt->id, 'confirm' => 1]);
echo $renderer->header();
echo $renderer->confirm($message, $confirm, $returnurl);
echo $renderer->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/>.
/**
* Capabilities definition.
*
* @package mod_adaptivequiz
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2026 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'mod/adaptivequiz:addinstance' => [
'riskbitmask' => RISK_XSS,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
'clonepermissionsfrom' => 'moodle/course:manageactivities',
],
'mod/adaptivequiz:viewreport' => [
'riskbitmask' => RISK_PERSONAL,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
// KNIGHT: non-editing teachers may view participants' results (but not manage attempts).
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
],
'mod/adaptivequiz:reviewattempts' => [
'riskbitmask' => RISK_PERSONAL,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
],
'mod/adaptivequiz:attempt' => [
'riskbitmask' => RISK_SPAM,
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'student' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
],
'mod/adaptivequiz:manage' => [
'riskbitmask' => RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
],
];
<?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
*/
defined('MOODLE_INTERNAL') || die();
$observers = [
[
'eventname' => '\mod_adaptivequiz\event\attempt_completed',
'callback' => '\mod_adaptivequiz\attempt_state_change_observers::attempt_completed'
]
];
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/adaptivequiz/db" VERSION="20260301" COMMENT="XMLDB file for Moodle mod/adaptivequiz"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="adaptivequiz" COMMENT="Adaptive quiz instances table" NEXT="adaptivequiz_question">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="course id foreign key"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Name of the activity instance"/>
<FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Description of activity"/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="activity intro text format"/>
<FIELD NAME="attempts" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Number of allowed attempts"/>
<FIELD NAME="password" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="A password that the student must enter before starting or continuing an adaptive quiz attempt."/>
<FIELD NAME="browsersecurity" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Restriciton on the browser the student must use. E.g. securewindow."/>
<FIELD NAME="attemptfeedback" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback given to students when their attempt has been completed."/>
<FIELD NAME="attemptfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Format of attempt feedback"/>
<FIELD NAME="attemptfeedbackenable" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="showabilitymeasurefeedback" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Whether the ability measure should be presented to a test-taker once the attempt is finished."/>
<FIELD NAME="showabilitymeasuresummary" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Whether the ability measure is displayed in user's attempts summary."/>
<FIELD NAME="showattemptprogress" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether quiz progress info should be presented to a test-taker during attempting a quiz."/>
<FIELD NAME="showownattemptresult" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="KNIGHT: Whether students may review their own attempts (question details)."/>
<FIELD NAME="questionschecked" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="KNIGHT: Completed-attempts count recorded at the last question-analysis review (0 = never reviewed); the reminder fires again once questionchecktrigger further attempts complete. Holds a count despite the legacy boolean-style name."/>
<FIELD NAME="questionchecktrigger" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="KNIGHT: Number of completed attempts after which a question-analysis review is advised; 0 disables the reminder."/>
<FIELD NAME="competencyfeedbackenable" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="KNIGHT: Whether the per-level competency feedback is enabled for this activity."/>
<FIELD NAME="showquestiondifficultylevel" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="KNIGHT: Whether the current question's difficulty level is shown to the student during the attempt."/>
<FIELD NAME="immediatefeedback" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="KNIGHT: Whether per-question immediate feedback (correctness + question feedback) is shown during the attempt."/>
<FIELD NAME="immediatefeedbackshowsolution" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="KNIGHT: Whether the immediate feedback also reveals the correct answer. Only relevant when immediatefeedback is on."/>
<FIELD NAME="highestlevel" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The highest difficulty level the adaptive quiz will go to."/>
<FIELD NAME="lowestlevel" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The lowest difficulty level the adaptive quiz will go to."/>
<FIELD NAME="acceptancethreshold" TYPE="number" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="2" COMMENT="KNIGHT: Minimum point fraction (0..1) for a question to count as correct. 0 means any partial credit counts."/>
<FIELD NAME="minimumquestions" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The minimum number of questions that mus be attempted by the user"/>
<FIELD NAME="maximumquestions" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The maximum number of question that can be attempted by the user"/>
<FIELD NAME="standarderror" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0.0" SEQUENCE="false" DECIMALS="5" COMMENT="The standard error that must be met before ending the attempt."/>
<FIELD NAME="startinglevel" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The level of difficult all attempts will start with"/>
<FIELD NAME="grademethod" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Which of multiple attempts should be reported to the grade book. 1=highest, 3=first, 4=last."/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time created timestamp"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time modified timesampt"/>
<FIELD NAME="completionattemptcompleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Stores whether a custom completion rule based on whether a user has a completed attempt enabled."/>
<FIELD NAME="debuginfoenable" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="course_foreign" TYPE="foreign" FIELDS="course" REFTABLE="course" REFFIELDS="id" COMMENT="Foreign key to the course table."/>
</KEYS>
</TABLE>
<TABLE NAME="adaptivequiz_question" COMMENT="An association table for activity instance and question categories.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="instance" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Activity instance id. Foreign key to activityquiz."/>
<FIELD NAME="questioncategory" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Question category id. Foreign key to questions."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="instance_foreign" TYPE="foreign" FIELDS="instance" REFTABLE="adaptivequiz" REFFIELDS="id" COMMENT="Foreign key to adaptivequiz table."/>
<KEY NAME="questioncategory_foreign" TYPE="foreign" FIELDS="questioncategory" REFTABLE="question_categories" REFFIELDS="id" COMMENT="Foreign key to questino_categories table."/>
</KEYS>
</TABLE>
<TABLE NAME="adaptivequiz_qbank" COMMENT="Stores links between activity instances and Moodle question banks.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="adaptivequizid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key for adaptive quiz instance."/>
<FIELD NAME="qbankid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key for question bank instance."/>
<FIELD NAME="qbankcontextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key for question bank context."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="adaptivequizid" TYPE="foreign" FIELDS="adaptivequizid" REFTABLE="adaptivequiz" REFFIELDS="id"/>
<KEY NAME="qbankid" TYPE="foreign" FIELDS="qbankid" REFTABLE="qbank" REFFIELDS="id"/>
<KEY NAME="qbankcontextid" TYPE="foreign" FIELDS="qbankcontextid" REFTABLE="context" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="adaptivequiz-qbank" UNIQUE="true" FIELDS="adaptivequizid, qbankid, qbankcontextid"/>
</INDEXES>
</TABLE>
<TABLE NAME="adaptivequiz_attempt" COMMENT="Logging of attempts">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="instance"/>
<FIELD NAME="instance" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Activity instance the attempt was for"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="User id. Foreign key from user."/>
<FIELD NAME="uniqueid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Question usage id. Foreign key to question_usages."/>
<FIELD NAME="attemptstate" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false" COMMENT="The state of the attempt"/>
<FIELD NAME="attemptstopcriteria" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The reason why the attempt was stopped"/>
<FIELD NAME="questionsattempted" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The number of question attempted"/>
<FIELD NAME="difficultysum" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0.0" SEQUENCE="false" DECIMALS="7" COMMENT="The sum of difficulty levels attempted measured in logits"/>
<FIELD NAME="standarderror" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0.0" SEQUENCE="false" DECIMALS="5" COMMENT="The standard error that was achieved during the attempt"/>
<FIELD NAME="measure" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0.0" SEQUENCE="false" DECIMALS="5" COMMENT="The attempt ability measure in logits"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time created timestamp"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time modified timestamp"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="instance_foreign"/>
<KEY NAME="instance_foreign" TYPE="foreign" FIELDS="instance" REFTABLE="adaptivequiz" REFFIELDS="id" COMMENT="Foreign key to adaptivequiz table."/>
<KEY NAME="userid_foreign" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id" COMMENT="Foreign key to user table."/>
<KEY NAME="uniqueid_foreign" TYPE="foreign" FIELDS="uniqueid" REFTABLE="question_usages" REFFIELDS="id" COMMENT="Foreign key to question_usages table."/>
</KEYS>
<INDEXES>
<INDEX NAME="instance_userid_idx" UNIQUE="false" FIELDS="instance, userid" COMMENT="Instance and user id index"/>
</INDEXES>
</TABLE>
<TABLE NAME="adaptivequiz_competencydesc" COMMENT="KNIGHT: per-level competency descriptions used to build attempt feedback.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="adaptivequizid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The adaptivequiz instance the description belongs to."/>
<FIELD NAME="level" TYPE="int" LENGTH="5" NOTNULL="true" SEQUENCE="false" COMMENT="The difficulty level the description applies to."/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The competency description shown for this level."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="adaptivequiz_fk" TYPE="foreign" FIELDS="adaptivequizid" REFTABLE="adaptivequiz" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
<?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/>.
/**
* Definition of log events for the adaptive quiz module.
*
* @package mod_adaptivequiz
* @category log
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $DB;
$logs = array(
array('module' => 'adaptivequiz', 'action' => 'view', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'add', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'update', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'report', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'submit', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'review', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'start attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'close attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'start attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'continue attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'),
array('module' => 'adaptivequiz', 'action' => 'start attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'),
);
<?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/>.
/**
* External function and service definitions.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$functions = [
'mod_adaptivequiz_search_question_banks' => [
'classname' => '\mod_adaptivequiz\external\search_question_banks',
'description' => 'Get a list of filtered question banks.',
'type' => 'read',
'ajax' => 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/>.
/**
* Adaptive testing uninstall script
*
* This module was created as a collaborative effort between Middlebury College
* and Remote Learner.
*
* @package mod_adaptivequiz
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Custom uninstallation procedure
* @return bool: only returns truel
*/
function xmldb_adaptivequiz_uninstall() {
return 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/>.
/**
* Contains function with the definition of upgrade steps for the plugin.
*
* @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
*/
/**
* Defines upgrade steps for the plugin.
*
* @param mixed $oldversion
*/
function xmldb_adaptivequiz_upgrade($oldversion) {
global $CFG, $DB;
$dbman = $DB->get_manager();
if ($oldversion < 2014020400) {
// Define field grademethod.
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('grademethod', XMLDB_TYPE_INTEGER, '3', null, XMLDB_NOTNULL, null, 1, 'startinglevel');
// Conditionally add field grademethod.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Quiz savepoint reached.
upgrade_mod_savepoint(true, 2014020400, 'adaptivequiz');
}
if ($oldversion < 2022012600) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('showabilitymeasure', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, false, '0',
'attemptfeedbackformat');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2022012600, 'adaptivequiz');
}
if ($oldversion < 2022092600) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('completionattemptcompleted', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, false, 0);
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2022092600, 'adaptivequiz');
}
if ($oldversion < 2022110200) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('showattemptprogress', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0,
'showabilitymeasure');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2022110200, 'adaptivequiz');
}
if ($oldversion < 2025092700) {
$table = new xmldb_table('adaptivequiz');
// The default value is set to '-1' to indicate the transition state of the setting for the existing instances.
$field = new xmldb_field('attemptfeedbackenable', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL,
null, '-1', 'attemptfeedbackformat');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2025092700, 'adaptivequiz');
}
if ($oldversion < 2025092701) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('showabilitymeasurefeedback', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL,
null, '0', 'showabilitymeasure');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$field = new xmldb_field('showabilitymeasuresummary', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL,
null, '0', 'showabilitymeasurefeedback');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2025092701, 'adaptivequiz');
}
if ($oldversion < 2025092702) {
// Both new fields will acquire their values from the original 'showabilitymeasure'.
$sql = "UPDATE {adaptivequiz}
SET showabilitymeasurefeedback = showabilitymeasure,
showabilitymeasuresummary = showabilitymeasure";
$DB->execute($sql);
upgrade_mod_savepoint(true, 2025092702, 'adaptivequiz');
}
if ($oldversion < 2026030100) {
$table = new xmldb_table('adaptivequiz_qbank');
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE);
$table->add_field('adaptivequizid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL);
$table->add_field('qbankid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL);
$table->add_field('qbankcontextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL);
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('adaptivequizid', XMLDB_KEY_FOREIGN, ['adaptivequizid'], 'adaptivequiz', ['id']);
$table->add_key('qbankid', XMLDB_KEY_FOREIGN, ['qbankid'], 'qbank', ['id']);
$table->add_key('qbankcontextid', XMLDB_KEY_FOREIGN, ['qbankcontextid'], 'context', ['id']);
$table->add_index('adaptivequiz-qbank', XMLDB_INDEX_UNIQUE, ['adaptivequizid', 'qbankid', 'qbankcontextid']);
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
upgrade_mod_savepoint(true, 2026030100, 'adaptivequiz');
}
if ($oldversion < 2026030101) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('debuginfoenable', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL,
null, '0', 'completionattemptcompleted');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026030101, 'adaptivequiz');
}
// KNIGHT Feature 1: acceptance threshold for partial-credit questions.
if ($oldversion < 2026062500) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('acceptancethreshold', XMLDB_TYPE_NUMBER, '4, 2', null, XMLDB_NOTNULL, null, '0', 'lowestlevel');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026062500, 'adaptivequiz');
}
// KNIGHT Feature 1: unify the acceptance threshold default across install histories. A direct
// upgrade from the 4.4 KNIGHT line keeps the field's old default of 0.5 (the add step above is a
// no-op when the field already exists), whereas a fresh 5.x install defaults to 0. Force the
// canonical default so new activities behave identically regardless of history. Stored values
// of existing activities are not affected.
if ($oldversion < 2026062503) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('acceptancethreshold', XMLDB_TYPE_NUMBER, '4, 2', null, XMLDB_NOTNULL, null, '0', 'lowestlevel');
if ($dbman->field_exists($table, $field)) {
$dbman->change_field_default($table, $field);
}
upgrade_mod_savepoint(true, 2026062503, 'adaptivequiz');
}
// KNIGHT: let non-editing teachers view participants' results. This matches the new access.php
// archetype default, but archetype changes are not re-applied to existing roles on upgrade, so
// grant the capability to the existing 'teacher' roles explicitly (without overwriting any
// customisation an admin may already have made).
if ($oldversion < 2026062504) {
$systemcontext = context_system::instance();
foreach (get_archetype_roles('teacher') as $role) {
assign_capability('mod/adaptivequiz:viewreport', CAP_ALLOW, $role->id, $systemcontext->id, false);
}
upgrade_mod_savepoint(true, 2026062504, 'adaptivequiz');
}
// KNIGHT Feature 2: setting that lets students review their own attempts.
if ($oldversion < 2026062506) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('showownattemptresult', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026062506, 'adaptivequiz');
}
// KNIGHT Feature 3: question-analysis reminder. questionschecked stores the completed-attempts count
// recorded at the last review (0 = never reviewed); questionchecktrigger is the interval after which a
// review is advised. Storing a count rather than a boolean keeps the reminder from being re-armed by
// merely reloading a page. The KNIGHT 4.x line already has questionschecked as a boolean (int/1); widen
// it in place to hold the count. A fresh 5.x install does not have the field yet, so add it.
if ($oldversion < 2026062509) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field('questionschecked', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
if ($dbman->field_exists($table, $field)) {
$dbman->change_field_precision($table, $field);
} else {
$dbman->add_field($table, $field);
}
$field = new xmldb_field('questionchecktrigger', XMLDB_TYPE_INTEGER, '3', null, XMLDB_NOTNULL, null, '0');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026062509, 'adaptivequiz');
}
// KNIGHT Feature 4: per-level competency descriptions that feed the attempt feedback page.
if ($oldversion < 2026062510) {
$table = new xmldb_table('adaptivequiz_competencydesc');
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE);
$table->add_field('adaptivequizid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL);
$table->add_field('level', XMLDB_TYPE_INTEGER, '5', null, XMLDB_NOTNULL);
$table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null);
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('adaptivequiz_fk', XMLDB_KEY_FOREIGN, ['adaptivequizid'], 'adaptivequiz', ['id']);
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
upgrade_mod_savepoint(true, 2026062510, 'adaptivequiz');
}
// KNIGHT Feature 4: explicit on/off switch for the competency feedback.
if ($oldversion < 2026062511) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field(
'competencyfeedbackenable',
XMLDB_TYPE_INTEGER,
'1',
null,
XMLDB_NOTNULL,
null,
'0',
'questionchecktrigger'
);
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026062511, 'adaptivequiz');
}
// KNIGHT Feature 4: preserve behaviour from the KNIGHT 4.x line, where the competency feedback was
// active whenever descriptions existed. Enable it for existing activities that already have
// descriptions and a valid level range (positive, ascending, at most 10 levels - the level cap at
// this version). Activities whose range is too large stay disabled until the range is narrowed.
if ($oldversion < 2026062512) {
$sql = "UPDATE {adaptivequiz}
SET competencyfeedbackenable = 1
WHERE lowestlevel >= 1
AND highestlevel > lowestlevel
AND (highestlevel - lowestlevel + 1) <= 10
AND id IN (SELECT DISTINCT adaptivequizid FROM {adaptivequiz_competencydesc})";
$DB->execute($sql);
upgrade_mod_savepoint(true, 2026062512, 'adaptivequiz');
}
// KNIGHT Feature 5: show the current question's difficulty level to students during the attempt.
if ($oldversion < 2026062516) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field(
'showquestiondifficultylevel',
XMLDB_TYPE_INTEGER,
'1',
null,
XMLDB_NOTNULL,
null,
'0',
'competencyfeedbackenable'
);
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026062516, 'adaptivequiz');
}
// KNIGHT Feature 6: per-question immediate feedback during the attempt (+ optional solution reveal).
if ($oldversion < 2026062517) {
$table = new xmldb_table('adaptivequiz');
$field = new xmldb_field(
'immediatefeedback',
XMLDB_TYPE_INTEGER,
'1',
null,
XMLDB_NOTNULL,
null,
'0',
'showquestiondifficultylevel'
);
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$field = new xmldb_field(
'immediatefeedbackshowsolution',
XMLDB_TYPE_INTEGER,
'1',
null,
XMLDB_NOTNULL,
null,
'0',
'immediatefeedback'
);
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026062517, 'adaptivequiz');
}
return 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/>.
/**
* Confirmation page to remove student attempts.
*
* @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');
$attemptid = required_param('attempt', PARAM_INT);
$confirm = optional_param('confirm', 0, PARAM_INT);
$attempt = $DB->get_record('adaptivequiz_attempt', ['id' => $attemptid], '*', MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attempt->instance], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $adaptivequiz->course, false, MUST_EXIST);
$course = $DB->get_record('course', ['id' => $adaptivequiz->course], '*', MUST_EXIST);
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
// KNIGHT: deleting an attempt is a managing action, not a viewing one. Require the manage capability
// so a view-only role (e.g. non-editing teacher with viewreport) cannot delete attempts.
require_capability('mod/adaptivequiz:manage', $context);
$user = $DB->get_record('user', ['id' => $attempt->userid], '*', MUST_EXIST);
$PAGE->set_url('/mod/adaptivequiz/delattempt.php', ['attempt' => $attempt->id]);
$PAGE->set_title(format_string($adaptivequiz->name));
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$returnurl = new moodle_url('/mod/adaptivequiz/viewattemptreport.php', ['cmid' => $cm->id, 'userid' => $user->id]);
$a = new stdClass();
$a->name = fullname($user);
$a->timecompleted = userdate($attempt->timemodified);
if ($confirm) {
question_engine::delete_questions_usage_by_activity($attempt->uniqueid);
$DB->delete_records('adaptivequiz_attempt', ['id' => $attempt->id]);
adaptivequiz_update_grades($adaptivequiz, $user->id);
$message = get_string('attemptdeleted', 'adaptivequiz', $a);
redirect($returnurl, $message, 4);
}
$message = get_string('confirmdeleteattempt', 'adaptivequiz', $a);
$confirm = new moodle_url('/mod/adaptivequiz/delattempt.php', ['attempt' => $attempt->id, 'confirm' => 1]);
echo $OUTPUT->header();
echo $OUTPUT->confirm($message, $confirm, $returnurl);
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/>.
/**
* KNIGHT (Feature 4): competency feedback page for a completed attempt.
*
* @package mod_adaptivequiz
* @copyright 2026 KNIGHT, Hochschule fuer Technik Stuttgart
* @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');
use mod_adaptivequiz\local\attempt\attempt_state;
use mod_adaptivequiz\output\competency_feedback;
$id = required_param('id', PARAM_INT); // Course module id.
$attemptid = required_param('attempt', PARAM_INT); // Attempt id.
$cm = get_coursemodule_from_id('adaptivequiz', $id, 0, false, MUST_EXIST);
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $cm->instance], '*', MUST_EXIST);
$attempt = $DB->get_record('adaptivequiz_attempt', ['id' => $attemptid], '*', MUST_EXIST);
if ($attempt->instance != $adaptivequiz->id) {
throw new moodle_exception('invalidattemptid', 'adaptivequiz');
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
// Feedback is available only when the feature is enabled and the attempt is completed. Teachers who can
// view reports may see any attempt's feedback; a student may see only their own. (The original page let
// any user open any attempt.)
$iscompleted = strtolower($attempt->attemptstate) === strtolower(attempt_state::COMPLETED);
$canviewany = has_capability('mod/adaptivequiz:viewreport', $context);
$canview = !empty($adaptivequiz->competencyfeedbackenable) && $iscompleted
&& ($canviewany || $attempt->userid == $USER->id);
if (!$canview) {
throw new moodle_exception('nopermission', 'adaptivequiz');
}
$PAGE->set_url(new moodle_url('/mod/adaptivequiz/feedback.php', ['id' => $id, 'attempt' => $attemptid]));
$PAGE->set_title(format_string($adaptivequiz->name));
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$PAGE->add_body_class('limitedwidth');
echo $OUTPUT->header();
echo $OUTPUT->render(new competency_feedback($adaptivequiz, $attempt));
$backurl = new moodle_url('/mod/adaptivequiz/view.php', ['id' => $id, 'forceview' => 1]);
echo html_writer::div(
html_writer::link($backurl, get_string('backtousersallattemptspage', 'adaptivequiz'), ['class' => 'btn btn-primary mb-3']),
'text-center'
);
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/>.
/**
* Redirect users who clicked on a link in the gradebook.
*
* @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
*/
require_once(dirname(__FILE__).'/../../config.php');
use mod_adaptivequiz\attempt as read_attempt;
use mod_adaptivequiz\local\attempt as attempt_entity;
$id = required_param('id', PARAM_INT); // Course module ID.
$itemnumber = optional_param('itemnumber', 0, PARAM_INT); // Item number, may be != 0 for activities that allow more than one
// grade per user.
$userid = optional_param('userid', 0, PARAM_INT); // Graded user ID (optional).
if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) {
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);
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
if (!has_capability('mod/adaptivequiz:viewreport', $context) || $userid == $USER->id) {
redirect(new moodle_url('/mod/adaptivequiz/view.php', ['id' => $id]));
}
if ($userid) {
if (!attempt_entity::user_has_completed_on_quiz($adaptivequiz->id, $userid)) {
redirect(new moodle_url('/mod/adaptivequiz/view.php', ['id' => $id]));
}
/** @var read_attempt $attempt */
$attempt = call_user_func(function (stdClass $adaptivequiz, int $userid): read_attempt {
if ($adaptivequiz->grademethod == ADAPTIVEQUIZ_GRADEHIGHEST) {
return read_attempt::get_with_highest_score_for_user($userid, $adaptivequiz->id);
}
if ($adaptivequiz->grademethod == ADAPTIVEQUIZ_ATTEMPTFIRST) {
return read_attempt::get_first_for_user($userid, $adaptivequiz->id);
}
// The grading method is ADAPTIVEQUIZ_ATTEMPTLAST.
return read_attempt::get_last_for_user($userid, $adaptivequiz->id);
}, $adaptivequiz, $userid);
redirect(new moodle_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $attempt->get('id')]));
}
redirect(new moodle_url('/mod/adaptivequiz/view.php', ['id' => $id]));
<?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 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(dirname(__FILE__).'/../../config.php');
require_once($CFG->dirroot.'/mod/adaptivequiz/lib.php');
use mod_adaptivequiz\event\course_module_instance_list_viewed;
$id = required_param('id', PARAM_INT); // Course.
$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
require_course_login($course);
course_module_instance_list_viewed::create_from_course($course)->trigger();
$coursecontext = context_course::instance($course->id);
$PAGE->set_url('/mod/adaptivequiz/index.php', array('id' => $id));
$PAGE->set_title(format_string($course->fullname));
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($coursecontext);
echo $OUTPUT->header();
if (!$adaptivequizinstances = get_all_instances_in_course('adaptivequiz', $course)) {
notice(get_string('nonewmodules', 'adaptivequiz'), new moodle_url('/course/view.php', array('id' => $course->id)));
}
$table = new html_table();
if ($course->format == 'weeks') {
$table->head = array(get_string('week'), get_string('name'));
$table->align = array('center', 'left');
} else if ($course->format == 'topics') {
$table->head = array(get_string('topic'), get_string('name'));
$table->align = array('center', 'left', 'left', 'left');
} else {
$table->head = array(get_string('name'));
$table->align = array('left', 'left', 'left');
}
foreach ($adaptivequizinstances as $adaptivequizinstance) {
if (!$adaptivequizinstance->visible) {
$link = html_writer::link(
new moodle_url('/mod/adaptivequiz/view.php', array('id' => $adaptivequizinstance->coursemodule)),
format_string($adaptivequizinstance->name, true),
array('class' => 'dimmed'));
} else {
$link = html_writer::link(
new moodle_url('/mod/adaptivequiz/view.php', array('id' => $adaptivequizinstance->coursemodule)),
format_string($adaptivequizinstance->name, true));
}
if ($course->format == 'weeks' || $course->format == 'topics') {
$table->data[] = array($adaptivequizinstance->section, $link);
} else {
$table->data[] = array($link);
}
}
echo $OUTPUT->heading(get_string('modulenameplural', 'adaptivequiz'), 2);
echo html_writer::table($table);
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/>.
/**
* Page to manage item bank for an adaptive quiz instance.
*
* @package mod_adaptivequiz
* @copyright 2026 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
use core\output\notification;
use core_question\local\bank\question_bank_helper;
use mod_adaptivequiz\item_bank;
$id = required_param('id', PARAM_INT);
$unassignqbankid = optional_param('unassignqbank', 0, PARAM_INT);
$unassignqcatid = optional_param('unassignqcat', 0, PARAM_INT);
$confirm = optional_param('confirm', 0, PARAM_BOOL);
[$course, $cm] = get_course_and_cm_from_cmid($id, 'adaptivequiz');
if ($unassignqbankid && $confirm) {
item_bank::unassign_qbank_from_adaptivequiz($cm->instance, $unassignqbankid);
redirect(
new moodle_url('/mod/adaptivequiz/itembank.php', ['id' => $id]),
get_string('itembankqbankunassignsuccess', 'adaptivequiz')
);
}
if ($unassignqcatid && $confirm) {
item_bank::unassign_question_category_from_adaptivequiz($cm->instance, $unassignqcatid);
redirect(
new moodle_url('/mod/adaptivequiz/itembank.php', ['id' => $id]),
get_string('itembankunlinksuccess', 'adaptivequiz')
);
}
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $cm->instance], '*', MUST_EXIST);
$context = context_module::instance($cm->id);
$PAGE->set_context($context);
$PAGE->set_url('/mod/adaptivequiz/itembank.php', ['id' => $id]);
$title = get_string('itembankpagetitle', 'adaptivequiz', format_string($adaptivequiz->name));
$PAGE->set_title($title);
require_login($course, true, $cm);
// KNIGHT: the item bank is a management area; require the manage capability so view-only roles
// (e.g. a non-editing teacher with viewreport) cannot reach it or its add/remove actions.
require_capability('mod/adaptivequiz:manage', $context);
/** @var mod_adaptivequiz_renderer $renderer */
$renderer = $PAGE->get_renderer('mod_adaptivequiz');
$qbankmigrated = question_bank_helper::has_bank_migration_task_completed_successfully();
if (!$qbankmigrated) {
$defaultqbankmod = question_bank_helper::get_default_question_bank_activity_name();
echo $renderer->header();
echo $renderer->notification(
message: get_string('transfernotfinished', 'mod_' . $defaultqbankmod),
type: notification::NOTIFY_WARNING,
closebutton: false
);
echo $renderer->footer();
exit;
}
if ($unassignqbankid || $unassignqcatid) {
$confirmurl = clone($PAGE->url);
$confirmurl->param('confirm', 1);
if ($unassignqbankid) {
$qbankcm = get_coursemodule_from_instance('qbank', $unassignqbankid, 0, false, MUST_EXIST);
$qbankcminfo = cm_info::create($qbankcm);
$confirmmessage = get_string('itembankqbankunassignconfirm', 'adaptivequiz', $qbankcminfo->get_formatted_name());
$confirmurl->param('unassignqbank', $unassignqbankid);
}
if ($unassignqcatid) {
$qcategory = $DB->get_record('question_categories', ['id' => $unassignqcatid], '*', MUST_EXIST);
$confirmmessage = get_string('itembankqcatunlinkconfirm', 'adaptivequiz', $qcategory->name);
$confirmurl->param('unassignqcat', $unassignqcatid);
}
echo $renderer->header();
echo $renderer->confirm($confirmmessage, $confirmurl, $PAGE->url);
echo $renderer->footer();
die;
}
$PAGE->set_heading($title);
$PAGE->add_body_class('limitedwidth');
echo $renderer->header();
echo $renderer->item_bank_page($adaptivequiz, $cm);
echo $renderer->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/>.
/**
* Strings for the German language (KNIGHT additions).
*
* @package mod_adaptivequiz
* @copyright 2026 KNIGHT, Hochschule fuer Technik Stuttgart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
// KNIGHT (UI): settings-form section header grouping the settings that show information to students.
$string['showinfoheader'] = 'Für Teilnehmer/innen sichtbare Informationen';
// KNIGHT (Feature 1): acceptance threshold for counting partial-credit answers as correct.
$string['acceptancethreshold'] = 'Akzeptanzschwelle für Teilpunkte';
$string['acceptancethreshold_help'] = 'Geben Sie einen Wert zwischen 0 und 1 ein, um festzulegen, ab welchem Punkteanteil eine Frage als richtig beantwortet gilt. Ein Wert von 0 akzeptiert jede Teilpunktzahl als korrekt; ein Wert von 1 verlangt eine vollständig richtige Antwort. So lassen sich bestehende Teilpunkte-Fragenpools für adaptives Testen weiterverwenden. Der Wert wird auf zwei Nachkommastellen gespeichert (z. B. 0,50).';
$string['formacceptanceleveloutofbounds'] = 'Die Akzeptanzschwelle muss ein Wert zwischen 0 und 1 sein.';
$string['formacceptancethresholdprecision'] = 'Die Akzeptanzschwelle unterstützt höchstens zwei Nachkommastellen.';
// KNIGHT (Feature 2): let students review their own completed attempts.
$string['showownattemptresult'] = 'Versuchseinsicht für Teilnehmer/innen anzeigen';
$string['showownattemptresult_help'] = 'Wenn diese Einstellung aktiviert ist, können Teilnehmer/innen ihre eigenen Versuche (die Fragedetails) nach Abschluss aus der Versuchsübersicht einsehen.';
$string['attemptuserprevious'] = 'Ihr vorheriger Versuch';
// KNIGHT (Feature 3): remind teachers to run the question analysis after further attempts.
$string['questionchecktrigger'] = 'Auslöser für Benachrichtigung zur Fragenanalyse';
$string['questionchecktrigger_help'] = 'Anzahl der abgeschlossenen Versuche, nach der eine Analyse der Fragen empfohlen wird. Der Standardwert 0 bedeutet, dass keine Benachrichtigung zur Überarbeitung erfolgt.';
$string['questioncheckmessage'] = 'Es wurden ausreichend Versuche durchgeführt, um eine Überarbeitung der Frageeinstufung zu ermöglichen. Sobald Sie den Reiter „Fragenanalyse“ besucht haben, wird diese Benachrichtigung ausgeblendet.';
// KNIGHT (Feature 4): result-dependent competency feedback.
$string['competencyfeedbackenable'] = 'Kompetenzstufen-Feedback aktivieren';
$string['competencyfeedbackenable_help'] = 'Wenn aktiviert, erhalten Teilnehmer/innen nach einem abgeschlossenen Versuch ein ergebnisabhängiges Kompetenz-Feedback, das aus den Beschreibungen je Stufe (unten) erstellt wird. Es kann nur für höchstens 10 Schwierigkeitsstufen aktiviert werden. Beachten Sie, dass dieses Feedback den Teilnehmer/innen ihr geschätztes Fähigkeitsniveau offenlegt.';
$string['competencyfeedbackrangeunsuitable'] = 'Kompetenz-Feedback benötigt einen positiven, aufsteigenden Schwierigkeitsbereich von höchstens {$a->max} Stufen. Der Bereich {$a->lowest} bis {$a->highest} erfüllt das nicht und kann daher nicht aktiviert werden – passen Sie die niedrigste/höchste Stufe an.';
$string['competencyfeedbacknolevelrange'] = 'Legen Sie den Schwierigkeitsbereich (niedrigste und höchste Stufe) fest und speichern Sie die Aktivität, bevor Sie das Kompetenz-Feedback aktivieren.';
$string['competencydescriptions'] = 'Kompetenz-Beschreibungen';
$string['competencydesc'] = 'Kompetenzbeschreibung für Stufe {$a}';
$string['competencydescinfo'] = 'Für jede Schwierigkeitsstufe zwischen der niedrigsten und höchsten Stufe wird ein Beschreibungsfeld angezeigt. Speichern Sie die Aktivität nach einer Änderung des Bereichs, um die hier angezeigten Felder zu aktualisieren.';
$string['detailedfeedback'] = 'Ausführliche Rückmeldung';
$string['invalidattemptid'] = 'Ungültige Versuchs-ID';
$string['backtousersallattemptspage'] = '&laquo; Zurück zur Versuchsübersicht';
$string['adaptivequizfeedbackheading'] = 'Ergebnisrückmeldung';
$string['feedbackheading'] = 'Feedback - Wo stehe ich aktuell?';
$string['feedforwardheading'] = 'Feedforward - Wie geht es jetzt weiter?';
$string['feedupheading'] = 'Feed-up - Was ist mein nächstes Ziel?';
$string['passinggradeheading'] = 'Bestehensgrenze';
$string['feedback_feedback'] = 'Sie haben bereits gute Kompetenzen in den folgenden Bereichen gezeigt';
$string['feedback_feedforward'] = 'Sie sind dabei, Ihre Fähigkeiten in den folgenden Bereichen weiter zu verfeinern';
$string['feedback_feedup'] = 'Ihr nächster Schritt sollte darin bestehen, die folgenden Fähigkeiten gezielt weiterzuentwickeln';
$string['feedback_encouragement'] = 'Sie sind auf einem guten Weg. Versuchen Sie, in einem weiteren Durchgang ihr Ergebnis zu verbessern!';
$string['feedback_summary'] = 'Der Algorithmus hat (mit einer gewissen Schätzunsicherheit) ermittelt, dass Ihre Leistung in diesem Test auf einer Skala von {$a->lowest} (Grundkenntnisse) bis {$a->highest} (Fortgeschritten) einem Fähigkeitsniveau von etwa {$a->ability} entspricht. Diese Einschätzung zeigt Ihre bereits vorhandenen Kompetenzen und bietet eine gute Grundlage für weiteres Wachstum.';
$string['feedback_algorithm_explanation'] = 'Anhand Ihres Antwortverhaltens innerhalb eines Testlaufs lernt der Algorithmus Schritt für Schritt, Ihr Potenzial für die Beantwortung künftiger Fragen besser einzuschätzen. Ein attestiertes Fähigkeitsniveau von ungefähr {$a->ability} bedeutet, dass Ihre Chancen, Fragen des Schwierigkeitsgrades {$a->floor} richtig zu lösen, höher als 50/50 sind. Bei Fragen der Schwierigkeitsstufe {$a->ceil} ist die Wahrscheinlichkeit einer richtigen Antwort jedoch geringer.';
$string['feedback_passingrequirement'] = 'Um den Kurs erfolgreich zu absolvieren, wird erwartet, dass Teilnehmer/innen nicht nur mit einem bestimmten Schwierigkeitsniveau gleichauf sind, sondern dieses auch weitgehend beherrschen. Die Mindestanforderung für das Bestehen des Tests ist daher ein geschätztes Fähigkeitsmaß von {$a}.';
$string['feedback_passing_success'] = 'Mit einem geschätzten Fähigkeitsniveau von {$a} haben Sie diesen Versuch erfolgreich bestanden. Herzlichen Glückwunsch!';
$string['feedback_passing_fail'] = 'Mit einem geschätzten Fähigkeitsniveau von {$a} haben Sie diesen Versuch leider noch nicht bestanden.';
// KNIGHT (Feature 5): show the question difficulty level to students during the attempt.
$string['modformshowquestiondifficultylevel'] = 'Den Teilnehmer/innen den Schwierigkeitsgrad der Fragen anzeigen';
$string['modformshowquestiondifficultylevel_help'] = 'Manchmal kann es sinnvoll sein, den Teilnehmer/innen die Möglichkeit zu geben, den Schwierigkeitsgrad der Fragen während des Quiz einzusehen.';
$string['attemptquestion_difficulty_level'] = 'Schwierigkeitsstufe';
// KNIGHT (Feature 6): immediate per-question feedback during the attempt.
$string['immediatefeedback'] = 'Sofort-Feedback';
$string['immediatefeedback_help'] = 'Wenn aktiviert, sehen Teilnehmer/innen direkt nach dem Beantworten, ob ihre Antwort richtig war – zusammen mit einem für die Frage hinterlegten Feedback. Die richtige Antwort wird nur angezeigt, wenn Sie zusätzlich „Musterlösung anzeigen“ aktivieren.';
$string['immediatefeedbackshowsolution'] = 'Musterlösung anzeigen';
$string['immediatefeedbackshowsolution_help'] = 'Zeigt nach jeder Frage zusätzlich die richtige Antwort. Mit Bedacht einsetzen: Adaptive Quizze verwenden Fragen über Versuche und Teilnehmer/innen hinweg wieder, daher kann das Offenlegen von Lösungen den Fragenpool entwerten und Fähigkeitsschätzungen verzerren.';
$string['immediatefeedbackheading'] = 'Rückmeldung zu Ihrer vorherigen Antwort';
<?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/>.
/**
* Strings for the English language.
*
* @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
*/
defined('MOODLE_INTERNAL') || die();
$string['abilityestimated'] = 'Estimated ability';
$string['abilityestimated_help'] = 'The estimated ability of a test-taker aligns with the question difficulty at which the test-taker has a 50% probability of answering the question correctly. To identify the performance level, match the ability value with the questions level range (see the range after the \'/\' symbol).';
$string['attemptdebuginfocaption'] = 'Attempt debugging info';
$string['attemptfeedbackdefaulttext'] = 'You\'ve finished the attempt, thank you for taking the quiz!';
$string['attemptquestion_ability'] = 'Ability Measure';
$string['attemptquestionsprogress'] = 'Questions progress: {$a}';
$string['attemptquestionsprogress_help'] = 'The maximum number of questions displayed here is not necessarily the number of questions you have to take during the quiz. It is the MAXIMUM POSSIBLE number of questions you might take, the quiz may finish earlier if the ability measure is sufficiently defined.';
$string['attempt_summary'] = 'Attempt Summary';
$string['attemptsusernoprevious'] = 'You haven\'t attempted this quiz yet.';
$string['attemptsuserprevious'] = 'Your previous attempts';
$string['attemptnofirstquestion'] = 'Sorry, but couldn\'t define the first question to start the attempt, the quiz is possibly misconfigured. ';
$string['completionattemptcompletedcminfo'] = 'Complete an attempt';
$string['completionattemptcompletedform'] = 'Student must have al least one completed attempt on this activity';
$string['debuginfoenable'] = 'Display debugging info during attempts';
$string['debuginfoenable_help'] = 'This is intended for developers only and will not add any valuable output for students.';
$string['eventattemptcompleted'] = 'Attempt completed';
$string['modformshowattemptprogress'] = 'Show quiz progress to students';
$string['modformshowattemptprogress_help'] = 'When selected, during attempt, a student will see a progress bar depicting how many questions are answered out of the maximum number.';
$string['noquestionbanks'] = 'No question banks assigned yet.';
$string['placeholdercopied'] = 'Copied to clipboard';
$string['showabilitymeasurefeedback'] = 'Show ability measure to students on the feedback page';
$string['showabilitymeasurefeedback_help'] = 'With this setting enabled, a student may see the ability estimation as a standard widget on the attempt finished page. Please note, this does not interfere with displaying the measure value inside the custom feedback text when configured using placeholders.';
$string['showabilitymeasuresummary'] = 'Show ability measure to students in their attempts overview';
$string['showabilitymeasuresummary_help'] = 'With this setting enabled, a student may see the ability estimation in their attempts overview.';
$string['reportanswersdistributionchartdisplaystacked'] = 'Display bars stacked';
$string['reportanswersdistributionchartnumrightlabel'] = 'Number of correct answers';
$string['reportanswersdistributionchartnumwronglabel'] = 'Number of wrong answers';
$string['reportanswersdistributionchartxaxislabel'] = 'Question difficulty';
$string['reportanswersdistributionchartyaxislabel'] = 'Number of answers';
$string['reportattemptadmanswerright'] = 'C';
$string['reportattemptadmanswerwrong'] = 'W';
$string['reportattemptadmchartadmdifflabel'] = 'Administered Difficulty';
$string['reportattemptadmcharttargetdifflabel'] = 'Target Difficulty';
$string['reportattemptanswerdistributiontab'] = 'Answer Distribution';
$string['reportattemptgraphtab'] = 'Attempt Graph';
$string['reportattemptgraphtabletitle'] = 'Table View of Attempt Graph';
$string['reportattemptquestionsdetailstab'] = 'Questions Details';
$string['reportattemptreviewpageheading'] = '{$a->quizname} - reviewing attempt by {$a->fullname} submitted on {$a->finished}';
$string['reportattemptsbothenrolledandnotenrolled'] = 'all users who ever made attempts';
$string['reportattemptsdownloadfilename'] = '{$a}_attempts_report';
$string['reportattemptsenrolledwithattempts'] = 'participants who made attempts';
$string['reportattemptsenrolledwithnoattempts'] = 'participants without attempts made';
$string['reportattemptsfilterformsubmit'] = 'Filter';
$string['reportattemptsfilterincludeinactiveenrolments'] = 'Include users with inactive enrolments';
$string['reportattemptsfilterincludeinactiveenrolments_help'] = 'Whether include users with suspended enrolments.';
$string['reportattemptsfilterusers'] = 'Show';
$string['reportattemptsfilterformheader'] = 'Filtering';
$string['reportattemptsnotenrolled'] = 'not enrolled users who made attempts';
$string['reportattemptspersistentfilter'] = 'Persistent filter';
$string['reportattemptspersistentfilter_help'] = 'When checked, the filter settings below will be stored when submitted, and then applied each time you visit the report page.';
$string['reportattemptsprefsformheader'] = 'Report Preferences';
$string['reportattemptsprefsformsubmit'] = 'Apply';
$string['reportattemptsresetfilter'] = 'Reset filter';
$string['reportattemptsshowinitialbars'] = 'Show initials bar';
$string['reportattemptsusersperpage'] = 'Number of users displayed:';
$string['reportattemptsummarytab'] = 'Attempt Summary';
$string['reportindividualuserattemptpageheading'] = '{$a->quizname} - individual user attempts report for {$a->username}';
$string['reportuserattemptstitleshort'] = '{$a}\'s attempts';
$string['reportquestionanalysispageheading'] = '{$a} - questions report';
$string['modulenameplural'] = 'Adaptive Quiz';
$string['modulename'] = 'Adaptive Quiz';
$string['modulename_help'] = 'The Adaptive Quiz activity enables a teacher to create quizes that efficiently measure the takers\' abilities. Adaptive quizes are comprised of questions selected from the question bank that are tagged with a score of their difficulty. The questions are chosen to match the estimated ability level of the current test-taker. If the test-taker succeeds on a question, a more challenging question is presented next. If the test-taker answers a question incorrectly, a less-challenging question is presented next. This technique will develop into a sequence of questions converging on the test-taker\'s effective ability level. The quiz stops when the test-taker\'s ability is determined to the required accuracy.
This activity is best suited to determining an ability measure along a unidimensional scale. While the scale can be very broad, the questions must all provide a measure of ability or aptitude on the same scale. In a placement test for example, questions low on the scale that novices are able to answer correctly should also be answerable by experts, while questions higher on the scale should only be answerable by experts or a lucky guess. Questions that do not discriminate between takers of different abilities on will make the test ineffective and may provide inconclusive results.
Questions used in the Adaptive Quiz must
* be automatically scored as correct/incorrect
* be tagged with their difficulty using \'adpq_\' followed by a positive integer that is within the range for the quiz
The Adaptive Quiz can be configured to
* define the range of question-difficulties/user-abilities to be measured. 1-10, 1-16, and 1-100 are examples of valid ranges.
* define the precision required before the quiz is stopped. Often an error of 5% in the ability measure is an appropriate stopping rule
* require a minimum number of questions to be answered
* require a maximum number of questions that can be answered
This description and the testing process in this activity are based on <a href="http://www.rasch.org/memo69.pdf">Computer-Adaptive Testing: A Methodology Whose Time Has Come</a> by John Michael Linacre, Ph.D. MESA Psychometric Laboratory - University of Chicago. MESA Memorandum No. 69.';
$string['pluginadministration'] = 'Adaptive Quiz';
$string['pluginname'] = 'Adaptive Quiz';
$string['nonewmodules'] = 'No Adaptive Quiz instances found';
$string['adaptivequizname'] = 'Name';
$string['adaptivequizname_help'] = 'Enter the name of the Adaptive Quiz instance';
$string['adaptivequiz:addinstance'] = 'Add a new adaptive quiz';
$string['adaptivequiz:manage'] = 'Manage item bank settings for adaptive quizzes';
$string['adaptivequiz:viewreport'] = 'View adaptive quiz reports';
$string['adaptivequiz:reviewattempts'] = 'Review adaptive quiz submissions';
$string['adaptivequiz:attempt'] = 'Attempt adaptive quiz';
$string['attemptsallowed'] = 'Attempts allowed';
$string['attemptsallowed_help'] = 'The number of times a student may attempt this activity';
$string['requirepassword'] = 'Required password';
$string['requirepassword_help'] = 'Students are required to enter a password before beginning their attempt';
$string['browsersecurity'] = 'Browser security';
$string['browsersecurity_help'] = 'If "Full screen pop-up with some JavaScript security" is selected the quiz will only start if the student has a JavaScript-enabled web-browser, the quiz appears in a full screen popup window that covers all the other windows and has no navigation controls and students are prevented, as far as is possible, from using facilities like copy and paste';
$string['minimumquestions'] = 'Minimum number of questions';
$string['minimumquestions_help'] = 'The minimum number of questions the student must attempt';
$string['maximumquestions'] = 'Maximum number of questions';
$string['maximumquestions_help'] = 'The maximum number of questions the student can attempt';
$string['startinglevel'] = 'Starting level of difficulty';
$string['startinglevel_help'] = 'The the student begins an attempt, the activity will randomly select a question matching the level of difficulty';
$string['lowestlevel'] = 'Lowest level of difficulty';
$string['lowestlevel_help'] = 'The lowest or least difficult level the assessment can select questions from. During an attempt the activity will not go beyond this level of difficulty';
$string['highestlevel'] = 'Highest level of difficulty';
$string['highestlevel_help'] = 'The highest or most difficult level the assessment can select questions from. During an attempt the activity will not go beyond this level of difficulty';
$string['formelementempty'] = 'Input a positive integer from 1 to 999';
$string['formelementnumeric'] = 'Input a numeric value from 1 to 999';
$string['formelementnegative'] = 'Input a positive number from 1 to 999';
$string['formminquestgreaterthan'] = 'Minimum number of questions must be less than maximum number of questions';
$string['formlowlevelgreaterthan'] = 'Lowest level must be less than highest level';
$string['formstartleveloutofbounds'] = 'The starting level must be a number that is inbetween the lowest and highest level';
$string['standarderror'] = 'Standard Error to stop';
$string['standarderror_help'] = 'When the amount of error in the measure of the user\'s ability drops below this amount, the quiz will stop. Tune this value from the default of 5% to require more or less precision in the ability measure';
$string['formelementdecimal'] = 'Input a decimal number. Maximum 10 digits long and maximum 5 digits to the right of the decimal point';
$string['attemptfeedback'] = 'Attempt feedback';
$string['attemptfeedback_help'] = 'The attempt feedback is displayed to the user once the attempt is finished.';
$string['attemptfeedbackenable'] = 'Enable custom attempt feedback';
$string['attemptfeedbackhdr'] = 'Attempt feedback';
$string['attemptfeedbackplaceholdersdesc'] = 'Available placeholders';
$string['attemptfeedbackplaceholdersdesc_help'] = 'Placeholders allow you to add a dynamic content, e.g. {{abilitymeasure}} placeholder will be replaced with the ability measure value in the feedback text displayed to user.';
$string['submitanswer'] = 'Submit answer';
$string['startattemptbtn'] = 'Start attempt';
$string['errorfetchingquest'] = 'Unable to fetch a question for level {$a->level}';
$string['leveloutofbounds'] = 'Requested level {$a->level} out of bounds for the attempt';
$string['errorattemptstate'] = 'There was an error in determining the state of the attempt';
$string['nopermission'] = 'You don\t have permission to view this resource';
$string['maxquestattempted'] = 'Maximum number of questions attempted';
$string['notyourattempt'] = 'This is not your attempt at the activity';
$string['noattemptsallowed'] = 'No more attempts allowed at this activity';
$string['updateattempterror'] = 'Error trying to update attempt record';
$string['numofattemptshdr'] = 'Number of attempts';
$string['standarderrorhdr'] = 'Standard error';
$string['errorlastattpquest'] = 'Error checking the response value for the last attempted question';
$string['errornumattpzero'] = 'Error with number of questions attempted equals zero, but user submitted an answer to previous question';
$string['errorsumrightwrong'] = 'Sum of correct and incorrect answers does not equal the total number of questions attempted';
$string['calcerrorwithinlimits'] = 'Calculated standard error of {$a->calerror} is within the limits imposed by the activity {$a->definederror}';
$string['missingtagprefix'] = 'Missing tag prefix';
$string['recentactquestionsattempted'] = 'Questions attempted: {$a}';
$string['recentattemptstate'] = 'State of attempt:';
$string['recentinprogress'] = 'In progress';
$string['notinprogress'] = 'This attempt is not in progress.';
$string['recentcomplete'] = 'Completed';
$string['functiondisabledbysecuremode'] = 'That functionality is currently disabled';
$string['enterrequiredpassword'] = 'Enter required password';
$string['requirepasswordmessage'] = 'To attempt this quiz you need to know the quiz password';
$string['wrongpassword'] = 'Password is incorrect';
$string['attemptstate'] = 'State of attempt';
$string['attemptstopcriteria'] = 'Reason for stopping attempt';
$string['questionsattempted'] = 'Sum of questions attempted';
$string['attemptfinishedtimestamp'] = 'Attempt finish time';
$string['reviewattempt'] = 'Review attempt';
$string['indvuserreport'] = 'Individual user attempts report for {$a}';
$string['activityreports'] = 'Attempts report';
$string['reviewattemptreport'] = 'Reviewing attempt by {$a->fullname} submitted on {$a->finished}';
$string['deleteattemp'] = 'Delete attempt';
$string['confirmdeleteattempt'] = 'Confirming the deletion of attempt from {$a->name} submitted on {$a->timecompleted}';
$string['attemptdeleted'] = 'Attempt deleted for {$a->name} submitted on {$a->timecompleted}';
$string['closeattempt'] = 'Close attempt';
$string['confirmcloseattempt'] = 'Are you sure that you wish to close and finalize this attempt of {$a->name}?';
$string['confirmcloseattemptstats'] = 'This attempt was started on {$a->started} and last updated on {$a->modified}.';
$string['confirmcloseattemptscore'] = '{$a->num_questions} questions were answered and the score so far is {$a->measure} {$a->standarderror}.';
$string['attemptclosedstatus'] = 'Manually closed by {$a->current_user_name} (user-id: {$a->current_user_id}) on {$a->now}.';
$string['attemptclosed'] = 'The attempt has been manually closed.';
$string['errorclosingattempt_alreadycomplete'] = 'This attempt is already complete, it cannot be manually closed.';
$string['formstderror'] = 'Must enter a percent less than 50 and greater than or equal to 0';
$string['score'] = 'Score';
$string['bestscore'] = 'Best Score';
$string['bestscorestderror'] = 'Standard Error';
$string['attempt_questiondetails'] = 'Question Details';
$string['attemptstarttime'] = 'Attempt start time';
$string['attempttotaltime'] = 'Total time (hh:mm:ss)';
$string['attempt_user'] = 'User';
$string['attempt_state'] = 'Attempt state';
$string['attemptquestion_level'] = 'Question Level';
$string['attemptquestion_rightwrong'] = 'Answer Correct/Wrong';
$string['attemptquestion_difficulty'] = 'Question Difficulty (logits)';
$string['attemptquestion_diffsum'] = 'Difficulty Sum';
$string['attemptquestion_abilitylogits'] = 'Measured Ability (logits)';
$string['attemptquestion_stderr'] = 'Standard Error (&plusmn;&nbsp;logits)';
$string['graphlegend_error'] = 'Standard Error';
$string['questionnumber'] = 'Question #';
$string['na'] = 'n/a';
$string['downloadcsv'] = 'Download CSV';
$string['grademethod'] = 'Grading method';
$string['gradehighest'] = 'Highest grade';
$string['attemptfirst'] = 'First attempt';
$string['attemptlast'] = 'Last attempt';
$string['grademethod_help'] = 'When multiple attempts are allowed, the following methods are available for calculating the final quiz grade:
* Highest grade of all attempts
* First attempt (all other attempts are ignored)
* Last attempt (all other attempts are ignored)';
$string['resetadaptivequizsall'] = 'Delete all Adaptive Quiz attempts';
$string['all_attempts_deleted'] = 'All Adaptive Quiz attempts were deleted';
$string['all_grades_removed'] = 'All Adaptive Quiz grades were removed';
$string['itembankaddqbankbn'] = 'Add selected question banks';
$string['itembankassignedqcats'] = 'Assigned single question categories';
$string['itembankassignedqbanks'] = 'Assigned question banks';
$string['itembankassignqbank'] = 'Assign new question bank';
$string['itembankassignqcat'] = 'Assign new question category';
$string['itembankbtn'] = 'Item bank';
$string['itembankeditqbanks'] = 'Edit question banks assignment';
$string['itembankinvalidlevel'] = 'No questions of this level found in the item bank';
$string['itembankitemadminvalidparams'] = 'Some item administration parameters for this module are invalid';
$string['itembankitemadmnoparams'] = 'Item administration parameters are not set for this module yet.';
$string['itembankitemadmnoqbanks'] = 'Item administration settings are available only when there are question banks assigned to the item bank.';
$string['itembankitemadmparams'] = 'Item administration parameters';
$string['itembankitemadmparamsedit'] = 'Edit item administration settings';
$string['itembankitemadmparamsedit'] = 'Edit parameters';
$string['itembanknewassignflash'] = 'New question banks have been assigned to the item bank.';
$string['itembanknoqcats'] = 'No single question categories assigned yet.';
$string['itembanknotconfiguredinfomanager'] = 'Item bank is not configured properly for this adaptive quiz module at this moment.<br />Your students will not be able to attempt the module until the item bank configuration is completed.';
$string['itembanknotconfiguredinfostudent'] = 'Item bank is not configured properly for this adaptive quiz module at this moment.<br />This is either a brand new module added to the course or the course manager is currently updating the item bank configuration.';
$string['itembankothercoursesqbanks'] = 'All other shared question banks';
$string['itembankpagetitle'] = '{$a}: item bank';
$string['itembankqbankunassignconfirm'] = 'Are you sure you want to unassign the question bank \'{$a}\' from the current adaptive quiz module? Please note, the quetion bank itself will not be affected.';
$string['itembankqcatunlinkconfirm'] = 'Are you sure you want to unassign the question category \'{$a}\' from the current adaptive quiz module? Please note, the quetion category itself will not be affected.';
// TODO: refactor to use a universal string.
$string['itembankqbankunassignsuccess'] = 'Question bank has been successfully unassigned.';
$string['itembankselectqbank'] = 'Select question bank';
$string['itembankthiscourseqbanks'] = 'Question banks in this course';
$string['itembankthiscourseqcats'] = 'Question categories in question banks in this course';
$string['itembankothercoursesqbanks'] = 'Question banks from other courses';
$string['itembankothercoursesqcats'] = 'Question categories from question banks from other courses';
$string['itembankunlinksuccess'] = 'Successfully unassigned from the item bank.';
// TODO: refactor to use a universal string.
$string['itembankunlinkqbank'] = 'Unassign from the item bank';
$string['itembankunlinkitem'] = 'Unassign from the item bank';
$string['questionanalysisbtn'] = 'Question Analysis';
$string['id'] = 'ID';
$string['name'] = 'Name';
$string['questions_report'] = 'Questions Report';
$string['question_report'] = 'Question Analysis';
$string['times_used_display_name'] = 'Times Used';
$string['percent_correct_display_name'] = '% Correct';
$string['discrimination_display_name'] = 'Discrimination';
$string['back_to_all_questions'] = '&laquo; Back to all questions';
$string['answers_display_name'] = 'Answers';
$string['answer'] = 'Answer';
$string['statistic'] = 'Statistic';
$string['value'] = 'Value';
$string['highlevelusers'] = 'Users above the question-level';
$string['midlevelusers'] = 'Users near the question-level';
$string['lowlevelusers'] = 'Users below the question-level';
$string['user'] = 'User';
$string['result'] = 'Result';
// KNIGHT additions, grouped by feature.
// KNIGHT (UI): settings-form section header grouping the settings that show information to students.
$string['showinfoheader'] = 'Information shown to students';
// KNIGHT (Feature 1): acceptance threshold for counting partial-credit answers as correct.
$string['acceptancethreshold'] = 'Acceptance threshold';
$string['acceptancethreshold_help'] = 'Enter a value between 0 and 1 to set the point fraction at which a question counts as answered correctly. A value of 0 accepts any partial credit as correct; a value of 1 requires a fully correct answer. This lets you reuse existing partial-credit question pools for adaptive testing. The value is stored to two decimal places (e.g. 0.50).';
$string['formacceptanceleveloutofbounds'] = 'The acceptance threshold must be a value between 0 and 1.';
$string['formacceptancethresholdprecision'] = 'The acceptance threshold supports at most two decimal places.';
// KNIGHT (Feature 2): let students review their own completed attempts.
$string['showownattemptresult'] = 'Show attempt review to students';
$string['showownattemptresult_help'] = 'With this setting enabled, a student may review their own attempts (the question details) from the attempts overview after finishing an attempt.';
$string['attemptuserprevious'] = 'Your previous attempt';
// KNIGHT (Feature 3): remind teachers to run the question analysis after further attempts.
$string['questionchecktrigger'] = 'Question analysis trigger';
$string['questionchecktrigger_help'] = 'Number of attempts that need to be completed before question analysis is advised. The default value of 0 means there will be no question revision alert.';
$string['questioncheckmessage'] = 'A sufficient number of attempts has now been conducted to allow revision of question categorisation. Once you have visited the Question Analysis tab, this notification will disappear.';
// KNIGHT (Feature 4): result-dependent competency feedback.
$string['competencyfeedbackenable'] = 'Enable competency level feedback';
$string['competencyfeedbackenable_help'] = 'When enabled, students receive result-dependent competency feedback after a completed attempt, built from the per-level descriptions below. It can only be enabled for at most 10 difficulty levels. Note that this feedback discloses the student\'s estimated ability to them.';
$string['competencyfeedbackrangeunsuitable'] = 'Competency feedback needs a positive, ascending difficulty level range of at most {$a->max} levels. The range {$a->lowest} to {$a->highest} does not qualify, so it cannot be enabled - adjust the lowest/highest level.';
$string['competencyfeedbacknolevelrange'] = 'Set the difficulty level range (lowest and highest level) and save the activity before enabling competency feedback.';
$string['competencydescriptions'] = 'Competency descriptions';
$string['competencydesc'] = 'Competency description for level {$a}';
$string['competencydescinfo'] = 'A description field is shown for each difficulty level between the lowest and highest level. After changing the level range, save the activity to update the fields shown here.';
$string['detailedfeedback'] = 'Detailed feedback';
$string['invalidattemptid'] = 'Invalid attempt id';
$string['backtousersallattemptspage'] = '&laquo; Back to all attempts';
$string['adaptivequizfeedbackheading'] = 'Adaptive Quiz Feedback';
$string['feedbackheading'] = 'Feedback – How am I doing?';
$string['feedforwardheading'] = 'Feedforward – Where to next?';
$string['feedupheading'] = 'Feed-up – What is my next goal?';
$string['passinggradeheading'] = 'Passing Grade';
$string['feedback_feedback'] = 'You have demonstrated some proficiency in the following skills';
$string['feedback_feedforward'] = 'You are in the process of honing the following skills';
$string['feedback_feedup'] = 'Your next step should be to focus on further developing the following skills';
$string['feedback_encouragement'] = 'Keep up the good work! Try improving your score with another attempt!';
$string['feedback_summary'] = 'The algorithm has estimated that your performance in this test reflects an ability level of approximately {$a->ability} on a scale from {$a->lowest} (basic level) to {$a->highest} (advanced). This assessment acknowledges your existing skills and provides a solid foundation for further growth.';
$string['feedback_algorithm_explanation'] = 'Based on your response pattern within a test run, the algorithm step by step learns to better assess your potential for answering future questions. The estimate of an ability level of approximately {$a->ability} means that your chances of answering questions with a difficulty level of {$a->floor} correctly are most of the time higher than 50%. However, the probability of a correct answer for questions with the difficulty level {$a->ceil} is still lower.';
$string['feedback_passingrequirement'] = 'In order to successfully complete the course we expect students not only to be on par with a certain level but rather to have mastered it. The minimum requirement for passing is therefore an estimated ability level of {$a}.';
$string['feedback_passing_success'] = 'With an estimated ability level of {$a}, you have passed successfully in this attempt. Congratulations!';
$string['feedback_passing_fail'] = 'With an estimated ability level of {$a}, you have not yet passed in this attempt.';
// KNIGHT (Feature 5): show the question difficulty level to students during the attempt.
$string['modformshowquestiondifficultylevel'] = 'Show question difficulty level to students';
$string['modformshowquestiondifficultylevel_help'] = 'Sometimes it may be useful to provide students with the ability to see the question difficulty level during the quiz.';
$string['attemptquestion_difficulty_level'] = 'Difficulty level';
// KNIGHT (Feature 6): immediate per-question feedback during the attempt.
$string['immediatefeedback'] = 'Immediate feedback';
$string['immediatefeedback_help'] = 'When enabled, students see whether each answer was correct, along with any feedback defined for the question, right after answering. The correct answer is only revealed if you also enable \'Reveal the correct answer\'.';
$string['immediatefeedbackshowsolution'] = 'Reveal the correct answer';
$string['immediatefeedbackshowsolution_help'] = 'Also shows the correct answer after each question. Use with care: adaptive quizzes reuse questions across attempts and students, so revealing solutions can compromise the question bank and bias ability estimates.';
$string['immediatefeedbackheading'] = 'Feedback on your previous answer';
<?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/>.
/**
* Strings for the French language.
*
* @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();
$string['modulenameplural'] = 'Questionnaires adaptatifs';
$string['modulename'] = 'Questionnaire adaptatif';
$string['modulename_help'] = 'L’application Questionnaire adaptative permet à un enseignant de créer des questionnaires mesurant efficacement les capacités des candidats.Les questionnaires adaptatifs sont composés de questions selectionnées dans la banque d\'items et répertoriées selon leur niveau de difficulté.Les items sont choisis pour correspondre au niveau de capacité estimé du candidat en cours. Si le candidat répond correctement à un item, un item plus difficile lui est ensuite proposé. Si le candidat répond de manière incorrecte à un item, un item moins difficile lui est ensuite proposé. Cette technique prendra la forme d’une série d’items convergeant vers le niveau réel du candidat. Le test s’arrête quand le niveau du ou des candidats est déterminé avec la précision souhaitée. Cette application est particulièrement adaptée pour déterminer un niveau sur une échelle de mesure unidimensionnelle. Bien que l’échelle de mesure puisse être très large, les items eux, doivent tous fournir un niveau ou une indication d’aptitude étalonnés sur la même échelle. Par exemple, pour un test de positionnement, les items placés bas dans l’échelle de mesure et auxquelles les débutants sont capables de répondre correctement, devraient recevoir une réponse correcte de la part des experts, à l’inverse les questions placées plus haut dans l’échelle de mesure ne devraient recevoir de réponse correcte que par les experts ou grâce à la chance. Les items ne discriminant pas les candidats de différents niveaux de capacité rendront le test inefficace et pourront mener à des résultats non concluants.
Les questions utilisées dans « questionnaire adaptatif » doivent :
* être automatiquement répertoriées comme étant correctes ou incorrectes.
* être répertoriées par difficulté en utilisant \'adpq_\' suivi d’un entier positif compris dans le classement prévu pour le questionnaire.
Le questionnaire adaptatif peut être configuré pour :
* définir lui-même le ratio item-difficulté / utilisateur-niveaux à mesurer. 1-10, 1-16 et 1-100 sont des exemples de classements valides.
* définir la précision requise avant que le questionnaire ne s’arrête. Pour établir un niveau, on considère souvent qu’une erreur de 5 % est une règle d’arrêt appropriée.
* définir un nombre minimum de questions nécessitant une réponse
* définir un nombre maximum de questions pouvant faire l’objet d’une réponse
La description et le processus de tests dans cette application sont basées sur <a href="http://www.rasch.org/memo69.pdf">Computer-Adaptive Testing: A Methodology Whose Time Has Come</a> by John Michael Linacre, Ph.D. MESA Psychometric Laboratory - University of Chicago. MESA Memorandum No. 69.';
$string['pluginadministration'] = 'questionnaire adaptatif';
$string['pluginname'] = 'questionnaire adaptatif';
$string['nonewmodules'] = 'Aucune occurrence de questionnaire adaptatif n’a été trouvée';
$string['adaptivequizname'] = 'Nom';
$string['adaptivequizname_help'] = 'Entrez le nom de l’occurence questionnaire adaptatif';
$string['adaptivequiz:addinstance'] = 'Ajoutez un nouveau questionnaire adaptatif';
$string['adaptivequiz:viewreport'] = 'Voir les rapports du questionnaire adaptatif';
$string['adaptivequiz:reviewattempts'] = 'Revoir les propositions du questionnaire adaptatif';
$string['adaptivequiz:attempt'] = 'Débuter un test adaptatif';
$string['attemptsallowed'] = 'Nombre de tentative autorisée';
$string['attemptsallowed_help'] = 'Nombre de tentative autorisée pour le candidat';
$string['requirepassword'] = 'Mot de passe requis';
$string['requirepassword_help'] = 'Les candidats doivent entrer un mot de passe pour ouvrir leur session';
$string['browsersecurity'] = 'Sécurité du navigateur';
$string['browsersecurity_help'] = 'Si « Full screen pop-up with some JavaScript security » est sélectionné, le questionnaire débutera seulement si le candidat dispose d’un navigateur-web autorisant Javascipt . Le questionnaire apparaît en plein écran dans une fenêtre contextuelle couvrant toutes les autres fenêtres et qui ne dispose d’aucun contrôle de navigation. De même, dans la mesure du possible, l’utilisation de certaines commandes comme « copier » et « coller » sont désactivées pour les candidats';
$string['minimumquestions'] = 'Nombre minimum d’items';
$string['minimumquestions_help'] = 'Nombre minimum d’items auxquels le candidat doit répondre';
$string['maximumquestions'] = 'Nombre maximum d’items';
$string['maximumquestions_help'] = 'Nombre maximum d’items qu’un candidat peut tenter';
$string['startinglevel'] = 'Niveau de difficulté de départ';
$string['startinglevel_help'] = 'Lorsque le candidat commence une session, l’application sélectionnera aléatoirement un item correspondant au niveau de difficulté';
$string['lowestlevel'] = 'Niveau de difficulté le plus bas';
$string['lowestlevel_help'] = 'Niveau le moins difficile duquel les items seront sélectionnés à l’occasion de ce test. Lors d’une session, l’activité n’ira pas au delà de ce niveau de difficulté';
$string['highestlevel'] = 'Niveau de difficulté le plus élevé';
$string['highestlevel_help'] = 'Niveau de difficulté le plus difficile duquel les items seront sélectionnés à l’occasion de ce test. Lors d’une session, l’activité n’ira pas au delà de ce niveau de difficulté';
$string['questionpool'] = 'Banque d’items';
$string['questionpool_help'] = 'Sélectionnez les catégories desquelles les items pourront être tirés durant une session';
$string['formelementempty'] = 'Entrez un entier positif compris entre 1 et 999';
$string['formelementnumeric'] = 'Entrez une valeur chiffrée comprise entre 1 et 999';
$string['formelementnegative'] = 'Entrez un nombre positif compris entre 1 et 999';
$string['formminquestgreaterthan'] = 'Le nombre minimum de questions doit être inférieur au nombre maximum de question';
$string['formlowlevelgreaterthan'] = 'Le niveau le plus bas doit être inférieur au niveau le plus élevé';
$string['formstartleveloutofbounds'] = 'Le niveau de départ doit être un nombre compris entre le niveau le plus bas et le niveau le plus élevé';
$string['standarderror'] = 'Erreur standard provoquant l’arrêt';
$string['standarderror_help'] = 'Lorsque le nombre d’erreurs fait que la capacité de l’utilisateur est évaluée en dessous du niveau seuil le questionnaire s’arrêtera. Réglez cette valeur dans la limite de 5% pour obtenir plus ou moins de précision pour mesurer sa capacité';
$string['formelementdecimal'] = 'Entrez un nombre décimal d’une longueur maximum de 10 chiffres et comportant un maximum de 5 chiffres après la virgule';
$string['attemptfeedback'] = 'Commentaire';
$string['attemptfeedback_help'] = 'Un commentaire est proposé à l’utilisateur une fois la session est terminée';
$string['formquestionpool'] = 'Sélectionnez au moins une catégorie de question';
$string['submitanswer'] = 'Soumettre la réponse';
$string['startattemptbtn'] = 'Démarrez la session';
$string['viewreportbtn'] = 'Voir le rapport';
$string['errorfetchingquest'] = 'Impossible de récupérer un item pour ce niveau {$a->level}';
$string['leveloutofbounds'] = 'Le niveau requis {$a->level} n’est pas celui prévu pour cette session';
$string['errorattemptstate'] = 'Une erreur s’est produite en déterminant l’état de la session';
$string['nopermission'] = 'Accès réservé';
$string['maxquestattempted'] = 'Nombre maximum de items tentés';
$string['notyourattempt'] = 'Cette tentative n’est pas la votre pour cette activité';
$string['noattemptsallowed'] = 'Plus aucune tentative autorisée pour cette activité';
$string['updateattempterror'] = 'Erreur lors de la mise à jour de l’enregistrement';
$string['numofattemptshdr'] = 'Nombre de tentatives';
$string['standarderrorhdr'] = 'Erreur standard';
$string['errorlastattpquest'] = 'Erreur lors de la vérification de réponse du dernier item';
$string['errornumattpzero'] = 'Le nombre de tentatives est égal à zéro, bien que l’utilisateur ait soumis une réponse à la question précédente';
$string['errorsumrightwrong'] = 'La somme des réponses correctes et incorrectes est différent du nombre total de items tentées';
$string['calcerrorwithinlimits'] = 'L’erreur standard calculée par {$a->calerror} est comprise dans les limites imposées par l’application {$a->definederror}';
$string['missingtagprefix'] = 'Tag prefix manquant';
$string['recentactquestionsattempted'] = 'Items tentés: {$a}';
$string['recentattemptstate'] = 'État de la tentative';
$string['recentinprogress'] = 'En court';
$string['notinprogress'] = 'Cette tentative n’est pas en court';
$string['recentcomplete'] = 'Terminé';
$string['functiondisabledbysecuremode'] = 'Cette fonctionnalité est actuellement désactivée';
$string['enterrequiredpassword'] = 'Entrez le mot de passe requis';
$string['requirepasswordmessage'] = 'Pour débuter ce questionnaire vous devez connaître son mot de passe';
$string['wrongpassword'] = 'Mot de passe incorrect';
$string['attemptstate'] = 'État de la tentative';
$string['attemptstopcriteria'] = 'Raison de l’abandon';
$string['questionsattempted'] = 'Total des items tentés';
$string['attemptfinishedtimestamp'] = 'Heure de fin de la tentative';
$string['backtomainreport'] = 'Retour au rapport principal';
$string['reviewattempt'] = 'Revoir sur la tentative';
$string['indvuserreport'] = 'Rapport individuel d’activité pour l’utilisateur {$a}';
$string['activityreports'] = 'Rapport d’activité';
$string['stopingconditionshdr'] = 'Conditions d’arrêt';
$string['backtoviewattemptreport'] = 'Retour vers le rapport de tentative';
$string['backtoviewreport'] = 'Retour vers le rapport principal';
$string['reviewattemptreport'] = 'Revue de la tentative par {$a->fullname} soumise à {$a->finished}';
$string['deleteattemp'] = 'Supprimez la tentative';
$string['confirmdeleteattempt'] = 'Confirmation de la suppression de la tentative à partir de {$a->name} soumise à {$a->timecompleted}';
$string['attemptdeleted'] = 'Tentative supprimée pour {$a->name} soumise à {$a->timecompleted}';
$string['closeattempt'] = 'Clôturer la tentative';
$string['confirmcloseattempt'] = 'Êtes vous certain(e) de vouloir clôturer et finaliser cette tentative de {$a->name}?';
$string['confirmcloseattemptstats'] = 'Cette tentative commencée le {$a->started} a été mise à jour le {$a->modified}';
$string['confirmcloseattemptscore'] = '{$a->num_questions} items ont été complétés et le score est de {$a->measure} {$a->standarderror}.';
$string['attemptclosedstatus'] = 'Tentative clôturée manuellement par {$a->current_user_name} (user-id: {$a->current_user_id}) le {$a->now}.';
$string['attemptclosed'] = 'La tentative a été clôturée manuellement';
$string['errorclosingattempt_alreadycomplete'] = 'Cette tentative est déjà validée et ne peut être clôturée manuellement';
$string['formstderror'] = 'Un pourcentage inférieur à 50 et supérieur ou égal à 0 doit être entré';
$string['backtoviewattemptreport'] = 'Retour vers le rapport de tentative';
$string['backtoviewreport'] = 'Retour vers le rapport principal';
$string['reviewattemptreport'] = 'Revue de la tentative par {$a->fullname} soumise à {$a->finished}';
$string['score'] = 'Résultat';
$string['bestscore'] = 'Meilleur résultat';
$string['bestscorestderror'] = 'Erreur standard';
$string['attempt_summary'] = 'Résumé de la tentative';
$string['scoring_table'] = 'Table des résultats';
$string['attempt_questiondetails'] = 'Détails de l’item';
$string['attemptstarttime'] = 'Heure de début de la tentative';
$string['attempttotaltime'] = 'Temps total (hh:mm:ss)';
$string['attempt_user'] = 'utilisateur';
$string['attempt_state'] = 'État de la tentative';
$string['attemptquestion_num'] = 'Item #';
$string['attemptquestion_level'] = 'Niveau de difficulté de l’item';
$string['attemptquestion_rightwrong'] = 'Vrai/faux';
$string['attemptquestion_ability'] = 'Mesure de capacité';
$string['attemptquestion_error'] = 'Erreur standard (&plusmn;&nbsp;x%)';
$string['attemptquestion_difficulty'] = 'Difficulté de l’item (logits)';
$string['attemptquestion_diffsum'] = 'Somme des difficultés';
$string['attemptquestion_abilitylogits'] = 'Capacité mesurée (logits)';
$string['attemptquestion_stderr'] = 'Erreur standard (&plusmn;&nbsp;logits)';
$string['graphlegend_target'] = 'Niveau cible';
$string['graphlegend_error'] = 'Erreur standard';
$string['answerdistgraph_title'] = 'Communication de la réponse pour {$a->firstname} {$a->lastname}';
$string['answerdistgraph_questiondifficulty'] = 'Niveau de l\'item';
$string['answerdistgraph_numrightwrong'] = 'Nombre incorrect (-) / Nombre correct (+)';
$string['numright'] = 'Nombre correct';
$string['numwrong'] = 'Nombre incorrect';
$string['questionnumber'] = 'Item #';
$string['na'] = 'Non disponible';
$string['downloadcsv'] = 'Téléchargez le fichier CSV';
$string['grademethod'] = 'Méthode de notation';
$string['gradehighest'] = 'Note la plus élevée';
$string['attemptfirst'] = 'Première tentative';
$string['attemptlast'] = 'Dernière tentative';
$string['grademethod_help'] = 'Lorsque plusieurs tentatives sont autorisées, les méthodes suivantes sont disponibles pour calculer la note du questionnaire final
* Note la plus haute pour l’ensemble des tentatives
* Première tentative (toutes les autres tentatives sont ignorées)
* Dernière tentative (toutes les autres tentatives sont ignorées)
';
$string['resetadaptivequizsall'] = 'Effacer toutes les tentatives du questionnaire adaptatif';
$string['all_attempts_deleted'] = 'Toutes les tentatives du questionnaire adaptatif ont été effacées';
$string['all_grades_removed'] = 'Toutes les notes du questionnaire adaptatif ont été retirées';
$string['questionanalysisbtn'] = 'Analyse de la question';
$string['id'] = 'Identifiant';
$string['name'] = 'Nom';
$string['questions_report'] = 'Rapport sur les items';
$string['question_report'] = 'Analyse de l’item';
$string['times_used_display_name'] = 'Temps écoulé';
$string['percent_correct_display_name'] = '% de réponse correcte';
$string['discrimination_display_name'] = 'Discrimination';
$string['back_to_all_questions'] = '&laquo Retour aux questions';
$string['answers_display_name'] = 'Réponses';
$string['answer'] = 'Réponse';
$string['statistic'] = 'Statistique(s)';
$string['value'] = 'Valeur';
$string['highlevelusers'] = 'Utilisateurs au dessus du niveau requis';
$string['midlevelusers'] = 'Utilisateurs proche du niveau requis';
$string['lowlevelusers'] = 'Utilisateurs en dessous du niveau requis';
$string['user'] = 'Utilisateur';
$string['result'] = 'Résultat';
<?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/>.
/**
* Plugin's system and internal functions.
*
* @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
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/question/engine/lib.php');
use mod_adaptivequiz\local\attempt\attempt_state;
/**
* Option controlling what options are offered on the quiz settings form.
*/
define('ADAPTIVEQUIZMAXATTEMPT', 10);
define('ADAPTIVEQUIZNAME', 'adaptivequiz');
/**
* Options determining how the grades from individual attempts are combined to give
* the overall grade for a user
*/
define('ADAPTIVEQUIZ_GRADEHIGHEST', '1');
define('ADAPTIVEQUIZ_ATTEMPTFIRST', '3');
define('ADAPTIVEQUIZ_ATTEMPTLAST', '4');
/**
* Returns the information on whether the module supports a feature
*
* @see plugin_supports() in lib/moodlelib.php
* @param string $feature: FEATURE_xx constant for requested feature
* @return mixed true if the feature is supported, null if unknown
*/
function adaptivequiz_supports($feature) {
switch($feature) {
case FEATURE_GROUPS: {
return true;
}
case FEATURE_GROUPINGS: {
return true;
}
case FEATURE_GROUPMEMBERSONLY: {
return true;
}
case FEATURE_MOD_INTRO: {
return true;
}
case FEATURE_BACKUP_MOODLE2: {
return true;
}
case FEATURE_SHOW_DESCRIPTION: {
return true;
}
case FEATURE_GRADE_HAS_GRADE: {
return true;
}
case FEATURE_USES_QUESTIONS: {
return true;
}
case FEATURE_MOD_PURPOSE: {
return MOD_PURPOSE_ASSESSMENT;
}
case FEATURE_COMPLETION_HAS_RULES: {
return true;
}
default: {
return null;
}
}
}
/**
* Saves a new instance of the adaptivequiz into the database.
*
* Given an object containing all the necessary data (defined by the form in mod_form.php), this function will create
* a new instance and return the id number of the new instance.
*
* @param stdClass $adaptivequiz An object from the form in mod_form.php.
* @param mod_adaptivequiz_mod_form|null $mform
* @return int The id of the newly inserted adaptivequiz record.
*/
function adaptivequiz_add_instance(stdClass $adaptivequiz, mod_adaptivequiz_mod_form $mform = null) {
global $DB;
$context = context_module::instance($adaptivequiz->coursemodule);
$time = time();
$adaptivequiz->timecreated = $time;
$adaptivequiz->timemodified = $time;
$attemptfeedbacktext = '';
$attemptfeedbackformat = FORMAT_MOODLE;
if ($adaptivequiz->attemptfeedbackenable) {
$attemptfeedbacktext = $adaptivequiz->attemptfeedbackeditor['text'];
if (isset($adaptivequiz->attemptfeedbackeditor['itemid'])) {
$attemptfeedbacktext = file_save_draft_area_files($adaptivequiz->attemptfeedbackeditor['itemid'], $context->id,
'mod_adaptivequiz', 'attemptfeedback', 0, ['subdirs' => true], $adaptivequiz->attemptfeedbackeditor['text']);
}
$attemptfeedbackformat = $adaptivequiz->attemptfeedbackeditor['format'];
}
$adaptivequiz->attemptfeedback = $attemptfeedbacktext;
$adaptivequiz->attemptfeedbackformat = $attemptfeedbackformat;
$instance = $DB->insert_record('adaptivequiz', $adaptivequiz);
if (empty($instance) && is_int($instance)) {
return $instance;
}
$adaptivequiz->id = $instance;
// Update related grade item.
adaptivequiz_grade_item_update($adaptivequiz);
// KNIGHT (Feature 4): persist the per-level competency descriptions.
require_once(__DIR__ . '/locallib.php');
adaptivequiz_save_competency_descriptions($adaptivequiz->id, $adaptivequiz);
return $instance;
}
/**
* Updates fields related to item administration settings.
*
* @param stdClass $adaptivequiz An instance of the 'adaptivequiz' activity.
*/
function adaptivequiz_update_item_administration_params(stdClass $adaptivequiz): void {
global $DB;
// Clean up the passed data to contain only what's related to the function's scope.
// KNIGHT: acceptancethreshold and questionchecktrigger (Feature 3) are part of the item
// administration params - both are configured on the item administration form.
$settings = ['highestlevel', 'lowestlevel', 'startinglevel',
'minimumquestions', 'maximumquestions', 'standarderror', 'acceptancethreshold', 'questionchecktrigger'];
foreach ($adaptivequiz as $field => $unused) {
if ($field == 'id') {
continue;
}
if (!in_array($field, $settings)) {
unset($adaptivequiz->{$field});
}
}
$DB->update_record('adaptivequiz', $adaptivequiz);
// KNIGHT (Feature 4): changing the difficulty range here can make the competency feedback invalid
// (e.g. widening it past the level cap). Disable the feature in that case, so its feedback links do
// not stay visible for an out-of-range activity - matching what the settings form would enforce.
require_once(__DIR__ . '/locallib.php');
$current = $DB->get_record(
'adaptivequiz',
['id' => $adaptivequiz->id],
'id, competencyfeedbackenable, lowestlevel, highestlevel'
);
$nowinvalid = $current && $current->competencyfeedbackenable
&& !adaptivequiz_competency_feedback_selectable((int) $current->lowestlevel, (int) $current->highestlevel);
if ($nowinvalid) {
$DB->set_field('adaptivequiz', 'competencyfeedbackenable', 0, ['id' => $adaptivequiz->id]);
}
}
/**
* This function creates question category association record(s).
*
* @deprecated Since version 2.6.0.
* @param int $instance Activity instance id.
* @param stdClass $adaptivequiz An object from the form in mod_form.php.
*/
function adaptivequiz_add_questcat_association(int $instance, stdClass $adaptivequiz): void {
global $DB;
if (0 != $instance && !empty($adaptivequiz->questionpool)) {
$qtag = new stdClass();
$qtag->instance = $instance;
foreach ($adaptivequiz->questionpool as $questioncatid) {
$qtag->questioncategory = $questioncatid;
$DB->insert_record('adaptivequiz_question', $qtag);
}
}
}
/**
* This function updates the question category association records.
*
* @param int $instance Activity instance id.
* @param stdClass $adaptivequiz An object from the form in mod_form.php.
*/
function adaptivequiz_update_questcat_association(int $instance, stdClass $adaptivequiz): void {
global $DB;
// Remove old references.
if (!empty($instance)) {
$DB->delete_records('adaptivequiz_question', ['instance' => $instance]);
}
// Insert new references.
adaptivequiz_add_questcat_association($instance, $adaptivequiz);
}
/**
* Updates an instance of the adaptivequiz in the database.
*
* Given an object containing all the necessary data (defined by the form in mod_form.php), this function will update
* an existing instance with new data.
*
* @param stdClass $adaptivequiz An object from the form in mod_form.php.
* @param mod_adaptivequiz_mod_form|null $mform
* @return bool
*/
function adaptivequiz_update_instance(stdClass $adaptivequiz, mod_adaptivequiz_mod_form $mform = null) {
global $DB;
$context = context_module::instance($adaptivequiz->coursemodule);
$adaptivequiz->timemodified = time();
$adaptivequiz->id = $adaptivequiz->instance;
// Get the current value, so we can see what changed.
$oldquiz = $DB->get_record('adaptivequiz', ['id' => $adaptivequiz->instance]);
if ($adaptivequiz->attemptfeedbackenable) {
$attemptfeedbacktext = $adaptivequiz->attemptfeedbackeditor['text'];
if (isset($adaptivequiz->attemptfeedbackeditor['itemid'])) {
$attemptfeedbacktext = file_save_draft_area_files($adaptivequiz->attemptfeedbackeditor['itemid'], $context->id,
'mod_adaptivequiz', 'attemptfeedback', 0, ['subdirs' => true], $adaptivequiz->attemptfeedbackeditor['text']);
}
$adaptivequiz->attemptfeedback = $attemptfeedbacktext;
$adaptivequiz->attemptfeedbackformat = $adaptivequiz->attemptfeedbackeditor['format'];
}
$instanceid = $DB->update_record('adaptivequiz', $adaptivequiz);
// Save question tag association data.
adaptivequiz_update_questcat_association($adaptivequiz->id, $adaptivequiz);
// Update related grade item.
if ($oldquiz->grademethod != $adaptivequiz->grademethod) {
adaptivequiz_update_grades($adaptivequiz);
} else {
adaptivequiz_grade_item_update($adaptivequiz);
}
// KNIGHT (Feature 4): persist the per-level competency descriptions.
require_once(__DIR__ . '/locallib.php');
adaptivequiz_save_competency_descriptions($adaptivequiz->id, $adaptivequiz);
return $instanceid;
}
/**
* Removes an instance of the adaptivequiz from the database
*
* Given an ID of an instance of this module,
* this function will permanently delete the instance
* and any data that depends on it.
*
* @param int $id: Id of the module instance
* @return boolean Success/Failure
*/
function adaptivequiz_delete_instance($id) {
global $DB;
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $id));
if (!$adaptivequiz) {
return false;
}
// Remove question_usage_by_activity records.
$attempts = $DB->get_records('adaptivequiz_attempt', array('instance' => $id));
if (!empty($attempts)) {
foreach ($attempts as $attempt) {
question_engine::delete_questions_usage_by_activity($attempt->uniqueid);
}
// Remove attempts data.
$DB->delete_records('adaptivequiz_attempt', array('instance' => $id));
}
// Remove association table data.
if ($DB->record_exists('adaptivequiz_question', array ('instance' => $id))) {
$DB->delete_records('adaptivequiz_question', array('instance' => $id));
}
// KNIGHT (Feature 4): remove the per-level competency descriptions (before the parent row, so an
// enforced foreign key does not block the delete).
$DB->delete_records('adaptivequiz_competencydesc', ['adaptivequizid' => $id]);
// Delete the quiz record itself.
$DB->delete_records('adaptivequiz', array('id' => $id));
// Delete the grade item.
adaptivequiz_grade_item_delete($adaptivequiz);
return true;
}
/**
* Returns a small object with summary information about what a
* user has done with a given particular instance of this module
* Used for user activity reports.
* $return->time = the time they did it
* $return->info = a short text description
*
* @return stdClass|null
*/
function adaptivequiz_user_outline($course, $user, $mod, $adaptivequiz) {
$return = new stdClass();
$return->time = 0;
$return->info = '';
return $return;
}
/**
* Prints a detailed representation of what a user has done with
* a given particular instance of this module, for user activity reports.
*
* @param stdClass $course: the current course record
* @param stdClass $user: the record of the user we are generating report for
* @param cm_info $mod: course module info
* @param stdClass $adaptivequiz: the module instance record
* @return void, is supposed to echp directly
*/
function adaptivequiz_user_complete($course, $user, $mod, $adaptivequiz) {
}
/**
* Given a course and a time, this module should find recent activity
* that has occurred in adaptivequiz activities and print it out.
* Return true if there was output, or false is there was none.
*
* @return boolean
*/
function adaptivequiz_print_recent_activity($course, $viewfullnames, $timestart) {
return false; // True if anything was printed, otherwise false.
}
/**
* Prepares the recent activity data
*
* This callback function is supposed to populate the passed array with
* custom activity records. These records are then rendered into HTML via
* {@link adaptivequiz_print_recent_mod_activity()}.
*
* @param array $activities: sequentially indexed array of objects with the 'cmid' property
* @param int $index: the index in the $activities to use for the next record
* @param int $timestart: append activity since this time
* @param int $courseid: the id of the course we produce the report for
* @param int $cmid: course module id
* @param int $userid: check for a particular user's activity only, defaults to 0 (all users)
* @param int $groupid: check for a particular group's activity only, defaults to 0 (all groups)
* @return void adds items into $activities and increases $index
*/
function adaptivequiz_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid = 0, $groupid = 0) {
global $COURSE, $DB, $USER;
if ($COURSE->id == $courseid) {
$course = $COURSE;
} else {
$course = $DB->get_record('course', array('id' => $courseid));
}
$modinfo = get_fast_modinfo($course);
$cm = $modinfo->cms[$cmid];
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance));
if ($userid) {
$userselect = "AND u.id = :userid";
$params['userid'] = $userid;
} else {
$userselect = '';
}
if ($groupid) {
$groupselect = 'AND gm.groupid = :groupid';
$groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id';
$params['groupid'] = $groupid;
} else {
$groupselect = '';
$groupjoin = '';
}
$params['timestart'] = $timestart;
$params['instance'] = $adaptivequiz->id;
$sql = "SELECT aa.*, u.firstname, u.lastname, u.email, u.picture, u.imagealt
FROM {adaptivequiz_attempt} aa
JOIN {user} u ON u.id = aa.userid
$groupjoin
WHERE aa.timemodified > :timestart
AND aa.instance = :instance
$userselect
$groupselect
ORDER BY aa.timemodified ASC";
$rs = $DB->get_recordset_sql($sql, $params);
// Check if recordset contains records.
if (!$rs->valid()) {
return;
}
$context = context_module::instance($cm->id);
$accessallgroups = has_capability('moodle/site:accessallgroups', $context);
$viewfullnames = has_capability('moodle/site:viewfullnames', $context);
$viewreport = has_capability('mod/adaptivequiz:viewreport', $context);
$groupmode = groups_get_activity_groupmode($cm, $course);
if (is_null($modinfo->groups)) {
// Load all my groups and cache it in modinfo.
$modinfo->groups = groups_get_user_groups($course->id);
}
$usersgroups = null;
$aname = format_string($cm->name, true);
foreach ($rs as $attempt) {
if ($attempt->userid != $USER->id) {
if (!$viewreport) {
// View report permission required to view activity other user attempts.
continue;
}
if ($groupmode == SEPARATEGROUPS && !$accessallgroups) {
if (is_null($usersgroups)) {
$usersgroups = groups_get_all_groups($course->id, $attempt->userid, $cm->groupingid);
if (is_array($usersgroups)) {
$usersgroups = array_keys($usersgroups);
} else {
$usersgroups = array();
}
}
if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
continue;
}
}
}
$tmpactivity = new stdClass();
$tmpactivity->content = new stdClass();
$tmpactivity->user = new stdClass();
$tmpactivity->type = 'adaptivequiz';
$tmpactivity->cmid = $cm->id;
$tmpactivity->name = $aname;
$tmpactivity->sectionnum = $cm->sectionnum;
$tmpactivity->timestamp = $attempt->timemodified;
$tmpactivity->content->attemptid = $attempt->id;
$tmpactivity->content->attemptstate = get_string('recent'.$attempt->attemptstate, 'adaptivequiz');
$tmpactivity->content->questionsattempted = $attempt->questionsattempted;
$tmpactivity->user->id = $attempt->userid;
$tmpactivity->user->firstname = $attempt->firstname;
$tmpactivity->user->lastname = $attempt->lastname;
$tmpactivity->user->picture = $attempt->picture;
$tmpactivity->user->imagealt = $attempt->imagealt;
$tmpactivity->user->email = $attempt->email;
$activities[$index++] = $tmpactivity;
}
$rs->close();
return;
}
/**
* Prints single activity item prepared by {@see adaptivequiz_get_recent_mod_activity()}
* @param stdClass $activity an object whose properties come from {@see adaptivequiz_get_recent_mod_activity()}
* @param int $courseid the id of the course we produce the report for
* @param bool $detail set to true to show more detail for the recent activity
* @param array $modnames an array of module names
* @param bool $viewfullnames true if the user has the capability to view full names
* @param bool $return set to true to return output, else false to echo the output
* @return string|void HTML markup
*/
function adaptivequiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames, $return = false) {
global $CFG, $OUTPUT;
$output = '';
$cols = '';
$contect = '';
// Define table.
$attr = array('border' => '0', 'cellpadding' => '3', 'cellspacing' => '0', 'class' => 'adaptivequiz-recent');
$output .= html_writer::start_tag('table', $attr);
// Define table columns.
$attr = array('class' => 'userpicture', 'valign' => 'top');
$content = $OUTPUT->user_picture($activity->user, array('courseid' => $courseid));
$cols .= html_writer::tag('td', $content, $attr);
$content = '';
if ($detail) {
$modname = $modnames[$activity->type];
// Start div.
$attr = array('class' => 'title');
$content .= html_writer::start_tag('div', $attr);
// Create img markup.
$attr = array('src' => $OUTPUT->image_url('icon', $activity->type), 'class' => 'icon', 'alt' => $modname);
$content .= html_writer::empty_tag('img', $attr);
// Create anchor markup.
$attr = array('href' => "{$CFG->wwwroot}/mod/adaptivequiz/view.php?id={$activity->cmid}",
'class' => 'icon', 'alt' => $modname);
$content .= html_writer::tag('a', $activity->name, $attr);
// End div.
$content .= html_writer::end_tag('div');
}
// Create div with the state of the attempt.
$attr = array('class' => 'attemptstate');
$string = get_string('recentattemptstate', 'adaptivequiz');
$content .= html_writer::tag('div', $string.'&nbsp;'.$activity->content->attemptstate, $attr);
// Create div with the number of questions attempted.
$attr = array('class' => 'questionsattempted');
$string = get_string('recentactquestionsattempted', 'adaptivequiz', $activity->content->questionsattempted);
$content .= html_writer::tag('div', $string, $attr);
// Start div.
$attr = array('class' => 'user');
$content .= html_writer::start_tag('div', $attr);
// Create anchor for link to user's profile.
$attr = array('href' => $CFG->wwwroot.'/user/view.php?id='.$activity->user->id.'&amp;course='.$courseid);
$fullname = fullname($activity->user, $viewfullnames);
$content .= html_writer::tag('a', $fullname, $attr);
// Add timestamp.
$content .= '&nbsp'.userdate($activity->timestamp);
// End div.
$content .= html_writer::end_tag('div');
// Add all of the data for the columns to the table row.
$cols .= html_writer::tag('td', $content);
$output .= html_writer::tag('tr', $cols);
// End table.
$output .= html_writer::end_tag('table');
if (!empty($return)) {
// The return statemtn is not required, but it here so that this function can be PHPUnit testsed.
return $output;
} else {
// Echo output to the page.
echo $output;
}
}
/**
* Function to be run periodically according to the moodle cron
* This function searches for things that need to be done, such
* as sending out mail, toggling flags etc ...
*
* @return boolean
**/
function adaptivequiz_cron() {
return false;
}
/**
* Returns all other caps used in the module
*
* @example return array('moodle/site:accessallgroups');
* @return array
*/
function adaptivequiz_get_extra_capabilities() {
return array();
}
/**
* Extends the global navigation tree by adding adaptivequiz nodes if there is a relevant content
* This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly.
*
* @param navigation_node $navref An object representing the navigation tree node of the adaptivequiz module instance
* @param stdClass $course
* @param stdClass $module
* @param cm_info $cm
*/
function adaptivequiz_extend_navigation(navigation_node $navref, stdclass $course, stdclass $module, cm_info $cm) {
}
/**
* A system callback, allows to add custom nodes to the settings navigation.
*
* @param settings_navigation $settingsnav
* @param navigation_node $adaptivequiznode
*/
function adaptivequiz_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $adaptivequiznode): void {
$context = $settingsnav->get_page()->cm->context;
if (!has_capability('mod/adaptivequiz:viewreport', $context)) {
return;
}
$cmid = $settingsnav->get_page()->cm->id;
// KNIGHT: the item bank is a management area, so only offer its link to users who may manage - a
// view-only role (e.g. non-editing teacher with viewreport) must not see a link it cannot use.
if (has_capability('mod/adaptivequiz:manage', $context)) {
$node = navigation_node::create(
get_string('itembankbtn', 'adaptivequiz'),
new moodle_url('/mod/adaptivequiz/itembank.php', ['id' => $cmid]),
navigation_node::TYPE_SETTING,
null,
'mod_adaptivequiz_item_bank',
new pix_icon('i/report', '')
);
$adaptivequiznode->add_node($node);
}
$node = navigation_node::create(
get_string('questionanalysisbtn', 'adaptivequiz'),
new moodle_url('/mod/adaptivequiz/questionanalysis/overview.php', ['cmid' => $cmid]),
navigation_node::TYPE_SETTING, null, 'mod_adaptivequiz_question_analysis', new pix_icon('i/report', '')
);
$adaptivequiznode->add_node($node);
}
/**
* Delete the grade item for given quiz
*
* @category grade
* @param object $adaptivequiz object
* @return int 0 if ok, error code otherwise
*/
function adaptivequiz_grade_item_delete(stdClass $adaptivequiz) {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
$params = array('deleted' => 1);
return grade_update('mod/adaptivequiz', $adaptivequiz->course, 'mod', 'adaptivequiz', $adaptivequiz->id, 0, null, $params);
}
/**
* Create or update the grade item for given quiz.
*
* @param stdClass $adaptivequiz
* @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook.
* @return int 0 if ok, error code otherwise.
*/
function adaptivequiz_grade_item_update(stdClass $adaptivequiz, $grades = null) {
global $CFG;
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
require_once($CFG->libdir . '/gradelib.php');
if (!empty($adaptivequiz->id)) { // May not be always present.
$params = array('itemname' => $adaptivequiz->name, 'idnumber' => $adaptivequiz->id);
} else {
$params = array('itemname' => $adaptivequiz->name);
}
if (isset($adaptivequiz->highestlevel)) {
if ($adaptivequiz->highestlevel > 0) {
$params['gradetype'] = GRADE_TYPE_VALUE;
$params['grademax'] = $adaptivequiz->highestlevel;
$params['grademin'] = $adaptivequiz->lowestlevel;
} else {
$params['gradetype'] = GRADE_TYPE_NONE;
}
}
if ($grades === 'reset') {
$params['reset'] = true;
$grades = null;
}
return grade_update('mod/adaptivequiz', $adaptivequiz->course, 'mod', 'adaptivequiz', $adaptivequiz->id, 0, $grades, $params);
}
function adaptivequiz_update_grades(stdClass $adaptivequiz, $userid=0, $nullifnone = true) {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
require_once($CFG->libdir.'/gradelib.php');
if ($grades = adaptivequiz_get_user_grades($adaptivequiz, $userid)) {
// Set all user grades.
adaptivequiz_grade_item_update($adaptivequiz, $grades);
} else if ($userid && $nullifnone) {
// Reset all user grades.
$grade = new stdClass();
$grade->userid = $userid;
$grade->rawgrade = null;
adaptivequiz_grade_item_update($adaptivequiz, $grade);
} else {
// Don't change user grades.
adaptivequiz_grade_item_update($adaptivequiz);
}
}
/**
* Called by course/reset.php
*/
function adaptivequiz_reset_course_form_definition(&$mform) {
$mform->addElement('header', 'apaptivequizheader', get_string('modulenameplural', 'adaptivequiz'));
$mform->addElement('checkbox', 'reset_adaptivequiz_all', get_string('resetadaptivequizsall', 'adaptivequiz'));
}
/**
* Course reset form defaults.
*/
function adaptivequiz_reset_course_form_defaults($course) {
return array('reset_adaptivequiz_all' => 0);
}
/**
* This function is used by the reset_course_userdata function in moodlelib.
* This function will remove all attempts from the specified adaptivequiz
* and clean up any related data.
* @param $data the data submitted from the reset course.
* @return array status array
*/
function adaptivequiz_reset_userdata($data) {
global $CFG, $DB;
$componentstr = get_string('modulenameplural', 'adaptivequiz');
$status = array();
// Delete our attempts.
if (!empty($data->reset_adaptivequiz_all)) {
$adaptivequizes = $DB->get_records('adaptivequiz', array('course' => $data->courseid));
foreach ($adaptivequizes as $adaptivequiz) {
$attempts = $DB->get_records('adaptivequiz_attempt', array('instance' => $adaptivequiz->id));
if (!empty($attempts)) {
// Remove question_usage_by_activity records.
foreach ($attempts as $attempt) {
question_engine::delete_questions_usage_by_activity($attempt->uniqueid);
}
// Remove attempts data.
$DB->delete_records('adaptivequiz_attempt', array('instance' => $adaptivequiz->id));
}
}
}
$status[] = array(
'component' => $componentstr,
'item' => get_string('all_attempts_deleted', 'adaptivequiz'),
'error' => false,
);
// Delete our grades.
if (!empty($data->reset_gradebook_grades)) {
adaptivequiz_reset_gradebook($data->courseid);
$status[] = array(
'component' => $componentstr,
'item' => get_string('all_grades_removed', 'adaptivequiz'),
'error' => false,
);
}
return $status;
}
/**
* Removes all grades from gradebook
*
* @param int $courseid The ID of the course to reset
*/
function adaptivequiz_reset_gradebook($courseid) {
global $CFG, $DB;
$adaptivequizes = $DB->get_records('adaptivequiz', array('course' => $courseid));
foreach ($adaptivequizes as $adaptivequiz) {
adaptivequiz_grade_item_update($adaptivequiz, 'reset');
}
}
/**
* Serves the module's files.
*
* @param stdClass $course
* @param stdClass $cm
* @param stdClass $context
* @param string $filearea
* @param array $args Extra arguments.
* @param bool $forcedownload Whether force download.
* @param array $options Additional options affecting the file serving.
* @return bool|void
*/
function adaptivequiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options= []) {
require_login($course, false, $cm);
if ($filearea != 'attemptfeedback') {
return false;
}
$relativepath = implode('/', $args);
$fullpath = "/$context->id/mod_adaptivequiz/$filearea/$relativepath";
$fs = get_file_storage();
if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
return false;
}
send_stored_file($file, 0, 0, true, $options);
}
/**
* Called via pluginfile.php -> question_pluginfile to serve files belonging to a question in a question_attempt when that attempt
* is a quiz attempt.
*
* @param stdClass $course Course settings object.
* @param context $context
* @param string $component The name of the component we are serving files for.
* @param string $filearea The name of the file area.
* @param int $qubaid The attempt usage id.
* @param int $slot The id of a question in this quiz attempt.
* @param array $args The remaining bits of the file path.
* @param bool $forcedownload Whether the user must be forced to download the file.
* @param array $options Additional options affecting the file serving.
* @return bool False if file not found, does not return if found - just send the file.
*/
function mod_adaptivequiz_question_pluginfile($course, context $context, $component, $filearea, $qubaid, $slot, $args,
$forcedownload, array $options=[]) {
global $CFG, $DB, $USER;
$attemptrec = $DB->get_record('adaptivequiz_attempt', ['uniqueid' => $qubaid], '*', MUST_EXIST);
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attemptrec->instance], '*', MUST_EXIST);
$course = $DB->get_record('course', ['id' => $adaptivequiz->course], '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $adaptivequiz->course, false, MUST_EXIST);
require_login($course, true, $cm);
$modcontext = context_module::instance($cm->id);
// Check if the user has the attempt capability.
if (!has_capability('mod/adaptivequiz:attempt', $modcontext) && !has_capability('mod/adaptivequiz:viewreport', $modcontext)) {
throw new moodle_exception('nopermission', 'adaptivequiz');
}
// If we are reviewing an attempt, require the viewreport capability.
if ($attemptrec->userid != $USER->id) {
require_capability('mod/adaptivequiz:viewreport', $modcontext);
} else {
require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php');
if ($attemptrec->attemptstate === attempt_state::COMPLETED) {
// KNIGHT (Feature 2): serving question files while the user reviews their own completed
// attempt. Gate this on the same permission as the review page itself, so it exposes
// nothing beyond what that review already shows.
if (!adaptivequiz_user_can_review_attempt($adaptivequiz, $attemptrec, $modcontext, $USER->id)) {
throw new moodle_exception('nopermission', 'adaptivequiz');
}
} else {
// Otherwise the user is attempting: check that the attempt is active and belongs to them.
// 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');
}
// Check if the uniqueid belongs to the same attempt record the user is currently using.
if (!adaptivequiz_uniqueid_part_of_attempt($qubaid, $cm->instance, $USER->id)) {
throw new moodle_exception('uniquenotpartofattempt', 'adaptivequiz');
}
// Verify that the attempt is still in progress.
if ($attemptrec->attemptstate != attempt_state::IN_PROGRESS) {
throw new moodle_exception('notinprogress', 'adaptivequiz');
}
}
}
$fs = get_file_storage();
$relativepath = implode('/', $args);
$fullpath = "/$context->id/$component/$filearea/$relativepath";
$file = $fs->get_file_by_hash(sha1($fullpath));
if (!$file) {
send_file_not_found();
}
if ($file->is_directory()) {
send_file_not_found();
}
send_stored_file($file, 0, 0, $forcedownload, $options);
}
/**
* A system callback.
*
* Given a course_module object, this function returns any "extra" information that may be needed when printing this activity
* in a course listing. See get_array_of_activities() in course/lib.php.
*
* @param stdClass $coursemodule the course module object (record).
* @return false|cached_cm_info
*/
function adaptivequiz_get_coursemodule_info(stdClass $coursemodule) {
global $DB;
if (!$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $coursemodule->instance])) {
return false;
}
$result = new cached_cm_info();
$result->name = $adaptivequiz->name;
if ($coursemodule->showdescription) {
$result->content = format_module_intro('adaptivequiz', $adaptivequiz, $coursemodule->id, false);
}
if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
$result->customdata['customcompletionrules']['completionattemptcompleted'] = $adaptivequiz->completionattemptcompleted;
}
return $result;
}
/**
* Definition of user preferences used by the plugin.
*
* @return array[]
*/
function mod_adaptivequiz_user_preferences(): array {
return [
'/^mod_adaptivequiz_answers_distribution_chart_settings_(\d)+$/' => [
'isregex' => true,
'type' => PARAM_RAW, // JSON.
'default' => null,
'permissioncallback' => [core_user::class, 'is_current_user'],
],
];
}
<?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/>.
/**
* Some utility functions for the adaptive quiz 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
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/adaptivequiz/lib.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/lib/questionlib.php');
require_once($CFG->dirroot . '/question/engine/lib.php');
use core_question\local\bank\question_edit_contexts;
use mod_adaptivequiz\event\attempt_completed;
use mod_adaptivequiz\local\attempt\attempt_state;
use mod_adaptivequiz\local\catalgo;
use qbank_managecategories\helper as qbank_managecategories_helper;
// Default tagging used.
define('ADAPTIVEQUIZ_QUESTION_TAG', 'adpq_');
// Number of attempts to display on the reporting page.
define('ADAPTIVEQUIZ_REC_PER_PAGE', 30);
// Number of questions to display for review on the page at one time.
define('ADAPTIVEQUIZ_REV_QUEST_PER_PAGE', 10);
// Attempt stopping criteria.
// The maximum number of question, defined by the adaptive parameters was achieved.
define('ADAPTIVEQUIZ_STOPCRI_MAXQUEST', 'maxqest');
// The standard error value, defined by the adaptive parameters, was achieved.
define('ADAPTIVEQUIZ_STOPCRI_STANDERR', 'stderr');
// Unable to retrieve a question, because the user either answered all of the questions in the level or no questions were found.
define('ADAPTIVEQUIZ_STOPCRI_NOQUESTFOUND', 'noqest');
// The user achieved the maximum difficulty level defined by the adaptive parameters, unable to retrieve another question.
define('ADAPTIVEQUIZ_STOPCRI_MAXLEVEL', 'maxlevel');
// The user achieved the minimum difficulty level defined by the adaptive parameters, unable to retrieve another question.
define('ADAPTIVEQUIZ_STOPCRI_MINLEVEL', 'minlevel');
// KNIGHT (Feature 4): the largest level span for which the competency feedback can be enabled. Each
// level needs its own description field, so an unbounded span would generate an unusable settings form.
define('ADAPTIVEQUIZ_MAX_COMPETENCY_LEVELS', 10);
/**
* This function returns an array of question bank categories accessible to the current user in the given context.
*
* @param context $context A context object.
* @return array An array whose keys are the question category ids and values are the name of the question category.
* @deprecated Since version 2.6.0.
*/
function adaptivequiz_get_question_categories(context $context) {
if (empty($context)) {
return array();
}
$options = array();
$qesteditctx = new question_edit_contexts($context);
$contexts = $qesteditctx->having_one_edit_tab_cap('editq');
$questioncats = qbank_managecategories_helper::question_category_options($contexts);
if (!empty($questioncats)) {
foreach ($questioncats as $questioncatcourse) {
foreach ($questioncatcourse as $key => $questioncat) {
// Key format is [question cat id, question cat context id], we need to explode it.
$questidcontext = explode(',', $key);
$questid = array_shift($questidcontext);
$options[$questid] = $questioncat;
}
}
}
return $options;
}
/**
* This function is helper method to create default.
*
* @param object $context A context object.
* @return mixed The default category in the course context or false.
* @deprecated Since version 2.6.0.
*/
function adaptivequiz_make_default_categories($context) {
if (empty($context)) {
return false;
}
// Create default question categories.
$defaultcategoryobj = question_make_default_categories(array($context));
return $defaultcategoryobj;
}
/**
* This function returns an array of question categories that were selected for use for the activity instance.
*
* @param int $instance Instance id.
* @return array An array of question category ids.
* @deprecated Since version 2.6.0.
*/
function adaptivequiz_get_selected_question_cateogires($instance) {
global $DB;
$selquestcat = array();
if (empty($instance)) {
return array();
}
$records = $DB->get_records('adaptivequiz_question', array('instance' => $instance));
if (empty($records)) {
return array();
}
foreach ($records as $record) {
$selquestcat[] = $record->questioncategory;
}
return $selquestcat;
}
/**
* This function returns a count of the user's previous attempts that have been marked
* as completed
* @param int $instanceid activity instance id
* @param int $userid user id
* @return int a count of the user's previous attempts
*/
function adaptivequiz_count_user_previous_attempts($instanceid = 0, $userid = 0) {
global $DB;
if (empty($instanceid) || empty($userid)) {
return 0;
}
$param = array('instance' => $instanceid, 'userid' => $userid, 'attemptstate' => attempt_state::COMPLETED);
$count = $DB->count_records('adaptivequiz_attempt', $param);
return $count;
}
/**
* This function determins if the user has used up all of their attempts
* @param int $maxattempts The maximum allowed attempts, 0 denotes unlimited attempts
* @param int $attempts The number of attempts taken thus far
* @return bool true if the attempt is allowed, otherwise false
*/
function adaptivequiz_allowed_attempt($maxattempts = 0, $attempts = 0) {
if (0 == $maxattempts || $maxattempts > $attempts) {
return true;
} else {
return false;
}
}
/**
* This functions validates that the unique id belongs to a user attempt of the activity instance
* @param int $uniqueid uniqueid value of the adaptivequiz_attempt record
* @param int $instance instance value of the adaptivequiz_attempt record
* @param int $userid unerid value of the adaptivequiz_attempt record
* @return bool true if the unique is part of an attempt of the activity instance, otherwise false
*/
function adaptivequiz_uniqueid_part_of_attempt($uniqueid, $instance, $userid) {
global $DB;
$param = array('uniqueid' => $uniqueid, 'instance' => $instance, 'userid' => $userid);
return $DB->record_exists('adaptivequiz_attempt', $param);
}
/**
* This function increments the difficultysum value and the number of questions attempted for the adaptivequiz_attempt record
* @throws dml_exception A DML specific exception
* @param int $uniqueid uniqueid value of the adaptivequiz_attempt record
* @param int $instance instance value of the adaptivequiz_attempt record
* @param int $userid unerid value of the adaptivequiz_attempt record
* @param float $level the logit of the difficulty level
* @param float $standarderror the standard error of the user's attempt
* @param float $measure the measure of ability for the attempt
* @return bool true of update successful, otherwise false
*/
function adaptivequiz_update_attempt_data($uniqueid, $instance, $userid, $level, $standarderror, $measure) {
global $DB;
// Check if the is an infinity.
if (is_infinite($level)) {
return false;
}
$param = array('uniqueid' => $uniqueid, 'instance' => $instance, 'userid' => $userid);
try {
$fields = 'id,difficultysum,questionsattempted,timemodified,standarderror,measure';
$attempt = $DB->get_record('adaptivequiz_attempt', $param, $fields, MUST_EXIST);
} catch (dml_exception $e) {
$debuginfo = '';
if (!empty($e->debuginfo)) {
$debuginfo = $e->debuginfo;
}
throw new moodle_exception('updateattempterror', 'adaptivequiz', '', $e->getMessage(), $debuginfo);
}
$attempt->difficultysum = (float) $attempt->difficultysum + (float) $level;
$attempt->questionsattempted = (int) $attempt->questionsattempted + 1;
$attempt->standarderror = (float) $standarderror;
$attempt->measure = (float) $measure;
$attempt->timemodified = time();
$DB->update_record('adaptivequiz_attempt', $attempt);
return true;
}
/**
* This function sets the complete status for an attempt.
*
* @throws dml_exception
* @throws coding_exception
*/
function adaptivequiz_complete_attempt(
int $uniqueid,
stdClass $adaptivequiz,
context_module $context,
int $userid,
string $standarderror,
string $statusmessage
): void {
global $DB;
$attempt = $DB->get_record('adaptivequiz_attempt',
['uniqueid' => $uniqueid, 'instance' => $adaptivequiz->id, 'userid' => $userid], '*', MUST_EXIST);
// Need to keep the record as it is before triggering the event below.
$attemptrecordsnapshot = clone $attempt;
$attempt->attemptstate = attempt_state::COMPLETED;
$attempt->attemptstopcriteria = $statusmessage;
$attempt->timemodified = time();
$attempt->standarderror = $standarderror;
$DB->update_record('adaptivequiz_attempt', $attempt);
adaptivequiz_update_grades($adaptivequiz, $userid);
$event = attempt_completed::create([
'objectid' => $attempt->id,
'context' => $context,
'userid' => $userid
]);
$event->add_record_snapshot('adaptivequiz_attempt', $attemptrecordsnapshot);
$event->add_record_snapshot('adaptivequiz', $adaptivequiz);
$event->trigger();
}
/**
* This function checks whether the minimum number of attmepts have been achieved for an attempt
* @param int $uniqueid uniqueid value of the adaptivequiz_attempt record
* @param int $instance instance value of the adaptivequiz_attempt record
* @param int $userid unerid value of the adaptivequiz_attempt record
* @return bool true of record exists, otherwise false
*/
function adaptivequiz_min_attempts_reached($uniqueid, $instance, $userid) {
global $DB;
$sql = "SELECT adpq.id
FROM {adaptivequiz} adpq
JOIN {adaptivequiz_attempt} adpqa ON adpq.id = adpqa.instance
WHERE adpqa.uniqueid = :uniqueid
AND adpqa.instance = :instance
AND adpqa.userid = :userid
AND adpq.minimumquestions <= adpqa.questionsattempted
ORDER BY adpq.id ASC";
$param = array('uniqueid' => $uniqueid, 'instance' => $instance, 'userid' => $userid);
$exists = $DB->record_exists_sql($sql, $param);
return $exists;
}
/**
* This checks if the session property, needed to beging an attempt with a password, has been initialized
* @param int $instance the activity instance id
* @return bool true
*/
function adaptivequiz_user_entered_password($instance) {
global $SESSION;
$conditions = isset($SESSION->passwordcheckedadpq) && is_array($SESSION->passwordcheckedadpq) &&
array_key_exists($instance, $SESSION->passwordcheckedadpq) && true === $SESSION->passwordcheckedadpq[$instance];
return $conditions;
}
/**
* Given a list of tags on a question, answer the question's difficulty.
*
* @param array $tags the tags on a question.
* @return int|null the difficulty level or null if unknown.
*/
function adaptivequiz_get_difficulty_from_tags(array $tags) {
foreach ($tags as $tag) {
if (preg_match('/^'.ADAPTIVEQUIZ_QUESTION_TAG.'([0-9]+)$/', $tag, $matches)) {
return (int) $matches[1];
}
}
return null;
}
/**
* @return array int => lang string the options for calculating the quiz grade
* from the individual attempt grades.
*/
function adaptivequiz_get_grading_options() {
return array(
ADAPTIVEQUIZ_GRADEHIGHEST => get_string('gradehighest', 'adaptivequiz'),
ADAPTIVEQUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'adaptivequiz'),
ADAPTIVEQUIZ_ATTEMPTLAST => get_string('attemptlast', 'adaptivequiz')
);
}
/**
* Return grade for given user or all users.
*
* @param stdClass $adaptivequiz The adaptivequiz
* @param int $userid optional user id, 0 means all users
* @return array array of grades, false if none. These are raw grades. They should
* be processed with adaptivequiz_format_grade for display.
*/
function adaptivequiz_get_user_grades($adaptivequiz, $userid = 0) {
global $CFG, $DB;
$params = array(
'instance' => $adaptivequiz->id,
'attemptstate' => attempt_state::COMPLETED,
);
$userwhere = '';
if ($userid) {
$params['userid'] = $userid;
$userwhere = 'AND aa.userid = :userid';
}
$sql = "SELECT aa.uniqueid, aa.userid, aa.measure, aa.timemodified, aa.timecreated, a.highestlevel,
a.lowestlevel
FROM {adaptivequiz_attempt} aa
JOIN {adaptivequiz} a ON aa.instance = a.id
WHERE aa.instance = :instance
AND aa.attemptstate = :attemptstate
$userwhere";
$records = $DB->get_records_sql($sql, $params);
$grades = array();
foreach ($records as $grade) {
$grade->rawgrade = catalgo::map_logit_to_scale($grade->measure,
$grade->highestlevel, $grade->lowestlevel);
if (empty($grades[$grade->userid])) {
// Store the first attempt.
$grades[$grade->userid] = $grade;
} else {
// If additional attempts are recorded, uses the settings to determine
// which one to report.
if ($adaptivequiz->grademethod == ADAPTIVEQUIZ_ATTEMPTFIRST) {
if ($grade->timemodified < $grades[$grade->userid]->timemodified) {
$grades[$grade->userid] = $grade;
}
} else if ($adaptivequiz->grademethod == ADAPTIVEQUIZ_ATTEMPTLAST) {
if ($grade->timemodified > $grades[$grade->userid]->timemodified) {
$grades[$grade->userid] = $grade;
}
} else {
// By default, use the highst grade.
if ($grade->rawgrade > $grades[$grade->userid]->rawgrade) {
$grades[$grade->userid] = $grade;
}
}
}
}
return $grades;
}
/**
* KNIGHT (Feature 2): decides whether a user may review a given attempt.
*
* Teachers who can view reports may review any attempt. A student may review only their own attempt, only
* when the activity has enabled review of own attempts (showownattemptresult), and only once that
* attempt is completed - a student must never be able to review the questions of an attempt that is
* still in progress (e.g. by calling reviewattempt.php directly).
*
* @param stdClass $adaptivequiz The adaptivequiz instance.
* @param stdClass $attempt A record from {adaptivequiz_attempt}.
* @param context $context The module context.
* @param int $userid The user requesting to review.
* @return bool
*/
function adaptivequiz_user_can_review_attempt(stdClass $adaptivequiz, stdClass $attempt, context $context, int $userid): bool {
if (has_capability('mod/adaptivequiz:viewreport', $context, $userid)) {
return true;
}
return $attempt->userid == $userid
&& !empty($adaptivequiz->showownattemptresult)
&& $attempt->attemptstate === attempt_state::COMPLETED;
}
/**
* KNIGHT (Feature 3): counts completed attempts across all users of an activity.
*
* @param int $instanceid The adaptivequiz instance id.
* @return int
*/
function adaptivequiz_count_completed_attempts(int $instanceid): int {
global $DB;
return $DB->count_records(
'adaptivequiz_attempt',
['instance' => $instanceid, 'attemptstate' => attempt_state::COMPLETED]
);
}
/**
* KNIGHT (Feature 3): whether a review of the question analysis is currently due.
*
* The reminder is due once at least one further trigger interval of completed attempts has accrued
* since the last review. The completed-attempts count at the last review is stored in questionschecked
* (0 = never reviewed), so the reminder cannot be re-armed merely by reloading a page. A trigger of 0
* disables the reminder.
*
* @param stdClass $adaptivequiz The adaptivequiz instance.
* @param int $completedattempts Total completed attempts across all users.
* @return bool
*/
function adaptivequiz_question_check_reminder_due(stdClass $adaptivequiz, int $completedattempts): bool {
return $adaptivequiz->questionchecktrigger > 0
&& $completedattempts >= $adaptivequiz->questionschecked + $adaptivequiz->questionchecktrigger;
}
/**
* KNIGHT (Feature 4): maps an attempt's ability measure (logits) onto the activity's level scale.
*
* @param stdClass $attempt The attempt record (uses its measure).
* @param stdClass $adaptivequiz The adaptivequiz instance (uses lowestlevel/highestlevel).
* @return float|null The ability on the level scale, or null when the attempt has no measure.
*/
function adaptivequiz_ability_from_measure(stdClass $attempt, stdClass $adaptivequiz): ?float {
if (!isset($attempt->measure) || $attempt->measure === null) {
return null;
}
$abilityfraction = 1 / (1 + exp(-1 * $attempt->measure));
return (($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel) * $abilityfraction) + $adaptivequiz->lowestlevel;
}
/**
* KNIGHT (Feature 4): the levels whose competency descriptions feed the feedback / feedforward / feed-up
* blocks for a given ability.
*
* When the ability is within a small margin of a whole level, the three blocks use the level below, the
* level itself and the level above. Otherwise the ability sits between two levels: feedback uses the
* lower level, feed-up the higher one, and there is no feedforward.
*
* @param float $ability The ability on the level scale.
* @return array{feedback: int, feedforward: int|null, feedup: int}
*/
function adaptivequiz_competency_feedback_levels(float $ability): array {
$round = (int) round($ability);
$margin = 0.2;
// A small tolerance so a value mathematically on the margin (e.g. exactly 0.2 away) is treated as
// within it, rather than being tipped out by floating-point representation error.
$epsilon = 1e-9;
if (abs($ability - $round) <= $margin + $epsilon) {
return ['feedback' => $round - 1, 'feedforward' => $round, 'feedup' => $round + 1];
}
return ['feedback' => (int) floor($ability), 'feedforward' => null, 'feedup' => (int) ceil($ability)];
}
/**
* KNIGHT (Feature 4): stores the per-level competency descriptions entered in the activity settings.
*
* Acts only on the competencydesc{level} fields actually submitted (the settings form generates one per
* level in the configured range). It deliberately does NOT rely on lowestlevel/highestlevel - in Moodle 5
* those live in a separate item-administration form and are absent from the normal activity save - and it
* only touches the submitted levels, so descriptions for levels the form did not show are preserved.
*
* @param int $adaptivequizid The adaptivequiz instance id.
* @param stdClass $data Submitted form data, with a competencydesc{level} property per shown level.
* @return void
*/
function adaptivequiz_save_competency_descriptions(int $adaptivequizid, stdClass $data): void {
global $DB;
foreach ((array) $data as $field => $value) {
if (!preg_match('/^competencydesc(\d+)$/', $field, $matches)) {
continue;
}
$level = (int) $matches[1];
$existing = $DB->get_record(
'adaptivequiz_competencydesc',
['adaptivequizid' => $adaptivequizid, 'level' => $level]
);
if (trim((string) $value) === '') {
// An emptied field removes the stored description for that level.
if ($existing) {
$DB->delete_records('adaptivequiz_competencydesc', ['id' => $existing->id]);
}
} else if (!$existing) {
$DB->insert_record('adaptivequiz_competencydesc', (object) [
'adaptivequizid' => $adaptivequizid,
'level' => $level,
'description' => $value,
]);
} else if ($existing->description !== $value) {
$existing->description = $value;
$DB->update_record('adaptivequiz_competencydesc', $existing);
}
}
}
/**
* KNIGHT (Feature 4): whether the competency feedback may be enabled for a given level range.
*
* The feature needs one description field per difficulty level, so it can only be enabled for a valid
* difficulty range - positive levels that ascend (lowest >= 1 and lowest < highest, matching what the
* item-administration settings expect) - that spans at most ADAPTIVEQUIZ_MAX_COMPETENCY_LEVELS levels.
*
* @param int|null $lowestlevel The activity's lowest difficulty level (null if not set yet).
* @param int|null $highestlevel The activity's highest difficulty level (null if not set yet).
* @return bool
*/
function adaptivequiz_competency_feedback_selectable(?int $lowestlevel, ?int $highestlevel): bool {
if ($lowestlevel === null || $highestlevel === null || $lowestlevel < 1 || $highestlevel <= $lowestlevel) {
return false;
}
return ($highestlevel - $lowestlevel + 1) <= ADAPTIVEQUIZ_MAX_COMPETENCY_LEVELS;
}
<?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/>.
/**
* Definition of activity settings form.
*
* @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
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/moodleform_mod.php');
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
use mod_adaptivequiz\attempt_feedback_placeholders_helper;
use mod_adaptivequiz\output\editor_placeholders;
/**
* Module instance settings form
*/
class mod_adaptivequiz_mod_form extends moodleform_mod {
/**
* Form definition.
*/
public function definition() {
global $OUTPUT;
$mform = $this->_form;
// Adding the "general" fieldset, where all the common settings are showed.
$mform->addElement('header', 'general', get_string('general', 'form'));
// Adding the standard "name" field.
$mform->addElement('text', 'name', get_string('adaptivequizname', 'adaptivequiz'), ['size' => '64']);
if (!empty($CFG->formatstringstriptags)) {
$mform->setType('name', PARAM_TEXT);
} else {
$mform->setType('name', PARAM_CLEANHTML);
}
$mform->addRule('name', null, 'required', null, 'client');
$mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
$mform->addHelpButton('name', 'adaptivequizname', 'adaptivequiz');
// Adding the standard "intro" and "introformat" fields.
// Use the non deprecated function if it exists.
if (method_exists($this, 'standard_intro_elements')) {
$this->standard_intro_elements();
} else {
// Deprecated as of Moodle 2.9.
$this->add_intro_editor();
}
// Number of attempts.
$attemptoptions = ['0' => get_string('unlimited')];
for ($i = 1; $i <= ADAPTIVEQUIZMAXATTEMPT; $i++) {
$attemptoptions[$i] = $i;
}
$mform->addElement('select', 'attempts', get_string('attemptsallowed', 'adaptivequiz'), $attemptoptions);
$mform->setDefault('attempts', 0);
$mform->addHelpButton('attempts', 'attemptsallowed', 'adaptivequiz');
// Require password to begin adaptivequiz attempt.
$mform->addElement('passwordunmask', 'password', get_string('requirepassword', 'adaptivequiz'));
$mform->setType('password', PARAM_TEXT);
$mform->addHelpButton('password', 'requirepassword', 'adaptivequiz');
// Browser security choices.
$options = [
get_string('no'),
get_string('yes'),
];
$mform->addElement('select', 'browsersecurity', get_string('browsersecurity', 'adaptivequiz'), $options);
$mform->addHelpButton('browsersecurity', 'browsersecurity', 'adaptivequiz');
$mform->setDefault('browsersecurity', 0);
// KNIGHT (UI): group the settings that show information to students under a dedicated section.
$mform->addElement('header', 'showinfoheader', get_string('showinfoheader', 'adaptivequiz'));
$mform->addElement('select', 'showattemptprogress', get_string('modformshowattemptprogress', 'adaptivequiz'),
[get_string('no'), get_string('yes')]);
$mform->addHelpButton('showattemptprogress', 'modformshowattemptprogress', 'adaptivequiz');
$mform->setDefault('showattemptprogress', 0);
$mform->addElement('select', 'showabilitymeasuresummary', get_string('showabilitymeasuresummary', 'adaptivequiz'),
[get_string('no'), get_string('yes')]);
$mform->addHelpButton('showabilitymeasuresummary', 'showabilitymeasuresummary', 'adaptivequiz');
$mform->setDefault('showabilitymeasuresummary', 0);
// KNIGHT (Feature 5): show the current question's difficulty level to students during the attempt.
$mform->addElement(
'select',
'showquestiondifficultylevel',
get_string('modformshowquestiondifficultylevel', 'adaptivequiz'),
[get_string('no'), get_string('yes')]
);
$mform->addHelpButton('showquestiondifficultylevel', 'modformshowquestiondifficultylevel', 'adaptivequiz');
$mform->setDefault('showquestiondifficultylevel', 0);
// KNIGHT (Feature 6): per-question immediate feedback during the attempt, with an optional solution reveal.
$mform->addElement(
'select',
'immediatefeedback',
get_string('immediatefeedback', 'adaptivequiz'),
[get_string('no'), get_string('yes')]
);
$mform->addHelpButton('immediatefeedback', 'immediatefeedback', 'adaptivequiz');
$mform->setDefault('immediatefeedback', 0);
$mform->addElement(
'select',
'immediatefeedbackshowsolution',
get_string('immediatefeedbackshowsolution', 'adaptivequiz'),
[get_string('no'), get_string('yes')]
);
$mform->addHelpButton('immediatefeedbackshowsolution', 'immediatefeedbackshowsolution', 'adaptivequiz');
$mform->setDefault('immediatefeedbackshowsolution', 0);
$mform->hideIf('immediatefeedbackshowsolution', 'immediatefeedback', 'eq', 0);
// KNIGHT (Feature 2): allow students to review their own attempts.
$mform->addElement(
'select',
'showownattemptresult',
get_string('showownattemptresult', 'adaptivequiz'),
[get_string('no'), get_string('yes')]
);
$mform->addHelpButton('showownattemptresult', 'showownattemptresult', 'adaptivequiz');
$mform->setDefault('showownattemptresult', 0);
$mform->addElement('header', 'attemptfeedbackhdr', get_string('attemptfeedbackhdr', 'adaptivequiz'));
$isnewinstance = !$this->current->instance;
if (!$isnewinstance) {
$customfeedbackenabled = $this->current->attemptfeedbackenable;
if ($customfeedbackenabled == 1 || $customfeedbackenabled == -1) {
$mform->setExpanded('attemptfeedbackhdr');
}
}
$mform->addElement('advcheckbox', 'attemptfeedbackenable', get_string('attemptfeedbackenable', 'adaptivequiz'));
$mform->addElement('editor', 'attemptfeedbackeditor', get_string('attemptfeedback', 'adaptivequiz'),
['rows' => 10],
['maxfiles' => EDITOR_UNLIMITED_FILES, 'noclean' => true, 'context' => $this->context, 'subdirs' => true]);
$mform->setType('attemptfeedbackeditor', PARAM_RAW);
$mform->addHelpButton('attemptfeedbackeditor', 'attemptfeedback', 'adaptivequiz');
$mform->disabledIf('attemptfeedbackeditor', 'attemptfeedbackenable', 'notchecked');
$feedbackplaceholders = new editor_placeholders(attempt_feedback_placeholders_helper::configured()->placeholder_options());
$feedbackplaceholderscontent = $OUTPUT->render_from_template('mod_adaptivequiz/editor_placeholders_desc',
$feedbackplaceholders->export_for_template($OUTPUT));
$mform->addElement('static', 'attemptfeedbackplaceholdersdesc', '', $feedbackplaceholderscontent);
$mform->addHelpButton('attemptfeedbackplaceholdersdesc', 'attemptfeedbackplaceholdersdesc', 'adaptivequiz');
$mform->addElement('select', 'showabilitymeasurefeedback', get_string('showabilitymeasurefeedback', 'adaptivequiz'),
[get_string('no'), get_string('yes')]);
$mform->addHelpButton('showabilitymeasurefeedback', 'showabilitymeasurefeedback', 'adaptivequiz');
$mform->setDefault('showabilitymeasurefeedback', 0);
// KNIGHT (Feature 4): result-dependent competency feedback. Placed right after the attempt feedback
// section, as it belongs with it. It is enabled explicitly, and only when the difficulty level span
// is small enough to keep the settings form usable (one description field per level - see
// ADAPTIVEQUIZ_MAX_COMPETENCY_LEVELS). The level range lives in the separate item-administration
// form, so it is read from the saved instance and updates after a save.
$mform->addElement('header', 'competencyheader', get_string('competencydescriptions', 'adaptivequiz'));
$mform->addElement(
'advcheckbox',
'competencyfeedbackenable',
get_string('competencyfeedbackenable', 'adaptivequiz')
);
$mform->addHelpButton('competencyfeedbackenable', 'competencyfeedbackenable', 'adaptivequiz');
$lowest = isset($this->current->lowestlevel) && is_numeric($this->current->lowestlevel)
? (int) $this->current->lowestlevel : null;
$highest = isset($this->current->highestlevel) && is_numeric($this->current->highestlevel)
? (int) $this->current->highestlevel : null;
if (adaptivequiz_competency_feedback_selectable($lowest, $highest)) {
// A description field per level, shown only when the feature is enabled.
for ($level = $lowest; $level <= $highest; $level++) {
$name = 'competencydesc' . $level;
$mform->addElement('textarea', $name, get_string('competencydesc', 'adaptivequiz', $level));
$mform->setType($name, PARAM_TEXT);
$mform->hideIf($name, 'competencyfeedbackenable', 'notchecked');
}
} else if ($lowest === null || $highest === null) {
// New instance / no range yet: explain that the fields appear once the range is saved.
$mform->addElement('static', 'competencydescinfo', '', get_string('competencydescinfo', 'adaptivequiz'));
} else {
// The range is set but not suitable (too many levels, or not positive/ascending): explain why.
$mform->addElement(
'static',
'competencydescrange',
'',
get_string(
'competencyfeedbackrangeunsuitable',
'adaptivequiz',
['max' => ADAPTIVEQUIZ_MAX_COMPETENCY_LEVELS, 'lowest' => $lowest, 'highest' => $highest]
)
);
}
$mform->addElement('header', 'advancedhdr', get_string('advanced'));
$mform->addElement('advcheckbox', 'debuginfoenable', get_string('debuginfoenable', 'adaptivequiz'));
$mform->addHelpButton('debuginfoenable', 'debuginfoenable', 'adaptivequiz');
// Grade settings.
$this->standard_grading_coursemodule_elements();
$mform->removeElement('grade');
// Grading method.
$mform->addElement('select', 'grademethod', get_string('grademethod', 'adaptivequiz'),
adaptivequiz_get_grading_options());
$mform->addHelpButton('grademethod', 'grademethod', 'adaptivequiz');
$mform->setDefault('grademethod', ADAPTIVEQUIZ_GRADEHIGHEST);
$mform->disabledIf('grademethod', 'attempts', 'eq', 1);
// Add standard elements, common to all modules.
$this->standard_coursemodule_elements();
// Add standard buttons, common to all modules.
$this->add_action_buttons();
}
/**
* Extra validation for the activity settings.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
// KNIGHT (Feature 4): the competency feedback can only be enabled for a set, small level range.
if (!empty($data['competencyfeedbackenable'])) {
$lowest = isset($this->current->lowestlevel) && is_numeric($this->current->lowestlevel)
? (int) $this->current->lowestlevel : null;
$highest = isset($this->current->highestlevel) && is_numeric($this->current->highestlevel)
? (int) $this->current->highestlevel : null;
if (!adaptivequiz_competency_feedback_selectable($lowest, $highest)) {
if ($lowest === null || $highest === null) {
$errors['competencyfeedbackenable'] = get_string('competencyfeedbacknolevelrange', 'adaptivequiz');
} else {
$errors['competencyfeedbackenable'] = get_string(
'competencyfeedbackrangeunsuitable',
'adaptivequiz',
['max' => ADAPTIVEQUIZ_MAX_COMPETENCY_LEVELS, 'lowest' => $lowest, 'highest' => $highest]
);
}
}
}
return $errors;
}
/**
* Custom completion rules support.
*/
public function add_completion_rules(): array {
$form = $this->_form;
$form->addElement('checkbox', 'completionattemptcompleted', ' ',
get_string('completionattemptcompletedform', 'adaptivequiz'));
return ['completionattemptcompleted'];
}
/**
* Custom completion rules support.
*/
public function completion_rule_enabled($data): bool {
if (!isset($data['completionattemptcompleted'])) {
return false;
}
return $data['completionattemptcompleted'] != 0;
}
/**
* Overrides the parent's method.
*
* @param array $defaultvalues Passed by reference, the parameter's original name is changed to meet the code style.
*/
public function data_preprocessing(&$defaultvalues) {
global $DB;
parent::data_preprocessing($defaultvalues);
$isnewinstance = !$this->current->instance;
if ($isnewinstance) {
return;
}
// KNIGHT (Feature 4): populate the per-level competency description fields from stored values.
$competencydescs = $DB->get_records(
'adaptivequiz_competencydesc',
['adaptivequizid' => $this->current->instance]
);
foreach ($competencydescs as $rec) {
$defaultvalues['competencydesc' . $rec->level] = $rec->description;
}
// Whether the instance is in 'transition state' to start using the editor-powered custom feedback.
$newcustomfeedbackpending = $this->current->attemptfeedbackenable == -1;
if ($newcustomfeedbackpending) {
$legacycustomfeedbackenabled = !empty($this->current->attemptfeedback);
if ($legacycustomfeedbackenabled) {
$defaultvalues['attemptfeedbackenable'] = 1;
$defaultvalues['attemptfeedbackeditor'] = [
'text' => $defaultvalues['attemptfeedback'],
'format' => FORMAT_HTML,
];
return;
}
$defaultvalues['attemptfeedbackenable'] = 0;
$defaultvalues['attemptfeedbackeditor'] = [
'text' => '',
'format' => FORMAT_HTML,
];
return;
}
$feedbackdraftitemid = file_get_submitted_draft_itemid('attemptfeedback');
if (!empty($defaultvalues['attemptfeedback'])) {
$defaultvalues['attemptfeedbackeditor'] = [
'text' => file_prepare_draft_area($feedbackdraftitemid, $this->context->id, 'mod_adaptivequiz', 'attemptfeedback',
0, ['subdirs' => 0], $defaultvalues['attemptfeedback']),
'itemid' => $feedbackdraftitemid,
'format' => $defaultvalues['attemptfeedbackformat'],
];
}
}
}
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