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

Initial commit

parents
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the French language.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['modulenameplural'] = 'Questionnaires adaptatifs';
$string['modulename'] = 'Questionnaire adaptatif';
$string['modulename_help'] = 'L’application Questionnaire adaptative permet à un enseignant de créer des questionnaires mesurant efficacement les capacités des candidats.Les questionnaires adaptatifs sont composés de questions selectionnées dans la banque d\'items et répertoriées selon leur niveau de difficulté.Les items sont choisis pour correspondre au niveau de capacité estimé du candidat en cours. Si le candidat répond correctement à un item, un item plus difficile lui est ensuite proposé. Si le candidat répond de manière incorrecte à un item, un item moins difficile lui est ensuite proposé. Cette technique prendra la forme d’une série d’items convergeant vers le niveau réel du candidat. Le test s’arrête quand le niveau du ou des candidats est déterminé avec la précision souhaitée. Cette application est particulièrement adaptée pour déterminer un niveau sur une échelle de mesure unidimensionnelle. Bien que l’échelle de mesure puisse être très large, les items eux, doivent tous fournir un niveau ou une indication d’aptitude étalonnés sur la même échelle. Par exemple, pour un test de positionnement, les items placés bas dans l’échelle de mesure et auxquelles les débutants sont capables de répondre correctement, devraient recevoir une réponse correcte de la part des experts, à l’inverse les questions placées plus haut dans l’échelle de mesure ne devraient recevoir de réponse correcte que par les experts ou grâce à la chance. Les items ne discriminant pas les candidats de différents niveaux de capacité rendront le test inefficace et pourront mener à des résultats non concluants.
Les questions utilisées dans « questionnaire adaptatif » doivent :
* être automatiquement répertoriées comme étant correctes ou incorrectes.
* être répertoriées par difficulté en utilisant \'adpq_\' suivi d’un entier positif compris dans le classement prévu pour le questionnaire.
Le questionnaire adaptatif peut être configuré pour :
* définir lui-même le ratio item-difficulté / utilisateur-niveaux à mesurer. 1-10, 1-16 et 1-100 sont des exemples de classements valides.
* définir la précision requise avant que le questionnaire ne s’arrête. Pour établir un niveau, on considère souvent qu’une erreur de 5 % est une règle d’arrêt appropriée.
* définir un nombre minimum de questions nécessitant une réponse
* définir un nombre maximum de questions pouvant faire l’objet d’une réponse
La description et le processus de tests dans cette application sont basées sur <a href="http://www.rasch.org/memo69.pdf">Computer-Adaptive Testing: A Methodology Whose Time Has Come</a> by John Michael Linacre, Ph.D. MESA Psychometric Laboratory - University of Chicago. MESA Memorandum No. 69.';
$string['pluginadministration'] = 'questionnaire adaptatif';
$string['pluginname'] = 'questionnaire adaptatif';
$string['nonewmodules'] = 'Aucune occurrence de questionnaire adaptatif n’a été trouvée';
$string['adaptivequizname'] = 'Nom';
$string['adaptivequizname_help'] = 'Entrez le nom de l’occurence questionnaire adaptatif';
$string['adaptivequiz:addinstance'] = 'Ajoutez un nouveau questionnaire adaptatif';
$string['adaptivequiz:viewreport'] = 'Voir les rapports du questionnaire adaptatif';
$string['adaptivequiz:reviewattempts'] = 'Revoir les propositions du questionnaire adaptatif';
$string['adaptivequiz:attempt'] = 'Débuter un test adaptatif';
$string['attemptsallowed'] = 'Nombre de tentative autorisée';
$string['attemptsallowed_help'] = 'Nombre de tentative autorisée pour le candidat';
$string['requirepassword'] = 'Mot de passe requis';
$string['requirepassword_help'] = 'Les candidats doivent entrer un mot de passe pour ouvrir leur session';
$string['browsersecurity'] = 'Sécurité du navigateur';
$string['browsersecurity_help'] = 'Si « Full screen pop-up with some JavaScript security » est sélectionné, le questionnaire débutera seulement si le candidat dispose d’un navigateur-web autorisant Javascipt . Le questionnaire apparaît en plein écran dans une fenêtre contextuelle couvrant toutes les autres fenêtres et qui ne dispose d’aucun contrôle de navigation. De même, dans la mesure du possible, l’utilisation de certaines commandes comme « copier » et « coller » sont désactivées pour les candidats';
$string['minimumquestions'] = 'Nombre minimum d’items';
$string['minimumquestions_help'] = 'Nombre minimum d’items auxquels le candidat doit répondre';
$string['maximumquestions'] = 'Nombre maximum d’items';
$string['maximumquestions_help'] = 'Nombre maximum d’items qu’un candidat peut tenter';
$string['startinglevel'] = 'Niveau de difficulté de départ';
$string['startinglevel_help'] = 'Lorsque le candidat commence une session, l’application sélectionnera aléatoirement un item correspondant au niveau de difficulté';
$string['lowestlevel'] = 'Niveau de difficulté le plus bas';
$string['lowestlevel_help'] = 'Niveau le moins difficile duquel les items seront sélectionnés à l’occasion de ce test. Lors d’une session, l’activité n’ira pas au delà de ce niveau de difficulté';
$string['highestlevel'] = 'Niveau de difficulté le plus élevé';
$string['highestlevel_help'] = 'Niveau de difficulté le plus difficile duquel les items seront sélectionnés à l’occasion de ce test. Lors d’une session, l’activité n’ira pas au delà de ce niveau de difficulté';
$string['questionpool'] = 'Banque d’items';
$string['questionpool_help'] = 'Sélectionnez les catégories desquelles les items pourront être tirés durant une session';
$string['formelementempty'] = 'Entrez un entier positif compris entre 1 et 999';
$string['formelementnumeric'] = 'Entrez une valeur chiffrée comprise entre 1 et 999';
$string['formelementnegative'] = 'Entrez un nombre positif compris entre 1 et 999';
$string['formminquestgreaterthan'] = 'Le nombre minimum de questions doit être inférieur au nombre maximum de question';
$string['formlowlevelgreaterthan'] = 'Le niveau le plus bas doit être inférieur au niveau le plus élevé';
$string['formstartleveloutofbounds'] = 'Le niveau de départ doit être un nombre compris entre le niveau le plus bas et le niveau le plus élevé';
$string['standarderror'] = 'Erreur standard provoquant l’arrêt';
$string['standarderror_help'] = 'Lorsque le nombre d’erreurs fait que la capacité de l’utilisateur est évaluée en dessous du niveau seuil le questionnaire s’arrêtera. Réglez cette valeur dans la limite de 5% pour obtenir plus ou moins de précision pour mesurer sa capacité';
$string['formelementdecimal'] = 'Entrez un nombre décimal d’une longueur maximum de 10 chiffres et comportant un maximum de 5 chiffres après la virgule';
$string['attemptfeedback'] = 'Commentaire';
$string['attemptfeedback_help'] = 'Un commentaire est proposé à l’utilisateur une fois la session est terminée';
$string['formquestionpool'] = 'Sélectionnez au moins une catégorie de question';
$string['submitanswer'] = 'Soumettre la réponse';
$string['startattemptbtn'] = 'Démarrez la session';
$string['viewreportbtn'] = 'Voir le rapport';
$string['errorfetchingquest'] = 'Impossible de récupérer un item pour ce niveau {$a->level}';
$string['leveloutofbounds'] = 'Le niveau requis {$a->level} n’est pas celui prévu pour cette session';
$string['errorattemptstate'] = 'Une erreur s’est produite en déterminant l’état de la session';
$string['nopermission'] = 'Accès réservé';
$string['maxquestattempted'] = 'Nombre maximum de items tentés';
$string['notyourattempt'] = 'Cette tentative n’est pas la votre pour cette activité';
$string['noattemptsallowed'] = 'Plus aucune tentative autorisée pour cette activité';
$string['updateattempterror'] = 'Erreur lors de la mise à jour de l’enregistrement';
$string['numofattemptshdr'] = 'Nombre de tentatives';
$string['standarderrorhdr'] = 'Erreur standard';
$string['errorlastattpquest'] = 'Erreur lors de la vérification de réponse du dernier item';
$string['errornumattpzero'] = 'Le nombre de tentatives est égal à zéro, bien que l’utilisateur ait soumis une réponse à la question précédente';
$string['errorsumrightwrong'] = 'La somme des réponses correctes et incorrectes est différent du nombre total de items tentées';
$string['calcerrorwithinlimits'] = 'L’erreur standard calculée par {$a->calerror} est comprise dans les limites imposées par l’application {$a->definederror}';
$string['missingtagprefix'] = 'Tag prefix manquant';
$string['recentactquestionsattempted'] = 'Items tentés: {$a}';
$string['recentattemptstate'] = 'État de la tentative';
$string['recentinprogress'] = 'En court';
$string['notinprogress'] = 'Cette tentative n’est pas en court';
$string['recentcomplete'] = 'Terminé';
$string['functiondisabledbysecuremode'] = 'Cette fonctionnalité est actuellement désactivée';
$string['enterrequiredpassword'] = 'Entrez le mot de passe requis';
$string['requirepasswordmessage'] = 'Pour débuter ce questionnaire vous devez connaître son mot de passe';
$string['wrongpassword'] = 'Mot de passe incorrect';
$string['attemptstate'] = 'État de la tentative';
$string['attemptstopcriteria'] = 'Raison de l’abandon';
$string['questionsattempted'] = 'Total des items tentés';
$string['attemptfinishedtimestamp'] = 'Heure de fin de la tentative';
$string['backtomainreport'] = 'Retour au rapport principal';
$string['reviewattempt'] = 'Revoir sur la tentative';
$string['indvuserreport'] = 'Rapport individuel d’activité pour l’utilisateur {$a}';
$string['activityreports'] = 'Rapport d’activité';
$string['stopingconditionshdr'] = 'Conditions d’arrêt';
$string['backtoviewattemptreport'] = 'Retour vers le rapport de tentative';
$string['backtoviewreport'] = 'Retour vers le rapport principal';
$string['reviewattemptreport'] = 'Revue de la tentative par {$a->fullname} soumise à {$a->finished}';
$string['deleteattemp'] = 'Supprimez la tentative';
$string['confirmdeleteattempt'] = 'Confirmation de la suppression de la tentative à partir de {$a->name} soumise à {$a->timecompleted}';
$string['attemptdeleted'] = 'Tentative supprimée pour {$a->name} soumise à {$a->timecompleted}';
$string['closeattempt'] = 'Clôturer la tentative';
$string['confirmcloseattempt'] = 'Êtes vous certain(e) de vouloir clôturer et finaliser cette tentative de {$a->name}?';
$string['confirmcloseattemptstats'] = 'Cette tentative commencée le {$a->started} a été mise à jour le {$a->modified}';
$string['confirmcloseattemptscore'] = '{$a->num_questions} items ont été complétés et le score est de {$a->measure} {$a->standarderror}.';
$string['attemptclosedstatus'] = 'Tentative clôturée manuellement par {$a->current_user_name} (user-id: {$a->current_user_id}) le {$a->now}.';
$string['attemptclosed'] = 'La tentative a été clôturée manuellement';
$string['errorclosingattempt_alreadycomplete'] = 'Cette tentative est déjà validée et ne peut être clôturée manuellement';
$string['formstderror'] = 'Un pourcentage inférieur à 50 et supérieur ou égal à 0 doit être entré';
$string['backtoviewattemptreport'] = 'Retour vers le rapport de tentative';
$string['backtoviewreport'] = 'Retour vers le rapport principal';
$string['reviewattemptreport'] = 'Revue de la tentative par {$a->fullname} soumise à {$a->finished}';
$string['score'] = 'Résultat';
$string['bestscore'] = 'Meilleur résultat';
$string['bestscorestderror'] = 'Erreur standard';
$string['attempt_summary'] = 'Résumé de la tentative';
$string['scoring_table'] = 'Table des résultats';
$string['attempt_questiondetails'] = 'Détails de l’item';
$string['attemptstarttime'] = 'Heure de début de la tentative';
$string['attempttotaltime'] = 'Temps total (hh:mm:ss)';
$string['attempt_user'] = 'utilisateur';
$string['attempt_state'] = 'État de la tentative';
$string['attemptquestion_num'] = 'Item #';
$string['attemptquestion_level'] = 'Niveau de difficulté de l’item';
$string['attemptquestion_rightwrong'] = 'Vrai/faux';
$string['attemptquestion_ability'] = 'Mesure de capacité';
$string['attemptquestion_error'] = 'Erreur standard (&plusmn;&nbsp;x%)';
$string['attemptquestion_difficulty'] = 'Difficulté de l’item (logits)';
$string['attemptquestion_diffsum'] = 'Somme des difficultés';
$string['attemptquestion_abilitylogits'] = 'Capacité mesurée (logits)';
$string['attemptquestion_stderr'] = 'Erreur standard (&plusmn;&nbsp;logits)';
$string['graphlegend_target'] = 'Niveau cible';
$string['graphlegend_error'] = 'Erreur standard';
$string['answerdistgraph_title'] = 'Communication de la réponse pour {$a->firstname} {$a->lastname}';
$string['answerdistgraph_questiondifficulty'] = 'Niveau de l\'item';
$string['answerdistgraph_numrightwrong'] = 'Nombre incorrect (-) / Nombre correct (+)';
$string['numright'] = 'Nombre correct';
$string['numwrong'] = 'Nombre incorrect';
$string['questionnumber'] = 'Item #';
$string['na'] = 'Non disponible';
$string['downloadcsv'] = 'Téléchargez le fichier CSV';
$string['grademethod'] = 'Méthode de notation';
$string['gradehighest'] = 'Note la plus élevée';
$string['attemptfirst'] = 'Première tentative';
$string['attemptlast'] = 'Dernière tentative';
$string['grademethod_help'] = 'Lorsque plusieurs tentatives sont autorisées, les méthodes suivantes sont disponibles pour calculer la note du questionnaire final
* Note la plus haute pour l’ensemble des tentatives
* Première tentative (toutes les autres tentatives sont ignorées)
* Dernière tentative (toutes les autres tentatives sont ignorées)
';
$string['resetadaptivequizsall'] = 'Effacer toutes les tentatives du questionnaire adaptatif';
$string['all_attempts_deleted'] = 'Toutes les tentatives du questionnaire adaptatif ont été effacées';
$string['all_grades_removed'] = 'Toutes les notes du questionnaire adaptatif ont été retirées';
$string['questionanalysisbtn'] = 'Analyse de la question';
$string['id'] = 'Identifiant';
$string['name'] = 'Nom';
$string['questions_report'] = 'Rapport sur les items';
$string['question_report'] = 'Analyse de l’item';
$string['times_used_display_name'] = 'Temps écoulé';
$string['percent_correct_display_name'] = '% de réponse correcte';
$string['discrimination_display_name'] = 'Discrimination';
$string['back_to_all_questions'] = '&laquo Retour aux questions';
$string['answers_display_name'] = 'Réponses';
$string['answer'] = 'Réponse';
$string['statistic'] = 'Statistique(s)';
$string['value'] = 'Valeur';
$string['highlevelusers'] = 'Utilisateurs au dessus du niveau requis';
$string['midlevelusers'] = 'Utilisateurs proche du niveau requis';
$string['lowlevelusers'] = 'Utilisateurs en dessous du niveau requis';
$string['user'] = 'Utilisateur';
$string['result'] = 'Résultat';
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Plugin's system and internal functions.
*
* @package mod_adaptivequiz
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/question/engine/lib.php');
use mod_adaptivequiz\local\attempt\attempt_state;
/**
* Option controlling what options are offered on the quiz settings form.
*/
define('ADAPTIVEQUIZMAXATTEMPT', 10);
define('ADAPTIVEQUIZNAME', 'adaptivequiz');
/**
* Options determining how the grades from individual attempts are combined to give
* the overall grade for a user
*/
define('ADAPTIVEQUIZ_GRADEHIGHEST', '1');
define('ADAPTIVEQUIZ_ATTEMPTFIRST', '3');
define('ADAPTIVEQUIZ_ATTEMPTLAST', '4');
/**
* Returns the information on whether the module supports a feature
*
* @see plugin_supports() in lib/moodlelib.php
* @param string $feature: FEATURE_xx constant for requested feature
* @return mixed true if the feature is supported, null if unknown
*/
function adaptivequiz_supports($feature) {
switch($feature) {
case FEATURE_GROUPS: {
return true;
}
case FEATURE_GROUPINGS: {
return true;
}
case FEATURE_GROUPMEMBERSONLY: {
return true;
}
case FEATURE_MOD_INTRO: {
return true;
}
case FEATURE_BACKUP_MOODLE2: {
return true;
}
case FEATURE_SHOW_DESCRIPTION: {
return true;
}
case FEATURE_GRADE_HAS_GRADE: {
return true;
}
case FEATURE_USES_QUESTIONS: {
return true;
}
case FEATURE_MOD_PURPOSE: {
return MOD_PURPOSE_ASSESSMENT;
}
case FEATURE_COMPLETION_HAS_RULES: {
return true;
}
default: {
return null;
}
}
}
/**
* Saves a new instance of the adaptivequiz into the database
*
* Given an object containing all the necessary data,
* (defined by the form in mod_form.php) this function
* will create a new instance and return the id number
* of the new instance.
*
* @param 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;
$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);
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));
$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);
}
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' => function($user, $preferencename) {
global $USER;
return $user->id == $USER->id;
},
],
];
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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;
}
<?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() {
$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('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$mform->addRule('acceptancethreshold', get_string('formelementdecimal', 'adaptivequiz'), 'numeric', null, 'client');
$mform->setType('acceptancethreshold', PARAM_FLOAT);
$mform->addElement('text', 'questionchecktrigger', get_string('questionchecktrigger', 'adaptivequiz'),
['size' => '2', 'maxlength' => '2']);
$mform->addHelpButton('questionchecktrigger', 'questionchecktrigger', 'adaptivequiz');
$mform->addRule('questionchecktrigger', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client');
$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', 0);
$mform->addElement('select', 'showattemptprogress', get_string('modformshowattemptprogress', 'adaptivequiz'),
[get_string('no'), get_string('yes')]);
$mform->addHelpButton('showattemptprogress', 'modformshowattemptprogress', 'adaptivequiz');
$mform->setDefault('showattemptprogress', 0);
//KNIGHT: For option to show attempt review to students.
$mform->addElement('select', 'showownattemptresult', get_string('modformshowownattemptresult', 'adaptivequiz'), [get_string('no'), get_string('yes')]);
$mform->addHelpButton('showownattemptresult', 'modformshowownattemptresult', 'adaptivequiz');
$mform->setDefault('showownattemptresult', 0);
$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);
// 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();
}
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 ($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['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;
}
/**
* @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 width="48px" height="48px" viewBox="0 0 48 48" version="1" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 48 48">
<g fill="#00BCD4">
<rect x="19" y="22" width="10" height="20"/>
<rect x="32" y="8" width="10" height="34"/>
<rect x="6" y="30" width="10" height="12"/>
</g>
<g fill="#3F51B5">
<polygon points="11,8 21,18 21,8"/>
<rect x="11" y="8.9" transform="matrix(-.707 -.707 .707 -.707 10.879 36.506)" width="4" height="14.1"/>
</g>
</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';
$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;
$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;
$row = new html_table_row();
$headercell = new html_table_cell(get_string('score', 'adaptivequiz'));
$headercell->header = true;
$abilityfraction = 1 / ( 1 + exp( (-1 * $attempt->measure) ) );
$ability = (($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel) * $abilityfraction) + $adaptivequiz->lowestlevel;
$stderror = catalgo::convert_logit_to_percent($attempt->standarderror);
$score = ($stderror > 0)
? round($ability, 2)." &nbsp; &plusmn; ".round($stderror * 100, 1)."%"
: 'n/a';
$datacell = new html_table_cell($score);
$row->cells = [$headercell, $datacell];
$table->data[] = $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;
$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;
$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;
$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);
}
//KNIGHT: Include bool to show all the tabs only if user has access to view report.
public function attempt_review_tabs(moodle_url $pageurl, string $selected, bool $showdetailedattempt): string {
$tabs = [];
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}.
* @param bool $showdeletebutton A boolean param to set the visibility of the delete button.
*/
//KNIGHT: Introduce bool to hide delete option from students in attempt review.
public function individual_user_attempt_actions(stdClass $attempt, bool $showdeletebutton): 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')
)
);
}
if($showdeletebutton){
$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);
$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){
//throw error when a student tries to watch someone else's attempt
if($attempt->userid != $USER->id){
throw new moodle_exception('notyourattempt', 'adaptivequiz');
}
$tab = optional_param('tab', 'questionsdetails', PARAM_ALPHA);
}
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;
}
{{!
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 }}
{{!
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_progress
Indicator of how many questions are answered out of the maximum number set for the attempt.
Variables required for this template:
* questionsanswerednumber - number of question already answered during the attempt
* maximumquestionsnumber - number of maximum questions set for the quiz in activity settings
* showprogressbar - whether a bar depicting the progress should be shown
* percentprogressbarfilled - percent of questions already answered out of maximum number during the attempt. Optional, but
should be present when 'showprogressbar' is present also
* helpiconcontent - already rendered help icon, optional.
Example context (json):
{
"questionsanswerednumber": "23",
"maximumquestionsnumber": "50",
"showprogressbar": true,
"percentprogressbarfilled": "46",
"helpiconcontent": "<a class='btn ..'><i class='icon fa fa-question-circle ..'></i></a>"
}
}}
<div class="attempt-progress">
<h3 class="no-out-of">
{{#str}} attemptquestionsprogress, adaptivequiz, {{questionsanswerednumber}} / {{maximumquestionsnumber}} {{/str}}
{{{helpiconcontent}}}
</h3>
{{#showprogressbar}}
<div class="progress-bar-wrapper">
<div class="bg-primary progress-bar-inner" style="width: {{percentprogressbarfilled}}%">&nbsp;</div>
</div>
{{/showprogressbar}}
</div>
{{!
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_report_chart
This is a copy of core/chart.
The purpose of copying is to enable custom initialization of the chart, as not all the config options can be passed through
the chart renderable objects. Thus, the js section is implemented as a block to allow overriding in specific charts templates.
See core/chart for the required context.
}}
<div class="chart-area" id="chart-area-{{uniqid}}">
<div class="chart-image" role="presentation" aria-describedby="chart-table-data-{{uniqid}}"></div>
<div class="chart-table {{^withtable}}accesshide{{/withtable}}">
<p class="chart-table-expand">
<a href="#" aria-controls="chart-table-data-{{uniqid}}" role="button">
{{#str}}showchartdata, moodle{{/str}}
</a>
</p>
<div class="chart-table-data" id="chart-table-data-{{uniqid}}" {{#withtable}}role="complementary" aria-expanded="false"{{/withtable}}></div>
</div>
</div>
{{$initjs}}
{{#js}}
require([
'jquery',
'core/chart_builder',
'core/chart_output_chartjs',
'core/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}}
{{!
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/report/individual_user_attempt_actions
Set of actions for a user attempt in the report's column.
Classes required for JS:
* none
Context variables required for this template:
* actions - array of actions to be rendered as action links, each action contain the following elements:
- url: action URL
- icon: rendered icon to depict the action, serves as content of the action link
- title: title for the action link tag
Example context (json):
{
"actions": [
{
"url": "http://examplemoodle.org/mod/adaptivequiz/action.php?attempt=5",
"icon":
"title": "Some action"
}
]
}
}}
{{#actions}}
<a href="{{url}}" title="{{title}}">{{{icon}}}</a>
{{/actions}}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_adaptivequiz;
use advanced_testcase;
use cm_info;
use context_module;
use mod_adaptivequiz\completion\custom_completion;
use mod_adaptivequiz\event\attempt_completed;
use mod_adaptivequiz\local\attempt\attempt_state;
use stdClass;
/**
* Tests observers of the attempt state change.
*
* @package mod_adaptivequiz
* @copyright 2022 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @covers \mod_adaptivequiz\attempt_state_change_observers::attempt_completed
*/
class attempt_state_change_observers_test extends advanced_testcase {
public function test_it_handles_completion_state(): void {
global $DB;
$this->resetAfterTest();
// Test it can set the activity as completed.
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
$questioncategory = $this->getDataGenerator()
->get_plugin_generator('core_question')
->create_question_category(['name' => 'My category']);
$adaptivequiz = $this->getDataGenerator()
->get_plugin_generator('mod_adaptivequiz')
->create_instance([
'course' => $course->id,
'completion' => 1,
'completionattemptcompleted' => 1,
'questionpool' => [$questioncategory->id],
]);
$user = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
// We don't have any attempt generation yet. Set random data manually.
$attemptrecordsnapshot = new stdClass();
$attemptrecordsnapshot->id = 1;
$attemptrecordsnapshot->instance = $adaptivequiz->id;
$attemptrecordsnapshot->userid = $user->id;
$attemptrecordsnapshot->uniqueid = 1;
$attemptrecordsnapshot->attemptstate = attempt_state::IN_PROGRESS;
$attemptrecordsnapshot->attemptstopcriteria = 'Unable to fetch a questions for level 1';
$attemptrecordsnapshot->questionsattempted = 1;
$attemptrecordsnapshot->difficultysum = 0.0000000;
$attemptrecordsnapshot->standarderror = 1.51186;
$attemptrecordsnapshot->measure = 1.94591;
$attemptrecordsnapshot->timecreated = 1658524979;
$attemptrecordsnapshot->timemodified = 1658525029;
$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $adaptivequiz->course);
$context = context_module::instance($cm->id);
$attemptid = 1;
$event = attempt_completed::create([
'objectid' => $attemptid,
'context' => $context,
'userid' => $user->id,
]);
$event->add_record_snapshot('adaptivequiz_attempt', $attemptrecordsnapshot);
$event->add_record_snapshot('adaptivequiz', $adaptivequiz);
attempt_state_change_observers::attempt_completed($event);
$completion = new custom_completion(cm_info::create($cm), $user->id);
$this->assertEquals(COMPLETION_COMPLETE, $completion->get_overall_completion_state());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment