Commit 7f723723 authored by 0815-xyz's avatar 0815-xyz
Browse files

Initial commit: Transfer from MOODLE_404_KNIGHT

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/>.
/**
* Redirect users who clicked on a link in the gradebook.
*
* @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');
$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', array('id' => $cm->course))) {
throw new moodle_exception("coursemisconf");
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
if (has_capability('mod/adaptivequiz:viewreport', $context)) {
$params = array('cmid' => $id);
if ($userid) {
$params['userid'] = $userid;
$url = new moodle_url('/mod/adaptivequiz/viewattemptreport.php', $params);
} else {
$url = new moodle_url('/mod/adaptivequiz/viewreport.php', $params);
}
} else {
$params = array('id' => $id);
$url = new moodle_url('/mod/adaptivequiz/view.php', $params);
}
redirect($url);
exit;
<?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/>.
/**
* Strings for the German 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
* strings addd by KNIGHT project
*/
defined('MOODLE_INTERNAL') || die();
$string['modformshowownattemptresult'] = 'Quiz-Ergebnis für Teilnehmer/innen anzeigen';
$string['modformshowownattemptresult_help'] = 'Es kann sinnvoll sein, den Teilnehmenden die Möglichkeit zu geben, ihre Abgaben zu überprüfen. Wenn diese Einstellung aktiviert ist, kann der Teilnehmer in der Zusammenfassung der Versuche auf das Lupensymbol klicken, um frühere Versuche einzusehen.';
$string['backtousersallattemptspage'] = '&laquo; Zurück zur Versuchsübersicht';
$string['acceptancethreshold'] = 'Akzeptanzschwelle für Teilpunkte';
$string['acceptancethreshold_help'] = 'Geben Sie einen Wert zwischen 0 und 1 ein, um das akzeptierte Maß an Korrektheit festzulegen. Ein Wert von 0 erlaubt jede Teilpunktzahl als korrekt.';
$string['formacceptanceleveloutofbounds'] = 'Die Akzeptanzschwelle muss ein Wert zwischen 0 and 1 sein.';
$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.';
$string['competencydescriptions'] = 'Kompetenz-Beschreibungen';
$string['estimatedscore'] = 'Geschätztes Fähigkeitsniveau';
$string['incomplete'] = 'Unvollständig';
$string['invalidattempt'] = 'Ungültiger Versuch';
$string['formtoomanylevels'] = 'Das dynamische Feedback kann derzeit nur einen Bereich von maximal 10 Schwierigkeitsstufen abdecken. Bitte geben Sie einen Wert kleiner als {$a} ein.';
$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ähigkeitsniveaus 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_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_passingrequirement'] = 'Um den Kurs erfolgreich zu absolvieren, wird erwartet, dass Studierende 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.';
$string['feedback_encouragement'] = 'Sie sind auf einem guten Weg. Versuchen Sie, in einem weiteren Durchgang ihr Ergebnis zu verbessern!';
<?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.
*
* @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['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['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.';
//KNIGHT: Strings for fields to show/hide attempts table to students.
$string['modformshowownattemptresult'] = 'Show attempt results to students';
$string['modformshowownattemptresult_help'] = 'Sometimes it may be useful to provide students with the ability to review their own results. With this setting enabled a student may press a link to review their previous attempts after finishing the attempt and in the attempts summary as well.';
$string['showabilitymeasure'] = 'Show ability measure to students';
$string['showabilitymeasure_help'] = 'Sometimes it may be useful to reveal ability estimations to students after taking an adaptive quiz. With this setting enabled a student may see ability estimation in the attempts summary and right after finishing an attempt as well.';
$string['questionspoolerrornovalidstartingquestions'] = 'The selected questions categories do not contain questions which are properly tagged to match the selected starting level of difficulty.';
$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 attempt';
// KNIGHT: String for the link to return to the Adaptive Quiz main page.
$string['backtousersallattemptspage'] = '&laquo; Back to all 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: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';
// KNIGHT
$string['acceptancethreshold'] = 'Acceptance Threshold';
$string['acceptancethreshold_help'] = 'Enter a value between 0 and 1 to specify the acceptable level of correctness, where 0 allows any fraction of partial credit.';
$string['formacceptanceleveloutofbounds'] = 'Acceptance threshold must ' .
'be a value between 0 and 1.';
$string['questionpool'] = 'Question pool';
$string['questionpool_help'] = 'Select the question category(ies) where the activity will pull questions from during an attempt.';
$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['formquestionpool'] = 'Select at least one question category';
$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['stopingconditionshdr'] = 'Stopping conditions';
$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['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.';
$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';
$string['competencydescriptions'] = 'Competency Descriptions';
$string['estimatedscore'] = 'Estimated Ability Score';
$string['invalidattempt'] = 'Invalid attempt';
$string['formtoomanylevels'] = 'The dynamic feedback feature can currently only cover a maximum span of 10 difficulty levels. Please enter a value less than {$a}.';
// Feedback narrative strings.
$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_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_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.';
$string['feedback_encouragement'] = 'Keep up the good work! Try improving your score with another attempt!';
// Detailed Feedback Button.
$string['detailedfeedback'] = 'Detailed Feedback';
//Difficulty Level Strings.
$string['difficultylevel'] = 'Difficulty Level {$a}';
\ No newline at end of file
<?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');
require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.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 object $adaptivequiz: An object from the form in mod_form.php
* @param mod_adaptivequiz_mod_form $mform: A formslib object
* @return int The id of the newly inserted adaptivequiz record
*/
function adaptivequiz_add_instance(stdClass $adaptivequiz, mod_adaptivequiz_mod_form $mform = null) {
global $DB;
$time = time();
$adaptivequiz->timecreated = $time;
$adaptivequiz->timemodified = $time;
$adaptivequiz->attemptfeedbackformat = 0;
// Ensure acceptancethreshold defaults to 0 if not provided.
/*
if (empty($adaptivequiz->acceptancethreshold)) {
$adaptivequiz->acceptancethreshold = 0;
}
*/
$instance = $DB->insert_record('adaptivequiz', $adaptivequiz);
if (empty($instance) && is_int($instance)) {
return $instance;
}
$adaptivequiz->id = $instance;
// Save question tag association data.
adaptivequiz_add_questcat_association($adaptivequiz->id, $adaptivequiz);
// Update related grade item.
adaptivequiz_grade_item_update($adaptivequiz);
//Saving Dynamic Competency Description
save_dynamic_competency_descriptions($adaptivequiz->id, $adaptivequiz);
return $instance;
}
/**
* This function creates question category association record(s).
*
* @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 object $adaptivequiz: An object from the form in mod_form.php
* @param mod_adaptivequiz_mod_form $mform: A formslib object
* @return boolean Success/Fail
*/
function adaptivequiz_update_instance(stdClass $adaptivequiz, mod_adaptivequiz_mod_form $mform = null) {
global $DB;
$adaptivequiz->timemodified = time();
$adaptivequiz->id = $adaptivequiz->instance;
// Get the current value, so we can see what changed.
$oldquiz = $DB->get_record('adaptivequiz', array('id' => $adaptivequiz->instance));
// Check if 'acceptancethreshold' is submitted empty.
if (empty($adaptivequiz->acceptancethreshold) && $oldquiz) {
if ($adaptivequiz->acceptancethreshold === 0
|| $adaptivequiz->acceptancethreshold === 0.0
|| $adaptivequiz->acceptancethreshold === '0'
|| $adaptivequiz->acceptancethreshold === "0.0"
) {
$adaptivequiz->acceptancethreshold = 0;
} else {
$adaptivequiz->acceptancethreshold = $oldquiz->acceptancethreshold;
}
} else if (empty($adaptivequiz->acceptancethreshold)) {
// valid values
$adaptivequiz->acceptancethreshold = 0;
}
$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);
}
save_dynamic_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));
}
// 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 {
if (!has_capability('mod/adaptivequiz:viewreport', $settingsnav->get_page()->cm->context)) {
return;
}
$node = navigation_node::create(get_string('questionanalysisbtn', 'adaptivequiz'),
new moodle_url('/mod/adaptivequiz/questionanalysis/overview.php', ['cmid' => $settingsnav->get_page()->cm->id]),
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
*
* @category grade
* @param object $adaptivequiz object
* @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 ($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');
}
}
/**
* 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 {
// Otherwise, check that the attempt is active.
require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php');
// 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.
*
* @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');
/**
* 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
*/
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 healper method to create default
* @param object $context A context object
* @return mixed The default category in the course context or false
*/
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
*/
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;
}
/**
* Calculate the score and standard error for an attempt.
*
* @param stdClass $attempt The attempt object containing measure and standard error.
* @param stdClass $adaptivequiz The adaptive quiz object containing highest and lowest levels.
* @return string Formatted score with ± standard error.
*/
function adaptivequiz_calculate_score(stdClass $attempt, stdClass $adaptivequiz): string {
if (!isset($attempt->measure)) {
return 'n/a';
}
$abilityfraction = 1 / (1 + exp(-1 * $attempt->measure));
$ability = (($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel) * $abilityfraction) + $adaptivequiz->lowestlevel;
$stderror = catalgo::convert_logit_to_percent($attempt->standarderror);
return ($stderror > 0)
? round($ability, 2) . " &nbsp; &plusmn; " . round($stderror * 100, 1) . "%"
: 'n/a';
}
//KNGIHT Save dynamic competency descriptions
/**
* Saves dynamic competency descriptions for the adaptivequiz.
*
* @param int $adaptivequizid The ID of the adaptivequiz.
* @param stdClass $data The data containing competency descriptions.
*/
function save_dynamic_competency_descriptions($adaptivequizid, $data) {
global $DB;
$DB->delete_records('adaptivequiz_competencydesc', ['adaptivequizid' => $adaptivequizid]);
for ($i = $data->lowestlevel; $i <= $data->highestlevel; $i++) {
$fieldname = 'competencydesc' . $i;
if (!empty($data->$fieldname)) {
$record = new stdClass();
$record->adaptivequizid = $adaptivequizid;
$record->level = $i;
$record->description = $data->$fieldname;
$DB->insert_record('adaptivequiz_competencydesc', $record);
}
}
}
<?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.
*
* @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\local\repository\questions_repository;
/**
* Module instance settings form
*/
class mod_adaptivequiz_mod_form extends moodleform_mod {
public function definition() {
global $PAGE;
$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);
// Retireve a list of available course categories.
adaptivequiz_make_default_categories($this->context);
$options = adaptivequiz_get_question_categories($this->context);
$selquestcat = adaptivequiz_get_selected_question_cateogires($this->_instance);
$select = $mform->addElement('select', 'questionpool', get_string('questionpool', 'adaptivequiz'), $options);
$mform->addHelpButton('questionpool', 'questionpool', 'adaptivequiz');
$select->setMultiple(true);
$mform->addRule('questionpool', null, 'required', null, 'client');
$mform->getElement('questionpool')->setSelected($selquestcat);
$mform->addElement('text', 'startinglevel', get_string('startinglevel', 'adaptivequiz'),
['size' => '3', 'maxlength' => '3']);
$mform->addHelpButton('startinglevel', 'startinglevel', 'adaptivequiz');
$mform->addRule('startinglevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$mform->addRule('startinglevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setType('startinglevel', PARAM_INT);
$mform->addElement('text', 'lowestlevel', get_string('lowestlevel', 'adaptivequiz'),
['size' => '3', 'maxlength' => '3']);
$mform->addHelpButton('lowestlevel', 'lowestlevel', 'adaptivequiz');
$mform->addRule('lowestlevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$mform->addRule('lowestlevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setType('lowestlevel', PARAM_INT);
$mform->addElement('text', 'highestlevel', get_string('highestlevel', 'adaptivequiz'),
['size' => '3', 'maxlength' => '3']);
$mform->addHelpButton('highestlevel', 'highestlevel', 'adaptivequiz');
$mform->addRule('highestlevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$mform->addRule('highestlevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setType('highestlevel', PARAM_INT);
// KNIGHT
$mform->addElement('text', 'acceptancethreshold', get_string('acceptancethreshold', 'adaptivequiz'),
['size' => '4', 'maxlength' => '4']);
$mform->addHelpButton('acceptancethreshold', 'acceptancethreshold', 'adaptivequiz');
$mform->addRule('acceptancethreshold', get_string('formelementdecimal', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setType('acceptancethreshold', PARAM_FLOAT);
//KNIGHT: question check trigger element
$mform->addElement('text', 'questionchecktrigger', get_string('questionchecktrigger', 'adaptivequiz'),
['size' => '3', 'maxlength' => '3']);
$mform->addHelpButton('questionchecktrigger', 'questionchecktrigger', 'adaptivequiz');
$mform->addRule('questionchecktrigger', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setDefault('questionchecktrigger', 0);
$mform->setType('questionchecktrigger', PARAM_INT);
$mform->addElement('textarea', 'attemptfeedback', get_string('attemptfeedback', 'adaptivequiz'),
'wrap="virtual" rows="10" cols="50"');
$mform->addHelpButton('attemptfeedback', 'attemptfeedback', 'adaptivequiz');
$mform->setType('attemptfeedback', PARAM_NOTAGS);
$mform->addElement('select', 'showabilitymeasure', get_string('showabilitymeasure', 'adaptivequiz'),
[get_string('no'), get_string('yes')]);
$mform->addHelpButton('showabilitymeasure', 'showabilitymeasure', 'adaptivequiz');
$mform->setDefault('showabilitymeasure', 1);
$mform->addElement('select', 'showattemptprogress', get_string('modformshowattemptprogress', 'adaptivequiz'),
[get_string('no'), get_string('yes')]);
$mform->addHelpButton('showattemptprogress', 'modformshowattemptprogress', 'adaptivequiz');
$mform->setDefault('showattemptprogress', 1);
//KNIGHT: Form field for options show/hide attempts table to students.
$mform->addElement('select', 'showownattemptresult', get_string('modformshowownattemptresult', 'adaptivequiz'), [get_string('no'), get_string('yes')]);
$mform->addHelpButton('showownattemptresult', 'modformshowownattemptresult', 'adaptivequiz');
$mform->setDefault('showownattemptresult', 1);
$mform->addElement('header', 'stopingconditionshdr', get_string('stopingconditionshdr', 'adaptivequiz'));
$mform->addElement('text', 'minimumquestions', get_string('minimumquestions', 'adaptivequiz'),
['size' => '3', 'maxlength' => '3']);
$mform->addHelpButton('minimumquestions', 'minimumquestions', 'adaptivequiz');
$mform->addRule('minimumquestions', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$mform->addRule('minimumquestions', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setType('minimumquestions', PARAM_INT);
$mform->addElement('text', 'maximumquestions', get_string('maximumquestions', 'adaptivequiz'),
['size' => '3', 'maxlength' => '3']);
$mform->addHelpButton('maximumquestions', 'maximumquestions', 'adaptivequiz');
$mform->addRule('maximumquestions', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$mform->addRule('maximumquestions', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setType('maximumquestions', PARAM_INT);
$mform->addElement('text', 'standarderror', get_string('standarderror', 'adaptivequiz'),
['size' => '10', 'maxlength' => '10']);
$mform->addHelpButton('standarderror', 'standarderror', 'adaptivequiz');
$mform->addRule('standarderror', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$mform->addRule('standarderror', get_string('formelementdecimal', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setDefault('standarderror', 5.0);
$mform->setType('standarderror', PARAM_FLOAT);
//KNIGHT Getting the current value from lowest level and highest level
$submitteddata = $this->_customdata['data'] ?? [];
$lowestlevel = optional_param('lowestlevel', $submitteddata['lowestlevel'] ?? 1, PARAM_INT);
$highestlevel = optional_param('highestlevel', $submitteddata['highestlevel'] ?? 4, PARAM_INT);
$mform->addElement('header', 'competencyheader', get_string('competencydescriptions', 'adaptivequiz'));
// Define the actual form elements for each level.
// for ($i = $lowestlevel; $i <= $highestlevel; $i++) {
for ($i = 1; $i <= 20; $i++) {
$name = 'competencydesc' . $i;
$label = get_string('difficultylevel', 'adaptivequiz', $i);
$mform->addElement('textarea', $name, $label);
$mform->setType($name, PARAM_TEXT);
}
// 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();
//KNIGHT JS code to update the Dynamic Competency Description
$PAGE->requires->js_init_code("
function updateCompetencyFieldVisibility() {
const lowestInput = document.getElementById('id_lowestlevel');
const highestInput = document.getElementById('id_highestlevel');
if (!lowestInput || !highestInput) return;
const lowest = parseInt(lowestInput.value);
let highest = parseInt(highestInput.value);
if (!isNaN(lowest) && !isNaN(highest) && highest > lowest + 9) {
alert('The dynamic feedback feature can only cover 10 levels. Max allowed highest level is ' + (lowest + 9) + '.');
highest = lowest + 9;
highestInput.value = highest;
}
for (let i = 1; i <= 20; i++) {
const fieldRow = document.getElementById('fitem_id_competencydesc' + i);
if (!fieldRow) continue;
fieldRow.style.display = (i >= lowest && i <= highest) ? '' : 'none';
}
}
function bindInputEvents() {
const lowest = document.getElementById('id_lowestlevel');
const highest = document.getElementById('id_highestlevel');
if (lowest) lowest.addEventListener('input', updateCompetencyFieldVisibility);
if (highest) highest.addEventListener('input', updateCompetencyFieldVisibility);
}
function observeFormChanges() {
const form = document.querySelector('form.mform');
if (!form) return;
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' || mutation.type === 'subtree') {
// Re-bind and re-run every time DOM updates
bindInputEvents();
updateCompetencyFieldVisibility();
break;
}
}
});
observer.observe(form, {
childList: true,
subtree: true
});
}
window.addEventListener('load', () => {
bindInputEvents();
updateCompetencyFieldVisibility();
observeFormChanges();
});
");
}
public function add_completion_rules(): array {
$form = $this->_form;
$form->addElement('checkbox', 'completionattemptcompleted', ' ',
get_string('completionattemptcompletedform', 'adaptivequiz'));
return ['completionattemptcompleted'];
}
public function completion_rule_enabled($data): bool {
if (!isset($data['completionattemptcompleted'])) {
return false;
}
return $data['completionattemptcompleted'] != 0;
}
/**
* Perform extra validation. @see validation() in moodleform_mod.php.
*
* @param array $data Array of submitted form values.
* @param array $files Array of file data.
* @return array Array of form elements that didn't pass validation.
* @throws coding_exception
* @throws dml_exception
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (empty($data['questionpool'])) {
$errors['questionpool'] = get_string('formquestionpool', 'adaptivequiz');
}
// Validate for positivity.
if (0 >= $data['minimumquestions']) {
$errors['minimumquestions'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['maximumquestions']) {
$errors['maximumquestions'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['startinglevel']) {
$errors['startinglevel'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['lowestlevel']) {
$errors['lowestlevel'] = get_string('formelementnegative', 'adaptivequiz');
}
if (0 >= $data['highestlevel']) {
$errors['highestlevel'] = get_string('formelementnegative', 'adaptivequiz');
}
// KNIGHT
if (isset($data['acceptancethreshold'])
&& ($data['acceptancethreshold'] < 0
|| $data['acceptancethreshold'] > 1)
) {
$errors['acceptancethreshold'] = get_string(
'formacceptanceleveloutofbounds', 'adaptivequiz'
);
}
if ((float) 0 > (float) $data['standarderror'] || (float) 50 <= (float) $data['standarderror']) {
$errors['standarderror'] = get_string('formstderror', 'adaptivequiz');
}
// Validate higher and lower values.
if ($data['minimumquestions'] >= $data['maximumquestions']) {
$errors['minimumquestions'] = get_string('formminquestgreaterthan', 'adaptivequiz');
}
if ($data['lowestlevel'] >= $data['highestlevel']) {
$errors['lowestlevel'] = get_string('formlowlevelgreaterthan', 'adaptivequiz');
}
if ($data['highestlevel'] > $data['lowestlevel'] + 9) {
$limit = $data['lowestlevel'] + 9;
$errors['highestlevel'] = get_string('formtoomanylevels', 'adaptivequiz', $limit + 1);
}
if (!($data['startinglevel'] >= $data['lowestlevel'] && $data['startinglevel'] <= $data['highestlevel'])) {
$errors['startinglevel'] = get_string('formstartleveloutofbounds', 'adaptivequiz');
}
if ($questionspoolerrormsg = $this->validate_questions_pool($data['questionpool'], $data['startinglevel'])) {
$errors['questionpool'] = $questionspoolerrormsg;
}
return $errors;
}
// KNIGHT: This function is used to preprocess the data before it is displayed in the form.
/**
* Processes the default values for the form.
* Checks if a previously saved value for 'acceptancethreshold'
* exists and applies it.
* If no value exists, sets the 'acceptancethreshold' to 0 by default.
* @param array $defaultvalues An associative array holding the
* default values for the form.
* @return void This function does not return a value.
* Preprocesses the data before it is displayed in the form.
* It retrieves the lowest and highest levels from the data or from the request parameters.
* It ensures that the highest level does not exceed the lowest level by more than 9.
* It populates competency description fields based on existing records in the database.
*
* @param array $data The data to be preprocessed.
*/
public function data_preprocessing(&$data) {
global $DB;
// Check if a previously saved value for 'acceptancethreshold' exists.
if (isset($data['acceptancethreshold'])) {
// Use the saved value.
$data['acceptancethreshold']
= $data['acceptancethreshold'];
} else {
// Default to 0 if no value is saved.
$data['acceptancethreshold'] = 0;
}
$lowest = $data['lowestlevel'] ?? optional_param('lowestlevel', 1, PARAM_INT);
$highest = $data['highestlevel'] ?? optional_param('highestlevel', 4, PARAM_INT);
//KNIGHT Enforce a max span of 10
$highest = min($highest, $lowest + 9);
if (!empty($data['id'])) {
$records = $DB->get_records('adaptivequiz_competencydesc', ['adaptivequizid' => $data['id']]);
foreach ($records as $rec) {
$data['competencydesc' . $rec->level] = $rec->description;
}
}
for ($i = $lowest; $i <= $highest; $i++) {
$fieldname = 'competencydesc' . $i;
if (!isset($data[$fieldname])) {
$data[$fieldname] = '';
}
}
}
/**
* @param int[] $qcategoryidlist A list of id of selected questions categories.
* @return string An error message if any.
* @throws coding_exception
*/
private function validate_questions_pool(array $qcategoryidlist, int $startinglevel): string {
return questions_repository::count_adaptive_questions_in_pool_with_level($qcategoryidlist, $startinglevel) > 0
? ''
: get_string('questionspoolerrornovalidstartingquestions', 'adaptivequiz');
}
}
// 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/>.
/**
* JavaScript library for the adaptivequiz module.
*
* @package mod_adaptivequiz
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2024 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
M.mod_adaptivequiz = M.mod_adaptivequiz || {};
M.mod_adaptivequiz.init_attempt_form = function(Y, url, secure) {
// Check if the page is on the password required page
if (null == document.getElementById('id_quizpassword')) {
require(['core_question/question_engine'], function(QuestionEngine) {
QuestionEngine.initForm('#responseform');
});
require(['core_form/changechecker'], function(FormChangeChecker) {
FormChangeChecker.watchFormById('responseform');
});
} else {
if ('1' == secure) {
Y.on('click', function(e) {
M.mod_adaptivequiz.secure_window.close(url, 0)
}, '#id_cancel');
}
}
};
M.mod_adaptivequiz.secure_window = {
init: function(Y) {
if (window.location.href.substring(0, 4) == 'file') {
window.location = 'about:blank';
}
Y.delegate('contextmenu', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('mousedown', M.mod_adaptivequiz.secure_window.prevent_mouse, document, '*');
Y.delegate('mouseup', M.mod_adaptivequiz.secure_window.prevent_mouse, document, '*');
Y.delegate('dragstart', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('selectstart', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('cut', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('copy', M.mod_adaptivequiz.secure_window.prevent, document, '*');
Y.delegate('paste', M.mod_adaptivequiz.secure_window.prevent, document, '*');
M.mod_adaptivequiz.secure_window.clear_status;
Y.on('beforeprint', function() {
Y.one(document.body).setStyle('display', 'none');
}, window);
Y.on('afterprint', function() {
Y.one(document.body).setStyle('display', 'block');
}, window);
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'press:67,86,88+ctrl');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'up:67,86,88+ctrl');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'down:67,86,88+ctrl');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'press:67,86,88+meta');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'up:67,86,88+meta');
Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'down:67,86,88+meta');
},
clear_status: function() {
window.status = '';
setTimeout(M.mod_adaptivequiz.secure_window.clear_status, 10);
},
prevent: function(e) {
alert(M.str.adaptivequiz.functiondisabledbysecuremode);
e.halt();
},
prevent_mouse: function(e) {
if (e.button == 1 && /^(INPUT|TEXTAREA|BUTTON|SELECT|LABEL|A)$/i.test(e.target.get('tagName'))) {
// Left click on a button or similar. No worries.
return;
}
e.halt();
},
/**
* Event handler for the adaptivequiz start attempt button.
*/
start_attempt_action: function(e, args) {
if (args.startattemptwarning == '') {
openpopup(e, args);
} else {
M.util.show_confirm_dialog(e, {
message: args.startattemptwarning,
callback: function() {
openpopup(e, args);
},
continuelabel: M.util.get_string('startattempt', 'quiz')
});
}
},
init_close_button: function(Y, url) {
Y.on('click', function(e) {
M.mod_adaptivequiz.secure_window.close(url, 0)
}, '#secureclosebutton');
},
close: function(Y, url, delay) {
setTimeout(function() {
if (window.opener) {
window.opener.document.location.reload();
window.close();
} else {
window.location.href = url;
}
}, delay * 1000);
}
};
M.mod_adaptivequiz.init_comment_popup = function(Y) {
// Add a close button to the window.
var closebutton = Y.Node.create('<input type="button" />');
closebutton.set('value', M.util.get_string('cancel', 'moodle'));
Y.one('#id_submitbutton').ancestor().append(closebutton);
Y.on('click', function() { window.close() }, closebutton);
}
M.mod_adaptivequiz.init_reviewattempt = function(Y) {
Y.one('#adpq_scoring_table').hide();
Y.one('#adpq_scoring_table_link_icon').setContent('&#9654;');
Y.use('node', function(Y) {
Y.delegate('click', function(e) {
if (e.currentTarget.get('id') === 'adpq_scoring_table_link') {
var table = Y.one('#adpq_scoring_table');
table.toggleView();
if (table.getComputedStyle('display') === 'none') {
Y.one('#adpq_scoring_table_link_icon').setContent('&#9654;');
} else {
Y.one('#adpq_scoring_table_link_icon').setContent('&#9660;');
}
}
}, document, 'a');
});
};
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" preserveAspectRatio="xMinYMid meet">
<path fill-rule="evenodd" clip-rule="evenodd" stroke="#212529" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" d="m 3.5255474,15.525547 v 1.7998 c 0,1.1201 0,1.6798 0.21799,2.1076 0.19174,0.3763 0.49748,0.6829 0.87381,0.8746 0.4274,0.2178 0.9871903,0.218556 2.10511,0.2178 l 13.7519956,-0.0093 m -16.9489056,-4.9905 0,-11.0220973 m -1e-7,11.0220973 3.8534,-3.2111 0.00318,-0.0026 c 0.69708,-0.5809 1.0463,-0.8719 1.42496,-0.9901 0.44733,-0.1397 0.92917,-0.1176 1.3620107,0.062 0.36695,0.1522 0.68875,0.474 1.33225,1.1175 l 0.0064,0.0064 c 0.6535,0.6535 0.9811,0.9812 1.354,1.133 0.4412,0.1798 0.9323,0.1953 1.3845,0.0453 0.3834,-0.1272 0.7335,-0.4331 1.4336,-1.0456 l 4.804566,-4.2770198"/>
</svg>
<?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 Middlebury College {@link http://www.middlebury.edu/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__).'/../../../config.php');
require_once($CFG->dirroot.'/lib/grouplib.php');
require_once(dirname(__FILE__).'/../locallib.php');
use mod_adaptivequiz\local\questionanalysis\quiz_analyser;
use mod_adaptivequiz\local\questionanalysis\statistics\discrimination_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\percent_correct_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\times_used_statistic;
$id = required_param('cmid', PARAM_INT);
$sortdir = optional_param('sortdir', 'DESC', PARAM_ALPHA);
$sort = optional_param('sort', 'times_used', PARAM_ALPHANUMEXT);
$page = optional_param('page', 0, PARAM_INT);
if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
throw new moodle_exception("coursemisconf");
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
require_capability('mod/adaptivequiz:viewreport', $context);
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*');
//KNIGHT: Turn on question check notification if number of completed attempts exceeds value of trigger
$completedattemptscount = adaptivequiz_count_user_previous_attempts($adaptivequiz->id, $USER->id);
if($adaptivequiz->questionschecked == 0 && $completedattemptscount >= $adaptivequiz->questionchecktrigger) {
$adaptivequiz->questionschecked = 1;
$DB->update_record('adaptivequiz', $adaptivequiz);
}
$PAGE->set_url('/mod/adaptivequiz/questionanalysis/overview.php', array('cmid' => $cm->id));
$title = get_string('reportquestionanalysispageheading', 'adaptivequiz', format_string($adaptivequiz->name));
$PAGE->set_title($title);
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$output = $PAGE->get_renderer('mod_adaptivequiz', 'questionanalysis');
$quizanalyzer = new quiz_analyser();
$quizanalyzer->load_attempts($cm->instance);
$quizanalyzer->add_statistic('times_used', new times_used_statistic());
$quizanalyzer->add_statistic('percent_correct', new percent_correct_statistic());
$quizanalyzer->add_statistic('discrimination', new discrimination_statistic());
$headers = $quizanalyzer->get_header();
$records = $quizanalyzer->get_records($sort, $sortdir);
$recordscount = count($records);
$records = array_slice($records, $page * ADAPTIVEQUIZ_REC_PER_PAGE, ADAPTIVEQUIZ_REC_PER_PAGE);
// Merge the question id and names into links.
unset($headers['id']);
foreach ($records as &$record) {
$id = array_shift($record);
$url = new moodle_url('/mod/adaptivequiz/questionanalysis/single.php',
array('cmid' => $cm->id, 'qid' => $id, 'sort' => $sort, 'sortdir' => $sortdir, 'page' => $page));
$record[0] = html_writer::link($url, $record[0]);
}
/* print header information */
$header = $output->print_header();
$title = $output->heading($title);
/* Output attempts table */
$reporttable = $output->get_report_table($headers, $records, $cm, '/mod/adaptivequiz/questionanalysis/overview.php', $sort,
$sortdir);
/* Output paging bar */
$pagingbar = $output->print_paging_bar($recordscount, $page, ADAPTIVEQUIZ_REC_PER_PAGE, $cm,
'/mod/adaptivequiz/questionanalysis/overview.php', $sort, $sortdir);
/* Output footer information */
$footer = $output->print_footer();
echo $header;
echo $pagingbar;
echo $title;
echo $reporttable;
echo $pagingbar;
echo $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/>.
/**
* @copyright 2013 Middlebury College {@link http://www.middlebury.edu/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__).'/../../../config.php');
require_once($CFG->dirroot.'/lib/grouplib.php');
require_once(dirname(__FILE__).'/../locallib.php');
use mod_adaptivequiz\local\questionanalysis\quiz_analyser;
use mod_adaptivequiz\local\questionanalysis\statistics\answers_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\discrimination_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\percent_correct_statistic;
use mod_adaptivequiz\local\questionanalysis\statistics\times_used_statistic;
$id = required_param('cmid', PARAM_INT);
$qid = required_param('qid', PARAM_INT);
$sortdir = optional_param('sortdir', 'DESC', PARAM_ALPHA);
$sort = optional_param('sort', 'times_used', PARAM_ALPHANUMEXT);
$page = optional_param('page', 0, PARAM_INT);
if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
throw new moodle_exception("coursemisconf");
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
require_capability('mod/adaptivequiz:viewreport', $context);
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*');
$quizanalyzer = new quiz_analyser();
$quizanalyzer->load_attempts($cm->instance);
$questionanalyzer = $quizanalyzer->get_question_analyzer($qid);
$definition = $questionanalyzer->get_question_definition();
$PAGE->set_url('/mod/adaptivequiz/questionanalysis/single.php', array('cmid' => $cm->id, 'qid' => $qid));
$PAGE->set_title(format_string($definition->name));
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$output = $PAGE->get_renderer('mod_adaptivequiz', 'questionanalysis');
$quizanalyzer->add_statistic('times_used', new times_used_statistic());
$quizanalyzer->add_statistic('percent_correct', new percent_correct_statistic());
$quizanalyzer->add_statistic('discrimination', new discrimination_statistic());
$quizanalyzer->add_statistic('answers', new answers_statistic());
$headers = $quizanalyzer->get_header();
$record = $quizanalyzer->get_record($qid);
// Get rid of the question id and name columns.
unset($headers['id'], $headers['name']);
array_shift($record);
array_shift($record);
/* print header information */
$header = $output->print_header();
$title = $output->heading(get_string('question_report', 'adaptivequiz'));
/* return link */
$url = new moodle_url('/mod/adaptivequiz/questionanalysis/overview.php',
array('cmid' => $cm->id, 'sort' => $sort, 'sortdir' => $sortdir, 'page' => $page));
$returnlink = html_writer::link($url, get_string('back_to_all_questions', 'adaptivequiz'));
/* Output attempts table */
$details = $output->get_question_details($questionanalyzer, $context);
$reporttable = $output->get_single_question_report($headers, $record, $cm, '/mod/adaptivequiz/questionanalysis/overview.php',
$sort, $sortdir);
/* Output footer information */
$footer = $output->print_footer();
echo $header;
echo $returnlink;
echo $title;
echo $details;
echo $reporttable;
echo $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/>.
/**
* Contains definition of the renderer class 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
*/
defined('MOODLE_INTERNAL') || die();
use mod_adaptivequiz\form\requiredpassword;
use mod_adaptivequiz\local\attempt\attempt_state;
use mod_adaptivequiz\local\catalgo;
use mod_adaptivequiz\output\ability_measure;
use mod_adaptivequiz\output\attempt_progress;
use mod_adaptivequiz\output\report\attempt_administration_report;
use mod_adaptivequiz\output\report\attempt_answers_distribution_report;
use mod_adaptivequiz\output\report\individual_user_attempts\individual_user_attempt_action;
use mod_adaptivequiz\output\report\individual_user_attempts\individual_user_attempt_actions;
use mod_adaptivequiz\output\user_attempt_summary;
/**
* The renderer class for the plugin.
*
* @package mod_adaptivequiz
*/
class mod_adaptivequiz_renderer extends plugin_renderer_base {
/** @var string $sortdir the sorting direction being used */
protected $sortdir = '';
/** @var moodle_url $sorturl the current base url used for keeping the table sorted */
protected $sorturl = '';
/** @var int $groupid variable used to reference the groupid that is currently being used to filter by */
public $groupid = 0;
/** @var array options that should be used for opening the secure popup. */
protected static $popupoptions = array(
'left' => 0,
'top' => 0,
'fullscreen' => true,
'scrollbars' => false,
'resizeable' => false,
'directories' => false,
'toolbar' => false,
'titlebar' => false,
'location' => false,
'status' => false,
'menubar' => false
);
/**
* Returns content for a button to start an adaptive quiz attempt or a notification when starting an attempt is not available.
*
* @param int $cmid
* @param bool $attemptallowed
* @param bool $browsersecurityenabled
* @return string
*/
public function attempt_controls_or_notification(int $cmid, bool $attemptallowed, bool $browsersecurityenabled): string {
if (!$attemptallowed) {
return html_writer::div(get_string('noattemptsallowed', 'adaptivequiz'), 'alert alert-info text-center');
}
if ($browsersecurityenabled) {
return $this->display_start_attempt_form_secured($cmid);
}
return html_writer::link(new moodle_url('/mod/adaptivequiz/attempt.php', ['cmid' => $cmid, 'sesskey' => sesskey()]),
get_string('startattemptbtn', 'adaptivequiz'), ['class' => 'btn btn-primary']);
}
/**
* This function sets up the javascript required by the page
* @return array a standard jsmodule structure.
*/
public function adaptivequiz_get_js_module() {
return array(
'name' => 'mod_adaptivequiz',
'fullpath' => '/mod/adaptivequiz/module.js',
'requires' => array('base', 'dom', 'event-delegate', 'event-key', 'core_question_engine',
'moodle-core-formchangechecker'),
'strings' => array(array('cancel', 'moodle'), array('changesmadereallygoaway', 'moodle'),
array('functiondisabledbysecuremode', 'adaptivequiz'))
);
}
/**
* This function generates the HTML markup to render the submission form.
*
* @param int $cmid
* @param question_usage_by_activity $quba
* @param int $slot Slot number of the question to be displayed.
* @param int $level Difficulty level of question.
* @param int $questionnumber The order number of question in the quiz.
*/
public function question_submit_form($cmid, $quba, $slot, $level, int $questionnumber): string {
$output = '';
$processurl = new moodle_url('/mod/adaptivequiz/attempt.php');
// Start the form.
$attr = array('action' => $processurl, 'method' => 'post', 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
'id' => 'responseform');
$output .= html_writer::start_tag('form', $attr);
$output .= html_writer::start_tag('div');
// Print the question.
$options = new question_display_options();
$options->hide_all_feedback();
$options->flags = question_display_options::HIDDEN;
$options->marks = question_display_options::HIDDEN;
$output .= $quba->render_question($slot, $options, $questionnumber);
$output .= html_writer::start_tag('div', ['class' => 'submitbtns adaptivequizbtn mdl-align']);
$output .= html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'submitanswer',
'value' => get_string('submitanswer', 'mod_adaptivequiz'), 'class' => 'btn btn-primary']);
$output .= html_writer::end_tag('div');
// Some hidden fields to track what is going on.
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cmid', 'value' => $cmid));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'uniqueid', 'value' => $quba->get_id()));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', 'value' => $slot));
$output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'dl', 'value' => $level));
// Finish the form.
$output .= html_writer::end_tag('div');
$output .= html_writer::end_tag('form');
return $output;
}
/**
* This function initializing the metadata that needs to be included in the page header
* before the page is rendered.
* @param question_usage_by_activity $quba a question usage by activity object
* @param int|array $slots slot number of the question to be displayed or an array of slot numbers
* @return string HTML header information for displaying the question
*/
public function init_metadata($quba, $slots) {
$meta = '';
if (is_array($slots)) {
foreach ($slots as $slot) {
$meta .= $quba->render_question_head_html($slot);
}
} else {
$meta .= $quba->render_question_head_html($slots);
}
$meta .= question_engine::initialise_js();
return $meta;
}
/**
* @throws coding_exception
*/
public function attempt_feedback(string $attemptfeedback, int $cmid, ?ability_measure $abilitymeasure,
bool $popup = false): string {
$output = html_writer::start_div('text-center');
$url = new moodle_url('/mod/adaptivequiz/view.php');
$attr = ['action' => $url, 'method' => 'post', 'id' => 'attemptfeedback'];
$output .= html_writer::start_tag('form', $attr);
if (empty(trim($attemptfeedback))) {
$attemptfeedback = get_string('attemptfeedbackdefaulttext', 'adaptivequiz');
}
$output .= html_writer::tag('p', s($attemptfeedback), ['class' => 'submitbtns adaptivequizfeedback']);
if ($abilitymeasure) {
$output .= $this->render($abilitymeasure);
}
if (empty($popup)) {
$attr = ['type' => 'submit', 'name' => 'attemptfinished', 'value' => get_string('continue'),
'class' => 'btn btn-primary'];
$output .= html_writer::empty_tag('input', $attr);
} else {
// In a 'secure' popup window.
$this->page->requires->js_init_call('M.mod_adaptivequiz.secure_window.init_close_button', [$url],
$this->adaptivequiz_get_js_module());
$output .= html_writer::empty_tag('input', ['type' => 'button', 'value' => get_string('continue'),
'id' => 'secureclosebutton', 'class' => 'btn btn-primary']);
}
$output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'id', 'value' => $cmid]);
$output .= html_writer::end_tag('form');
$output .= html_writer::end_div();
return $output;
}
/**
* Output a page with an optional message, and JavaScript code to close the
* current window and redirect the parent window to a new URL.
* @param moodle_url $url the URL to redirect the parent window to.
* @param string $message message to display before closing the window. (optional)
* @return string HTML to output.
*/
public function close_attempt_popup($url, $message = '') {
$output = '';
$output .= $this->header();
$output .= $this->box_start();
if ($message) {
$output .= html_writer::tag('p', $message);
$output .= html_writer::tag('p', get_string('windowclosing', 'quiz'));
$delay = 5;
} else {
$output .= html_writer::tag('p', get_string('pleaseclose', 'quiz'));
$delay = 0;
}
$this->page->requires->js_init_call('M.mod_quiz.secure_window.close',
array($url, $delay), false, adaptivequiz_get_js_module());
$output .= $this->box_end();
$output .= $this->footer();
return $output;
}
/**
* This function returns page header information to be printed to the page
* @return string HTML markup for header inforation
*/
public function print_header() {
return $this->header();
}
/**
* This function returns page footer information to be printed to the page
* @return string HTML markup for footer inforation
*/
public function print_footer() {
return $this->footer();
}
/**
* This function creates the table header links that will be used to allow instructor to sort the data
* @param stdClass $cm a course module object set to the instance of the activity
* @param string $sort the column the the table is to be sorted by
* @param string $sortdir the direction of the sort
* @return array an array of column headers (firstname / lastname, number of attempts, standard error)
*/
public function format_report_table_headers($cm, $sort, $sortdir) {
$firstname = '';
$lastname = '';
$email = '';
$numofattempts = '';
$measure = '';
$standarderror = '';
$timemodified = '';
/* Determine the next sorting direction and icon to display */
switch ($sortdir) {
case 'ASC':
$imageparam = array('src' => $this->image_url('t/down'), 'alt' => '');
$columnicon = html_writer::empty_tag('img', $imageparam);
$newsortdir = 'DESC';
break;
default:
$imageparam = array('src' => $this->image_url('t/up'), 'alt' => '');
$columnicon = html_writer::empty_tag('img', $imageparam);
$newsortdir = 'ASC';
break;
}
/* Set the sort direction class variable */
$this->sortdir = $sortdir;
/* Create header links */
$param = array('cmid' => $cm->id, 'sort' => 'firstname', 'sortdir' => 'ASC', 'group' => $this->groupid);
$firstnameurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'lastname';
$lastnameurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'email';
$emailurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'attempts';
$numofattemptsurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'measure';
$measureurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'stderror';
$standarderrorurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
$param['sort'] = 'timemodified';
$timemodifiedurl = new moodle_url('/mod/adaptivequiz/viewreport.php', $param);
/* Update column header links with a sorting directional icon */
switch ($sort) {
case 'firstname':
$firstnameurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $firstnameurl;
$firstname .= '&nbsp;'.$columnicon;
break;
case 'lastname':
$lastnameurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $lastnameurl;
$lastname .= '&nbsp;'.$columnicon;
break;
case 'email':
$emailurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $emailurl;
$email .= '&nbsp;'.$columnicon;
break;
case 'attempts':
$numofattemptsurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $numofattemptsurl;
$numofattempts .= '&nbsp;'.$columnicon;
break;
case 'measure':
$measureurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $measureurl;
$measure .= '&nbsp;'.$columnicon;
break;
case 'stderror':
$standarderrorurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $standarderrorurl;
$standarderror .= '&nbsp;'.$columnicon;
break;
case 'timemodified':
$timemodifiedurl->params(array('sortdir' => $newsortdir));
$this->sorturl = $timemodifiedurl;
$timemodified .= '&nbsp;'.$columnicon;
break;
}
// Create header HTML markup.
$firstname = html_writer::link($firstnameurl, get_string('firstname')).$firstname;
$lastname = html_writer::link($lastnameurl, get_string('lastname')).$lastname;
$email = html_writer::link($emailurl, get_string('email')).$email;
$numofattempts = html_writer::link($numofattemptsurl, get_string('numofattemptshdr', 'adaptivequiz')).$numofattempts;
$measure = html_writer::link($measureurl, get_string('bestscore', 'adaptivequiz')).$measure;
$standarderror = html_writer::link($standarderrorurl, get_string('bestscorestderror', 'adaptivequiz')).$standarderror;
$timemodified = html_writer::link($timemodifiedurl, get_string('attemptfinishedtimestamp', 'adaptivequiz')).$timemodified;
return array($firstname.' / '.$lastname, $email, $numofattempts, $measure, $standarderror, $timemodified);
}
/**
* This function adds rows to the html_table object
* @param stdClass $records adaptivequiz_attempt records
* @param stdClass $cm course module object set to the instance of the activity
* @param html_table $table an instance of the html_table class
*/
protected function get_report_table_rows($records, $cm, $table) {
foreach ($records as $record) {
$attemptlink = new moodle_url('/mod/adaptivequiz/viewattemptreport.php',
array('userid' => $record->id, 'cmid' => $cm->id));
$link = html_writer::link($attemptlink, $record->attempts);
$measure = $this->format_measure($record);
if ($record->uniqueid) {
$attemptlink = new moodle_url('/mod/adaptivequiz/reviewattempt.php',
array('userid' => $record->id, 'uniqueid' => $record->uniqueid, 'cmid' => $cm->id));
$measure = html_writer::link($attemptlink, $measure);
}
$stderror = $this->format_standard_error($record);
if (intval($record->timemodified)) {
$timemodified = userdate(intval($record->timemodified));
} else {
$timemodified = get_string('na', 'adaptivequiz');
}
$profileurl = new moodle_url('/user/profile.php', array('id' => $record->id));
$name = $record->firstname.' '.$record->lastname;
$namelink = html_writer::link($profileurl, $name);
$emaillink = html_writer::link('mailto:'.$record->email, $record->email);
$row = array($namelink, $emaillink, $link, $measure, $stderror, $timemodified);
$table->data[] = $row;
$table->rowclasses[] = 'studentattempt';
}
}
/**
* This function prints paging information
* @param int $totalrecords the total number of records returned
* @param int $page the current page the user is on
* @param int $perpage the number of records displayed on one page
* @return string HTML markup
*/
public function print_paging_bar($totalrecords, $page, $perpage) {
$baseurl = $this->sorturl;
/* Set the currently set group filter and sort dir */
$baseurl->params(array('group' => $this->groupid, 'sortdir' => $this->sortdir));
$output = '';
$output .= $this->paging_bar($totalrecords, $page, $perpage, $baseurl);
return $output;
}
/**
* This function prints a grouping selector
* @param stdClass $cm course module object set to the instance of the activity
* @param stdClass $course a data record for the current course
* @param stdClass $context the context instance for the activity
* @param int $userid the current user id
* @return string HTML markup
*/
public function print_groups_selector($cm, $course, $context, $userid) {
$output = '';
$groupmode = groups_get_activity_groupmode($cm, $course);
if (0 != $groupmode) {
$baseurl = new moodle_url('/mod/adaptivequiz/viewreport.php', array('cmid' => $cm->id));
$output = groups_print_activity_menu($cm, $baseurl, true);
}
return $output;
}
/**
* Initialize secure browsing mode.
*/
public function init_browser_security($disablejsfeatures = true) {
$this->page->set_popup_notification_allowed(false); // Prevent message notifications.
$this->page->set_cacheable(false);
$this->page->set_pagelayout('popup');
if ($disablejsfeatures) {
$this->page->add_body_class('quiz-secure-window');
$this->page->requires->js_init_call('M.mod_adaptivequiz.secure_window.init',
null, false, $this->adaptivequiz_get_js_module());
}
}
/**
* This function displays a form for users to enter a password before entering the attempt
* @param int $cmid course module id
* @return requiredpassword instance of a formslib object
*/
public function display_password_form($cmid): requiredpassword {
$url = new moodle_url('/mod/adaptivequiz/attempt.php');
return new requiredpassword($url->out_omit_querystring(),
array('hidden' => array('cmid' => $cmid, 'uniqueid' => 0)));
}
/**
* This function prints a form and a button that is centered on the page, then the user clicks on the button the user is taken
* to the url
* @param moodle_url $url a url
* @param string $buttontext button caption
* @return string - HTML markup displaying the description and form with a submit button
*/
public function print_form_and_button($url, $buttontext) {
$html = '';
$attributes = array('method' => 'POST', 'action' => $url);
$html .= html_writer::start_tag('form', $attributes);
$html .= html_writer::empty_tag('br');
$html .= html_writer::empty_tag('br');
$html .= html_writer::start_tag('center');
$params = array('type' => 'submit', 'value' => $buttontext, 'class' => 'submitbtns adaptivequizbtn');
$html .= html_writer::empty_tag('input', $params);
$html .= html_writer::end_tag('center');
$html .= html_writer::end_tag('form');
return $html;
}
/**
* This function formats the ability measure into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the ability measure. Ability measure is rounded to the nearest decimal.
*/
public function format_measure($record) {
if (is_null($record->measure)) {
return 'n/a';
}
return round(catalgo::map_logit_to_scale($record->measure, $record->highestlevel, $record->lowestlevel), 1);
}
/**
* This function formats the standard error into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the standard error. Standard error is
* rounded to the nearest one hundredth then multiplied by 100
*/
public function format_standard_error($record) {
if (is_null($record->stderror) || $record->stderror == 0.0) {
return 'n/a';
}
$percent = round(catalgo::convert_logit_to_percent($record->stderror), 2) * 100;
return '&plusmn; '.$percent.'%';
}
/**
* This function formats the standard error and ability measure into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the ability measure and standard error. Ability measure is rounded to the nearest
* decimal. Standard error is rounded to the
* nearest one hundredth then multiplied by 100
*/
protected function format_measure_and_standard_error($record) {
if (is_null($record->measure) || is_null($record->stderror) || $record->stderror == 0.0) {
return 'n/a';
}
$measure = round(catalgo::map_logit_to_scale($record->measure, $record->highestlevel, $record->lowestlevel), 1);
$percent = round(catalgo::convert_logit_to_percent($record->stderror), 2) * 100;
$format = $measure.' &plusmn; '.$percent.'%';
return $format;
}
/**
* Answer the summery information about an attempt
*
* @param stdClass $adaptivequiz See {@link mod_adaptivequiz_renderer::attempt_report_page_by_tab()}.
* @param stdClass $attempt See {@link mod_adaptivequiz_renderer::attempt_report_page_by_tab()}.
* @param stdClass $user The user who took the quiz that created the attempt.
* @return string
* @throws coding_exception
*/
public function attempt_summary_listing(stdClass $adaptivequiz, stdClass $attempt, stdClass $user): string {
$table = new html_table();
$table->attributes['class'] = 'generaltable attemptsummarytable';
// User information row.
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempt_user', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(fullname($user) . ' (' . $user->email . ')');
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
// Attempt state row.
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempt_state', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell($attempt->attemptstate);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
// Score row using the helper function.
$row = new html_table_row();
$headercell = new html_table_cell(get_string('score', 'adaptivequiz'));
$headercell->header = true;
$score = adaptivequiz_calculate_score($attempt, $adaptivequiz); // Use the centralized function.
$datacell = new html_table_cell($score);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
// Attempt start time row.
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptstarttime', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(userdate($attempt->timecreated));
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
// Attempt finish time row.
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptfinishedtimestamp', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(userdate($attempt->timemodified));
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
// Total time row.
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempttotaltime', 'adaptivequiz'));
$headercell->header = true;
$totaltime = $attempt->timemodified - $attempt->timecreated;
$hours = floor($totaltime / 3600);
$remainder = $totaltime - ($hours * 3600);
$minutes = floor($remainder / 60);
$seconds = $remainder - ($minutes * 60);
$cellcontent = sprintf('%02d', $hours).":".sprintf('%02d', $minutes).":".sprintf('%02d', $seconds);
$datacell = new html_table_cell($cellcontent);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
// Attempt stop criteria row.
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptstopcriteria', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell($attempt->attemptstopcriteria);
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
return html_writer::table($table);
}
public function attempt_review_tabs(moodle_url $pageurl, string $selected, bool $showDetailedAttempt): string {
$tabs = [];
//KNIGHT: show all the tabs only if user has access to view report.
if($showDetailedAttempt){
$attemptsummarytaburl = clone($pageurl);
$attemptsummarytaburl->param('tab', 'attemptsummary');
$tabs[] = new tabobject('attemptsummary', $attemptsummarytaburl,
get_string('reportattemptsummarytab', 'adaptivequiz'));
$attemptgraphtaburl = clone($pageurl);
$attemptgraphtaburl->param('tab', 'attemptgraph');
$tabs[] = new tabobject('attemptgraph', $attemptgraphtaburl,
get_string('reportattemptgraphtab', 'adaptivequiz'));
$answerdistributiontaburl = clone($pageurl);
$answerdistributiontaburl->param('tab', 'answerdistribution');
$tabs[] = new tabobject('answerdistribution', $answerdistributiontaburl,
get_string('reportattemptanswerdistributiontab', 'adaptivequiz'));
}
$questionsdetailstaburl = clone($pageurl);
$questionsdetailstaburl->param('tab', 'questionsdetails');
$tabs[] = new tabobject('questionsdetails', $questionsdetailstaburl,
get_string('reportattemptquestionsdetailstab', 'adaptivequiz'));
return $this->tabtree($tabs, $selected);
}
/**
* Renders attempt report content for the given tab option.
*
* @param string $tabid A tab defined in {@see self::attempt_review_tabs()}.
* @param stdClass $adaptivequiz A record from {adaptivequiz}.
* @param stdClass $attempt A record from {adaptivequiz_attempt}.
* @param stdClass $user A record from {user}.
* @param question_usage_by_activity $quba
* @param moodle_url $pageurl
* @param int $page
* @return string
*/
public function attempt_report_page_by_tab(
string $tabid,
stdClass $adaptivequiz,
stdClass $attempt,
stdClass $user,
question_usage_by_activity $quba,
moodle_url $pageurl,
int $page
): string {
if ($tabid == 'attemptgraph') {
return $this->attempt_administration_report($attempt->id);
}
if ($tabid == 'answerdistribution') {
return $this->attempt_answers_distribution_report($attempt->id);
}
if ($tabid == 'questionsdetails') {
return $this->attempt_questions_review($quba, $pageurl, $page);
}
return $this->attempt_summary_listing($adaptivequiz, $attempt, $user);
}
public function reset_users_attempts_filter_action(moodle_url $url): string {
return html_writer::link($url, get_string('reportattemptsresetfilter', 'adaptivequiz'));
}
/**
* A wrapper method to call rendering of attempt progress, accepts minimum parameters to base rendering on.
*/
public function attempt_progress(string $questionsanswered, string $maximumquestions): string {
return $this->render_attempt_progress(attempt_progress::with_defaults($questionsanswered, $maximumquestions));
}
/**
* Renders an attempt progress object, to be overridden by a theme if required.
*/
protected function render_attempt_progress(attempt_progress $progress): string {
$progress = $progress->with_help_icon_content($this->help_icon('attemptquestionsprogress', 'adaptivequiz'));
return $this->render_from_template('mod_adaptivequiz/attempt_progress', $progress->export_for_template($this));
}
/**
* Outputs available action links for an attempt in the user's attempts report.
*
* @param stdClass $attempt A record from {adaptivequiz_attempt}.
*/
public function individual_user_attempt_actions(stdClass $attempt): string {
$actions = new individual_user_attempt_actions();
$actions->add(
new individual_user_attempt_action(
new moodle_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $attempt->id]),
new pix_icon('i/search', ''),
get_string('reviewattempt', 'adaptivequiz')
)
);
if ($attempt->attemptstate !== attempt_state::COMPLETED) {
$actions->add(
new individual_user_attempt_action(
new moodle_url('/mod/adaptivequiz/closeattempt.php', ['attempt' => $attempt->id]),
new pix_icon('t/stop', ''),
get_string('closeattempt', 'adaptivequiz')
)
);
}
$actions->add(
new individual_user_attempt_action(
new moodle_url('/mod/adaptivequiz/delattempt.php', ['attempt' => $attempt->id]),
new pix_icon('t/delete', ''),
get_string('deleteattemp', 'adaptivequiz')
)
);
return $this->render($actions);
}
/**
* Renders the renderable actions object, intended to be overridden by the theme if needed.
*
* @param individual_user_attempt_actions $actions
*/
protected function render_individual_user_attempt_actions(individual_user_attempt_actions $actions): string {
return $this->render_from_template(
'mod_adaptivequiz/report/individual_user_attempt_actions',
$actions->export_for_template($this)
);
}
/**
* A wrapper method to render answers distribution report for the given attempt.
*
* @param int $attemptid
* @return string
*/
public function attempt_answers_distribution_report(int $attemptid): string {
return $this->render(new attempt_answers_distribution_report($attemptid));
}
/**
* A wrapper method to render items administration report for the given attempt.
*
* @param int $attemptid
* @return string
*/
public function attempt_administration_report(int $attemptid): string {
return $this->render(new attempt_administration_report($attemptid));
}
/**
* Renders answers distribution report.
*
* @param attempt_answers_distribution_report $report
* @return string
*/
protected function render_attempt_answers_distribution_report(attempt_answers_distribution_report $report): string {
return $this->render_from_template('mod_adaptivequiz/attempt_answers_distribution_report',
$report->export_for_template($this));
}
/**
* Renders items administration report.
*
* @param attempt_administration_report $report
* @return string
*/
protected function render_attempt_administration_report(attempt_administration_report $report): string {
return $this->render_from_template('mod_adaptivequiz/attempt_administration_report',
$report->export_for_template($this));
}
/**
* This function returns HTML markup of questions and student's responses.
* See {@link mod_adaptivequiz_renderer::attempt_report_page_by_tab} for partial parameters description.
*
* @param moodle_url $pageurl
* @param question_usage_by_activity $quba
* @param int $offset An offset used to determine which question to start processing from.
* @return string
* @throws coding_exception
* @throws moodle_exception
*/
protected function attempt_questions_review(
question_usage_by_activity $quba,
moodle_url $pageurl,
int $offset
): string {
$pager = $this->attempt_questions_review_pager($quba, $pageurl, $offset);
$questslots = $quba->get_slots();
$attr = ['class' => 'questiontags'];
$offset *= ADAPTIVEQUIZ_REV_QUEST_PER_PAGE;
$output = $pager;
// Take a portion of the array of question slots for display.
$pageqslots = array_slice($questslots, $offset, ADAPTIVEQUIZ_REV_QUEST_PER_PAGE);
// Setup display options.
$options = new question_display_options();
$options->readonly = true;
$options->flags = question_display_options::HIDDEN;
$options->marks = question_display_options::MAX_ONLY;
$options->rightanswer = question_display_options::VISIBLE;
$options->correctness = question_display_options::VISIBLE;
$options->numpartscorrect = question_display_options::VISIBLE;
// Setup quesiton header metadata.
$output .= $this->init_metadata($quba, $pageqslots);
foreach ($pageqslots as $slot) {
$label = html_writer::tag('label', get_string('questionnumber', 'adaptivequiz'));
$output .= html_writer::tag('div', $label.': '.format_string($slot));
// Retrieve question attempt object.
$questattempt = $quba->get_question_attempt($slot);
// Get question definition object.
$questdef = $questattempt->get_question();
// Retrieve the tags associated with this question.
$qtags = core_tag_tag::get_item_tags_array('core_question', 'question', $questdef->id);
$label = html_writer::tag('label', get_string('attemptquestion_level', 'adaptivequiz'));
$output .= html_writer::tag('div', $label.': '.format_string(adaptivequiz_get_difficulty_from_tags($qtags)));
$label = html_writer::tag('label', get_string('tags'));
$output .= html_writer::tag('div', $label.': '.format_string(implode(' ', $qtags)), $attr);
$output .= $quba->render_question($slot, $options);
$output .= html_writer::empty_tag('hr');
}
$output .= html_writer::empty_tag('br');
$output .= $pager;
return $output;
}
/**
* This function prints a paging link for the attempt review page.
* See {@link mod_adaptivequiz_renderer::attempt_questions_review()} for parameters description.
*
* @throws moodle_exception
*/
protected function attempt_questions_review_pager(
question_usage_by_activity $quba,
moodle_url $pageurl,
int $page
): string {
$questslots = $quba->get_slots();
$output = '';
$attr = ['class' => 'viewattemptreportpages'];
$pages = ceil(count($questslots) / ADAPTIVEQUIZ_REV_QUEST_PER_PAGE);
// Don't print anything if there is only one page.
if (1 == $pages) {
return '';
}
// Print all of the page links.
$output .= html_writer::start_tag('center');
for ($i = 0; $i < $pages; $i++) {
// If we are currently on this page, then don't make it an anchor tag.
if ($i == $page) {
$output .= '&nbsp'.html_writer::tag('span', $i + 1, $attr).'&nbsp';
continue;
}
$pageurl->params(['page' => $i]);
$output .= '&nbsp'.html_writer::link($pageurl, $i + 1, $attr).'&nbsp';
}
$output .= html_writer::end_tag('center');
return $output;
}
protected function render_ability_measure(ability_measure $measure): string {
$output = html_writer::start_div('box py-3');
$abilitymeasurecontents = get_string('abilityestimated', 'adaptivequiz') . ': ' .
html_writer::tag('strong', $this->format_measure($measure->as_object_to_format())) . ' / ' .
$measure->lowestquestiondifficulty . ' - ' . $measure->highestquestiondifficulty .
$this->help_icon('abilityestimated', 'adaptivequiz');
$output .= $this->heading($abilitymeasurecontents, 3);
$output .= html_writer::end_div();
return $output;
}
protected function render_user_attempt_summary(user_attempt_summary $summary): string {
$table = new html_table();
$table->attributes['class'] = 'generaltable attemptsummarytable';
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attempt_state', 'adaptivequiz'));
$headercell->header = true;
$datacell = new html_table_cell(get_string('recent' . $summary->attemptstate, 'adaptivequiz'));
$datacell->id = 'attemptstatecell';
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptfinishedtimestamp', 'adaptivequiz'));
$headercell->header = true;
$datacell = ($summary->attemptstate == attempt_state::COMPLETED)
? userdate($summary->timefinished)
: '-';
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
if (!empty($summary->abilitymeasure)) {
$row = new html_table_row();
$headercell = new html_table_cell(get_string('attemptquestion_ability', 'adaptivequiz') .
$this->help_icon('abilityestimated', 'adaptivequiz'));
$headercell->header = true;
$formatmeasure = new stdClass();
$formatmeasure->measure = $summary->abilitymeasure;
$formatmeasure->lowestlevel = $summary->lowestquestiondifficulty;
$formatmeasure->highestlevel = $summary->highestquestiondifficulty;
$datacell = new html_table_cell(html_writer::tag('strong', $this->format_measure($formatmeasure))
. ' / ' . $summary->lowestquestiondifficulty . ' - ' . $summary->highestquestiondifficulty);
$datacell->id = 'abilitymeasurecell';
$row->cells = [$headercell, $datacell];
$table->data[] = $row;
}
return html_writer::table($table);
}
/**
* This functions returns content for the start attempt button to start a secured browser attempt.
*
* @param int $cmid
* @return string
*/
private function display_start_attempt_form_secured(int $cmid): string {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', ['cmid' => $cmid]);
$startlink = new action_link($url, get_string('startattemptbtn', 'adaptivequiz'), null, ['class' => 'btn btn-primary']);
$this->page->requires->js_module($this->adaptivequiz_get_js_module());
$this->page->requires->js('/mod/adaptivequiz/module.js');
$popupaction = new popup_action('click', $url, 'adaptivequizpopup', self::$popupoptions);
$startlink->add_action(new component_action('click',
'M.mod_adaptivequiz.secure_window.start_attempt_action', [
'url' => $url->out(false),
'windowname' => 'adaptivequizpopup',
'options' => $popupaction->get_js_options(),
'fullscreen' => true,
'startattemptwarning' => '',
]));
$warning = html_writer::tag('noscript', $this->heading(get_string('noscript', 'quiz')));
return $this->render($startlink) . $warning;
}
}
/**
* A substitute renderer class that outputs CSV results instead of HTML.
*/
class mod_adaptivequiz_csv_renderer extends mod_adaptivequiz_renderer {
/**
* This function returns page header information to be printed to the page
* @return string HTML markup for header inforation
*/
public function print_header() {
header('Content-type: text/csv');
$filename = $this->page->title;
$filename = preg_replace('/[^a-z0-9_-]/i', '_', $filename);
$filename = preg_replace('/_{2,}/', '_', $filename);
$filename = $filename.'.csv';
header("Content-Disposition: attachment; filename=$filename");
}
/**
* This function returns page footer information to be printed to the page
* @return string HTML markup for footer inforation
*/
public function print_footer() {
// Do nothing.
}
/**
* This function prints paging information
* @param int $totalrecords the total number of records returned
* @param int $page the current page the user is on
* @param int $perpage the number of records displayed on one page
* @return string HTML markup
*/
public function print_paging_bar($totalrecords, $page, $perpage) {
// Do nothing.
}
/**
* This function returns the HTML markup to display a table of the attempts taken at the activity
* @param stdClass $records attempt records from adaptivequiz_attempt table
* @param stdClass $cm course module object set to the instance of the activity
* @param string $sort the column the the table is to be sorted by
* @param string $sortdir the direction of the sort
* @return string HTML markup
*/
public function print_report_table($records, $cm, $sort, $sortdir) {
ob_start();
$output = fopen('php://output', 'w');
$headers = array(
get_string('firstname'),
get_string('lastname'),
get_string('email'),
get_string('numofattemptshdr', 'adaptivequiz'),
get_string('bestscore', 'adaptivequiz'),
get_string('bestscorestderror', 'adaptivequiz'),
get_string('attemptfinishedtimestamp', 'adaptivequiz'),
);
fputcsv($output, $headers);
foreach ($records as $record) {
if (intval($record->timemodified)) {
$timemodified = date('c', intval($record->timemodified));
} else {
$timemodified = get_string('na', 'adaptivequiz');
}
$row = array(
$record->firstname,
$record->lastname,
$record->email,
$record->attempts,
$this->format_measure($record),
$this->format_standard_error($record),
$timemodified,
);
fputcsv($output, $row);
}
return ob_get_clean();
}
/**
* This function formats the ability measure into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the ability measure. Ability measure is rounded to the nearest decimal.
*/
public function format_measure($record) {
if (is_null($record->measure)) {
return 'n/a';
}
return round(catalgo::map_logit_to_scale($record->measure, $record->highestlevel, $record->lowestlevel), 2);
}
/**
* This function formats the standard error into a user friendly format
* @param stdClass an object with the following properties: measure, highestlevel, lowestlevel and stderror. The values must
* come from the activty instance and the user's
* attempt record
* @return string a user friendly format of the standard error. Standard error is
* rounded to the nearest one hundredth then multiplied by 100
*/
public function format_standard_error($record) {
if (is_null($record->stderror) || $record->stderror == 0.0) {
return 'n/a';
}
$percent = round(catalgo::convert_logit_to_percent($record->stderror), 2) * 100;
return $percent.'%';
}
}
<?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 view info about a certain attempt.
*
* @package mod_adaptivequiz
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/tag/lib.php');
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
$attemptid = required_param('attempt', PARAM_INT);
$page = optional_param('page', 0, PARAM_INT);
$tab = optional_param('tab', 'attemptsummary', PARAM_ALPHA);
$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);
require_capability('mod/adaptivequiz:reviewattempts', $context);
global $USER;
$user = $DB->get_record('user', ['id' => $attempt->userid], '*', MUST_EXIST);
$quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
$a = new stdClass();
$a->quizname = format_string($adaptivequiz->name);
$a->fullname = fullname($user);
$a->finished = userdate($attempt->timemodified);
// KNIGHT: Preparing a link to return to the Adaptive Quiz main page.
$url = new moodle_url('/mod/adaptivequiz/view.php', array('id' => $cm->id));
$returnlink = html_writer::link($url, get_string('backtousersallattemptspage', 'adaptivequiz'));
$title = get_string('reportattemptreviewpageheading', 'adaptivequiz', $a);
$PAGE->set_url('/mod/adaptivequiz/reviewattempt.php', ['attempt' => $attempt->id, 'tab' => $tab]);
$PAGE->set_title($title);
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_context($context);
$PAGE->navbar->add(get_string('reports'));
$PAGE->navbar->add(get_string('reportuserattemptstitleshort', 'adaptivequiz', fullname($user)));
$renderer = $PAGE->get_renderer('mod_adaptivequiz');
echo $renderer->print_header();
//KNIGHT: Setting the Question Details tab as default if user is student.
$showDetailedAttempt = has_capability('mod/adaptivequiz:viewreport', $context);
if(!$showDetailedAttempt){
//KNIGHT: Throw error when a student tries to watch someone else's attempt
if($attempt->userid != $USER->id){
throw new moodle_exception('nopermission', 'adaptivequiz');
}
$tab = optional_param('tab', 'questionsdetails', PARAM_ALPHA);
}
// KNIGHT: Adding a link to return to the Adaptive Quiz main page.
echo $returnlink;
echo $renderer->heading($title);
echo $renderer->attempt_review_tabs($PAGE->url, $tab, $showDetailedAttempt);
echo $renderer->attempt_report_page_by_tab($tab, $adaptivequiz, $attempt, $user, $quba, $PAGE->url, $page);
echo $renderer->print_footer();
.adaptivequiz-summarylist {
float: left;
}
.adaptivequiz-summarylist dt {
font-weight: bold;
}
.adaptivequiz-attemptgraph {
clear: both;
}
#adpq_scoring_table table {
float: left;
margin-right: 20px;
}
#adpq_scoring_table {
clear: both;
}
.adpq_download {
text-align: center;
margin: 10px;
}
/* Styles related to the question analysis reporting */
table.adpq_answers_table thead th.section {
text-align: left;
padding-top: 30px;
}
table.adpq_answers_table thead:first-child th.section {
padding-top: inherit;
}
.adpq_correct {
color: green;
}
.adpq_incorrect {
color: red;
}
.adpq_highlevel .adpq_incorrect {
font-weight: bold;
}
.adpq_lowlevel .adpq_correct {
font-weight: bold;
}
/**/
.usersattemptstable-wrapper {
margin-top: 1rem;
}
.usersattemptstable {
margin-bottom: 0.75rem !important;
}
.attempt-controls-or-notification-container {
padding-top: 10px;
}
#page-mod-adaptivequiz-view .attempt-progress-container {
margin-bottom: 25px;
padding-top: 10px;
}
#page-mod-adaptivequiz-view .attempt-progress .progress-bar-wrapper {
width: 60%;
border-radius: 0.5rem;
background-color: #dee2e6;
}
#page-mod-adaptivequiz-view .attempt-progress .progress-bar-inner {
height: 0.8rem;
border-radius: 0.5rem 0 0 0.5rem;
}
#page-mod-adaptivequiz-view .attempt-progress .no-out-of {
font-weight: 700;
font-size: 1.25rem;
}
.dimmed_text {
color: #aaa;
cursor: not-allowed;
text-decoration: none;
}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_administration_report
Renders a line chart for the questions administration data.
}}
{{< mod_adaptivequiz/attempt_report_chart }}
{{$initjs}}
{{#js}}
require([
'jquery',
'core/chart_builder',
'mod_adaptivequiz/attempt_administration_chart_output',
'mod_adaptivequiz/attempt_administration_chart_output_htmltable'
], function(
$,
Builder,
Output,
OutputTable
) {
var data = {{{chartdata}}},
uniqid = "{{uniqid}}",
chartArea = $('#chart-area-' + uniqid),
chartImage = chartArea.find('.chart-image'),
chartTable = chartArea.find('.chart-table-data'),
chartLink = chartArea.find('.chart-table-expand a');
Builder.make(data).then(function(ChartInst) {
new Output(chartImage, ChartInst);
new OutputTable(chartTable, ChartInst);
});
chartLink.on('click', function(e) {
e.preventDefault();
if (chartTable.is(':visible')) {
chartTable.hide();
chartLink.text({{#quote}}{{#str}}showchartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', false);
} else {
chartTable.show();
chartLink.text({{#quote}}{{#str}}hidechartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', true);
}
});
});
{{/js}}
{{/initjs}}
{{/ mod_adaptivequiz/attempt_report_chart }}
{{!
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/>.
}}
{{!
@template mod_adaptivequiz/attempt_answers_distribution_report
Renders a bar chart for the answers distribution data.
Context variables required for this template:
* showchartstacked - whether the chart should be displayed in stacked form
* userid - current user id
* adaptivequizid - id of the current adaptive quiz instance
Data attributes required for JS:
* data-action
Example context (json):
{
showchartstacked: true,
userid: 5,
adaptivequizid: 3
}
}}
<div class="mdl-align mb-3">
<span class="mr-1">
<input id="answers-distribution-chart-stacked" type="checkbox" data-action="set-answers-distribution-chart-stacked" {{#showchartstacked}}checked{{/showchartstacked}} />
<label class="mr-1" for="answers-distribution-chart-stacked">{{#str}}reportanswersdistributionchartdisplaystacked, adaptivequiz{{/str}}</label>
</span>
</div>
{{< mod_adaptivequiz/attempt_report_chart }}
{{$initjs}}
{{#js}}
require([
'jquery',
'core/chart_builder',
'core/chart_output_chartjs',
'core/chart_output_htmltable',
'mod_adaptivequiz/attempt_answers_distribution_chart_manager'
], function(
$,
Builder,
Output,
OutputTable,
ChartManager
) {
var data = {{{chartdata}}},
uniqid = "{{uniqid}}",
chartArea = $('#chart-area-' + uniqid),
chartImage = chartArea.find('.chart-image'),
chartTable = chartArea.find('.chart-table-data'),
chartLink = chartArea.find('.chart-table-expand a');
Builder.make(data).then(function(ChartInst) {
const output = new Output(chartImage, ChartInst);
ChartManager.init(output, ChartInst, {{userid}}, {{adaptivequizid}});
new OutputTable(chartTable, ChartInst);
});
chartLink.on('click', function(e) {
e.preventDefault();
if (chartTable.is(':visible')) {
chartTable.hide();
chartLink.text({{#quote}}{{#str}}showchartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', false);
} else {
chartTable.show();
chartLink.text({{#quote}}{{#str}}hidechartdata, moodle{{/str}}{{/quote}});
chartTable.attr('aria-expanded', true);
}
});
});
{{/js}}
{{/initjs}}
{{/ mod_adaptivequiz/attempt_report_chart }}
<div class="feedback-wrapper">
<h3>Adaptive Quiz Feedback</h3>
<p><strong>{{narrative_summary}}</strong></p>
<p>{{algorithm_explanation}}</p>
{{#show_feedback}}
<h4>Feedback</h4>
<p>{{feedback_intro}} (Level {{feedback_level}}):</p>
<p><strong>{{feedback_block}}</strong></p>
{{/show_feedback}}
{{#show_feedforward}}
<h4>Feedforward</h4>
<p>{{feedforward_intro}} (Level {{feedforward_level}}):</p>
<p><strong>{{feedforward_block}}</strong></p>
{{/show_feedforward}}
{{#show_feedup}}
<h4>Feed-up</h4>
<p>{{feedup_intro}} (Level {{feedup_level}}):</p>
<p><strong>{{feedup_block}}</strong></p>
{{/show_feedup}}
{{#passing_requirement}}
<h4>Passing grade</h4>
<div class="passing-grade-warning">
<p>{{passing_requirement}}</p>
{{#passing_result}}
<p><strong>{{passing_result}}</strong></p>
{{/passing_result}}
</div>
{{/passing_requirement}}
{{#encouragement}}
<div class="encouragement-message mt-3">
<p><em>{{encouragement}}</em></p>
</div>
{{/encouragement}}
</div>
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