mongaku/mongaku

View on GitHub
src/schemas/types/YearRange.js

Summary

Maintainability
D
2 days
Test Coverage
const yearRange = require("yearrange");

const numRange = bucket =>
    bucket.to ? `${bucket.from || 0}-${bucket.to}` : `${bucket.from}+`;

const defaultRanges = [
    {to: 999},
    {from: 1000, to: 1099},
    {from: 1100, to: 1199},
    {from: 1200, to: 1299},
    {from: 1300, to: 1399},
    {from: 1400, to: 1499},
    {from: 1500, to: 1599},
    {from: 1600, to: 1699},
    {from: 1700, to: 1799},
    {from: 1800},
];

const YearRange = function(options) {
    this.options = options;
    /*
    name
    type
    searchName
    ranges
    title(i18n)
    placeholder(i18n)
    */
};

YearRange.prototype = {
    searchName() {
        return this.options.searchName || this.options.name;
    },

    value(query) {
        const start = query[`${this.searchName()}.start`];
        const end = query[`${this.searchName()}.end`];

        if (start || end) {
            return {start, end};
        }
    },

    fields(value) {
        return {
            [`${this.searchName()}.start`]: value.start,
            [`${this.searchName()}.end`]: value.end,
        };
    },

    searchTitle(value, i18n) {
        const title = this.options.title(i18n);
        const range = numRange({
            from: value.start,
            to: value.end,
        });

        return `${title}: ${range}`;
    },

    filter(value) {
        // NOTE(jeresig): There has got to be a better way to handle this.
        const start = value.start || -10000;
        const end = value.end || new Date().getYear() + 1900;

        const startInside = {
            bool: {
                must: [
                    {
                        range: {
                            [`${this.options.name}.start`]: {
                                lte: parseFloat(start),
                            },
                        },
                    },

                    {
                        range: {
                            [`${this.options.name}.end`]: {
                                gte: parseFloat(start),
                            },
                        },
                    },
                ],
            },
        };

        const endInside = {
            bool: {
                must: [
                    {
                        range: {
                            [`${this.options.name}.start`]: {
                                lte: parseFloat(end),
                            },
                        },
                    },

                    {
                        range: {
                            [`${this.options.name}.end`]: {
                                gte: parseFloat(end),
                            },
                        },
                    },
                ],
            },
        };

        const contains = {
            bool: {
                must: [
                    {
                        range: {
                            [`${this.options.name}.start`]: {
                                gte: parseFloat(start),
                            },
                        },
                    },

                    {
                        range: {
                            [`${this.options.name}.end`]: {
                                lte: parseFloat(end),
                            },
                        },
                    },
                ],
            },
        };

        return {
            bool: {
                should: [startInside, endInside, contains],
            },
        };
    },

    facet() {
        return {
            [this.options.name]: {
                title: i18n => this.options.title(i18n),

                facet: value => {
                    let ranges = this.options.ranges || defaultRanges;

                    if (value) {
                        const start = parseFloat(value.start);
                        const end = parseFloat(value.end);

                        if (start && end && end - start < 300) {
                            ranges = [];
                            for (let year = start; year < end; year += 10) {
                                ranges.push({
                                    from: year,
                                    to: year + 9,
                                });
                            }
                        }
                    }

                    return {
                        range: {
                            field: `${this.options.name}.years`,
                            ranges,
                        },
                    };
                },

                formatBuckets: buckets =>
                    buckets.map(bucket => ({
                        text: numRange(bucket),
                        count: bucket.doc_count,
                        url: {
                            [this.options.name]: {
                                start: bucket.from,
                                end: bucket.to,
                            },
                        },
                    })),
            },
        };
    },

    sort() {
        return {
            asc: [
                {
                    [`${this.options.name}.start`]: {
                        order: "asc",
                    },
                },
                {
                    [`${this.options.name}.end`]: {
                        order: "asc",
                    },
                },
            ],

            desc: [
                {
                    [`${this.options.name}.end`]: {
                        order: "desc",
                    },
                },
                {
                    [`${this.options.name}.start`]: {
                        order: "desc",
                    },
                },
            ],
        };
    },

    schema(Schema) {
        const YearRangeSchema = new Schema({
            // An ID for the year range, computed from the original + start/end
            // properties before validation.
            _id: String,

            // The source string from which the year range was generated
            original: String,

            // A label associated with the year range (e.g. "modified")
            label: String,

            // If the year range should be treated as "circa"
            circa: Boolean,

            // The year range range start and end
            start: {type: Number, es_indexed: true},
            start_ca: Boolean,
            end: {type: Number, es_indexed: true},
            end_ca: Boolean,

            // If the end year is the current year
            current: {type: Boolean, es_indexed: true},

            // A generated list of years which this year range maps to. This is
            // indexed in Elasticsearch for things like histograms and
            // aggregations.
            years: [{type: Number, es_indexed: true}],
        });

        YearRangeSchema.methods = {
            toJSON() {
                const obj = this.toObject();
                delete obj.years;
                return obj;
            },
        };

        // We generate a list of years in which the record exists, in order
        // to improve querying inside Elasticsearch
        YearRangeSchema.pre("validate", function(next) {
            if (!this.start || !this.end || this.start > this.end) {
                return next();
            }

            const years = [];

            for (let year = this.start; year <= this.end; year += 1) {
                years.push(year);
            }

            this.years = years;

            next();
        });

        // Dynamically generate the _id attribute
        YearRangeSchema.pre("validate", function(next) {
            this._id = this.original || [this.start, this.end].join(",");
            next();
        });

        return {
            type: [YearRangeSchema],
            convert: obj =>
                typeof obj === "string" ? yearRange.parse(obj) : obj,
            validateArray: val => val.start || val.end,
            validationMsg: i18n =>
                i18n.gettext("Dates must have a start or end specified."),
        };
    },
};

module.exports = YearRange;