Source: src/js/collections/QualityReport.js

define([
  "jquery",
  "underscore",
  "backbone",
  "uuid",
  "models/QualityCheckModel",
], ($, _, Backbone, uuid, QualityCheck) => {
  /**
   *  @class QualityReport
   *  @classdesc A DataPackage represents a hierarchical collection of
   *  packages, metadata, and data objects, modeling an OAI-ORE RDF graph.
   *  TODO: incorporate Backbone.UniqueModel
   * @classcategory Collections
   *  @augments Backbone.Collection
   *  @class
   */
  const QualityReport = Backbone.Collection.extend(
    /** @lends QualityReport.prototype */ {
      // The name of this type of collection
      type: "QualityReport",
      runStatus: null,
      errorDescription: null,
      timestamp: null,

      /** @inheritdoc */
      initialize(_models, options = {}) {
        // Set the id or create a new one
        this.id = options.pid || `urn:uuid:${uuid.v4()}`;

        // this.on("add", this.handleAdd);
        // this.on("successSaving", this.updateRelationships);

        return this;
      },

      /*
       * The QualityReport collection stores a metadata quality
       * report that is generated from the MetaDIG quality engine.
       */
      model: QualityCheck,

      /** @inheritdoc */
      parse(response, _options) {
        // runStatus can be one of "success", "failure", "queued"
        this.runStatus = response.runStatus;
        this.errorDescription = response.errorDescription;
        this.timestamp = response.timestamp;
        return response.result;
      },

      /** @inheritdoc */
      fetch(options) {
        const collectionRef = this;
        let fetchOptions = {};
        if (!options) {
          return null;
        }
        fetchOptions = _.extend(options, {
          url: options.url,
          cache: false,
          contentType: false, // "multipart/form-data",
          processData: false,
          type: "GET",
          // headers: { 'Access-Control-Allow-Origin': 'http://localhost:8081' },
          headers: {
            Accept: "application/json",
          },
          success(_collection, _jqXhr, _options) {
            // collectionRef.run = data;
            collectionRef.trigger("fetchComplete");
          },
          error(_collection, jqXhr, _options) {
            collectionRef.fetchResponse = jqXhr;
            collectionRef.trigger("fetchError");
          },
        });
        // fetchOptions = _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings());
        return Backbone.Collection.prototype.fetch.call(
          collectionRef,
          fetchOptions,
        );
      },

      /**
       * Returns a list of all the results in the collection, grouped by color.
       * @param {Array} results - An array of QualityCheck models.
       * @returns {object} An object with keys for each color (BLUE, GREEN,
       * ORANGE, RED) and an array of results for each color.
       */
      groupResults(results) {
        const groupedResults = _.groupBy(results, (result) => {
          let color;

          const check = result.get("check");
          const status = result.get("status");
          // simple cases
          // always blue for info and skip
          if (check.level === "INFO") {
            color = "BLUE";
            return color;
          }
          if (status === "SKIP") {
            color = "BLUE";
            return color;
          }
          // always green for success
          if (status === "SUCCESS") {
            color = "GREEN";
            return color;
          }

          // handle failures and warnings
          if (status === "FAILURE") {
            color = "RED";
            if (check.level === "OPTIONAL") {
              color = "ORANGE";
            }
          }
          if (status === "ERROR") {
            color = "ORANGE";
            if (check.level === "REQUIRED") {
              color = "RED";
            }
          }
          return color;
        });

        // make sure we have everything, even if empty
        if (!groupedResults.BLUE) {
          groupedResults.BLUE = [];
        }
        if (!groupedResults.GREEN) {
          groupedResults.GREEN = [];
        }
        if (!groupedResults.ORANGE) {
          groupedResults.ORANGE = [];
        }
        if (!groupedResults.RED) {
          groupedResults.RED = [];
        }

        // if (groupedResults.BLUE) {
        //  total = total - groupedResults.BLUE.length;
        // }

        return groupedResults;
      },

      /**
       * Groups the results by their type, excluding those that should not be
       * counted in the totals (ERROR, SKIP, and optional failures).
       * @param {Array} results - An array of QualityCheck models.
       * @returns {object} An object with keys for each check type and an array
       * of results for each type.
       */
      groupByType(results) {
        const groupedResults = _.groupBy(results, (result) => {
          const check = result.get("check");
          const status = result.get("status");

          if (status === "ERROR" || status === "SKIP") {
            // orange or blue
            return "removeMe";
          }
          if (status === "FAILURE" && check.level === "OPTIONAL") {
            // orange
            return "removeMe";
          }

          let type = "";
          // Convert check type to lower case, so that the checks will be
          // grouped correctly, even if one check type has an incorrect capitalization.
          if (check.type != null) {
            // Normalize check type by converting entire string to lowercase
            type = check.type.toLowerCase();
            // Now convert to title case
            type = type.charAt(0).toUpperCase() + type.slice(1);
          }

          return type || "uncategorized";
        });

        // get rid of the ones that should not be counted in our totals
        delete groupedResults.removeMe;

        return groupedResults;
      },

      /**
       * Get the number of results in each group, including a total count.
       * @param {object} groupedResults - An object with keys for each group and
       * an array of results for each group. If not provided, it will use the
       * results from this.models, grouped by color.
       * @returns {object} An object with keys for each group and the count of
       * results in that group, plus a total count.
       * @since 2.34.0
       */
      getCountsPerGroup(groupedResults) {
        const data = groupedResults || this.groupResults(this.models);
        const counts = {};
        Object.entries(data).forEach(([group, items]) => {
          counts[group] = items.length;
        });
        counts.total = this.models.length;
        return counts;
      },

      /**
       * For each result in the collection, check the outputs for identifiers
       * and return a list of all unique ids.
       * @returns {Array} An array of unique output identifiers.
       * @since 2.34.0
       */
      getAllOutputIdentifiers() {
        const identifiers = new Set();
        this.models.forEach((result) => {
          const outputs = result.get("output") || [];
          outputs.forEach((output) => {
            if (output.identifier) {
              identifiers.add(output.identifier);
            }
          });
        });
        return Array.from(identifiers);
      },

      /**
       * For all result outputs in the collection that include identifiers, get
       * the names of those outputs from the Solr index. This is done in batches
       * to avoid exceeding maximum lengths of Solr queries.
       * @returns {Promise<object>} A promise that resolves to an object mapping
       * output identifiers to their names.
       * @since 2.34.0
       */
      async getAllOutputNames() {
        // too many ids per request will make the resust too long, so send in
        // batches.
        const batchSize = 10;
        const ids = this.getAllOutputIdentifiers();
        const promises = [];
        for (let i = 0; i < ids.length; i += batchSize) {
          const batch = ids.slice(i, i + batchSize);
          promises.push(this.getOutputNames(batch));
        }
        const results = await Promise.all(promises);
        const mergedResults = results.reduce(
          (acc, curr) => ({ ...acc, ...curr }),
          {},
        );
        return mergedResults;
      },

      /**
       * Given a batch of output identifiers, fetch their names from the Solr
       * index. Call getAllOutputNames instead of calling this directly.
       * @param {Array} ids - An array of output identifiers.
       * @returns {Promise<object>} A promise that resolves to an object mapping
       * output identifiers to their names.
       * @since 2.34.0
       */
      getOutputNames(ids) {
        return new Promise((resolve, reject) => {
          // eslint-disable-next-line import/no-dynamic-require
          require(["collections/SolrResults"], (SolrResults) => {
            const query = ids
              .map((id) => `id:"${id}" OR seriesId:"${id}"`)
              .join(" OR ");
            const rows = ids.length * 5; // Set a reasonable limit for the number of search
            const search = new SolrResults([], { query, rows });
            search.setfields("id,title,fileName");
            search.fetch({
              success: (collection) => {
                const results = {};
                collection.each((m) => {
                  results[m.get("id")] = m.get("title") || m.get("fileName");
                });
                resolve(results);
              },
              error: (_collection, response) => {
                reject(response);
              },
            });
          });
        });
      },
    },
  );
  return QualityReport;
});