Source: src/js/collections/SolrResults.js

/*global define */
define(['jquery', 'underscore', 'backbone', 'models/SolrHeader', 'models/SolrResult'],
  function($, _, Backbone, SolrHeader, SolrResult) {
  'use strict';

  /**
   @class SolrResults
   @classdesc A collection of SolrResult models that represent a list of search results from the DataONE query service.
   @extends Backbone.Collection
   @classcategory Collections
   @constructor
  */
  var SolrResults = Backbone.Collection.extend(
    /** @lends SolrResults.prototype */{
            
    // Reference to this collection's model.
    model: SolrResult,
      
    /**
     * The name of this type of collection.
     * @type {string}
     * @default "SolrResults"
     * @since 2.25.0
     */
    type: "SolrResults",

    initialize: function(models, options) {

      if( typeof options === "undefined" || !options ){
        var options = {};
      }

      this.docsCache    = options.docsCache || null;
      this.currentquery = options.query   || '*:*';
      this.rows       = options.rows    || 25;
      this.start       = options.start   || 0;
      this.sort       = options.sort    || 'dateUploaded desc';
      this.facet       = options.facet   || [];
      this.facetCounts  = "nothing";
      this.stats       = options.stats   || false;
      this.minYear     = options.minYear || 1900;
      this.maxYear     = options.maxYear || new Date().getFullYear();
      this.queryServiceUrl = options.queryServiceUrl || MetacatUI.appModel.get('queryServiceUrl');

      if( MetacatUI.appModel.get("defaultSearchFields")?.length )
        this.fields = MetacatUI.appModel.get("defaultSearchFields").join(",");
      else 
        this.fields = options.fields || "id,title";


      //If POST queries are disabled in the whole app, don't use POSTs here
      if( MetacatUI.appModel.get("disableQueryPOSTs") ){
        this.usePOST = false;
      }
      //If this collection was initialized with the usePOST option, use POSTs here
      else if( options.usePOST ){
        this.usePOST = true;
      }
      //Otherwise default to using GET
      else{
        this.usePOST = false;
      }
    },

    url: function() {
      //Convert facet keywords to a string
      var facetFields = "";

      this.facet = _.uniq(this.facet);

      for (var i=0; i<this.facet.length; i++){
        facetFields += "&facet.field=" + this.facet[i];
      }
      // limit to matches
      if (this.facet.length > 0) {
        facetFields += "&facet.mincount=1"; // only facets meeting the current search
        facetFields += "&facet.limit=-1"; // CAREFUL: -1 means no limit on the number of facets
      }

      //Do we need stats?
      if (!this.stats){
        var stats = "";
      }
      else{
        var stats = "&stats=true";
        for(var i=0; i<this.stats.length; i++){
          stats += "&stats.field=" + this.stats[i];
        }
      }

      //create the query url
      var endpoint = (this.queryServiceUrl || MetatcatUI.appModel.get("queryServiceUrl")) + "q=" + this.currentquery;

      if(this.fields)
        endpoint += "&fl=" + this.fields;
      if(this.sort)
        endpoint += "&sort=" + this.sort;
      if( typeof this.rows == "number" || (typeof this.rows == "string" && this.rows.length))
        endpoint += "&rows=" + this.rows;
      if( typeof this.start == "number" || (typeof this.start == "string" && this.start.length))
        endpoint += "&start=" + this.start;
      if( this.facet.length > 0 )
        endpoint += "&facet=true&facet.sort=index" + facetFields;

      endpoint += stats + "&wt=json";

      return endpoint;
    },

    parse: function(solr) {

      //Is this our latest query? If not, use our last set of docs from the latest query
      if((decodeURIComponent(this.currentquery).replace(/\+/g, " ") != solr.responseHeader.params.q) && this.docsCache)
        return this.docsCache;

      if(!solr.response){
        if(solr.error && solr.error.msg){
          console.log("Solr error: " + solr.error.msg);
        }
        return
      }

      //Save some stats
      this.header = new SolrHeader(solr.responseHeader);
      this.header.set({"numFound" : solr.response.numFound});
      this.header.set({"start" : solr.response.start});
      this.header.set({"rows" : solr.responseHeader.params.rows});

      //Get the facet counts and store them in this model
      if (solr.facet_counts) {
        this.facetCounts = solr.facet_counts.facet_fields;
      } else {
        this.facetCounts = "nothing";
      }

      //Cache this set of results
      this.docsCache = solr.response.docs;

      return solr.response.docs;
    },

    /** 
    *   Fetches the next page of results
    */
    nextpage: function() {
      // Only increment the page if the current page is not the last page
      if (this.start + this.rows < this.header.get("numFound")) {
        this.start += this.rows;
      }
      if (this.header != null) {
        this.header.set({"start" : this.start});
      }

      this.lastUrl = this.url();

      var fetchOptions = this.createFetchOptions();
      this.fetch(fetchOptions);
    },

    /** 
    *   Fetches the previous page of results
    */
    prevpage: function() {
      this.start -= this.rows;
      if (this.start < 0) {
        this.start = 0;
      }
      if (this.header != null) {
        this.header.set({"start" : this.start});
      }

      this.lastUrl = this.url();

      var fetchOptions = this.createFetchOptions();
      this.fetch(fetchOptions);
    },

    /** 
    *   Fetches the given page of results
    * @param {number} page
    */
    toPage: function(page) {
      // go to the requested page
      var requestedStart = this.rows * page;

      /*
      if (this.header != null) {
        if (requestedStart < this.header.get("numFound")) {
          this.start = requestedStart;
        }
        this.header.set({"start" : this.start});
      }*/

      this.start = requestedStart;

      this.lastUrl = this.url();

      var fetchOptions = this.createFetchOptions();
      this.fetch(fetchOptions);
    },

    setrows: function(numrows) {
      this.rows = numrows;
    },

    query: function(newquery) {

      if(typeof newquery != "undefined" && this.currentquery != newquery){
        this.currentquery = newquery;
        this.start = 0;
      }

      this.lastUrl = this.url();

      var fetchOptions = this.createFetchOptions();
      this.fetch(fetchOptions);
    },

    setQuery: function(newquery) {
      if (this.currentquery != newquery) {
        this.currentquery = newquery;
        this.start = 0;
        this.lastQuery = newquery;
      }
    },

    /** 
    *   Returns the last query that was fetched.
    * @returns {string}
    */
    getLastQuery: function(){
      return this.lastQuery;
    },

    setfields: function(newfields) {
      this.fields = newfields;
    },

    setSort: function(newsort) {
      this.sort = newsort;
      this.trigger("change:sort");
    },

    setFacet: function (fields) {
      if (!Array.isArray(fields)) {
        fields = [fields];
      }
      this.facet = fields;
      this.trigger("change:facet");
    },

    setStats: function(fields){
      this.stats = fields;
    },

    createFetchOptions: function(){
      var options = {
        start : this.start,
        reset: true
      }

      let usePOST = this.usePOST || (this.currentquery.length > 1500 && !MetacatUI.appModel.get("disableQueryPOSTs"));

      if( usePOST ){
        options.type = "POST";

        var queryData = new FormData();
        queryData.append("q", decodeURIComponent(this.currentquery));
        queryData.append("rows", this.rows);
        queryData.append("sort", this.sort.replace("+", " "));
        queryData.append("fl", this.fields);
        queryData.append("start", this.start);
        queryData.append("wt", "json");

        //Add the facet fields to the FormData
        if( this.facet.length ){

          queryData.append("facet", "true");

          for (var i=0; i<this.facet.length; i++){
            queryData.append("facet.field", this.facet[i]);
          }

          queryData.append("facet.mincount", "1");
          queryData.append("facet.limit", "-1");
          queryData.append("facet.sort", "index");

        }

        //Add stats to the FormData
        if( this.stats.length ){

          queryData.append("stats", "true");

          for(var i=0; i<this.stats.length; i++){
            queryData.append("stats.field", this.stats[i]);
          }

        }

        options.data = queryData;
        options.contentType = false;
        options.processData = false;
        options.dataType = "json";
        options.url = MetacatUI.appModel.get("queryServiceUrl");

      }

      return _.extend(options, MetacatUI.appUserModel.createAjaxSettings());
    },

    /**
    * Returns the total number of results that were just fetched, or undefined if nothing has been fetched yet
    * @since 2.22.0
    * @returns {number|undefined}
    */
    getNumFound: function(){
      return this.header?.get("numFound");
    },

    /**
    * Calculates and returns the total pages of results that was just fetched
    * @since 2.22.0
    * @returns {number}
    */
    getNumPages: function(){
      let total = this.getNumFound();
    
      if(total){
        return Math.ceil(total/this.header.get("rows"))-1; //-1 because our pages are zero-based numbered (where page 0 gets the first n results)
      }
      else{
        return 0;
      }
    },

    /**
     * Calculates and returns the current page of results that was just fetched
     * @since 2.22.0
     * @returns {number}
     */
    getCurrentPage: function(){
        
        if(this.header?.get("start") && this.header?.get("rows")){
            return Math.ceil(this.header.get("start")/this.header.get("rows"));
        }
        else{
            return 0;
        }
    },

    /**
     * Returns the index number of the first search result E.g. the first page of results may be 0-24, where 0 is the start.
     * @since 2.22.0
     * @returns {number}
     */
    getStart: function(){
        if(this.header){
            return this.header.get("start");
        }
        else{
            return this.start;
        }
    },

    /**
     * Calculates the index number of the last search result. E.g. the first page of results may be 0-24, where 24 is the end.
     * @since 2.22.0
     * @returns {number}
     */
    getEnd: function(){
        return parseInt(this.getStart()) + parseInt(this.getRows()) - 1; // -1 since it is zero-based numbering
    },

    /**
     * Returns the number of search result rows 
     * @since 2.22.0
     * @returns {number}
     */
    getRows: function(){
        if(this.header){
            return this.header.get("rows");
        }
        else{
            return this.rows;
        }
    },

    /**
     * Gets and returns the URL string that was sent during the last fetch. 
     * @since 2.22.0
     * @returns {string}
     */
    getLastUrl: function(){
        return this.lastUrl || "";
    },
    
    /**
     * Get the list of PIDs for the search results
     * @returns {string[]} - The list of PID strings for the search results
     * @since 2.25.0
     */
    getPIDs: function () {
      return this.pluck("id");
    },

    /**
     * Determines whether the search parameters have changed since the last fetch. Returns true the next URL
     * to be sent in a fetch() is different at all from the last url that was fetched.
     * @since 2.22.0
     * @returns {boolean}
     */
    hasChanged: function(){
        return this.url() != this.getLastUrl();
    }
  });

  return SolrResults;
});