Skip to content

Commit

Permalink
Update templates/tutorialv2/view/container.html
Browse files Browse the repository at this point in the history
Co-authored-by: Situphen <[email protected]>
  • Loading branch information
artragis and Situphen committed Oct 30, 2022
1 parent b2a0561 commit 15c9c8e
Show file tree
Hide file tree
Showing 21 changed files with 384 additions and 81 deletions.
160 changes: 142 additions & 18 deletions assets/js/content-quizz.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,34 @@ function extractAnswer(radio, answers) {
} else {
answers[rb.parentNode.parentNode.getAttribute('id')].push(rb.checked)
}
console.log(index - 1)
console.log('is checked')
console.log(rb.checked)
rb.setAttribute('value', answers[rb.parentNode.parentNode.getAttribute('id')].length - 1)
rb.disabled = false
rb.checked = false
})
}

function initializeCheckboxes(answers) {
function initializeCheckboxes(answers, blockIds) {
const checkboxes = document.querySelectorAll('.quizz ul li input[type=checkbox]')
extractAnswer(checkboxes, answers)
extractAnswer(checkboxes, answers, blockIds)
}


function initializeRadio(answers) {
function initializeRadio(answers, blockIds) {
const radio = document.querySelectorAll('.quizz ul li input[type=radio]')
extractAnswer(radio, answers)
extractAnswer(radio, answers, blockIds)
}

const initializePipeline = [initializeCheckboxes, initializeRadio]

function computeForm(formdata, answers) {
const badAnswers = []
const allAnswerNames = []
for (const entry of formdata.entries()) {
const name = entry[0]
const values = parseInt(entry[1], 10)
allAnswerNames.push(name)
if (!answers[name]) {
console.log('not found ' + name)
continue
Expand All @@ -57,11 +59,26 @@ function computeForm(formdata, answers) {
}
}
}
return badAnswers
return [badAnswers, allAnswerNames]
}

function markBadAnswers(names, answers) {
const toAdd = []
if (names.length === 0) {
Object.keys(answers).forEach(answer => {
let mustMarkBad = false
answers[answer].forEach((value, index) => {
const inputAnswer = document.querySelector(`#${answer} input[value="${index}"]`)
if (value && !inputAnswer.checked) {
inputAnswer.parentElement.classList.add('quizz-forget')
mustMarkBad = true
}
})
if (mustMarkBad) {
document.querySelector(`div[data-name="${answer}"]`).classList.add('quizz-bad')
}
})
}
names.forEach(({ name }) => {
document.querySelectorAll('input[name="' + name + '"]').forEach(field => {
if (answers[name][parseInt(field.getAttribute('value'), 10)] && !field.checked) {
Expand All @@ -74,25 +91,115 @@ function markBadAnswers(names, answers) {
})
document.querySelector(`.custom-block[data-name=${name}]`).classList.add('quizz-bad')
})
names.forEach(({ name, value }) => {
names.forEach(({
name,
value
}) => {
document.querySelector(`input[type=checkbox][name="${name}"][value="${value}"]`)
.parentElement.classList.add('quizz-bad')
})
toAdd.forEach(name => names.push(name))
}

function getWantedHeading(questionNode, nodeName, attr) {
let potentialHeading = questionNode
while (potentialHeading[attr] &&
potentialHeading.nodeName !== nodeName.toUpperCase()) {
potentialHeading = potentialHeading[attr]
}
if (potentialHeading.nodeName !== nodeName.toUpperCase()) {
return null
}
return potentialHeading
}

function injectForms(quizz, answers) {
const searchedTitle = quizz.getAttribute('data-heading-level') || 'h3'
const submitLabel = quizz.getAttribute('data-quizz-validate') || 'Validate'
const headings = {}
let idBias = 0
const wrappers = []
Object.keys(answers).forEach(blockId => {
const blockNode = document.getElementById(blockId)
if (!blockNode) {
// if the node was treated and therefore the clone has not been reinserted yet
return
}
const questionNode = blockNode.parentElement.parentElement
const heading = getWantedHeading(questionNode, searchedTitle, 'previousSibling') || quizz
if (!heading.getAttribute('id')) {
console.log('new id')
heading.setAttribute('id', `quizz-form-${idBias}`)
idBias++
}
if (heading && !headings[heading.getAttribute('id')]) {
const wrapper = document.createElement('div')

headings[heading.getAttribute('id')] = true
const form = document.createElement('form')
form.classList.add('quizz')

const submit = document.createElement('button')
submit.innerText = submitLabel
submit.classList.add('btn-submit')
submit.classList.add('btn')
const result = document.createElement('p')
result.classList.add('result')
let nodeToAddToForm = heading
if (heading === quizz) {
nodeToAddToForm = quizz.firstChild
} else {
nodeToAddToForm = heading.nextSibling
}
form.method = 'POST'
form.setAttribute('action', quizz.getAttribute('data-answer-url'))

while (nodeToAddToForm && nodeToAddToForm.nodeName !== searchedTitle.toUpperCase()) {
const current = nodeToAddToForm
nodeToAddToForm = nodeToAddToForm.nextSibling
form.appendChild(current.cloneNode(true))
current.parentNode.removeChild(current)
}
form.appendChild(result)
form.appendChild(submit)
wrappers.push(wrapper)
if (nodeToAddToForm && nodeToAddToForm.nodeName === searchedTitle.toUpperCase()) {
wrapper.append(form)
wrappers.push(nodeToAddToForm.cloneNode(true))
} else {
wrapper.appendChild(form)
if (heading.nextSibling) {
wrapper.appendChild(heading.nextSibling.cloneNode(true))
heading.parentNode.removeChild(heading.nextSibling)
}
}
}
if (heading.nodeName === searchedTitle.toUpperCase()) {
quizz.removeChild(heading)
}
})
console.log(wrappers)
wrappers.forEach((wrapper) => quizz.appendChild(wrapper))
}

const answers = {}
initializePipeline.forEach(func => func(answers))
document.querySelectorAll('div.quizz').forEach(div => {
injectForms(div, answers)
})
document.querySelectorAll('form.quizz').forEach(form => {
form.addEventListener('submit', e => {
e.preventDefault()
e.stopPropagation()
const formData = new FormData(form)
// result = name of bad answers
const result = computeForm(formData, answers)
markBadAnswers(result, answers)
const [badAnswerNames, allAnswerNames] = computeForm(formData, answers)
markBadAnswers(badAnswerNames, answers)
allAnswerNames.forEach(name => {
document.getElementById(name).parentElement.parentElement.classList.add('hasAnswer')
})
const questions = []
result.forEach(result => {
badAnswerNames.forEach(result => {
if (questions.indexOf(result.name) === -1) {
questions.push(result.name)
}
Expand All @@ -101,11 +208,13 @@ document.querySelectorAll('form.quizz').forEach(form => {
expected: {},
result: {}
}
let nbGood = 0
let nbTotal = 0
Object.keys(answers).forEach(name => {
const element = document.querySelector(`.custom-block[data-name="${name}"]`)
let title = element.querySelector('.custom-block-heading').textContent
const correction = element.querySelector('.custom-block-heading+div')
if (correction) {
if (correction && title.indexOf(correction.textContent) > 0) {
title = title.substr(0, title.indexOf(correction.textContent))
}
statistics.result[title] = {
Expand All @@ -118,10 +227,22 @@ document.querySelectorAll('form.quizz').forEach(form => {
statistics.expected[title][availableResponses[i].parentElement.textContent] = answers[name][i]
}
element.querySelectorAll('input:checked')
.forEach(node => statistics.result[title].labels.push(node.parentElement.textContent.trim()))
if (!element.classList.contains('quizz-bad')) {
element.classList.add('quizz-good')
statistics.result[title].evaluation = 'ok'
.forEach(node => {
let label = node.parentElement.textContent
if (correction && label.indexOf(correction.textContent) !== -1) {
label = label.substr(0, label.indexOf(correction.textContent))
}
statistics.result[title].labels.push(label.trim())
})
if (element.classList.contains('hasAnswer')) {
nbTotal++

if (!element.classList.contains('quizz-bad') &&
!element.classList.contains('quizz-forget')) {
element.classList.add('quizz-good')
statistics.result[title].evaluation = 'ok'
nbGood++
}
}
})
const csrfmiddlewaretoken = document.querySelector('input[name=\'csrfmiddlewaretoken\']').value
Expand All @@ -130,9 +251,12 @@ document.querySelectorAll('form.quizz').forEach(form => {
xhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
xhttp.setRequestHeader('Content-Type', 'application/json')
xhttp.setRequestHeader('X-CSRFToken', csrfmiddlewaretoken)
statistics.url = form.parentElement.previousElementSibling.firstElementChild.href
statistics.url = form.parentElement.parentElement.previousElementSibling.firstElementChild.href
xhttp.send(JSON.stringify(statistics))
// here send result
console.log(result)
const percentOfAnswers = (100 * 1.0 * nbGood / nbTotal).toLocaleString('fr-FR', {
minimumIntegerDigits: 1,
useGrouping: false
})
form.querySelector('.result').innerText = `Vous avez bien répondu à ${percentOfAnswers}% des questions.`
})
})
29 changes: 27 additions & 2 deletions assets/scss/base/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,32 @@ h6 {
display: none;
}
&.quizz-bad {
background-color: $color-danger !important;
display: block;
& .custom-block-body {
background-color: $accent-500 ;
}
.custom-block {
display: block;
border: solid 1px $grey-600;
}
}
&.quizz-good {
background:$color-success !important;
display: block;
& .custom-block-body{
background: $green-500;
}
.custom-block {
display: block;
border: solid 1px $grey-600;
.custom-block-body {
font-weight: normal;
}
}
}

}


.custom-block {
margin: $length-24 0;
}
Expand Down Expand Up @@ -257,6 +269,15 @@ h6 {
&:after {
display: none;
}

.custom-block {
display: none;
/* hide the correction while not answered */
&.quizz-good, &.quizz-bad, &.quizz-missing {
display: block;
}
}

}
}

Expand Down Expand Up @@ -634,6 +655,9 @@ li.task-list-item {

background-color: $true-white !important;
}
&.quizz-forget {
font-weight: bolder;
}
&.quizz-forget {
font-weight: bolder;
}
Expand All @@ -642,6 +666,7 @@ li.task-list-item {
}
}


.warn-typo {
height: $length-20;
}
14 changes: 14 additions & 0 deletions assets/scss/pages/_stats.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,17 @@
margin: 0 auto;
}
}

.stat-graph ul.quizz-answers {
max-width: 50%;
li.quizz-stats {
display: grid;
grid-template-columns: 3fr 3fr 1fr;
grid-gap: 1px;
.over-progress {
display: block;
position: relative;
left: -50%;
}
}
}
10 changes: 10 additions & 0 deletions doc/source/back-end/contents_manifest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,17 @@ Version 2.0
6. ``children`` : tableau vers les enfants de niveau inférieur si l'enfant est de type *container*. **Obligatoire**
7. ``text`` : nom du fichier contenant le texte quand l'enfant est de type *extract*. Nous conseillons de garder la convention ``nom de fichier = slug.md`` mais rien n'est obligatoire à ce sujet. **Obligatoire**

Version 2.1
-----------

Cette version ajoute le paramètre "ready_to_publish" dans les ``Container`` qui vaut ``True`` par défaut.
Cet attribut permet d'implémenter la fonctionnalité de "publication partielle".

Version 2.2
-----------

Cette version ajoute le paramètre ``is_quizz`` dans les ``Extract`` afin de créer des extraits de type Quizz qui auront leur
correction automatiquement calculée par le client.


Version 1.0
Expand Down
41 changes: 41 additions & 0 deletions doc/source/back-end/quizz.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
===============================
Ajout de Quizz à Zest de savoir
===============================

Zeste de savoir est capablede présenter des quizz à l'utilisateur. La philosophie qui a été retenue est :

- L'auteur écrit la **correction** du quizz dans un extrait qui est marqué comme étant un quizz.
- Lorsque le tutoriel est publié, côté client, un script parcourt cette correction et la transforme en formulaire.
- Une fois que le lecteur a rempli son quizz, la correction est automatiquement calculée et rendue au lecteur.
- Des statistiques sont alors envoyées à l'auteur pour qu'il voit où sont les erreurs de chacun.

L'ajout de quizz n'est pas une mince affaire dans notre code d'autant qu'il est important de les rendre **faciles** à rédiger.
En effet, rédiger sur ZDS demande pas mal d'effort et l'ajout de quizz avec les méthodes habituelles à base
de formulaires spéciiques, qui permettent de "configurer le rendu" de la question est peu pertinent.

La première version du module de quizz permet d'ajouter des QCM mais a été conçue pour permettre l'ajout d'autres types de réponse.

Liens avec ZMarkdown
====================

Le module des quizz nécessite ZMarkdown 9 pour être fonctionnel sur la version web et ZMarkdown 10 pour être proprement exporté en PDF.
Les epub affichent uniquement la correction pour l'instant.

Utiliser Zmarkdown > 10
-----------------------

Avec l'utilisation de zmarkdown > 10 vous pouvez utiliser le bloc ``[[quizz | Intitulé de la question]]``.
Ce bloc a le même comportement que le bloc neutre. Cependant il permet d'ajouter des informations de contexte nécessaire à l'export Latex.
De même on devrait pouvoir tirer partie du fait que ce bloc a une classe précise dans les implémentations des futurs types de questions.

Ajouter des types de questions
------------------------------

Globalement, l'ajout de type de questions, disons "texte libre" pour l'exemple se décompose en trois parties :

- Décider de quelle syntaxe Markdown on tirera partie pour déinir la correction. Imaginons ici qu'on utilise la syntaxe
des codes inlines (deux `)
- Implémenter dans zMarkdown un preprocessor de ``quizzCustomBlock`` qui permettra de remplacer les ``inlineCode`` par un texte
composé de ``______`` dans le quizz et les laissera intact dans la correction
- Adapter ``content_quizz.js`` et ``statistics.py`` pour que la correction se fasse et que les statistiques remontent

Loading

0 comments on commit 15c9c8e

Please sign in to comment.