jeresig/pharos-images

View on GitHub
logic/shared/search.js

Summary

Maintainability
B
6 hrs
Test Coverage
"use strict";

const sanitize = require("elasticsearch-sanitize");

const models = require("../../lib/models");
const urls = require("../../lib/urls");
const config = require("../../lib/config");
const options = require("../../lib/options");

const facets = require("./facets");
const queries = require("./queries");
const searchURL = require("./search-url");
const paramFilter = require("./param-filter");

const sorts = options.sorts;

module.exports = (req, res, tmplParams) => {
    // Collect all the values from the request to construct
    // the search URL and matches later
    // Generate the filters and facets which will be fed in to Elasticsearch
    // to build the query filter and aggregations
    const values = {};
    const filters = [];
    const aggregations = {};
    const fields = Object.assign({}, req.query, req.params);

    for (const name in queries) {
        const query = queries[name];
        let value = query.value(fields);

        if (!value && queries[name].defaultValue) {
            value = queries[name].defaultValue();
        }

        if (value !== undefined) {
            values[name] = value;

            if (query.filter) {
                filters.push(query.filter(value, sanitize));
            }
        }
    }

    for (const name in facets) {
        aggregations[name] = facets[name].facet();
    }

    const curURL = urls.gen(req.lang, req.originalUrl);
    const expectedURL = searchURL(req, values, true);

    if (expectedURL !== curURL) {
        return res.redirect(expectedURL);
    }

    let sort = null;

    if (values.sort) {
        const sortParts = values.sort.split(".");
        sort = queries[sortParts[0]].sort()[sortParts[1]];
    }

    // Query for the artworks in Elasticsearch
    models("Artwork").search({
        bool: {
            must: filters,
        },
    }, {
        size: values.rows,
        from: values.start,
        aggs: aggregations,
        sort,
        hydrate: true,
    }, (err, results) => {
        /* istanbul ignore if */
        if (err) {
            return res.status(500).render("Error", {
                title: err.message,
            });
        }

        // The number of the last item in this result set
        const end = values.start + results.hits.hits.length;

        // The link to the previous page of search results
        const prevStart = values.start - values.rows;
        const prevLink = (values.start > 0 ? searchURL(req, {
            start: (prevStart > 0 ? prevStart : ""),
        }, true) : "");

        // The link to the next page of the search results
        const nextStart = values.start + values.rows;
        const nextLink = (end < results.hits.total ? searchURL(req, {
            start: nextStart,
        }, true) : "");

        // Construct a nicer form of the facet data to feed in to
        // the templates
        const facetData = [];

        for (const name in aggregations) {
            const aggregation = results.aggregations[name];
            const facet = facets[name];
            const buckets = facet.formatBuckets(aggregation.buckets, req)
                .filter((bucket) => {
                    bucket.url = searchURL(req,
                        Object.assign({}, values, bucket.url));
                    return bucket.count > 0;
                });

            // Skip facets that won't filter anything
            if (buckets.length <= 1) {
                continue;
            }

            const result = {
                name: facet.title(req),
                buckets,
            };

            // Make sure that there aren't too many buckets displaying at
            // any one time, otherwise it gets too long. We mitigate this
            // by splitting the extra buckets into a separate container
            // and then allow the user to toggle its visibility.
            if (result.buckets.length > 10) {
                result.extra = result.buckets.slice(5);
                result.buckets = result.buckets.slice(0, 5);
            }

            facetData.push(result);
        }

        // Construct a list of the possible sorts, their translated
        // names and their selected state, for the template.
        const sortData = Object.keys(sorts).map((id) => ({
            id: id,
            name: sorts[id](req),
            selected: values.sort === id,
        }));

        // Figure out the title and breadcrumbs of the results
        let title = req.gettext("Search Results");
        const primary = paramFilter(values).primary;
        let breadcrumbs = [];

        if (primary.length > 1) {
            breadcrumbs = primary.map((param) => {
                const rmValues = Object.assign({}, values);
                delete rmValues[param];

                return {
                    name: queries[param].searchTitle(values[param], req),
                    url: searchURL(req, rmValues),
                };
            }).filter((crumb) => crumb.name);

        } else if (primary.length === 1) {
            const name = primary[0];
            const query = queries[name];
            title = query.searchTitle(values[name], req);

        } else {
            title = req.gettext("All Artworks");
        }

        res.render("Search", Object.assign({
            title,
            breadcrumbs,
            sources: models("Source").getSources()
                .filter((source) => source.numArtworks > 0),
            minDate: config.DEFAULT_START_DATE,
            maxDate: config.DEFAULT_END_DATE,
            values,
            queries,
            sorts: sortData,
            facets: facetData,
            artworks: results.hits.hits,
            total: results.hits.total,
            start: (results.hits.total > 0 ? values.start + 1 : 0),
            end,
            prev: prevLink,
            next: nextLink,
            // Don't index the search results
            noIndex: true,
        }, tmplParams));
    });
};