Chapitre 6.5 — Tutoriel complet : un module d’activité (mod_simplecheck)
Partie 6 : Développer des plugins — Chapitre 5/7 Public : développeur ayant fait les chapitres 6.3 (
local) et 6.4 (block). C’est le plus gros tutoriel du cours : le module d’activité est le type le plus riche de Moodle. Version de référence : Moodle 5.2 (PHP 8.3+, racine webpublic/).
⏱️ TL;DR
- Un module d’activité (
mod_*) est une activité de cours : Devoir, Quiz, Forum… On construitmod_simplecheck, une checklist notée : l’enseignant définit des items, l’étudiant les coche, la note = proportion cochée × note max.- Contrat obligatoire :
version.php,db/install.xml,db/access.php,lib.php(avec_supports,_add_instance,_update_instance,_delete_instance),mod_form.php,view.php,index.php,lang/en/simplecheck.php.FEATURE_*(dans_supports) déclare ce que le module sait faire : notes, groupes, achèvement, backup, complétion…- Carnet de notes (optionnel mais fréquent) :
_grade_item_update(crée/maj l’item de note) +_update_grades(pousse les notes des élèves). Appelés à la création/modif et à chaque changement de note.- Achèvement d’activité : règle standard + règle personnalisée via
classes/completion/custom_completion.php(« terminé quand tout est coché »).- Events : on déclenche
\mod_simplecheck\event\course_module_viewedà l’ouverture (traçabilité, rapports).- Backup/restore :
backup/moodle2/pour que l’activité survive à une sauvegarde/import de cours._reset_userdatapour la réinitialisation de cours.- Affichage sur la page de cours :
_cm_info/cm_infopour enrichir la vignette de l’activité.- Pour chaque brique : on précise obligatoire ou optionnel.
🎯 Objectifs
À la fin de ce chapitre, vous saurez :
- Écrire un module d’activité complet et l’ajouter à un cours.
- Implémenter le cycle add / update / delete instance et déclarer les
FEATURE_*. - Intégrer l’activité au carnet de notes (item de note + poussée des notes).
- Ajouter une règle d’achèvement personnalisée.
- Déclencher des events, gérer backup/restore et reset, enrichir la vignette de cours.
1. Le module d’activité : le sommet du système de plugins
Une activité est l’unité pédagogique de Moodle : elle vit dans un cours, dans une section, elle est instanciable (le même mod_quiz a dix instances dans dix cours), presque toujours notable, traçable, sauvegardable. C’est pourquoi son contrat est le plus vaste : il touche au carnet de notes, à l’achèvement, aux groupes, aux logs, au backup, à la recherche…
Notre cible, mod_simplecheck (« checklist notée ») : l’enseignant crée une activité et y définit une liste d’items (« Lire le cours », « Rendre l’exercice 1 »…). Chaque étudiant coche ce qu’il a fait. Sa note est la proportion d’items cochés × la note maximale. L’activité peut être configurée pour être terminée (achèvement) quand tout est coché.
C’est un exemple volontairement modeste sur le plan métier mais qui traverse tous les sous-systèmes d’un mod. C’est le but : voir le contrat en entier sur un cas compréhensible.
💡 Pour un dev React : si un
local_est un module de feature et unblock_un widget, unmod_est un type de contenu de premier ordre du CMS — avec son schéma, son cycle de vie (créer/éditer/supprimer une instance), ses hooks d’intégration (notes, achèvement), sa sérialisation (backup) et son rendu de vignette. Pensez à un « content type » Drupal ou à un bloc Gutenberg beaucoup plus intégré au noyau.
Arborescence
public/mod/simplecheck/
├── version.php
├── lib.php ← FEATURE_*, add/update/delete, grades, reset
├── mod_form.php ← formulaire de réglage de l'instance
├── view.php ← la page « ouvrir l'activité »
├── index.php ← liste des instances du module dans un cours
├── db/
│ ├── access.php ← capabilities
│ ├── install.xml ← schéma (3 tables)
│ └── upgrade.php
├── classes/
│ ├── completion/
│ │ └── custom_completion.php ← règle d'achèvement personnalisée
│ ├── event/
│ │ └── course_module_viewed.php
│ └── output/
│ ├── checklist.php ← renderable
│ └── renderer.php
├── backup/
│ └── moodle2/
│ ├── backup_simplecheck_activity_task.class.php
│ ├── backup_simplecheck_stepslib.php
│ ├── restore_simplecheck_activity_task.class.php
│ └── restore_simplecheck_stepslib.php
├── templates/
│ └── checklist.mustache
└── lang/
└── en/
└── simplecheck.php ← NB : nommé d'après le module, sans « mod_ »⚠️ Piège de nommage (spécifique aux mod) : plusieurs conventions rigides propres aux modules d’activité :
- Le dossier est
mod/simplecheck/; le nom du module estsimplecheck(sansmod_) ; le component frankenstyle estmod_simplecheck.- La table principale doit porter le nom du module :
simplecheck(pasmod_simplecheck). Les tables secondaires se préfixent par le nom du module :simplecheck_item,simplecheck_check.- Le fichier de langue est
lang/en/simplecheck.php(nom du module, sansmod_). On appelle les chaînes avecget_string('x', 'mod_simplecheck')ou'simplecheck'— les deux marchent pour un module.- Les fonctions de
lib.phpsont préfixées par le nom du module :simplecheck_add_instance(), pasmod_simplecheck_add_instance().
2. version.php
<?php
// public/mod/simplecheck/version.php
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_simplecheck';
$plugin->version = 2026070800;
$plugin->requires = 2026042000; // Moodle 5.2
$plugin->maturity = MATURITY_STABLE;
$plugin->release = 'v1.0';3. Le schéma : trois tables
Notre modèle : une activité simplecheck, ses items (simplecheck_item), et les coches des étudiants (simplecheck_check).
<?xml version="1.0" encoding="UTF-8" ?>
<!-- public/mod/simplecheck/db/install.xml -->
<XMLDB PATH="mod/simplecheck/db" VERSION="2026070800"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd">
<TABLES>
<!-- Table principale : DOIT s'appeler comme le module. -->
<TABLE NAME="simplecheck" COMMENT="Instances de l'activité checklist notée">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"
COMMENT="Cours propriétaire (obligatoire pour un mod)"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"
COMMENT="Nom de l'activité (obligatoire)"/>
<FIELD NAME="intro" TYPE="text" NOTNULL="false" SEQUENCE="false"
COMMENT="Description (obligatoire pour FEATURE_MOD_INTRO)"/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="grade" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="100" SEQUENCE="false"
COMMENT="Note maximale"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="course" TYPE="foreign" FIELDS="course" REFTABLE="course" REFFIELDS="id"/>
</KEYS>
</TABLE>
<!-- Items de la checklist, définis par l'enseignant. -->
<TABLE NAME="simplecheck_item" COMMENT="Items d'une checklist">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="simplecheckid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="position" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="simplecheckid" TYPE="foreign" FIELDS="simplecheckid" REFTABLE="simplecheck" REFFIELDS="id"/>
</KEYS>
</TABLE>
<!-- Coches des étudiants. -->
<TABLE NAME="simplecheck_check" COMMENT="État coché par étudiant et par item">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="simplecheckid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="checked" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="itemid" TYPE="foreign" FIELDS="itemid" REFTABLE="simplecheck_item" REFFIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="simplecheckid-userid" UNIQUE="false" FIELDS="simplecheckid, userid"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>Trois champs sont imposés par le contrat mod dans la table principale : course (le cours propriétaire), name (le nom affiché) et, si l’on active FEATURE_MOD_INTRO, intro + introformat (la description, en HTML formaté). Le reste est libre.
⚠️ Piège : un module n’a pas de champ
useriddans sa table principale — une activité appartient au cours, pas à un utilisateur. Les données par utilisateur vont dans des tables secondaires (icisimplecheck_check). Confondre les deux niveaux est l’erreur de modélisation classique du débutantmod.
4. Les capabilities
<?php
// public/mod/simplecheck/db/access.php
defined('MOODLE_INTERNAL') || die();
$capabilities = [
// Ajouter l'activité dans un cours (le nom :addinstance est attendu par le core).
'mod/simplecheck:addinstance' => [
'riskbitmask' => RISK_XSS,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => ['editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW],
'clonepermissionsfrom' => 'moodle/course:manageactivities',
],
// Voir/ouvrir l'activité.
'mod/simplecheck:view' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => ['guest' => CAP_ALLOW, 'student' => CAP_ALLOW,
'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW],
],
// Cocher des items (soumettre sa progression).
'mod/simplecheck:submit' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => ['student' => CAP_ALLOW],
],
// Voir les résultats de tous les participants.
'mod/simplecheck:viewreports' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => ['teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW],
],
];mod/simplecheck:addinstance est obligatoire : c’est ce qui fait apparaître l’activité dans le sélecteur « Ajouter une activité ». Les trois autres traduisent notre logique : voir, soumettre (étudiant), voir les rapports (enseignant).
5. lib.php : le cœur du contrat
C’est le fichier le plus important d’un mod. Le core y appelle des fonctions nommées par convention. On les regroupe par rôle.
5.1 _supports : déclarer les capacités
<?php
// public/mod/simplecheck/lib.php
defined('MOODLE_INTERNAL') || die();
/**
* Déclare les fonctionnalités supportées par le module.
*
* @param string $feature une constante FEATURE_*
* @return mixed true/false, ou une valeur, ou null si inconnu.
*/
function simplecheck_supports($feature) {
switch ($feature) {
case FEATURE_MOD_INTRO: return true; // a une description
case FEATURE_GRADE_HAS_GRADE: return true; // est noté
case FEATURE_COMPLETION_TRACKS_VIEW: return true; // achèvement « vu »
case FEATURE_COMPLETION_HAS_RULES: return true; // achèvement à règles custom
case FEATURE_BACKUP_MOODLE2: return true; // sauvegardable
case FEATURE_GROUPS: return true;
case FEATURE_GROUPINGS: return true;
case FEATURE_SHOW_DESCRIPTION: return true;
case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_ASSESSMENT; // catégorie d'icône
default: return null; // inconnu : ne rien affirmer
}
}Chaque FEATURE_* branche un sous-système :
| Constante | Effet |
|---|---|
FEATURE_MOD_INTRO | active le champ description dans mod_form (nécessite intro/introformat) |
FEATURE_GRADE_HAS_GRADE | l’activité apparaît au carnet de notes |
FEATURE_COMPLETION_TRACKS_VIEW | option d’achèvement « l’étudiant doit voir l’activité » |
FEATURE_COMPLETION_HAS_RULES | active nos règles d’achèvement personnalisées (§9) |
FEATURE_BACKUP_MOODLE2 | rend l’activité sauvegardable/restaurable (§10) |
FEATURE_GROUPS / FEATURE_GROUPINGS | prise en charge des groupes |
FEATURE_MOD_PURPOSE | classe l’icône de l’activité (contenu, évaluation, communication…) |
⚠️ Piège :
_supportsdoit renvoyernull(pasfalse) pour une feature inconnue.falsesignifie « je connais cette feature et je ne la supporte pas » ;nullsignifie « je ne sais pas ». Le core distingue les deux. Ledefault: return null;est donc important.
5.2 Le cycle de vie de l’instance : add / update / delete
Ces trois fonctions sont appelées par le core quand l’enseignant crée, modifie ou supprime l’activité. _add_instance et _delete_instance sont obligatoires dès qu’on a une table.
/**
* Crée une instance. Reçoit les données du mod_form.
*
* @param stdClass $data données du formulaire (+ coursemodule)
* @param mod_simplecheck_mod_form|null $mform
* @return int l'id de la nouvelle instance
*/
function simplecheck_add_instance(stdClass $data, $mform = null): int {
global $DB;
$data->timecreated = time();
$data->timemodified = $data->timecreated;
// Insère l'activité, récupère son id.
$data->id = $DB->insert_record('simplecheck', $data);
// Enregistre les items saisis (voir mod_form §6 : un textarea, un item par ligne).
simplecheck_save_items($data);
// Crée l'item de note dans le carnet de notes (§8).
simplecheck_grade_item_update($data);
return $data->id;
}
/**
* Met à jour une instance.
*/
function simplecheck_update_instance(stdClass $data, $mform = null): bool {
global $DB;
$data->timemodified = time();
$data->id = $data->instance; // le core met l'id dans 'instance' à l'update.
$DB->update_record('simplecheck', $data);
simplecheck_save_items($data);
simplecheck_grade_item_update($data);
simplecheck_update_grades($data); // recalcule les notes (la note max a pu changer).
return true;
}
/**
* Supprime une instance et TOUTES ses données dépendantes.
*/
function simplecheck_delete_instance(int $id): bool {
global $DB;
if (!$simplecheck = $DB->get_record('simplecheck', ['id' => $id])) {
return false;
}
// Ordre : données filles d'abord.
$DB->delete_records('simplecheck_check', ['simplecheckid' => $id]);
$DB->delete_records('simplecheck_item', ['simplecheckid' => $id]);
$DB->delete_records('simplecheck', ['id' => $id]);
// Retire l'item du carnet de notes.
simplecheck_grade_item_update($simplecheck, 'reset');
return true;
}Deux points cruciaux :
- À l’
add, l’id est retourné et Moodle le stocke dans le course module. À l’update, l’id arrive dans$data->instance(piège classique : on l’oublie et on met à jour la mauvaise ligne, voire aucune). _delete_instancedoit tout nettoyer : tables filles, item de note, et — si on stockait des fichiers — les fichiers via la File API. Une suppression incomplète laisse des données orphelines (dette et fuite RGPD potentielle).
⚠️ Piège : ne supprimez pas le course module ni le contexte dans
_delete_instance— le core s’en charge autour de votre appel. Vous ne nettoyez que vos données.
5.3 Le helper d’items (interne)
/**
* (Ré)enregistre les items depuis le textarea du formulaire (un item par ligne).
*/
function simplecheck_save_items(stdClass $data): void {
global $DB;
// Stratégie simple : on efface et on réécrit les items.
$DB->delete_records('simplecheck_item', ['simplecheckid' => $data->id]);
$lines = preg_split('/\r\n|\r|\n/', (string) $data->items);
$position = 0;
foreach ($lines as $line) {
$name = trim($line);
if ($name === '') {
continue;
}
$DB->insert_record('simplecheck_item', (object) [
'simplecheckid' => $data->id,
'name' => $name,
'position' => $position++,
]);
}
}💡 Pour un dev React :
_add_instance/_update_instance/_delete_instanceforment le CRUD du type de contenu au niveau du framework — l’équivalent des handlerscreate/update/deleted’une entité, mais appelés par le core au bon moment du cycle de vie de l’activité. Vous ne les invoquez jamais vous-même : vous les implémentez, le core les orchestre.
6. mod_form.php : régler l’instance
Le formulaire de création/édition d’une activité étend moodleform_mod (variante de moodleform qui injecte les champs communs à toute activité : nom, description, réglages communs, achèvement, groupes, notes…).
<?php
// public/mod/simplecheck/mod_form.php
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/moodleform_mod.php');
class mod_simplecheck_mod_form extends moodleform_mod {
public function definition(): void {
$mform = $this->_form;
// --- En-tête « Généraux » ---
$mform->addElement('header', 'general', get_string('general', 'form'));
// Nom de l'activité (champ standard obligatoire).
$mform->addElement('text', 'name', get_string('name'), ['size' => 64]);
$mform->setType('name', PARAM_TEXT);
$mform->addRule('name', null, 'required', null, 'client');
// Description standard (fournie par le core si FEATURE_MOD_INTRO).
$this->standard_intro_elements();
// --- Champs spécifiques ---
$mform->addElement('header', 'itemshdr', get_string('items', 'mod_simplecheck'));
$mform->addElement('textarea', 'items',
get_string('items', 'mod_simplecheck'),
['rows' => 8, 'cols' => 60]);
$mform->setType('items', PARAM_TEXT);
$mform->addHelpButton('items', 'items', 'mod_simplecheck');
// --- Éléments communs : notes, achèvement, groupes, visibilité… ---
$this->standard_grading_coursemodule_elements(); // section « Note »
$this->standard_coursemodule_elements(); // achèvement, groupes, restrictions…
// Boutons standard (avec sesskey).
$this->add_action_buttons();
}
/**
* Pré-remplit le textarea d'items à l'édition (les items ne sont pas dans la table principale).
*/
public function data_preprocessing(&$defaults): void {
global $DB;
if (!empty($this->current->id)) {
$items = $DB->get_records('simplecheck_item',
['simplecheckid' => $this->current->id], 'position ASC');
$defaults['items'] = implode("\n", array_map(fn($i) => $i->name, $items));
}
}
/**
* Règles d'achèvement personnalisées (affichées dans la section Achèvement).
*/
public function add_completion_rules(): array {
$mform = $this->_form;
$mform->addElement('checkbox', 'completionallchecked',
'', get_string('completionallchecked', 'mod_simplecheck'));
return ['completionallchecked'];
}
public function completion_rule_enabled($data): bool {
return !empty($data['completionallchecked']);
}
}Ce que moodleform_mod apporte gratuitement :
standard_intro_elements(): le champ description (WYSIWYG) — nécessiteFEATURE_MOD_INTRO.standard_grading_coursemodule_elements(): toute la section Note (type de note, note max, catégorie de note…). C’est ce qui remplit$data->grade.standard_coursemodule_elements(): les sections communes Achèvement, Groupes, Restrictions d’accès, Visibilité. C’est là que nosadd_completion_rules()s’insèrent.
⚠️ Piège : nos items ne sont pas dans la table principale
simplecheckmais danssimplecheck_item. Le formulaire ne les pré-remplit donc pas tout seul : il fautdata_preprocessing()pour recharger le textarea à l’édition. Oublier cette méthode donne un bug frustrant : « quand je réédite l’activité, mes items ont disparu du formulaire » (alors qu’ils sont bien en base).
7. view.php : ouvrir l’activité
La page qu’on atteint en cliquant sur l’activité dans le cours. Elle applique le pattern sécurisé du chapitre 6.3, adapté aux activités (le contexte est un CONTEXT_MODULE, obtenu via le course module).
<?php
// public/mod/simplecheck/view.php
require(__DIR__ . '/../../config.php');
require_once(__DIR__ . '/lib.php');
// L'activité s'ouvre soit par ?id=<cmid> (id du course module), soit par ?s=<instanceid>.
$id = optional_param('id', 0, PARAM_INT);
// get_coursemodule_from_id résout cm + course en vérifiant la cohérence.
[$course, $cm] = get_course_and_cm_from_cmid($id, 'simplecheck');
$simplecheck = $DB->get_record('simplecheck', ['id' => $cm->instance], '*', MUST_EXIST);
require_login($course, true, $cm); // login + inscription au cours + visibilité du cm
$context = context_module::instance($cm->id); // contexte MODULE
require_capability('mod/simplecheck:view', $context);
// --- Déclenche l'event « activité vue » (traçabilité, achèvement « vu ») ---
$event = \mod_simplecheck\event\course_module_viewed::create([
'objectid' => $simplecheck->id,
'context' => $context,
]);
$event->add_record_snapshot('course', $course);
$event->add_record_snapshot('simplecheck', $simplecheck);
$event->trigger();
// Marque la vue pour l'achèvement (FEATURE_COMPLETION_TRACKS_VIEW).
$completion = new completion_info($course);
$completion->set_module_viewed($cm);
// --- Traitement de la soumission (l'étudiant coche) ---
if (($data = data_submitted()) && confirm_sesskey()
&& has_capability('mod/simplecheck:submit', $context)) {
simplecheck_save_user_checks($simplecheck, $USER->id, $data->check ?? []);
simplecheck_update_grades($simplecheck, $USER->id); // met à jour SA note
// Ré-évalue l'achèvement (une règle custom a pu passer à « terminé »).
$completion->update_state($cm, COMPLETION_UNKNOWN, $USER->id);
redirect(new moodle_url('/mod/simplecheck/view.php', ['id' => $cm->id]),
get_string('saved', 'mod_simplecheck'), null,
\core\output\notification::NOTIFY_SUCCESS);
}
// --- Configuration de page + rendu ---
$PAGE->set_url(new moodle_url('/mod/simplecheck/view.php', ['id' => $cm->id]));
$PAGE->set_context($context);
$PAGE->set_cm($cm, $course);
$PAGE->set_title(format_string($simplecheck->name));
$PAGE->set_heading(format_string($course->fullname));
$output = $PAGE->get_renderer('mod_simplecheck');
$renderable = new \mod_simplecheck\output\checklist($simplecheck, $cm, $context, $USER->id);
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($simplecheck->name));
if (!empty($simplecheck->intro)) {
echo $OUTPUT->box(format_module_intro('simplecheck', $simplecheck, $cm->id), 'generalbox', 'intro');
}
echo $output->render($renderable);
echo $OUTPUT->footer();Les spécificités activité par rapport au pattern générique :
- On résout le course module avec
get_course_and_cm_from_cmid($id, 'simplecheck'): il vérifie que lecmexiste, appartient bien à ce module, et renvoie$course+$cm. require_login($course, true, $cm)vérifie non seulement la connexion mais l’inscription au cours et la visibilité du course module (activité cachée, restrictions d’accès).- Le contexte est un
context_module. - On déclenche un event et on marque la vue pour l’achèvement (voir §9).
format_module_intro()rend la description en appliquant les filtres et la File API (images intégrées).
⚠️ Piège : ne jamais faire confiance à
?id=sans passer parget_course_and_cm_from_cmid+require_login(..., $cm). C’est la porte d’entrée d’une activité, et c’est là que se joue tout le contrôle d’accès (cours, inscription, visibilité, restrictions). Unview.phpqui charge directement$DB->get_record('simplecheck', ['id' => $id])sans ces vérifications est une faille béante.
Le helper de sauvegarde des coches (interne à lib.php) :
/**
* Enregistre l'état coché des items pour un utilisateur.
*
* @param stdClass $simplecheck
* @param int $userid
* @param array $checked tableau [itemid => 1] des cases cochées
*/
function simplecheck_save_user_checks(stdClass $simplecheck, int $userid, array $checked): void {
global $DB;
$items = $DB->get_records('simplecheck_item', ['simplecheckid' => $simplecheck->id]);
$now = time();
foreach ($items as $item) {
$ischecked = !empty($checked[$item->id]) ? 1 : 0;
$existing = $DB->get_record('simplecheck_check',
['simplecheckid' => $simplecheck->id, 'itemid' => $item->id, 'userid' => $userid]);
if ($existing) {
$existing->checked = $ischecked;
$existing->timemodified = $now;
$DB->update_record('simplecheck_check', $existing);
} else {
$DB->insert_record('simplecheck_check', (object) [
'simplecheckid' => $simplecheck->id,
'itemid' => $item->id,
'userid' => $userid,
'checked' => $ischecked,
'timemodified' => $now,
]);
}
}
}8. Le carnet de notes
C’est souvent la partie la plus déroutante pour un débutant. Moodle a un sous-système de notes central ; une activité y pousse ses notes, elle ne les stocke pas dans le carnet elle-même. Deux fonctions dans lib.php.
8.1 _grade_item_update : déclarer l’item de note
/**
* Crée ou met à jour l'item de note de l'activité dans le carnet.
*
* @param stdClass $simplecheck l'instance (avec ->grade = note max)
* @param mixed $grades notes à pousser, 'reset' pour supprimer, ou null
* @return int GRADE_UPDATE_OK, etc.
*/
function simplecheck_grade_item_update(stdClass $simplecheck, $grades = null): int {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
$params = [
'itemname' => $simplecheck->name,
'gradetype' => GRADE_TYPE_VALUE,
'grademax' => $simplecheck->grade,
'grademin' => 0,
];
if ($grades === 'reset') {
$params['deleted'] = true;
$grades = null;
}
return grade_update('mod/simplecheck', $simplecheck->course, 'mod', 'simplecheck',
$simplecheck->id, 0, $grades, $params);
}8.2 _update_grades : pousser les notes des élèves
/**
* Calcule et pousse les notes (pour un utilisateur, ou tous).
*
* @param stdClass $simplecheck
* @param int $userid 0 = tous les utilisateurs
*/
function simplecheck_update_grades(stdClass $simplecheck, int $userid = 0): void {
global $DB;
// Nombre d'items de cette activité.
$itemcount = $DB->count_records('simplecheck_item', ['simplecheckid' => $simplecheck->id]);
if ($itemcount == 0) {
// Rien à noter : on pousse des notes nulles.
simplecheck_grade_item_update($simplecheck);
return;
}
// Utilisateurs concernés.
$params = ['simplecheckid' => $simplecheck->id, 'checked' => 1];
$usersql = '';
if ($userid) {
$usersql = ' AND userid = :userid';
$params['userid'] = $userid;
}
// Nombre d'items cochés par utilisateur.
$sql = "SELECT userid, COUNT(*) AS done
FROM {simplecheck_check}
WHERE simplecheckid = :simplecheckid AND checked = :checked $usersql
GROUP BY userid";
$rows = $DB->get_records_sql($sql, $params);
$grades = [];
foreach ($rows as $row) {
$grades[$row->userid] = (object) [
'userid' => $row->userid,
// note = proportion cochée × note max.
'rawgrade' => ($row->done / $itemcount) * $simplecheck->grade,
];
}
if ($grades) {
simplecheck_grade_item_update($simplecheck, $grades);
}
}Le mécanisme, résumé :
grade_update()(degradelib.php) est l’unique porte vers le carnet de notes. On lui passe l’identité de l’item (source, course,'mod','simplecheck', instanceid) et soit ses paramètres (grademax…), soit les notes des élèves.- On appelle
_grade_item_updateà la création/modif de l’activité (l’item existe même sans note), et_update_gradesà chaque changement de note (soumission d’un étudiant, ou recalcul global à l’édition). - Le carnet gère ensuite tout : agrégation, catégories, export, historique. On ne réimplémente rien.
⚠️ Piège : ne jamais écrire directement dans les tables
mdl_grade_*. Toujours passer pargrade_update()/ l’APIgradelib. Écrire à la main casse l’agrégation, l’historique et les caches du carnet — un grand classique des plugins amateurs.
💡 Pour un dev React : le carnet de notes est un service central ; votre activité est un producteur d’événements de note qui publie via une API (
grade_update). Vous ne « possédez » pas la donnée de note affichée — vous la poussez vers un système qui l’agrège avec toutes les autres activités. Pensez à un bus d’événements où le gradebook est l’agrégateur.
9. L’achèvement d’activité personnalisé
Moodle offre l’achèvement standard (manuel, ou « l’étudiant doit voir l’activité » via FEATURE_COMPLETION_TRACKS_VIEW, déjà branché au §7). On ajoute une règle personnalisée : « terminé quand tous les items sont cochés ». Cela passe par une classe dédiée (API moderne d’achèvement, Moodle 3.11+).
<?php
// public/mod/simplecheck/classes/completion/custom_completion.php
namespace mod_simplecheck\completion;
use core_completion\activity_custom_completion;
defined('MOODLE_INTERNAL') || die();
class custom_completion extends activity_custom_completion {
/**
* Détermine l'état d'une règle pour l'utilisateur courant.
*
* @param string $rule le nom de la règle (ex. 'completionallchecked')
* @return int COMPLETION_COMPLETE ou COMPLETION_INCOMPLETE
*/
public function get_state(string $rule): int {
global $DB;
$this->validate_rule($rule); // garde-fou
$simplecheckid = $this->cm->instance;
$userid = $this->userid;
$total = $DB->count_records('simplecheck_item', ['simplecheckid' => $simplecheckid]);
if ($total == 0) {
return COMPLETION_INCOMPLETE;
}
$done = $DB->count_records('simplecheck_check',
['simplecheckid' => $simplecheckid, 'userid' => $userid, 'checked' => 1]);
return ($done >= $total) ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE;
}
/** Les règles définies par ce module. */
public static function get_defined_custom_rules(): array {
return ['completionallchecked'];
}
/** Descriptions lisibles (affichées dans les rapports d'achèvement). */
public function get_custom_rule_descriptions(): array {
return [
'completionallchecked' => get_string('completionallchecked_desc', 'mod_simplecheck'),
];
}
/** Ordre d'affichage des règles (standard + custom). */
public function get_sort_order(): array {
return ['completionview', 'completionallchecked', 'completionusegrade'];
}
}Le flux d’ensemble :
FEATURE_COMPLETION_HAS_RULES(dans_supports) active les règles custom.mod_form::add_completion_rules()(§6) ajoute la case « Terminé quand tout est coché » à la section Achèvement du formulaire.- À chaque soumission,
view.phpappelle$completion->update_state($cm, COMPLETION_UNKNOWN, $USER->id), qui interroge notrecustom_completion::get_state(). - Moodle affiche « À faire / Terminé » sur la vignette de l’activité et dans le rapport d’achèvement.
⚠️ Piège : après avoir changé une règle d’achèvement en base, ou modifié le code, les états sont mis en cache par élève. En dev, l’achèvement peut sembler « bloqué » ; en réel, Moodle recalcule via une tâche planifiée et les appels
update_state. Pensez àupdate_stateexplicitement après toute action qui peut changer l’état (comme on le fait dansview.php).
💡 Pour un dev React : l’achèvement est une machine à états dérivée :
get_state()est une fonction pure(cm, user) → COMPLETE|INCOMPLETEque Moodle appelle et met en cache. Vous ne stockez pas « terminé » vous-même — vous fournissez le calcul, le core gère le stockage, l’invalidation et l’affichage. Très proche d’un sélecteur mémoïsé.
10. Backup / restore
Pour qu’une activité survive à une sauvegarde de cours, à un import ou à une duplication, il faut décrire quelles données sauvegarder et comment les restaurer. C’est le dossier backup/moodle2/. FEATURE_BACKUP_MOODLE2 (dans _supports) l’active.
Structure de sauvegarde — le stepslib décrit l’arbre XML des données du module :
<?php
// public/mod/simplecheck/backup/moodle2/backup_simplecheck_stepslib.php
defined('MOODLE_INTERNAL') || die();
class backup_simplecheck_activity_structure_step extends backup_activity_structure_step {
protected function define_structure(): backup_nested_element {
// Faut-il inclure les données utilisateur (les coches) ?
$userinfo = $this->get_setting_value('userinfo');
// Élément racine : l'instance.
$simplecheck = new backup_nested_element('simplecheck', ['id'], [
'name', 'intro', 'introformat', 'grade', 'timecreated', 'timemodified',
]);
// Items (structure, sans données utilisateur).
$items = new backup_nested_element('items');
$item = new backup_nested_element('item', ['id'], ['name', 'position']);
// Coches (données utilisateur).
$checks = new backup_nested_element('checks');
$check = new backup_nested_element('check', ['id'],
['itemid', 'userid', 'checked', 'timemodified']);
// Arbre.
$simplecheck->add_child($items);
$items->add_child($item);
$simplecheck->add_child($checks);
$checks->add_child($check);
// Sources (requêtes qui alimentent l'arbre).
$simplecheck->set_source_table('simplecheck', ['id' => backup::VAR_ACTIVITYID]);
$item->set_source_table('simplecheck_item', ['simplecheckid' => backup::VAR_PARENTID]);
if ($userinfo) {
$check->set_source_table('simplecheck_check', ['simplecheckid' => backup::VAR_PARENTID]);
}
// Les champs référençant des utilisateurs sont « annotés » (remapping à la restauration).
$check->annotate_ids('user', 'userid');
// Fichiers éventuels (intro).
$simplecheck->annotate_files('mod_simplecheck', 'intro', null);
return $this->prepare_activity_structure($simplecheck);
}
}Structure de restauration — le stepslib inverse : lire le XML, réinsérer, remapper les ids.
<?php
// public/mod/simplecheck/backup/moodle2/restore_simplecheck_stepslib.php (extrait)
defined('MOODLE_INTERNAL') || die();
class restore_simplecheck_activity_structure_step extends restore_activity_structure_step {
protected function define_structure(): array {
$paths = [];
$userinfo = $this->get_setting_value('userinfo');
$paths[] = new restore_path_element('simplecheck', '/activity/simplecheck');
$paths[] = new restore_path_element('item', '/activity/simplecheck/items/item');
if ($userinfo) {
$paths[] = new restore_path_element('check', '/activity/simplecheck/checks/check');
}
return $this->prepare_activity_structure($paths);
}
protected function process_simplecheck(array $data): void {
global $DB;
$data = (object) $data;
$data->course = $this->get_courseid();
$newid = $DB->insert_record('simplecheck', $data);
$this->apply_activity_instance($newid); // relie le cm au nouvel id
}
protected function process_item(array $data): void {
global $DB;
$data = (object) $data;
$data->simplecheckid = $this->get_new_parentid('simplecheck');
$newid = $DB->insert_record('simplecheck_item', $data);
$this->set_mapping('simplecheck_item', $data->id, $newid); // pour remapper itemid des checks
}
protected function process_check(array $data): void {
global $DB;
$data = (object) $data;
$data->simplecheckid = $this->get_new_parentid('simplecheck');
$data->itemid = $this->get_mappingid('simplecheck_item', $data->itemid); // remap !
$data->userid = $this->get_mappingid('user', $data->userid); // remap !
$DB->insert_record('simplecheck_check', $data);
}
}(Les fichiers *_activity_task.class.php orchestrent, encodent les liens inter-activités et appellent ces steps ; ils sont courts et très standard — voir un module du core.)
Le concept central de la restauration est le remapping d’ids : quand on restaure dans un autre cours, les userid, itemid, cmid changent. annotate_ids (backup) + get_mappingid (restore) assurent que les coches pointent vers les bons nouveaux items et utilisateurs.
⚠️ Piège : omettre
FEATURE_BACKUP_MOODLE2ou le dossierbackup/moodle2/a une conséquence grave et silencieuse : votre activité disparaît des sauvegardes et des imports/duplications de cours. Les enseignants perdent leur travail sans erreur visible. Le backup n’est pas optionnel pour une activité destinée à la production.
💡 Pour un dev React : backup/restore est de la sérialisation/désérialisation avec ré-ancrage de références. Le backup produit un « JSON » (ici XML) de vos entités ; la restauration le rejoue en traduisant les clés étrangères vers le nouvel environnement (nouveaux ids d’utilisateurs, de cours). Le
set_mapping/get_mappingidest exactement une table de correspondance ancien→nouveau id, comme quand vous réimportez des données relationnelles dans une autre base.
11. Reset de cours, index, et vignette
11.1 Réinitialisation de cours : _reset_userdata
Quand un enseignant « réinitialise » un cours (nouvelle promotion), il veut effacer les données des étudiants en gardant la structure. Notre module doit y répondre.
/**
* Efface les données utilisateur lors d'un reset de cours.
*/
function simplecheck_reset_userdata(stdClass $data): array {
global $DB;
$status = [];
if (!empty($data->reset_simplecheck_checks)) {
$sql = "DELETE FROM {simplecheck_check}
WHERE simplecheckid IN (SELECT id FROM {simplecheck} WHERE course = :course)";
$DB->execute($sql, ['course' => $data->courseid]);
// Remet les notes à zéro.
$simplechecks = $DB->get_records('simplecheck', ['course' => $data->courseid]);
foreach ($simplechecks as $sc) {
simplecheck_grade_item_update($sc, 'reset');
}
$status[] = [
'component' => get_string('modulenameplural', 'mod_simplecheck'),
'item' => get_string('resetchecks', 'mod_simplecheck'),
'error' => false,
];
}
return $status;
}
/**
* Ajoute notre case à cocher au formulaire de reset.
*/
function simplecheck_reset_course_form_definition($mform): void {
$mform->addElement('header', 'simplecheckheader', get_string('modulenameplural', 'mod_simplecheck'));
$mform->addElement('checkbox', 'reset_simplecheck_checks', get_string('resetchecks', 'mod_simplecheck'));
}11.2 index.php : lister les activités du module dans un cours
Page atteinte via « Plus → checklists notées » : la liste de toutes les instances du module dans le cours. Obligatoire (le core y renvoie).
<?php
// public/mod/simplecheck/index.php
require(__DIR__ . '/../../config.php');
$id = required_param('id', PARAM_INT); // course id
$course = $DB->get_record('course', ['id' => $id], '*', MUST_EXIST);
require_login($course);
$context = context_course::instance($course->id);
$PAGE->set_url(new moodle_url('/mod/simplecheck/index.php', ['id' => $id]));
$PAGE->set_context($context);
$PAGE->set_title(get_string('modulenameplural', 'mod_simplecheck'));
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('modulenameplural', 'mod_simplecheck'));
$instances = get_all_instances_in_course('simplecheck', $course);
if (!$instances) {
notice(get_string('noinstances', 'mod_simplecheck'), new moodle_url('/course/view.php', ['id' => $id]));
}
$table = new html_table();
$table->head = [get_string('name'), get_string('items', 'mod_simplecheck')];
foreach ($instances as $instance) {
$link = new moodle_url('/mod/simplecheck/view.php', ['id' => $instance->coursemodule]);
$count = $DB->count_records('simplecheck_item', ['simplecheckid' => $instance->id]);
$table->data[] = [html_writer::link($link, format_string($instance->name)), $count];
}
echo html_writer::table($table);
echo $OUTPUT->footer();11.3 Enrichir la vignette : _cm_info_view / _get_coursemodule_info
Pour afficher un indicateur sur la page de cours (ex. « 3/5 items »), le module implémente _get_coursemodule_info (rendu léger, mis en cache) :
/**
* Infos affichées sur la page de cours pour cette activité.
*/
function simplecheck_get_coursemodule_info(stdClass $cm): cached_cm_info {
global $DB;
$info = new cached_cm_info();
if ($simplecheck = $DB->get_record('simplecheck', ['id' => $cm->instance], 'id, name, intro, introformat')) {
$info->name = $simplecheck->name;
if ($cm->showdescription) {
$info->content = format_module_intro('simplecheck', $simplecheck, $cm->id, false);
}
}
return $info;
}⚠️ Piège :
_get_coursemodule_infoest appelée pour chaque activité au rendu de la page de cours — elle est mise en cache et doit rester très légère (pas de calcul lourd, pas de boucle de requêtes). Mettre un calcul de progression par étudiant ici plomberait la page de cours. Gardez le lourd pourview.php.
12. Carte du module et récap « obligatoire / optionnel »
| Brique | Statut | Pourquoi |
|---|---|---|
version.php, install.xml, access.php | Obligatoire | identité, schéma, :addinstance |
_supports, _add/_update/_delete_instance | Obligatoire | cycle de vie de l’instance |
mod_form.php, view.php, index.php | Obligatoire | régler, ouvrir, lister |
Carnet de notes (_grade_item_update/_update_grades) | Optionnel | si l’activité est notée (ici oui) |
| Achèvement custom | Optionnel | si règles au-delà de « vu »/« noté » |
| Events | Recommandé | traçabilité, rapports, achèvement |
| Backup/restore | Optionnel mais critique en prod | sinon l’activité est perdue en sauvegarde/import |
_reset_userdata | Recommandé | réinit. de cours propre |
_get_coursemodule_info | Optionnel | vignette enrichie |
💡 Pour un dev React : remarquez la progressivité du contrat. Le minimum (
version+ schéma +_supports+ add/delete +view) donne une activité qui marche. Chaque sous-système (notes, achèvement, backup, events) s’ajoute par une fonction/classe nommée que le core détecte et appelle. C’est de l’opt-in par convention : vous n’héritez pas d’une méga-classe à tout implémenter — vous branchez ce dont vous avez besoin, une capacité à la fois. C’est ce qui rend le typemod, malgré son ampleur, apprenable brique par brique.
✏️ Exercices
Exercice 1 — L’update écrit sur la mauvaise ligne.
Un développeur constate qu’à l’édition d’une activité, les modifications ne sont jamais enregistrées (ou touchent une autre instance). Son simplecheck_update_instance fait $DB->update_record('simplecheck', $data); sans autre traitement. Où est le bug ?
✅ Solution
À l’update, le core passe l’id de l’instance dans $data->instance, pas dans $data->id. Sans la ligne $data->id = $data->instance;, update_record reçoit un id absent ou incorrect et met à jour la mauvaise ligne (ou échoue). C’est le piège n°1 du cycle de vie d’un mod. Correctif : ajouter $data->id = $data->instance; avant l’update_record, comme au §5.2.
Exercice 2 — Les items disparaissent à la réédition. À l’édition d’une activité existante, le textarea des items est vide, alors que les items sont bien en base. Pourquoi, et comment corriger ?
✅ Solution
Les items ne sont pas dans la table principale simplecheck : le core ne peut donc pas pré-remplir le champ items automatiquement (il ne remplit que les colonnes de la table du module). Il faut implémenter data_preprocessing(&$defaults) dans mod_form.php pour recharger les items depuis simplecheck_item et les concaténer dans $defaults['items'] (voir §6). Sans cette méthode, le formulaire s’ouvre avec un textarea vide et écrase les items à l’enregistrement (puisque save_items réécrit depuis le textarea).
Exercice 3 — La note ne monte pas dans le carnet.
L’étudiant coche tout, mais sa note reste vide dans « Notes ». simplecheck_update_grades est bien appelée. Citez deux causes possibles.
✅ Solution
(1) L’item de note n’existe pas : simplecheck_grade_item_update n’a pas été appelée à la création/édition (ou FEATURE_GRADE_HAS_GRADE n’est pas déclaré) → il n’y a pas de colonne dans le carnet où pousser. (2) rawgrade mal calculé ou hors bornes : si $itemcount == 0 on ne pousse rien ; si grademax (le ->grade de l’instance) est 0, la note reste nulle. Vérifier aussi qu’on passe bien par grade_update() (jamais d’écriture directe dans mdl_grade_*) et que $grades a la bonne forme [userid => (object)['userid'=>…, 'rawgrade'=>…]].
Exercice 4 — Achèvement bloqué à « à faire ».
La règle « terminé quand tout est coché » ne bascule jamais à « Terminé », même quand l’étudiant a tout coché. custom_completion::get_state est correcte. Que manque-t-il probablement ?
✅ Solution
Il manque très probablement l’appel à $completion->update_state($cm, COMPLETION_UNKNOWN, $USER->id) après la soumission dans view.php : sans lui, Moodle ne réévalue pas l’état et l’affichage reste figé sur l’ancien état (mis en cache). Vérifier aussi que FEATURE_COMPLETION_HAS_RULES est déclaré dans _supports (sinon la règle n’est même pas active) et que la case completionallchecked a bien été activée dans les réglages de l’activité (via add_completion_rules + completion_rule_enabled).
Exercice 5 — Activité absente des sauvegardes.
Un client signale qu’après duplication d’un cours, les activités simplecheck ont disparu de la copie. Diagnostic et correction ?
✅ Solution
Le module ne supporte pas le backup Moodle2 : soit FEATURE_BACKUP_MOODLE2 n’est pas retourné par _supports, soit le dossier backup/moodle2/ (tasks + stepslib) est absent ou incomplet. Sans lui, l’activité est silencieusement ignorée lors de toute sauvegarde/import/duplication. Correction : déclarer la feature et fournir les classes backup_simplecheck_* et restore_simplecheck_* (§10), en n’oubliant pas les annotations/remapping (annotate_ids('user', 'userid'), set_mapping/get_mappingid pour itemid et userid) — sans quoi la restauration recréerait des coches pointant vers de mauvais items/utilisateurs.
Exercice 6 — Charge sur la page de cours.
Un développeur veut afficher « 3/5 items cochés » par étudiant directement sur la vignette de l’activité, dans simplecheck_get_coursemodule_info. Pourquoi est-ce une mauvaise idée, et quelle est l’alternative ?
✅ Solution
_get_coursemodule_info est appelée pour chaque activité au rendu de chaque page de cours, et son résultat est mis en cache de façon partagée (indépendante de l’utilisateur pour l’essentiel). Y mettre un calcul par étudiant (requête sur simplecheck_check) provoque des requêtes en boucle sur une page très fréquentée (problème N+1) et casse la logique de cache. Alternative : afficher la progression détaillée dans view.php (page de l’activité, calcul à la demande), et se contenter sur la vignette d’un indicateur léger et non personnalisé, ou s’appuyer sur l’achèvement (déjà calculé et mis en cache par élève par le core) pour le « Terminé / À faire ».
🧠 Quiz de révision
Q1. Quelles sont les contraintes de nommage propres à un module d’activité (dossier, table, lang, fonctions) ?
Réponse
mod/<nom>/ ; component mod_<nom> ; table principale nommée exactement <nom> (tables filles préfixées <nom>_) ; fichier de langue lang/en/<nom>.php (sans mod_) ; fonctions de lib.php préfixées par <nom>_ (ex. simplecheck_add_instance). Ici : simplecheck.Q2. À quoi sert _supports et pourquoi retourner null par défaut ?
Réponse
_supports($feature) déclare les capacités du module via les constantes FEATURE_* (notes, intro, achèvement, backup, groupes…), ce qui branche les sous-systèmes correspondants. On retourne null pour une feature inconnue (« je ne sais pas »), à distinguer de false (« je connais et je ne supporte pas ») — le core traite les deux différemment.Q3. Comment une activité pousse-t-elle une note dans le carnet, et que ne doit-elle jamais faire ?
Réponse
gradelib : _grade_item_update crée/met à jour l’item de note (bornes, nom), et _update_grades pousse les notes des élèves, tous deux appelant grade_update(). Elle ne doit jamais écrire directement dans les tables mdl_grade_* (cela casse agrégation, historique et caches).Q4. Quel est le rôle du remapping d’ids dans la restauration ?
Réponse
annotate_ids('user', 'userid')) et la restauration les retraduit via set_mapping/get_mappingid, pour que les données filles (les coches) pointent vers les nouveaux items et utilisateurs. Sans ce remapping, les références seraient corrompues.Q5. Pourquoi le backup n’est-il « optionnel » qu’en théorie pour une activité de production ?
Réponse
FEATURE_BACKUP_MOODLE2 + backup/moodle2/, l’activité est silencieusement omise des sauvegardes, imports et duplications de cours : les enseignants perdent leur contenu sans message d’erreur. Pour toute activité destinée à de vrais cours, le backup est donc de fait indispensable.Chapitre suivant : 06 — Les autres types de plugins en pratique — des exemples courts mais complets : un admin tool (tool_) avec tâche planifiée, un filtre de texte, un format de cours, une condition de disponibilité (availability_), un champ personnalisé (customfield_), et des survols honnêtes des types complexes (qtype_, repository_, tiny_).