Home Reference Source Repository

js/Collections/BaseCollection.js

import $ from 'jquery';
import _ from 'underscore';
import Backbone from 'backbone';
import RODAN_EVENTS from 'js/Shared/RODAN_EVENTS';
import Pagination from 'js/Models/Pagination';
import Radio from 'backbone.radio';

/**
 * Subclass of Backbone.Collection.
 *
 * Some functionality of Backbone.Collection is overridden to facilitate server-based pagination, filtering, and sorting.
 */
export default class BaseCollection extends Backbone.Collection
{
///////////////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS
///////////////////////////////////////////////////////////////////////////////////////
    /**
     * Constructor.
     *
     * @param {object} options initialization parameters for Backbone.Collection
     */
    constructor(options)
    {
        super(options);
        this._pagination = new Pagination();
        this._lastData = {};
        this._initializeRadio();
        this._filters = {};
        this._sort = {};//{ordering: '-created'};
        this._page = {};
        this._enumerations = this._enumerations ? this._enumerations : [];
        this.on('add', (model, collection, options) => this._onAdd(model, collection, options));
    }

    /**
     * Returns route.
     *
     * @return {string} route
     */
    get route()
    {
        return this._route;
    }

    /**
     * Returns enumerations of this Collection. These are custom-defined in the subclasses.
     *
     * Enumerations should be defined in subclasses as follows:
     * - [{field: string, label: string, values: [{value: primitive type, label: string}] (optional)}]
     *
     * In the above:
     * - "field" is a property of the associated Model in the Collection
     * - "label" is a string that will appear in the table header
     * - "values" is optional; populate this array with explicit "value"/"label"s if desired, else BaseCollection will determine the values for enumeration based on the contents of the Collection
     * 
     * @todo Rodan server should provide explicit enumerations
     *
     * @return {array} enumerations
     */
    getEnumerations()
    {
        return this._enumerations;
    }

    /**
     * Parse results.
     *
     * @param {object} response JSON object
     * @return {object} JSON object
     */
    parse(response)
    {
        if (response.count)
        {
            this._parsePagination(response);
        }

        if (this._enumerations && this._enumerations.length > 0)
        {
            this._populateEnumerations(response);
        }

        if (response.hasOwnProperty('results'))
        {
            return response.results;
        }
        return response;
    }

    /**
     * Parses ID out of URL.
     *
     * @param {string} url URL
     * @return {string} string representing UUID of Collection
     * @todo this should be moved to a utility class
     */
    parseIdFromUrl(url)
    {
        var lastSlash = url.lastIndexOf('/');
        var subString = url.substring(0, lastSlash);
        var secondLastSlash = subString.lastIndexOf('/');
        return url.substring(secondLastSlash + 1, lastSlash);
    }

    /**
     * Override of fetch to allow for generic handling.
     *
     * Note that we save the data options. This is in case we do a create
     * and have to reload/fetch the previous collection. We need to preserve
     * the fetch parameters.
     *
     * @param {object} options Backbone.Collection.fetch options object
     */
    fetch(options)
    {
        if (!options)
        {
            options = {};
        }

        // Set task.
        options.task = 'fetch';

        // Save last data.
        this._lastData = options.data ? options.data : {};

        // Build final options.
        var finalOptions = {};
        if (options.error)
        {
            finalOptions.error = options.error;
        }
        if (options.success)
        {
            finalOptions.success = options.success;
        }
        finalOptions.reset = options.reset ? options.reset : false;
        finalOptions.data = {};
        $.extend(finalOptions.data, this._filters);
        $.extend(finalOptions.data, this._sort);
        $.extend(finalOptions.data, this._page);
        $.extend(finalOptions.data, options.data);

        // Fech.
        super.fetch(finalOptions);
    }

    /**
     * Override of create.
     *
     * This override exists because we do NOT want to add it to the collection
     * by default (as there's a limit to what the server returns for collections,
     * and we need to respect that). However, if the save worked, we do want to do a fetch
     * to update the Collection. The fetch is called in the custom success handler for creation.
     *
     * There's also the case if this Collection is local and not associated with a DB Collection.
     *
     * @param {object} options Backbone.Collection.create options object
     * @return {Backbone.Model} instance of Backbone.Model or subclass of Backbone.Model
     */
    create(options)
    {
        var instance = new this.model(options);
        if (this.hasOwnProperty('url'))
        {
            instance.save({}, {success: () => this._handleCreateSuccess()});
        }
        else
        {
            instance.save({}, {success: (model) => this.add(model)});
        }
        return instance;
    }

    /**
     * Requests a sorted fetch. This is not called "sort" because backbone already has
     * a sort method for the Collection.
     *
     * If no options.data is passed, the options.data from the last fetch are used.
     *
     * @param {boolean} ascending results will return in ascending order iff true
     * @param {string} field name of field to sort by
     * @param {object} options Backbone.Collection.fetch options object
     */
    fetchSort(ascending, field, options)
    {
        if (options && options.data)
        {
            this._lastData = options.data;
        }
        this._sort.ordering = field;
        if (!ascending)
        {
            this._sort.ordering = '-' + field;
        }
        this.fetch({data: this._lastData, reset: true});
    }

    /**
     * Requests a filtered fetch.
     *
     * If no options.data is passed, the options.data from the last fetch are used.
     *
     * @param {array} filters array of objects; {name: string, value: primitive}; what filters can be used is defined in Rodan
     * @param {object} options Backbone.Collection.fetch options object
     * @todo give more info on filters
     */
    fetchFilter(filters, options)
    {
        if (options && options.data)
        {
            this._lastData = options.data;
        }
        this._filters = filters;
        this._page = {};
        this.fetch({data: this._lastData, reset: true});
    }

    /**
     * Requests a page to be fetched.
     *
     * If no options.data is passed, the options.data from the last fetch are used.
     *
     * @param {integer} page non-negative page number to retrieve from server
     * @param {object} options Backbone.Collection.fetch options object
     */
    fetchPage(page, options)
    {
        if (options && options.data)
        {
            this._lastData = options.data;
        }
        this._page = page;
        this.fetch({data: this._lastData, reset: true});
    }

    /**
     * Returns pagination object for this Collection.
     *
     * @return {object} pagination object
     * @todo point to pagination info on Rodan server
     */
    getPagination()
    {
        return this._pagination;
    }

    /**
     * Returns the URL associated with this Collection.
     *
     * @return {string} URL associated with this Collection
     */
    url()
    {
        return Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SERVER_GET_ROUTE, this._route);
    }

    /**
     * Syncs the Collection while preserving the last used fetch options.data.
     */
    syncCollection()
    {
        this.fetch({data: this._lastData});
    }

///////////////////////////////////////////////////////////////////////////////////////
// PRIVATE METHODS
///////////////////////////////////////////////////////////////////////////////////////
    /**
     * Initialize Radio.
     */
    _initializeRadio()
    {
        // dummy
    }

    /**
     * Handles a succesful creation. All this does is "properly" reload the collection.
     */
    _handleCreateSuccess()
    {
        this.syncCollection({});
    }

    /**
     * Parses pagination parameters from response.
     */
    _parsePagination(options)
    {
        this._pagination.set({'count': options.count,
                              'next': options.next !== null ? options.next : '#',
                              'previous': options.previous !== null ? options.previous : '#',
                              'current': options.current_page,
                              'total': options.total_pages});
    }

    /**
     * Populates enumerations.
     */
    _populateEnumerations(response)
    {
        var items = response.results ? response.results : response;
        for (var j in this._enumerations)
        {
            var field = this._enumerations[j].field;
            if (!this._enumerations[j].values || this._enumerations[j].values.length === 0)
            {
                this._enumerations[j].values = [];
                for (var i in items)
                {
                    var result = items[i];
                    this._enumerations[j].values.push({value: result[field], label: result[field]});
                }
                this._enumerations[j].values = _.uniq(this._enumerations[j].values, false, function(item) {return item.value;});
            }
        }
    }

    /**
     * Handle backbone add event.
     */
    _onAdd(model, collection, options)
    {
        Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__COLLECTION_ADD, {model: model, collection: collection, options: options});
    }
}