CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Exemple : modélisation de QCMs

Introduction

On souhaite modéliser des QCMs, avec des questions acceptant soit une seule réponse, soit plusieurs réponses. Un modèle de QCM doit permettre de générer le QCM à différents formats : page web, formulaire papier, outil de gestion de questionnaires en ligne.

Métamodèle

Le métamodèle décrit les concepts supportés par le langage de modélisation, ainsi que les relations entre ces concepts.

Dans notre cas, les concepts premiers sont :

QCM
représente un questionnaire
SingleChoiceQuestion
représente une question admettant une unique réponse
MultipleChoiceQuestion
représente une question admettant plusieurs réponses
Answer
représente une réponse possible à une question

Les relations sont très simples : un QCM est composé de questions qui ont des réponses possibles.

On obtient le métamodèle suivant :

Remarques

  • Dans ce métamodèle, on a explicité la notion abstraite de Question, dont SingleChoice et MultipleChoice sont des raffinements, et on a factorisé le fait d'avoir une description dans la classe abstraite DescribedEntity.
  • Les classes n'ont pas de comportement, uniquement des attributs, car un métamodèle décrit une syntaxe abstraite et ne donne pas de sémantique. On a donné un attribut name de type String à QCM afin de pouvoir identifier les QCMs, et d'avoir un nom pour les fichiers à générer. On a donné un attribut score de type Integer à Answer pour représenter le nombre de points (positif ou négatif) que rapporte cette réponse.
  • Les relations de composition permettent bien d'atteindre tous les éléments d'un modèle à partir d'un élément racine (ici, une instance de QCM). La structure d'un modèle selon ces relations est bien un arbre.

Génération de code HTML/JavaScript

On souhaite maintenant générer une page web qui affiche un QCM, qui permet de sélectionner des réponses, et qui affiche un score sur 20 points qui ne doit pas être négatif.

On considère l'exemple de QCM suivant :

  • QCM name="qcm1" description="Example QCM"
    • SingleChoice description="Is it easy?"
      • Answer description="yes" score=1
      • Answer description="no" score=-1
    • MultipleChoice description="Why MDE is important?"
      • Answer description="It is fashionable" score=-1
      • Answer description="It is cool" score=0
      • Answer description="It helps to design systems" score=2

qui devrait donner cette page HTML.

Conception de la transformation

Il s'agit ici d'une transformation model to text, que nous réalisons en Acceleo. Le principe de la conception d'une telle transformation est d'écrire manuellement le résultat pour un modèle de petite taille, puis de repérer dans ce résultat les éléments qui dépendent du modèle, et de les remplacer par des instructions Acceleo de parcours et d'accès aux éléments du modèle.

Points clefs

  • le nom du fichier généré est obtenu en ajoutant '.html' au nom du QCM.
  • les crochets ouvrants [ doivent être remplacés par ['['/] (la chaîne de caractères '[' dans la syntaxe Acceleo) puisque ces crochets indiquent que l'on sort du texte à générer pour passer dans les instructions Acceleo.
  • dans une boucle [for ...] [/for], on a accès à une variable i qui varie de 1 à n si la boucle fait n tours. L'attribut separator d'une boucle [for] permet de spécifier le texte à insérer entre les éléments générés par les itérations successives de la boucle.
  • il est possible de définir d'autres templates que le template principal et de les appeler pour traiter des sous-parties du modèle.
  • les template sont polymorphes : il fonctionnent comme des méthodes dans un langage à objets. On peut ainsi définir un template pour une classe, le redéfinir pour les différentes sous-classes, et c'est bien le type effectif de l'élément du modèle passé en argument qui détermine quelle version du template est appelée.

Code de la transformation

[comment encoding = UTF-8 /]
[module generate('http://www.example.org/qCM')]

[comment Génération du code HTML pour les questions/]
[template private generateQuestionHTML(q : Question, n : Integer)]
<!-- Error: template called on abstract class -->
[/template]

[comment Traitement des questions à une seule réponse possible (boutons radio)/]
[template private generateQuestionHTML(q : SingleChoice, n : Integer)]
	    <h2>Question [n/]</h2>
	    <p>
	        [q.description/]
	    </p>
	    <ul>
[for (a : Answer | q.answers)]
	        <li><input type="radio" name="q[n/]" id="q[n/]-[i/]"/>[a.description/]</li>
[/for]
	    </ul>
[/template]

[comment Traitement des questions à plusieurs réponses possibles (cases à cocher)/]
[template private generateQuestionHTML(q : MultipleChoice, n : Integer)]
	    <h2>Question [n/]</h2>
	    <p>
	        [q.description/]
	    </p>
	    <ul>
[for (a : Answer | q.answers)]
	        <li><input type="checkbox" id="q[n/]-[i/]"/>[a.description/]</li>
[/for]
	    </ul>
[/template]

[comment Calcul du score maximal pour les questions/]
[template private generateScore(q : Question, n : Integer)]
<!-- Error: template called on abstract class -->
[/template]

[comment Pour les questions à une seule réponse, c'est le max des scores/]
[template private generateScore(q : SingleChoice, n : Integer)]
	    // Single choice question 1: get the max for all answers
	    max += Math.max([for (a : Answer | q.answers) separator (', ')][a.score/][/for]);
[/template]

[comment Pour les questions à plusieurs réponses, c'est la somme des scores positifs/]
[template private generateScore(q : MultipleChoice, n : Integer)]
	    // Multiple choice question 2: get the sum of the max score of all answers
	    max += ['['/][for (a : Answer | q.answers) separator (', ')][if (a.score > 0)][a.score/][else]0[/if][/for]].reduce((a, b) => a + b, 0);
[/template]

[comment Calcul du score obtenu à une question/]
[template private computeScore(q: Question, n: Integer)]
[for (a : Answer | q.answers)]
    if (document.getElementById("q[n/]-[i/]").checked) {
        score += [a.score/];
    }
[/for]
[/template]


[comment Génération du code HTML/CSS pour un QCM/]
[template public generateElement(aQCM : QCM)]
[comment @main/]
[file (aQCM.name.concat('.html'), false, 'UTF-8')]
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>[aQCM.name/]</title>

<style type="text/css">
ul {
    list-style-type:none;
}
</style>

<script>
// Grade the questions and display the result
function validate() {
    var score = 0;
    var max = 0;

    // Compute the maximum number of points that can be earnt
[for (q : Question | aQCM.questions)]
[generateScore(q, i)/]
[/for]
    // Compute the total number of points earnt
[for (q : Question | aQCM.questions)]
	[computeScore(q, i)/]
[/for]

    // Scale the points to get a score on 20 points
    var grade = Math.max(0, Math.round((score * 20) / max));
    document.getElementById("grade").textContent = grade;
}

// Reset the answers and the grade
function initQCM() {
    // Reset all answers to "not set"
[for (q : Question | aQCM.questions)]
[let n:Integer=i]
[for (a : Answer | q.answers)]
    document.getElementById("q[n/]-[i/]").checked = false;
[/for]
[/let]
[/for]

    // Clear the grade
    document.getElementById("grade").textContent = '?';
}
</script>
</head>

<body>
<h1>
    [aQCM.description/]
</h1>
[for (q : Question | aQCM.questions)]
[generateQuestionHTML(q, i)/]
[/for]

    <p>
        <input type="button" value="check" onclick="validate()"/>
        <input type="button" value="reset" onclick="initQCM()"/>
    </p>

    <p>
        Note : <span id="grade">?</span> / 20
    </p>
</body>
</html>
[/file]
[/template]