report_SessionReg.js

/**
 *  File    : report/SessionReg.js
 *  Created : 17/05/2016
 *  By      : Francesc Busquets <francesc@gmail.com>
 *
 *  JClic.js
 *  An HTML5 player of JClic activities
 *  https://projectestac.github.io/jclic.js
 *
 *  @source https://github.com/projectestac/jclic.js
 *
 *  @license EUPL-1.2
 *  @licstart
 *  (c) 2000-2020 Catalan Educational Telematic Network (XTEC)
 *
 *  Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
 *  the European Commission- subsequent versions of the EUPL (the "Licence");
 *  You may not use this work except in compliance with the Licence.
 *
 *  You may obtain a copy of the Licence at:
 *  https://joinup.ec.europa.eu/software/page/eupl
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *  Licence for the specific language governing permissions and limitations
 *  under the Licence.
 *  @licend
 *  @module
 */

import SequenceReg from './SequenceReg.js';

/**
 * This class encapsulates data of a user's working session, usually associated to a single {@link module:project/JClicProject.JClicProject JClicProject}
 * It's main component is `sequences`, an array of {@link module:report/SequenceReg.SequenceReg SequenceReg} objects.
 */
export class SessionReg {
  /**
   * SessionReg constructor
   * @param {module:project/JClicProject.JClicProject} project - The JClicProject referenced by this session.
   * @param {string} [code] - Optional code to be used by this SessionReg
   */
  constructor(project, code) {
    this.projectName = project.name;
    this.code = code || project.code;
    this.sequences = [];
    this.actNames = [];
    this.started = new Date();
    this.info = new SessionRegInfo(this);
    this.reportableActs = project.reportableActs;
  }

  /**
   * Builds a complex object with the results of all activities done during this working session
   * @param {boolean} recalcInfo - When `true`, global variables (number of sequences, score, total time...)
   * will be recalculated from the data stored in the {@link module:report/SequenceReg.SequenceReg SequenceReg} objects.
   * @param {boolean} includeEmpty - When `true`, sequences without reported activities will be also included in the results
   * @returns {object} - An object containing the full session data
   */
  getData(recalcInfo, includeEmpty) {
    if (recalcInfo)
      this.info.recalc();

    const result = {
      projectName: this.projectName,
      played: this.info.nActivities,
      ratioPlayed: Math.round(this.info.ratioPlayed * 100),
      solved: this.info.nActSolved,
      ratioSolved: Math.round(this.info.ratioSolved * 100),
      actions: this.info.nActions,
      score: this.info.tScore,
      time: Math.round(this.info.tTime / 10) / 100,
      sequences: []
    };

    this.sequences.forEach(s => {
      const seq = s.getData();
      if (includeEmpty || seq.activities.length > 0)
        result.sequences.push(seq);
    });
    return result;
  }

  /**
   * Returns the `info` element associated to this SessionReg.
   * @returns {module:report/SessionReg.SessionRegInfo}
   */
  getInfo() {
    return this.info.recalc();
  }

  /**
   * Closes this session
   */
  end() {
    this.endSequence();
  }

  /**
   * This method should be called when the current working session finishes.
   */
  endSequence() {
    if (this.currentSequence && this.currentSequence.totalTime === 0)
      this.currentSequence.endSequence();
    this.currentSequence = null;
    this.info.valid = false;
  }

  /**
   * This method should be invoked when a new sequence starts
   * @param {module:bags/ActivitySequenceElement.ActivitySequenceElement} ase - The {@link module:bags/ActivitySequenceElement.ActivitySequenceElement ActivitySequenceElement} referenced by this sequence.
   */
  newSequence(ase) {
    this.endSequence();
    this.currentSequence = new SequenceReg(ase);
    this.sequences.push(this.currentSequence);
    this.info.valid = false;
  }

  /**
   * This method should be invoked when the user starts a new activity
   * @param {module:Activity.Activity} act - The {@link module:Activity.Activity Activity} that has just started
   */
  newActivity(act) {
    if (this.currentSequence) {
      // Save activity name if not yet registered
      if (this.actNames.indexOf(act.name) === -1)
        this.actNames.push(act.name);
      this.currentSequence.newActivity(act);
      this.info.valid = false;
    }
  }

  /**
   * This method should be called when the current activity finishes. Data about user's final results
   * on the activity will then be saved.
   * @param {number} score - The final score, usually in a 0-100 scale.
   * @param {number} numActions - The total number of actions done by the user to solve the activity
   * @param {boolean} solved - `true` if the activity was finally solved, `false` otherwise.
   */
  endActivity(score, numActions, solved) {
    if (this.currentSequence) {
      this.currentSequence.endActivity(score, numActions, solved);
      this.info.valid = false;
    }
  }

  /**
   * Reports a new action done by the user while playing the current activity
   * @param {string} type - Type of action (`click`, `write`, `move`, `select`...)
   * @param {string}+ source - Description of the object on which the action is done.
   * @param {string}+ dest - Description of the object that acts as a target of the action (used in pairings)
   * @param {boolean} ok - `true` if the action was OK, `false`, `null` or `undefined` otherwise
   */
  newAction(type, source, dest, ok) {
    if (this.currentSequence) {
      this.currentSequence.newAction(type, source, dest, ok);
      this.info.valid = false;
    }
  }

  /**
   * Gets the name of the current sequence
   * @returns {string}
   */
  getCurrentSequenceTag() {
    return this.currentSequence ? this.currentSequence.name : null;
  }

  /**
   * Gets information about the current sequence
   * @returns {module:report/SequenceReg.SequenceRegInfo}
   */
  getCurrentSequenceInfo() {
    return this.currentSequence ? this.currentSequence.getInfo() : null;
  }
}

Object.assign(SessionReg.prototype, {
  /**
   * Number of activities suitable to be reported in this session
   * @name module:report/SessionReg.SessionReg#reportableActs
   * @type {number} */
  reportableActs: 0,
  /**
   * Array with unique names of the activities being reported in this session
   * @name module:report/SessionReg.SessionReg#actNames
   * @type {string[]} */
  actNames: null,
  /**
   * List of sequences done in this session
   * @name module:report/SessionReg.SessionReg#sequences
   * @type {module:report/SequenceReg.SequenceReg[]} */
  sequences: null,
  /**
   * The sequence currently active
   * @name module:report/SessionReg.SessionReg#currentSequence
   * @type {module:report/SequenceReg.SequenceReg} */
  currentSequence: null,
  /**
   * Starting date and time of this session
   * @name module:report/SessionReg.SessionReg#started
   * @type {external:Date} */
  started: null,
  /**
   * Name of the {@link module:project/JClicProject.JClicProject JClicProject} associated to this session
   * @name module:report/SessionReg.SessionReg#projectName
   * @type {string} */
  projectName: '',
  /**
   * Current session info
   * @name module:report/SessionReg.SessionReg#info
   * @type {module:report/SessionReg.SessionRegInfo} */
  info: null,
  /**
   * Optional code to be used with this session
   * @name module:report/SessionReg.SessionReg#code
   * @type {string} */
  code: null,
});

/**
 * This object stores the global results of a {@link module:report/SessionReg.SessionReg SessionReg}
 */
export class SessionRegInfo {
  /**
   * SessionRegInfo constructor
   * @param {module:report/SessionReg.SessionReg} sReg - The {@link module:report/SessionReg.SessionReg SessionReg} associated tho this `Info` object.
   */
  constructor(sReg) {
    this.sReg = sReg;
  }

  /**
   * Clears all data associated with this working session
   */
  clear() {
    this.numSequences = this.nActivities = this.nActSolved = this.nActScore = 0;
    this.ratioSolved = this.ratioPlayed = this.nActions = this.tScore = this.tTime = 0;
    this.valid = false;
  }

  /**
   * Computes the value of all global variables based on the data stored in `sequences`
   * @returns {module:report/SessionReg.SessionRegInfo} - This "info" object
   */
  recalc() {
    if (!this.valid) {
      this.clear();
      this.sReg.sequences.forEach(sr => {
        const sri = sr.getInfo();
        if (sri.nActivities > 0) {
          this.numSequences++;
          if (sri.nActClosed > 0) {
            this.nActivities += sri.nActClosed;
            this.nActions += sri.nActions;
            if (sri.nActScore > 0) {
              this.nActScore += sri.nActScore;
              this.tScore += sri.tScore * sri.nActScore;
            }
            this.tTime += sri.tTime;
            this.nActSolved += sri.nActSolved;
          }
        }
      });
      if (this.nActScore > 0)
        this.tScore = Math.round(this.tScore / this.nActScore);
      if (this.nActivities > 0) {
        this.ratioSolved = this.nActSolved / this.nActivities;
        if (this.sReg.reportableActs > 0)
          this.ratioPlayed = this.sReg.actNames.length / this.sReg.reportableActs;
      }
      this.valid = true;
    }
    return this;
  }
}

Object.assign(SessionRegInfo.prototype, {
  /**
   * The SessionReg linked to this Info object
   * @name module:report/SessionReg.SessionRegInfo#sReg
   * @type {module:report/SessionReg.SessionReg} */
  sReg: null,
  /**
   * When `false`, this session info needs to be recalculated
   * @name module:report/SessionReg.SessionRegInfo#valid
   * @type {boolean} */
  valid: false,
  /**
   * Number of sequences played
   * @name module:report/SessionReg.SessionRegInfo#numSequences
   * @type {number} */
  numSequences: 0,
  /**
   * Number of activities played
   * @name module:report/SessionReg.SessionRegInfo#nActivities
   * @type {number} */
  nActivities: 0,
  /**
   * Number of activities solved
   * @name module:report/SessionReg.SessionRegInfo#nActSolved
   * @type {number} */
  nActSolved: 0,
  /**
   * Number of activities with score > 0
   * @name module:report/SessionReg.SessionRegInfo#nActScore
   * @type {number} */
  nActScore: 0,
  /**
   * Percentage of solved activities
   * @name module:report/SessionReg.SessionRegInfo#ratioSolved
   * @type {number} */
  ratioSolved: 0,
  /**
   * Percentage of reportable activities played
   * @name module:report/SessionReg.SessionRegInfo#ratioPlayed
   * @type {number} */
  ratioPlayed: 0,
  /**
   * Number of actions done by the user while in this working session
   * @name module:report/SessionReg.SessionRegInfo#nActions
   * @type {number} */
  nActions: 0,
  /**
   * Sum of the scores of all the activities played
   * @name module:report/SessionReg.SessionRegInfo#tScore
   * @type {number} */
  tScore: 0,
  /**
   * Sum of the playing time reported by each activity (not always equals to the session's total time)
   * @name module:report/SessionReg.SessionRegInfo#tTime
   * @type {number} */
  tTime: 0,
});

SessionReg.Info = SessionRegInfo;

export default SessionReg;