concord-consortium/rigse

View on GitHub
rails/react-components/src/library/helpers/standards-helpers.tsx

Summary

Maintainability
D
3 days
Test Coverage
import React from "react";
/**
 *
 * Helpers for displaying standards statements.
 *
 */

/**
 *
 * Helper to Display NGSS standards statements.
 *
 */
const NgssHelper = function () {
  const PE = "Performance Expectations";
  const DCI = "Disciplinary Core Ideas";
  const PRACTICES = "Science and Engineering Practices";
  const CONCEPTS = "Crosscutting Concepts";

  //
  // Four groups in NGSS.
  //      Two are lists PE and DCI (handle statements individually.)
  //      Two are maps (grouped by sub nodes one level below root.)
  //
  this.pe = [];
  this.dci = [];
  this.practicesGroup = {};
  this.conceptsGroup = {};

  //
  // Add a statement to the NGSS groupings.
  //
  this.add = function (statement: any) {
    const descArr = statement.description;

    const arrMap: any = {};
    arrMap[DCI] = this.dci;

    const subGroup: any = {};
    subGroup[PRACTICES] = this.practicesGroup;
    subGroup[CONCEPTS] = this.conceptsGroup;

    //
    // First check for groupings from practices and concepts. (sub grouped)
    // Then check for DCIs.
    // Finally default to "Performance Expectations" (PE)
    //
    if (descArr.length > 0 && descArr[0]) {
      const group = descArr[0];
      const sub = subGroup[group];
      if (sub) {
        //
        // This is a practice or a crosscutting concept
        //
        if (descArr.length > 1 && descArr[1]) {
          const title = descArr[1];
          if (!sub[title]) {
            sub[title] = [];
          }
          const list = sub[title];
          list.push(statement);
        }
      } else {
        //
        // This is a DCI, or a PE.
        //
        let arr = this.pe;
        if (arrMap[group]) {
          arr = arrMap[group];
        }
        arr.push(statement);
      }
    }
  };

  //
  // Return a div with NGSS statements grouped for display.
  //
  this.getDiv = function () {
    if (this.pe.length === 0 &&
        this.dci.length === 0 &&
        Object.keys(this.practicesGroup).length === 0 &&
        Object.keys(this.conceptsGroup).length === 0) {
      return null;
    }

    //
    // Create a non-null div for each top level group with applicable items.
    //
    let peDiv = null;
    let dciDiv = null;

    //
    // Simplest case PE (any statement not matching the other
    // three types.) Display notation and full concat description.
    //
    if (this.pe.length > 0) {
      peDiv = (
        <div className="standards-ngss-pe">
          <h4>{ PE }</h4>
          { this.pe.map(function (s: any) {
            let description = s.description;
            if (Array.isArray(description)) {
              let formatted = "";
              for (let i = 0; i < description.length; i++) {
                if (description[i].endsWith(":")) {
                  description[i] += " ";
                } else if (!description[i].endsWith(".")) {
                  description[i] += ". ";
                }
                formatted += description[i];
              }
              description = formatted;
            }
            return (
              <>
                <h5>{ s.notation }</h5>
                <p>{ description }</p>
              </>
            );
          }) }
        </div>
      );
    }

    //
    // Most complicated case is DCI in which notation is composed of
    // leaf grade level and parent notation
    //
    if (this.dci.length > 0) {
      //
      // For each statement display:
      //
      // - Notation ( DCI notation is derived:
      //              leaf grade level + leaf parent notation )
      // - Title  ( leaf parent desc )
      // - Desc   ( leaf desc )
      //
      dciDiv = (
        <div className="standards-ngss-dci">
          <h4>{ DCI }</h4>
          { this.dci.map((s: any) => {
            const arrDesc = s.description;
            if (arrDesc.length < 3) { return null; }
            let notation = "";
            if (s.parents.length > 0) {
              const parent = s.parents[0];
              const grade = this.getGradeLevel(s.education_level);
              notation = grade + "-" + parent.statement_notation;
            }
            return (
              <>
                <strong>{ notation }</strong> { arrDesc[1] }
                <p>{ arrDesc[2] }</p>
              </>
            );
          }) }
        </div>
      );
    }

    //
    // Create grouped divs - grouped by second level node desc.
    // Practices and Crosscutting Concepts are displayed this way.
    //
    const getGroupedDiv = function (groupMap: any, heading: any) {
      if (Object.keys(groupMap).length > 0) {
        //
        // For each statement display:
        //
        // - Title  ( from second level node desc )
        // - Desc   ( desc from all remaining child nodes down
        //              to leaf )
        //
        const groupedDiv = (
          <div>
            <h4>{ heading }</h4>
            { Object.keys(groupMap).map(function (title) {
              const statements = groupMap[title];
              return (
                <>
                  <strong>{ title }</strong>
                  { statements.map(function (s: any, idx: number) {
                    const arrDesc = s.description;
                    if (arrDesc.length < 3) { return null; }
                    let desc = "";
                    for (let i = 2; i < arrDesc.length; i++) {
                      desc += arrDesc[i];
                      if (arrDesc[i].endsWith(".")) {
                        desc += " ";
                      }
                    }
                    return <p key={idx}>{ desc }</p>;
                  }) }
                </>
              );
            }) }
          </div>
        );

        return groupedDiv;
      }
    };

    const practicesDiv = getGroupedDiv(this.practicesGroup, PRACTICES);
    const conceptsDiv = getGroupedDiv(this.conceptsGroup, CONCEPTS);

    return (
      <div>
        { peDiv }
        { dciDiv }
        { practicesDiv }
        { conceptsDiv }
      </div>
    );
  };

  //
  // Grade level arrays for DCI notation generation.
  //
  const ES = ["K", "1", "2", "3", "4", "5"];
  const MS = ["6", "7", "8"].sort();
  const HS = ["9", "10", "11", "12"].sort();

  //
  // Returns a grade level string suitable for composing a
  // notation string for a DCI.
  //
  this.getGradeLevel = function (gradeArray: any) {
    // some gradeArrays have been null
    if (!gradeArray) {
      return "UNKNOWN";
    }

    //
    // For single grade level return single grade.
    //
    if (gradeArray.length === 1) {
      return gradeArray[0];
    }

    gradeArray.sort();

    const isMatch = function (arr: any) {
      if (gradeArray.length === arr.length) {
        let match = true;
        for (let i = 0; i < gradeArray.length; i++) {
          if (gradeArray[i] !== arr[i]) {
            match = false;
            break;
          }
        }
        return match;
      }
      return false;
    };

    //
    // Check if array "b" is a subset of array "a".
    //
    const isSubset = function (a: any, b: any) {
      for (let i = 0; i < b.length; i++) {
        if (a.indexOf(b[i]) < 0) {
          return false;
        }
      }
      return true;
    };

    if (isMatch(HS)) { return "HS"; }
    if (isMatch(MS)) { return "MS"; }

    if (isSubset(ES, gradeArray)) { return "ES"; }

    //
    // Could not determine grade level.
    //
    return "UNKNOWN";
  };

  return this;
};

/**
 *
 * Get a helper for a standards type.
 *
 * @param standardType - The standard type. E.g. "NGSS"
 *
 */
const getStandardsHelper = function (standardType: any) {
  if (standardType === "NGSS") {
    return new (NgssHelper as any)();
  }
};

export default {
  getStandardsHelper,
  NgssHelper
};