This is a preview of the Reports functionality.
It allows defining report templates and then create reports based on the templates. A template defines: - parameters - to be set in the report (currently only selection of campaigns, in the future to be extended to selection of lists/segments, and selection from pre-defined options) - data retrieval / processing code (in Javascript) - rendering template (in Handlebars) This main functionality is accompanied by a few minor tweaks here and there. Worth notice is the ability to use server-side ajax table s for multi-selection of campaigns. This is meant for reports that compare data across multiple campaigns. This could possibly be even used for some poor man's A/B testing. Note that the execution of custom JavaScript in the data retrieval / processing code and definition of custom Handlebars templates is a security issue. This should however be OK in the general case once proper user management with granular permissions is in. This is because definition of a report template is anyway such an expert task that it would normally be performed only by admin. Instantiation of reports based on report templates can be then done by any user because this should no longer be any security problem.
This commit is contained in:
parent
2afeb74e68
commit
6ba04d7ff4
31 changed files with 1737 additions and 13 deletions
|
@ -722,8 +722,8 @@ router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.post('/selection/ajax', (req, res) => {
|
||||
campaigns.filter(req.body, Number(req.query.parent) || false, (err, data, total, filteredTotal) => {
|
||||
router.post('/quicklist/ajax', (req, res) => {
|
||||
campaigns.filterQuicklist(req.body, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
|
@ -735,13 +735,13 @@ router.post('/selection/ajax', (req, res) => {
|
|||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
'',
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
'<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/campaigns/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
|
||||
htmlescape(striptags(row.description) || ''),
|
||||
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>']
|
||||
)
|
||||
data: data.map((row, i) => ({
|
||||
"0": (Number(req.body.start) || 0) + 1 + i,
|
||||
"1": '<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/campaigns/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
|
||||
"2": htmlescape(striptags(row.description) || ''),
|
||||
"3": '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
|
||||
"DT_RowId": row.id
|
||||
}))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
282
routes/report-templates.js
Normal file
282
routes/report-templates.js
Normal file
|
@ -0,0 +1,282 @@
|
|||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const passport = require('../lib/passport');
|
||||
const router = new express.Router();
|
||||
const _ = require('../lib/translate')._;
|
||||
const reportTemplates = require('../lib/models/report-templates');
|
||||
const tools = require('../lib/tools');
|
||||
const util = require('util');
|
||||
const htmlescape = require('escape-html');
|
||||
const striptags = require('striptags');
|
||||
|
||||
const allowedMimeTypes = {
|
||||
'text/html': 'HTML',
|
||||
'text/csv': 'CSV'
|
||||
};
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('reports');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('report-templates/report-templates', {
|
||||
title: _('Report Templates')
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/ajax', (req, res) => {
|
||||
reportTemplates.filter(req.body, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
htmlescape(row.name || ''),
|
||||
htmlescape(striptags(row.description) || ''),
|
||||
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/report-templates/edit/' + row.id + '"> ' + _('Edit') + '</a>']
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/create', passport.csrfProtection, (req, res) => {
|
||||
const data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
const wizard = req.query['type'] || '';
|
||||
|
||||
if (wizard == 'subscribers-all') {
|
||||
if (!('description' in data)) data.description = 'This sample shows how to generate a report listing all subscribers along with their statistics.';
|
||||
|
||||
if (!('mimeType' in data)) data.mimeType = 'text/html';
|
||||
|
||||
if (!('userFields' in data)) data.userFields =
|
||||
'[\n' +
|
||||
' {\n' +
|
||||
' "id": "campaign",\n' +
|
||||
' "name": "Campaign",\n' +
|
||||
' "type": "campaign",\n' +
|
||||
' "minOccurences": 1,\n' +
|
||||
' "maxOccurences": 1\n' +
|
||||
' }\n' +
|
||||
']';
|
||||
|
||||
if (!('js' in data)) data.js =
|
||||
'const reports = require("../lib/models/reports");\n' +
|
||||
'\n' +
|
||||
'reports.getCampaignResults(inputs.campaign, ["*"], "", (err, results) => {\n' +
|
||||
' if (err) {\n' +
|
||||
' return callback(err);\n' +
|
||||
' }\n' +
|
||||
'\n' +
|
||||
' const data = {\n' +
|
||||
' title: "Sample Report on " + inputs.campaign.name,\n' +
|
||||
' results: results\n' +
|
||||
' };\n' +
|
||||
'\n' +
|
||||
' return callback(null, data);\n' +
|
||||
'});';
|
||||
|
||||
if (!('hbs' in data)) data.hbs =
|
||||
'<h2>{{title}}</h2>\n' +
|
||||
'\n' +
|
||||
'<div class="table-responsive">\n' +
|
||||
' <table class="table table-bordered table-hover data-table display nowrap" width="100%" data-row-sort="1,1,1" data-paging="false">\n' +
|
||||
' <thead>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Email{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Tracker Count{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' </thead>\n' +
|
||||
' {{#if results}}\n' +
|
||||
' <tbody>\n' +
|
||||
' {{#each results}}\n' +
|
||||
' <tr>\n' +
|
||||
' <th scope="row">\n' +
|
||||
' {{email}}\n' +
|
||||
' </th>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{tracker_count}}\n' +
|
||||
' </td>\n' +
|
||||
' </tr>\n' +
|
||||
' {{/each}}\n' +
|
||||
' </tbody>\n' +
|
||||
' {{/if}}\n' +
|
||||
' </table>\n' +
|
||||
'</div>';
|
||||
|
||||
} else if (wizard == 'subscribers-grouped') {
|
||||
if (!('description' in data)) data.description = 'This sample shows how to generate a report where results are aggregated by some (typically custom) field. The sample assumes that the list associated with the campaign contains a custom field "Country" (which would be filled in via the subscription form).';
|
||||
|
||||
if (!('mimeType' in data)) data.mimeType = 'text/html';
|
||||
|
||||
if (!('userFields' in data)) data.userFields =
|
||||
'[\n' +
|
||||
' {\n' +
|
||||
' "id": "campaign",\n' +
|
||||
' "name": "Campaign",\n' +
|
||||
' "type": "campaign",\n' +
|
||||
' "minOccurences": 1,\n' +
|
||||
' "maxOccurences": 1\n' +
|
||||
' }\n' +
|
||||
']';
|
||||
|
||||
if (!('js' in data)) data.js =
|
||||
'const reports = require("../lib/models/reports");\n' +
|
||||
'\n' +
|
||||
'reports.getCampaignResults(inputs.campaign, ["custom_country", "count(*) AS countAll", "SUM(IF(tracker.count IS NULL, 0, 1)) AS countOpened"], "GROUP BY custom_country", (err, results) => {\n' +
|
||||
' if (err) {\n' +
|
||||
' return callback(err);\n' +
|
||||
' }\n' +
|
||||
'\n' +
|
||||
' for (let row of results) {\n' +
|
||||
' row["percentage"] = Math.round((row.countOpened / row.countAll) * 100);\n' +
|
||||
' }\n' +
|
||||
'\n' +
|
||||
' let data = {\n' +
|
||||
' title: "Sample Report on " + inputs.campaign.name,\n' +
|
||||
' results: results\n' +
|
||||
' };\n' +
|
||||
'\n' +
|
||||
' return callback(null, data);\n' +
|
||||
'});';
|
||||
|
||||
if (!('hbs' in data)) data.hbs =
|
||||
'<h2>{{title}}</h2>\n' +
|
||||
'\n' +
|
||||
'<div class="table-responsive">\n' +
|
||||
' <table class="table table-bordered table-hover data-table display nowrap" width="100%" data-row-sort="1,1,1,1" data-paging="false">\n' +
|
||||
' <thead>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Country{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Opened{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}All{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Percentage{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' </thead>\n' +
|
||||
' {{#if results}}\n' +
|
||||
' <tbody>\n' +
|
||||
' {{#each results}}\n' +
|
||||
' <tr>\n' +
|
||||
' <th scope="row">\n' +
|
||||
' {{custom_zone}}\n' +
|
||||
' </th>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{countOpened}}\n' +
|
||||
' </td>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{countAll}}\n' +
|
||||
' </td>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{percentage}}%\n' +
|
||||
' </td>\n' +
|
||||
' </tr>\n' +
|
||||
' {{/each}}\n' +
|
||||
' </tbody>\n' +
|
||||
' {{/if}}\n' +
|
||||
' </table>\n' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.title = _('Create Report Template');
|
||||
data.useEditor = true;
|
||||
|
||||
data.mimeTypes = Object.keys(allowedMimeTypes).map(key => ({
|
||||
key: key,
|
||||
value: allowedMimeTypes[key],
|
||||
selected: data.mimeType == key
|
||||
}));
|
||||
|
||||
res.render('report-templates/create', data);
|
||||
});
|
||||
|
||||
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
reportTemplates.createOrUpdate(true, req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create report template'));
|
||||
return res.redirect('/report-templates/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', util.format(_('Report template “%s” created'), req.body.name));
|
||||
res.redirect('/report-templates');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/edit/:id', passport.csrfProtection, (req, res) => {
|
||||
reportTemplates.get(req.params.id, (err, template) => {
|
||||
if (err || !template) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find report template with specified ID'));
|
||||
return res.redirect('/report-templates');
|
||||
}
|
||||
|
||||
template.csrfToken = req.csrfToken();
|
||||
template.title = _('Edit Report Template');
|
||||
template.useEditor = true;
|
||||
|
||||
template.mimeTypes = Object.keys(allowedMimeTypes).map(key => ({
|
||||
key: key,
|
||||
value: allowedMimeTypes[key],
|
||||
selected: template.mimeType == key
|
||||
}));
|
||||
|
||||
res.render('report-templates/edit', template);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
reportTemplates.createOrUpdate(false, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Report template updated'));
|
||||
} else {
|
||||
req.flash('info', _('Report template not updated'));
|
||||
}
|
||||
|
||||
if (req.body['submit'] == 'update-and-stay') {
|
||||
return res.redirect('/report-templates/edit/' + req.body.id);
|
||||
} else {
|
||||
return res.redirect('/report-templates');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
reportTemplates.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Report template deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified report template'));
|
||||
}
|
||||
|
||||
return res.redirect('/report-templates');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
361
routes/reports.js
Normal file
361
routes/reports.js
Normal file
|
@ -0,0 +1,361 @@
|
|||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const passport = require('../lib/passport');
|
||||
const router = new express.Router();
|
||||
const _ = require('../lib/translate')._;
|
||||
const reportTemplates = require('../lib/models/report-templates');
|
||||
const reports = require('../lib/models/reports');
|
||||
const campaigns = require('../lib/models/campaigns');
|
||||
const tools = require('../lib/tools');
|
||||
const util = require('util');
|
||||
const htmlescape = require('escape-html');
|
||||
const striptags = require('striptags');
|
||||
const hbs = require('hbs');
|
||||
const vm = require('vm');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('reports');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('reports/reports', {
|
||||
title: _('Reports')
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/ajax', (req, res) => {
|
||||
reports.filter(req.body, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
htmlescape(row.name || ''),
|
||||
htmlescape(row.reportTemplateName || ''),
|
||||
htmlescape(striptags(row.description) || ''),
|
||||
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
|
||||
'<a href="/reports/view/' + row.id + '"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></a> ' +
|
||||
'<a href="/reports/edit/' + row.id + '"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span></a>']
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/create', passport.csrfProtection, (req, res) => {
|
||||
const reqData = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
reqData.csrfToken = req.csrfToken();
|
||||
reqData.title = _('Create Report');
|
||||
reqData.useEditor = true;
|
||||
|
||||
reportTemplates.quicklist((err, items) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
|
||||
const reportTemplateId = Number(reqData.reportTemplate);
|
||||
|
||||
if (reportTemplateId) {
|
||||
items.forEach(item => {
|
||||
if (item.id === reportTemplateId) {
|
||||
item.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reqData.reportTemplates = items;
|
||||
|
||||
if (!reportTemplateId) {
|
||||
res.render('reports/create-select-template', reqData);
|
||||
} else {
|
||||
addUserFields(reportTemplateId, reqData, null, (err, data) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
|
||||
res.render('reports/create', data);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
const reqData = req.body;
|
||||
const reportTemplateId = Number(reqData.reportTemplate);
|
||||
|
||||
addParamsObject(reportTemplateId, reqData, (err, data) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create report'));
|
||||
return res.redirect('/reports/create?' + tools.queryParams(data));
|
||||
}
|
||||
|
||||
reports.createOrUpdate(true, data, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create report'));
|
||||
return res.redirect('/reports/create?' + tools.queryParams(data));
|
||||
}
|
||||
req.flash('success', util.format(_('Report “%s” created'), data.name));
|
||||
res.redirect('/reports');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/edit/:id', passport.csrfProtection, (req, res) => {
|
||||
const reqData = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
reports.get(req.params.id, (err, template) => {
|
||||
if (err || !template) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find report with specified ID'));
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
|
||||
template.csrfToken = req.csrfToken();
|
||||
template.title = _('Edit Report');
|
||||
template.useEditor = true;
|
||||
|
||||
reportTemplates.quicklist((err, items) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
const reportTemplateId = template.reportTemplate;
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.id === reportTemplateId) {
|
||||
item.selected = true;
|
||||
}
|
||||
});
|
||||
|
||||
template.reportTemplates = items;
|
||||
|
||||
addUserFields(reportTemplateId, reqData, template, (err, data) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
|
||||
res.render('reports/edit', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
const reqData = req.body;
|
||||
const reportTemplateId = Number(reqData.reportTemplate);
|
||||
|
||||
addParamsObject(reportTemplateId, reqData, (err, data) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not update report'));
|
||||
return res.redirect('/reports/create?' + tools.queryParams(data));
|
||||
}
|
||||
|
||||
reports.createOrUpdate(false, data, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not update report'));
|
||||
return res.redirect('/reports/edit/' + data.id + '?' + tools.queryParams(data));
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Report updated'));
|
||||
} else {
|
||||
req.flash('info', _('Report not updated'));
|
||||
}
|
||||
|
||||
return res.redirect('/reports');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
reports.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Report deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified report'));
|
||||
}
|
||||
|
||||
return res.redirect('/reports');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
||||
reports.get(req.params.id, (err, template) => {
|
||||
if (err || !template) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find report with specified ID'));
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
|
||||
reportTemplates.get(template.reportTemplate, (err, reportTemplate) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
resolveUserFields(reportTemplate.userFieldsObject, template.paramsObject, (err, inputs) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
|
||||
const sandbox = {
|
||||
require: require,
|
||||
inputs: inputs,
|
||||
callback: (err, outputs) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
|
||||
const hbsTmpl = hbs.handlebars.compile(reportTemplate.hbs);
|
||||
const report = hbsTmpl(outputs);
|
||||
|
||||
const data = {
|
||||
csrfToken: req.csrfToken(),
|
||||
report: new hbs.handlebars.SafeString(report),
|
||||
title: outputs.title
|
||||
};
|
||||
|
||||
res.render('reports/view', data);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const script = new vm.Script(reportTemplate.js);
|
||||
script.runInNewContext(sandbox, { displayErrors: true, timeout: 10000 });
|
||||
} catch (err) {
|
||||
req.flash('danger', 'Error in the report template script ... ' + err.stack.replace(/at ContextifyScript.Script.runInContext[\s\S]*/,''));
|
||||
return res.redirect('/reports');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function resolveCampaigns(ids, callback) {
|
||||
const idsRemaining = ids.slice();
|
||||
const resolved = [];
|
||||
|
||||
function doWork() {
|
||||
if (idsRemaining.length == 0) {
|
||||
return callback(null, resolved);
|
||||
}
|
||||
|
||||
campaigns.get(idsRemaining.shift(), false, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
resolved.push(campaign);
|
||||
return doWork();
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(doWork);
|
||||
}
|
||||
|
||||
function resolveUserFields(userFields, params, callback) {
|
||||
const userFieldsRemaining = userFields.slice();
|
||||
const resolved = {};
|
||||
|
||||
function doWork() {
|
||||
if (userFieldsRemaining.length == 0) {
|
||||
return callback(null, resolved);
|
||||
}
|
||||
|
||||
const spec = userFieldsRemaining.shift();
|
||||
if (spec.type == 'campaign') {
|
||||
return resolveCampaigns(params[spec.id], (err, campaigns) => {
|
||||
if (spec.minOccurences == 1 && spec.maxOccurences == 1) {
|
||||
resolved[spec.id] = campaigns[0];
|
||||
} else {
|
||||
resolved[spec.id] = campaigns;
|
||||
}
|
||||
|
||||
doWork();
|
||||
});
|
||||
}
|
||||
|
||||
return callback(new Error(_('Unknown user field type "' + spec.type + '".')));
|
||||
}
|
||||
|
||||
setImmediate(doWork);
|
||||
}
|
||||
|
||||
function addUserFields(reportTemplateId, reqData, template, callback) {
|
||||
reportTemplates.get(reportTemplateId, (err, reportTemplate) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const userFields = [];
|
||||
|
||||
for (const spec of reportTemplate.userFieldsObject) {
|
||||
let value = '';
|
||||
if ((spec.id + 'Selection') in reqData) {
|
||||
value = reqData[spec.id + 'Selection'];
|
||||
} else if (template && template.paramsObject && spec.id in template.paramsObject) {
|
||||
value = template.paramsObject[spec.id].join(',');
|
||||
}
|
||||
|
||||
userFields.push({
|
||||
'id': spec.id,
|
||||
'name': spec.name,
|
||||
'type': spec.type,
|
||||
'value': value,
|
||||
'isMulti': !(spec.minOccurences == 1 && spec.maxOccurences == 1)
|
||||
});
|
||||
}
|
||||
|
||||
const data = template ? template : reqData;
|
||||
data.userFields = userFields;
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
function addParamsObject(reportTemplateId, data, callback) {
|
||||
reportTemplates.get(reportTemplateId, (err, reportTemplate) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const paramsObject = {};
|
||||
|
||||
for (const spec of reportTemplate.userFieldsObject) {
|
||||
const sel = data[spec.id + 'Selection'];
|
||||
|
||||
if (!sel) {
|
||||
paramsObject[spec.id] = [];
|
||||
} else {
|
||||
paramsObject[spec.id] = sel.split(',').map(item => Number(item));
|
||||
}
|
||||
}
|
||||
|
||||
data.paramsObject = paramsObject;
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
Loading…
Add table
Add a link
Reference in a new issue