Source: constructors/Ressource.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 l'application Sésathèque, créée par l'association 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)
 */

'use strict'
import tools from 'sesajstools'
import filters from 'sesajstools/utils/filters'

const { hasProp } = tools

/**
 * Filtre une liste de personne en vérifiant que c'est bien de la forme baseId/xxx
 * @param {string[]} list
 * @return {Array} la liste filtrée (toujours un array, éventuellement vide si list était vide ou undefined)
 */
function filterUserList (list) {
  if (!list) return []
  if (!Array.isArray(list)) {
    console.error(Error('liste de pids invalide (pas un array)'), list)
    return []
  }
  if (!list.length) return []
  // c'est un array non vide
  // avec mocha (p'tet aussi autrement), on peut avoir du [null], on essaie de trouver où
  const listCleaned = list.filter(pid => pid)
  if (listCleaned.length < list.length) console.error(Error('liste de pid avec des null ou undefined'))

  return listCleaned.map(pid => {
    if (typeof pid !== 'string') {
      console.error(Error(`pid de type invalide : ${typeof pid} (${pid})`))
      return null
    }
    const slashPos = pid.indexOf('/')
    if (slashPos < 1 || slashPos === pid.length) {
      console.error(Error(`pid invalide : ${pid}`))
      return null
    }
    return pid
  }).filter(pid => pid) // vire les éventuels null mis par le map
}

/**
 * Constructeur de l'objet Ressource (utilisé par l'entity Ressource coté serveur ou les plugins coté client)
 * @constructor
 * @param {Object} initObj Un objet ayant des propriétés d'une ressource
 * @param {string} myBaseId @deprecated
 */
function Ressource (initObj, myBaseId) {
  /**
   * @private
   * @type {Ressource}
   */
  var values = Object.assign({}, initObj)

  // on a déjà l'oid
  if (values.oid) {
    /**
     * L'identifiant interne à cette Sésathèque
     * @type {string}
     */
    this.oid = values.oid
    if (values.rid) {
      /**
       * Identifiant unique de ressource (baseId/oid pour usage inter Sesathèques)
       * @type {string}
       */
      this.rid = values.rid
    } else if (values.baseId) {
      this.rid = values.baseId + '/' + values.oid
    } else if (myBaseId) {
      this.rid = myBaseId + '/' + values.oid
    } else {
      throw Error('Impossible d’instancier une Ressource avec oid sans rid ni baseId')
    }
  } else if (values.rid) {
    this.rid = values.rid
    // et on en déduit l'oid si on peut
    if (myBaseId && values.rid.indexOf(myBaseId + '/') === 0) {
      this.oid = values.rid.substr(myBaseId.length + 1)
    }
  }

  if (values.aliasOf) {
    /**
     * Pointe vers la ressource réelle (si ça existe on est un alias)
     * @default undefined
     * @type {string}
     */
    this.aliasOf = values.aliasOf
    // si on nous passe une Ref qui provient d'un alias, on a aussi le rid de l'alias
    if (values.aliasRid) {
      if (!this.rid) this.rid = values.aliasRid
      else if (this.rid !== values.aliasRid) throw Error(`aliasRid (${values.aliasRid}) et rid ${this.rid} incompatibles`)
    }
  }
  if (values.cle) {
    /**
     * Une clé permettant de lire la ressource (si elle est publiée) en outrepassant les droits
     * @default undefined
     * @type {string}
     */
    this.cle = filters.string(values.cle)
  }
  /**
   * identifiant du dépôt d'origine (où est stockée et géré la ressource), 'local' si créé sur cette sesatheque
   * @default ''
   * @type {string}
   */
  this.origine = filters.string(values.origine)
  /**
   * Id de la ressource dans son dépôt d'origine
   * @default ''
   * @type {string}
   */
  this.idOrigine = filters.string(values.idOrigine)
  /**
   * Le code du plugin qui gère la ressource
   * @default ''
   * @type {string}
   */
  this.type = filters.string(values.type)
  /**
   * Titre
   * @default ''
   * @type {string}
   */
  this.titre = filters.string(values.titre)
  /**
   * Résumé qui apparait souvent au survol du titre ou dans les descriptions brèves, destiné à tous
   * @default ''
   * @type {string}
   */
  this.resume = filters.string(values.resume)
  /**
   *  Description plus complète, facultative (préférer le résumé)
   *  @default ''
   *  @type {string}
   */
  this.description = filters.string(values.description)
  /**
   * Commentaires destinés aux éditeurs, ou au prescipteur de la ressource mais pas à l'utilisateur
   * @default ''
   * @type {string}
   */
  this.commentaires = filters.string(values.commentaires)
  if (values.enfants) {
    /**
     * Les enfants de l'arbre (à la place de la propriété parametres si type vaut 'arbre')
     * @type {Object}
     */
    this.enfants = Array.isArray(values.enfants) ? values.enfants : []
    // on accepte une chaîne json
    if (values.enfants && typeof values.enfants === 'string') {
      try {
        var enfants = JSON.parse(values.enfants)
        if (Array.isArray(enfants)) this.enfants = enfants
        else throw Error('enfants invalides')
      } catch (error) {
        console.error(error)
      }
    }
  }
  if (values.parametres) {
    if (values.parametres instanceof Object) {
      /**
       * Contenu qui dépend du type (toutes les infos spécifique à ce type)
       * @type {Object}
       */
      this.parametres = values.parametres
    } else if (typeof values.parametres === 'string') {
      // on accepte une chaîne json
      try {
        const parametres = JSON.parse(values.parametres)
        if (typeof parametres === 'object') this.parametres = parametres
      } catch (error) {
        console.error(error)
      }
    }
  }
  if (!this.parametres) this.parametres = {}
  /**
   * Niveaux scolaire de la ressource
   * (faudra gérér ultérieurement différents système éducatif, fr_FR pour tout le monde en attendant)
   * @type {Array}
   */
  this.niveaux = filters.arrayString(values.niveaux, false)
  /**
   * Un id de catégorie correspond à un recoupement de types, par ex [7] pour 'exercice interactif'
   * @type {Array}
   */
  this.categories = filters.arrayInt(values.categories, false)
  /**
   * Type pédagogique (5.2 - scolomfr-voc-010) : cours, exercice...
   * C'est un champ conditionné par la catégorie, mais à priori seulement, l'utilisateur peut modifier / enrichir
   * @see {@link http://www.lom-fr.fr/scolomfr/la-norme/manuel-technique.html?tx_scolomfr_pi1[detailElt]=62}
   * @type {Array}
   */
  this.typePedagogiques = filters.arrayInt(values.typePedagogiques, false)
  /**
   * type documentaire (1.9 - scolomfr-voc-004) : image, ressource interactive, son, texte
   * Idem, conditionné par la catégorie mais à priori seulement
   * @see {@link http://www.lom-fr.fr/scolomfr/la-norme/manuel-technique.html?tx_scolomfr_pi1[detailElt]=49}
   * @type {Array}
   */
  this.typeDocumentaires = filters.arrayInt(values.typeDocumentaires, false)

  if (values.relations && Array.isArray(values.relations) && values.relations.length) {
    /**
     * Liste des ressources liées, une liaison étant un array [idLiaison, idRessourceLiée]
     * idRessourceLiée peut être un oid ou une string origine/idOrigine
     * @type {relation[]}
     */
    this.relations = values.relations
      .filter(rel => Array.isArray(rel) && rel.length === 2)
      .map(([relId, relTarget]) => [filters.int(relId), filters.string(relTarget)])
      .filter(([relId, relTarget]) => relId && relTarget)
  } else {
    this.relations = []
  }
  /**
   * Liste d'auteurs
   * @type {string[]}
   */
  this.auteurs = filterUserList(values.auteurs, myBaseId)
  /**
   * Liste d'url pour les auteurs précédents (lors d'un fork)
   * @type {string[]}
   */
  this.auteursParents = filterUserList(values.auteursParents, myBaseId)
  /**
   * Liste de contributeurs
   * @type {string[]}
   */
  this.contributeurs = filterUserList(values.contributeurs, myBaseId)
  /**
   * Liste de noms de groupes dans lesquels cette ressource est publiée
   * @type {string[]}
   */
  this.groupes = filters.arrayString(values.groupes, false).map(nom => nom.trim())
  /**
   * Liste de noms de groupes dont les membres peuvent modifier cette ressource
   * @type {string[]}
   */
  this.groupesAuteurs = filters.arrayString(values.groupesAuteurs, false).map(nom => nom.trim())
  /**
   * code langue ISO 639-2
   * @see {@link http://fr.wikipedia.org/wiki/Liste_des_codes_ISO_639-2}
   * @type {string}
   */
  this.langue = filters.string(values.langue) || 'fra'
  /**
   * Vrai si la ressource est publiée (les non-publiées sont visibles par leur auteur
   * et ceux ayant les droits en écriture dessus)
   * false par défaut
   * @type {boolean}
   */
  this.publie = !!values.publie
  /**
   * Restriction sur la ressource, cf lassi.settings.ressource.constantes.restriction
   * @type {Integer}
   */
  this.restriction = filters.int(values.restriction)
  if (hasProp(values, 'public') && !hasProp(values, 'restriction')) {
    if (values.public) this.restriction = 0
    else if (values.groupes && values.groupes.length) this.restriction = 2
    else this.restriction = 3
  }
  /**
   * Date de création
   * @type {Date}
   */
  this.dateCreation = filters.date(values.dateCreation) || new Date()
  /**
   * Date de mise à jour
   * @type {Date}
   */
  this.dateMiseAJour = filters.date(values.dateMiseAJour)
  /**
   * Version de la ressource
   * @type {Integer}
   */
  this.version = filters.int(values.version) || 1
  /**
   * Un suffixe pour les urls publiques (pour le cache des navigateurs)
   * @type {number}
   */
  this.inc = filters.int(values.inc)
  /**
   * Si la ressource est indexable elle peut sortir dans un résultat de recherche
   * Passer à false pour des ressources 'obsolètes' car remplacées par d'autres, mais toujours publiées car utilisées.
   * @type {boolean}
   * @default true
   */
  this.indexable = hasProp(values, 'indexable') ? !!values.indexable : true
  /**
   * L'oid de l'archive correspondant à la version précédente
   * @default undefined
   * @type {string}
   */
  this.archiveOid = filters.string(values.archiveOid)
  /**
   * Une liste d'avertissements éventuels (incohérences, données manquantes, etc.)
   * Pratique d'avoir un truc pour faire du push dedans sans vérifier qu'il existe
   * Non sauvegardé
   * @default {[]}
   * @type {string[]}
   */
  this.$warnings = []
  if (values.$warnings) {
    this.$warnings = filters.arrayString(values.$warnings)
  }
  /**
   * Une liste d'erreurs éventuelles (incohérences, données manquantes, etc.)
   * Bloque l'enregistrement s'il n'est pas vide (sinon viré avant enregistrement)
   * @default {[]}
   * @type {string[]}
   */
  this.$errors = []
  if (values.$errors) {
    this.$errors = filters.arrayString(values.$errors)
  }
  // et on ajoute des erreurs si on a viré des trucs
  Object.getOwnPropertyNames(values).forEach(p => {
    /* global log */
    // this est bien l'objet courant car c'est une fct fléchée
    // mais on assure le coup en le passant en 2e param de forEach

    // on ignore public, déjà traité et mis dans restriction
    if (p === 'public') return
    // les archives passent par notre constructeur, on ignore donc silencieusement cette propriété supplémentaire
    if (p === 'dateArchivage') return
    // on ignore les propriétés ajoutées par un form pour du contexte
    if (p === 'new' || p === 'token' || p === 'force' || p.substr(0, 1) === '_') return
    // et celles qui commencent par $
    if (p.substr(0, 1) === '$') return

    if (Array.isArray(this[p])) {
      // pour les tableaux on regarde si on a toujours autant d'éléments
      if (Array.isArray(values[p])) {
        if (this[p].length < values[p].length) {
          this.$errors.push(`des éléments de la propriété ${p} étaient invalides et ont été ignorés`)
          // log est global dans la sésathèque mais on peut être utilisé coté client
          if (typeof log === 'function' && log.debug) {
            log.debug(`pour ${p} on a initialement`, values[p])
            log.debug('qui donne', this[p])
          } else {
            console.error(`${p} valait`, values[p], 'et est devenu', this[p])
          }
        }
      } else if (values[p]) {
        // on voulait un array mais on nous a filé autre chose
        this.$errors.push(`La propriété ${p} n’était pas un tableau, elle a été ignorée`)
        console.error('contenu invalide', values[p])
      }

      // sinon c'est scalaire ou objet
    } else if (!hasProp(this, p)) {
      // falsy ignorés, c'est normal
      if (!values[p]) return
      this.$warnings.push(`La propriété ${p} n’existe pas dans une ressource, elle a été ignorée`)
      if (typeof log !== 'undefined') log.dataError(`propriété ${p} ignorée dans`, values)
      else console.error(`propriété ${p} ignorée`, values[p])
    }
  }, this) // au cas où qqun virerait la fct flèchée…
}

/**
 * Cast en string d'une ressource (son titre)
 * @returns {string}
 */
Ressource.prototype.toString = function () {
  return this.titre
}

export default Ressource