Source: resultatFormatters/j3p.js

/**
 * This file is part of Sesatheque.
 *   Copyright 2014-2015, Association Sésamath
 *
 * Sesatheque is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * Sesatheque 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Sesatheque (LICENCE.txt).
 * @see http://www.gnu.org/licenses/agpl.txt
 *
 *
 * Ce fichier fait partie de lapplication Sésathèque, créée par lassociation Sésamath.
 *
 * Sésathèque est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant
 * les termes de la GNU Affero General Public License version 3 telle que publiée par la
 * Free Software Foundation.
 * Sésathèque est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE
 * sans même la garantie tacite de QUALITÉ MARCHANDE ou d'ADÉQUATION à UN BUT PARTICULIER.
 * Consultez la GNU Affero General Public License pour plus de détails.
 * Vous devez avoir reçu une copie de la GNU General Public License en même temps que Sésathèque
 * (cf LICENCE.txt et http://vvlibri.org/fr/Analyse/gnu-affero-general-public-license-v3-analyse
 * pour une explication en français)
 */

import { getBaseUrlFromResultat } from './helper'
let i = 0

function getScoresByNode (contenu) {
  // Pour le score global, on veut un seul score par nœud (le meilleur si y'en a plusieurs)
  const scoresByNode = {}
  contenu.scores.forEach((score, index) => {
    const node = contenu.noeuds[index]
    if (!node) return console.warn('résultat incohérent avec un score sans nœud correspondant', contenu)
    if (typeof score !== 'number') return
    if (typeof scoresByNode[node] !== 'number' || scoresByNode[node] < score) scoresByNode[node] = score
  })
  return scoresByNode
}

/**
 * Retourne le score d'un résultat (ce code devrait être dans j3p, mis là en attendant que les pbs de scores soient réglés dans j3p pour recalcul à chaque affichage)
 * @param resultat
 * @return {number|undefined}
 */
function getRealScore (resultat) {
  // tant que c'est pas fini c'est undefined
  if (!resultat.fin) return undefined
  if (resultat.contenu && resultat.contenu.scores && resultat.contenu.scores.length) {
    let total = 0
    let nbNodes = 0
    if (resultat.nextId) {
      // on moyenne sur tous les nœuds avec scores
      resultat.contenu.scores.forEach(score => {
        if (typeof score === 'number' && score >= 0 && score <= 1) {
          total += score
          nbNodes++
        }
      })
    } else {
      // avant l'ajout de nextId dans le résultat, on avait tous les scores concaténés dans le même résultat, faut prendre le meilleur par node
      const scoresByNode = getScoresByNode(resultat.contenu)
      Object.entries(scoresByNode).forEach(([node, score]) => {
        if (typeof score === 'number' && score >= 0 && score <= 1) {
          nbNodes++
          total += score
        } else {
          console.warn(`score ${score} du nœud ${node} incohérent dans ce résultat`, resultat)
        }
      })
    }
    if (nbNodes) return total / nbNodes
    // sinon ça reste undefined
  }
}

function getReponse (resultat, isTxt) {
  const separator = isTxt ? '\n' : '<br />'
  if (resultat && resultat.contenu && resultat.contenu.scores && resultat.contenu.scores.length) {
    const nbScores = resultat.contenu.scores.length
    // à une époque noeuds pouvait avoir un élément de plus
    if (resultat.contenu.noeuds.length >= nbScores) {
      const results = []
      resultat.contenu.scores.forEach((score, index) => {
        const noeud = resultat.contenu.noeuds[index]
        if (noeud) {
          if (typeof score === 'number' && score >= 0 && score <= 1) results.push(`Nœud ${noeud} : ${Math.round(100 * score)}%`)
        } else {
          console.error(`score d’index ${index} sans nœud correspondant dans ce résultat`, resultat)
        }
      })
      if (results.length) return results.join(separator)
    } else {
      // bizarre on prend le meilleur score par nœud
      console.warn('resultat bizarre, avec noeuds', resultat.contenu.noeuds, 'et scores', resultat.contenu.scores)
      const scoresByNode = getScoresByNode(resultat.contenu)
      return Object.entries(scoresByNode)
        .map(([node, score]) => `Nœud ${node} : ${Math.round(100 * score)}%`)
        .join(separator)
    }
  }
  return 'Pas d’information enregistrée'
}

/**
 * Exo j3p (graphe d'exercices en js), avec getHtmlFullReponse dispo
 * @type TypeFormatters
 * @memberOf resultatFormatters
 */
export const j3p = {
  getScore: (resultat) => {
    const score = getRealScore(resultat)
    if (resultat.fin && typeof score !== 'number') {
      console.error(`Pas de score sur un parcours terminé (1 imposé pour permettre de passer à la suite en cas de condition de réussite), résultat ${resultat.oid}`)
      return 1
    }
    return score
  },

  /**
   * Retourne le score à afficher sur la page html des bilans (pas forcément un nombre)
   * @param {Resultat} resultat
   * @return {string}
   */
  getHtmlScore: (resultat) => {
    // pour j3p on s'attend à avoir
    if (!resultat.fin) return 'Parcours non terminé'
    if (resultat.score === undefined) console.error('parcours terminé sans score', resultat)
    // y'a eu un gros bug dans j3p, un résultat enregistré plusieurs fois avec le même oid, avec un tableau scores qui augmente mais un score du resultat jamais mis à jour
    // ex {"noeuds":["1","1","1","1","1","1","1","1"],"pe":[0.75,1,1,1],"scores":[0.75,1,1,1],"ns":["fin","fin","fin","fin"],"boucle":[1,1,1,1],"graphe":[[],["1","squelettemtg32_Calc_Ecriture_Scientifique",[{"pe":">=0","nn":"fin","conclusion":"fin"},{"ex":"Lycee_Fraction_Reduction_1","limite":"","nbEssais":7,"nbchances":2,"nbrepetitions":4,"boitedialogue":"oui","indicationfaute":false,"simplifier":true,"a":"random","b":"random","c":"random","d":"random","e":"random","f":"random","g":"random","h":"random","j":"random","k":"random","l":"random","m":"random","n":"random","r":"random","p":"random","q":"random"}]]]}
    // on recalcule score à chaque affichage
    const oldScore = resultat.score
    let score = getRealScore(resultat)
    if (typeof score === 'number' && typeof oldScore === 'number' && Math.abs(oldScore - score) > 0.005) {
      console.warn(`le score enregistré ${oldScore} ne correspond pas à celui calculé ${score}`, resultat)
      if (score < oldScore && oldScore <= 1) score = oldScore // on baisse pas un score déjà attribué, même s'il est foireux…
    }

    if (typeof score === 'number') return Math.round(resultat.score * 100) + '%'
    return 'pas de score pour ce parcours (mis à 100% pour permettre le passage au suivant en cas de minimum de réussite exigé)'
  },

  /**
   * Retourne la réponse à insérer sur la page html des bilans
   * @param {Resultat} resultat
   * @param {FormatterOptions} [options] isTxt géré
   * @return {string}
   */
  getHtmlReponse: (resultat) => getReponse(resultat, false),

  /**
   * Retourne la réponse à insérer dans un csv
   * @param {Resultat} resultat
   * @return {string}
   */
  getTxtReponse: (resultat) => getReponse(resultat, true),

  /**
   * Réponse en pleine page
   * @param {Resultat} resultat
   * @return {string}
   */
  getHtmlFullReponse: function getHtmlFullReponse (resultat) {
    if (resultat && resultat.contenu && resultat.contenu.noeuds && resultat.contenu.noeuds.length > 1) {
      // on a un bug qq part qui donnait par ex du
      // noeuds: ['1', '1'], 'pe': [1]
      if (resultat.contenu.noeuds.every(node => node === '1')) {
        console.warn('graphe à un seul nœud bizarrement multiple', JSON.stringify(resultat.contenu))
        return ''
      }

      // faut virer les nœuds fin éventuels (ils ne sont plus dans les résultats récents mais toujours dans les anciens)
      const noeudsActifs = resultat.contenu.graphe.filter(n => n && typeof n[1] === 'string' && n[1].toLowerCase() !== 'fin')
      if (noeudsActifs.length < 2) return ''

      // on gère l'unicité des ids, même si à priori on est tout seul dans une iframe
      const myId = 'j3pParcours' + (i++)
      const baseUrl = getBaseUrlFromResultat(resultat)
      const { hostname } = new URL(baseUrl)
      const isDev = /(sesamath\.dev|localhost|\.local)$/.test(hostname)
      // plus besoin, webpack inclue ça dans le js
      // const cssUrl = baseUrl + 'showParcours.css'
      const jsUrl = `https://j3p.sesamath.${isDev ? 'dev' : 'net'}/build/loader.js`
      const contenuResultat = JSON.stringify(resultat.contenu)
      // dans ce qui suit, on peut mettre de l'es6 dans les ${} mais pas en dehors
      // normalement le js expose en global une fct stshowParcours, mais avec un pb webpack on peut se retrouver
      // avec le module complet et pas son export par défaut, et vu que c'est arrivé et resté comme ça pendant des mois…
      // …on traite les deux cas
      return `
<div id="${myId}"></div>
<script type="application/javascript">
  (function () {
    'use strict'
    var conteneur = document.getElementById("${myId}")
    if (typeof showParcours === 'function') return showParcours(conteneur, ${contenuResultat})
    // sinon c'est le premier appel, faut le charger
    var head = window.document.getElementsByTagName('head')[0]
    var body = window.document.getElementsByTagName('body')[0]
    if (!head || !body) throw Error('Page html malformée (head ou body manquant)')
    // ajout du js
    var scriptTag = document.createElement('script')
    scriptTag.type = 'application/javascript'
    scriptTag.src = '${jsUrl}'
    scriptTag.addEventListener('load', function () {
      if (typeof showParcours === 'function') return showParcours(conteneur, ${contenuResultat})
      console.error('Problème de chargement de showParcours')
      conteneur.innerHTML = '<p style="color:"#d33">Impossible d’afficher le parcours réalisé (problème de chargement de l’afficheur)</p>'
    })
    body.appendChild(scriptTag)
  })()
</script>`
    } else {
      return ''
    }
  }
}