gfw-api/gfw-forms-api

View on GitHub
app/src/routes/api/v1/reports.router.js

Summary

Maintainability
F
4 days
Test Coverage
F
27%
const Router = require('koa-router');
const logger = require('logger');
const ReportsSerializer = require('serializers/reportsSerializer');
const ReportsModel = require('models/reportsModel');
const ReportsValidator = require('validators/reportsValidator');
const AnswersModel = require('models/answersModel');
const AnswersService = require('services/answersService');
const TeamService = require('services/teamService');
const passThrough = require('stream').PassThrough;
const { ObjectId } = require('mongoose').Types;
const config = require('config');
const { RWAPIMicroservice } = require('rw-api-microservice-node');
const CSV = require('services/csvService');

const router = new Router({
    prefix: '/reports'
});

class ReportsRouter {

    static* getAll() {
        logger.info('Obtaining all reports');
        const filter = {
            $and: [
                {
                    $or: [
                        { $and: [{ public: true }, { status: 'published' }] },
                        { user: new ObjectId(this.state.loggedUser.id) }]
                }
            ]
        };
        if (this.state.query) {
            Object.keys(this.state.query).forEach((key) => {
                filter.$and.push({ [key]: this.state.query[key] });
            });
        }
        const reports = yield ReportsModel.find(filter);

        // get answer count for each report
        const numReports = reports.length;
        for (let i = 1; i < numReports; i++) {
            let answersFilter = {};
            if (this.state.loggedUser.role === 'ADMIN' || this.state.loggedUser.id === reports[i].user) {
                answersFilter = {
                    report: new ObjectId(reports[i].id)
                };
            } else {
                answersFilter = {
                    user: new ObjectId(this.state.loggedUser.id),
                    report: new ObjectId(reports[i].id)
                };
            }
            const answers = yield AnswersModel.count(answersFilter);
            logger.info(answers);
            reports[i].answersCount = answers || 0;
        }

        this.body = ReportsSerializer.serialize(reports);
    }

    static* get() {
        logger.info(`Obtaining reports with id ${this.params.id}`);
        const report = yield ReportsModel.findOne({ _id: this.params.id });
        if (!report) {
            this.throw(404, 'Report not found');
            return;
        }

        // get answers count for the report
        let answersFilter = {};
        if (this.state.loggedUser.role === 'ADMIN' || this.state.loggedUser.id === report.user) {
            answersFilter = {
                report: new ObjectId(this.params.id)
            };
        } else {
            answersFilter = {
                user: new ObjectId(this.state.loggedUser.id),
                report: new ObjectId(this.params.id)
            };
        }
        const answers = yield AnswersModel.count(answersFilter);
        report.answersCount = answers;

        this.body = ReportsSerializer.serialize(report);
    }

    static* save() {
        logger.info('Saving reports', this.request.body);
        const request = this.request.body;

        if (request.public && this.state.loggedUser.role !== 'ADMIN') {
            this.throw(403, 'Admin permissions required to save public templates');
            return;
        }

        const report = yield new ReportsModel({
            name: request.name,
            user: this.state.loggedUser.id,
            languages: request.languages,
            defaultLanguage: request.defaultLanguage,
            questions: request.questions,
            public: request.public,
            status: request.status
        }).save();

        // PATCH templateId onto area
        // Remove report if PATCH fails
        if (request.areaOfInterest) {
            const reportId = report._id.toString();
            try {
                yield RWAPIMicroservice.requestToMicroservice({
                    uri: `/area/${request.areaOfInterest}`,
                    method: 'PATCH',
                    json: true,
                    body: {
                        templateId: reportId,
                        userId: this.state.loggedUser.id
                    }
                });
            } catch (e) {
                yield ReportsModel.remove({ _id: reportId });
                logger.error('request to microservice failed');
                logger.error(e);
                this.throw(500, 'Error creating templates: patch to area failed');
                return;
            }
        }

        this.body = ReportsSerializer.serialize(report);
    }

    static* put() {
        logger.info('Updating report', this.request.body);
        const { body } = this.request;

        if (this.state.loggedUser.role !== 'ADMIN') {
            this.throw(403, 'Only admins can update reports.');
            return;
        }

        const report = yield ReportsModel.findOne({ _id: new ObjectId(this.params.id) });

        if (!report) {
            this.throw(404, 'Report not found with these permissions');
            return;
        }

        Object.assign(report, body);

        // add answers count to return and updated date
        const answers = yield AnswersModel.count({ report: new ObjectId(this.params.id) });
        report.answersCount = answers;
        report.updatedDate = Date.now;

        yield report.save();
        this.body = ReportsSerializer.serialize(report);
    }

    static* patch() {
        logger.info(`Updating template with id ${this.params.id}...`);

        const reportFilter = {
            $and: [
                { _id: new ObjectId(this.params.id) }
            ]
        };
        if (this.state.loggedUser.role !== 'ADMIN') {
            reportFilter.$and.push({ user: new ObjectId(this.state.loggedUser.id) });
        }
        const report = yield ReportsModel.findOne(reportFilter);
        const request = this.request.body;

        // if user did not create then return error
        if (!report) {
            this.throw(404, 'Report not found.');
            return;
        }

        // props allow to change even with answers
        if (request.name) {
            report.name = request.name;
        }

        if (request.status) {
            report.status = request.status;
        }

        if (request.languages) {
            report.languages = request.languages;
        }

        // if user is an admin, they can make the report public
        if (this.state.loggedUser.role === 'ADMIN' && request.public) {
            report.public = request.public;
        }

        // PATCH templateId onto area
        // Remove report if PATCH fails
        if (request.areaOfInterest !== request.oldAreaOfInterest) {

            // remove old area
            if (request.oldAreaOfInterest) {
                logger.info(`PATCHing old area of interest ${request.oldAreaOfInterest}...`);
                try {
                    yield RWAPIMicroservice.requestToMicroservice({
                        uri: `/area/${request.oldAreaOfInterest}`,
                        method: 'PATCH',
                        json: true,
                        body: {
                            templateId: null,
                            userId: this.state.loggedUser.id
                        }
                    });
                } catch (e) {
                    logger.error(e);
                    this.throw(500, 'PATCHing old area failed');
                    return;
                }
            }

            // PATCH new area
            if (request.areaOfInterest) {
                logger.info(`PATCHing new area of interest ${request.oldAreaOfInterest}...`);
                try {
                    yield RWAPIMicroservice.requestToMicroservice({
                        uri: `/area/${request.areaOfInterest}`,
                        method: 'PATCH',
                        json: true,
                        body: {
                            templateId: this.params.id,
                            userId: this.state.loggedUser.id
                        }
                    });
                } catch (e) {
                    logger.error(e);
                    this.throw(500, 'PATCHing new area failed');
                    return;
                }
            }
        }

        // add answers count to return and updated date
        const answers = yield AnswersModel.count({ report: new ObjectId(this.params.id) });
        report.answersCount = answers;
        report.updatedDate = Date.now;

        yield report.save();
        this.body = ReportsSerializer.serialize(report);
    }

    static* delete() {
        const { role } = this.state.loggedUser;
        const aoi = this.state.query && this.state.query.aoi !== null ? this.state.query.aoi.split(',') : null;
        logger.info(`Checking report for answers...`);
        const answers = yield AnswersModel.count({ report: new ObjectId(this.params.id) });
        if (answers > 0 && role !== 'ADMIN') {
            this.throw(403, 'This report has answers, you cannot delete. Please unpublish instead.');
            return;
        }
        logger.info(`Report has no answers.`);
        logger.info(`Deleting report with id ${this.params.id}...`);
        if (aoi !== null) {
            for (let i = 0; i < aoi.length; i++) {
                logger.info(`PATCHing area ${aoi[i]} to remove template association...`);
                try {
                    yield RWAPIMicroservice.requestToMicroservice({
                        uri: `/area/${aoi[i]}`,
                        method: 'PATCH',
                        json: true,
                        body: {
                            templateId: null,
                            userId: this.state.loggedUser.id
                        }
                    });
                    logger.info(`Area ${aoi[i]} patched.`);
                } catch (e) {
                    logger.error(e);
                    this.throw(500, e);
                    return;
                }
            }
            logger.info('Areas patched. Removing template...');
        }

        // finally remove template
        const query = {
            $and: [
                { _id: new ObjectId(this.params.id) }
            ]
        };
        if (role !== 'ADMIN') {
            query.$and.push({ user: new ObjectId(this.state.loggedUser.id) });
            query.$and.push({ status: ['draft', 'unpublished'] });
        } else if (answers > 0) {
            logger.info('User is admin, removing report answers...');
            yield AnswersModel.remove({ report: new ObjectId(this.params.id) });
        }
        const result = yield ReportsModel.remove(query);

        if (!result || !result.result || result.result.ok === 0) {
            this.throw(404, 'Report not found with these permissions. You must be the owner to remove.');
            return;
        }
        this.statusCode = 204;
    }

    static* downloadAnswers() {
        logger.info(`Downloading answers for report ${this.params.id}`);
        this.set('Content-disposition', `attachment; filename=${this.params.id}.csv`);
        this.set('Content-type', 'text/csv');
        this.body = passThrough();

        let report = yield ReportsModel.findOne({
            $and: [
                { _id: new ObjectId(this.params.id) },
                { $or: [{ public: true }, { user: new ObjectId(this.state.loggedUser.id) }] }
            ]
        });
        if (!report) {
            this.throw(404, 'Report not found');
            return;
        }

        report = report.toObject();

        const questionLabels = report.questions.reduce((acc, question) => ({
            ...acc,
            [question.name]: question.label[report.defaultLanguage],
            ...question.childQuestions.reduce((acc2, childQuestion) => ({
                ...acc2,
                [childQuestion.name]: childQuestion.label[report.defaultLanguage]
            }), {})
        }), {
            userId: 'User',
            reportName: 'Name',
            areaOfInterest: 'Area of Interest',
            areaOfInterestName: 'Area of Interest name',
            clickedPositionLat: 'Position of report lat',
            clickedPositionLon: 'Position of report lon',
            userPositionLat: 'Position of user lat',
            userPositionLon: 'Position of user lon',
            layer: 'Alert type'
        });

        const team = yield TeamService.getTeam(this.state.loggedUser.id);
        let teamData = null;
        if (team.data && team.data.attributes) {
            teamData = team.data.attributes;
        }

        const answers = yield AnswersService.getAllAnswers({
            team: teamData,
            reportId: this.params.id,
            template: report,
            query: null,
            loggedUser: this.state.loggedUser
        });

        logger.info('Obtaining data');

        const data = answers.map((answer) => answer.toObject())
            .map((answer) => {
                const responses = {
                    userId: answer.user || null,
                    reportName: answer.reportName,
                    areaOfInterest: answer.areaOfInterest || null,
                    areaOfInterestName: answer.areaOfInterestName || null,
                    clickedPositionLat: answer.clickedPosition.length ? answer.clickedPosition[0].lat : null,
                    clickedPositionLon: answer.clickedPosition.length ? answer.clickedPosition[0].lon : null,
                    userPositionLat: answer.userPosition.length ? answer.userPosition[0] : null,
                    userPositionLon: answer.userPosition.length ? answer.userPosition[1] : null,
                };

                answer.responses.forEach((response) => {
                    const currentQuestion = { ...report.questions.find((question) => (question.name && question.name === response.name)) };

                    responses[response.name] = response.value;
                    if (response.value !== null && ['checkbox', 'radio', 'select'].includes(currentQuestion.type)) {
                        const getCurrentValue = (list, val) => (list.find((item) => (item.value === val || item.value === parseInt(val, 10))));
                        // eslint-disable-next-line no-restricted-globals
                        const values = !response.value.includes(',') && !isNaN(parseInt(response.value, 10)) ? [response.value] : response.value.split(',');
                        const questionValues = currentQuestion.values[report.defaultLanguage];
                        responses[response.name] = values.reduce((acc, value) => {
                            const val = getCurrentValue(questionValues, value);
                            const accString = acc !== '' ? `${acc}, ` : acc;
                            return typeof val !== 'undefined' ? `${accString}${val.label}` : `${accString}${value}`;
                        }, '');
                    }
                });
                return Object.entries(responses).reduce((acc, [key, value]) => {
                    const label = questionLabels[key] || key;
                    return {
                        ...acc,
                        [label]: value
                    };
                }, {});
            });
        this.body.write(CSV.convert(data));
        this.body.end();
    }

}

function* mapTemplateParamToId(next) {
    if (this.params.id === config.get('legacyTemplateId') || this.params.id === 'default') {
        this.params.id = config.get('defaultTemplateId');
    }
    yield next;
}

function* loggedUserToState(next) {
    if (this.query && this.query.loggedUser) {
        this.state.loggedUser = JSON.parse(this.query.loggedUser);
        delete this.query.loggedUser;
    } else if (this.request.body && this.request.body.loggedUser) {
        this.state.loggedUser = this.request.body.loggedUser;
        delete this.request.body.loggedUser;
    } else {
        this.throw(401, 'Unauthorized');
        return;
    }
    yield next;
}

function* queryToState(next) {
    if (this.request.query && Object.keys(this.request.query).length > 0) {
        this.state.query = this.request.query;
    }
    yield next;
}

// check permission must be added at some point
router.post('/', loggedUserToState, ReportsValidator.create, ReportsRouter.save);
router.patch('/:id', mapTemplateParamToId, loggedUserToState, ReportsValidator.patch, ReportsRouter.patch);
router.get('/', loggedUserToState, queryToState, ReportsRouter.getAll);
router.get('/:id', mapTemplateParamToId, loggedUserToState, queryToState, ReportsRouter.get);
router.put('/:id', mapTemplateParamToId, loggedUserToState, queryToState, ReportsValidator.create, ReportsRouter.put);
router.delete('/:id', mapTemplateParamToId, loggedUserToState, queryToState, ReportsRouter.delete);
router.get('/:id/download-answers', mapTemplateParamToId, loggedUserToState, ReportsRouter.downloadAnswers);

module.exports = router;