Added the option to select lists in report.

Added an option to generate a CSV report.
This commit is contained in:
Tomas Bures 2017-04-17 16:30:31 -04:00
parent 6ba04d7ff4
commit 2056645023
8 changed files with 177 additions and 50 deletions

View file

@ -17,6 +17,10 @@ module.exports.filter = (request, parent, callback) => {
tableHelpers.filter('lists', ['*'], request, ['#', 'name', 'cid', 'subscribers', 'description'], ['name'], 'name ASC', null, callback); tableHelpers.filter('lists', ['*'], request, ['#', 'name', 'cid', 'subscribers', 'description'], ['name'], 'name ASC', null, callback);
}; };
module.exports.filterQuicklist = (request, callback) => {
tableHelpers.filter('lists', ['id', 'name', 'subscribers'], request, ['#', 'name', 'subscribers'], ['name'], 'name ASC', null, callback);
};
module.exports.quicklist = callback => { module.exports.quicklist = callback => {
db.getConnection((err, connection) => { db.getConnection((err, connection) => {
if (err) { if (err) {

View file

@ -16,7 +16,7 @@ module.exports.list = (start, limit, callback) => {
module.exports.filter = (request, callback) => { module.exports.filter = (request, callback) => {
tableHelpers.filter('reports JOIN report_templates ON reports.report_template = report_templates.id', tableHelpers.filter('reports JOIN report_templates ON reports.report_template = report_templates.id',
['reports.id AS id', 'reports.name AS name', 'reports.description AS description', 'reports.report_template AS report_template', 'reports.params AS params', 'reports.created AS created', 'report_templates.name AS report_template_name' ], ['reports.id AS id', 'reports.name AS name', 'reports.description AS description', 'reports.report_template AS report_template', 'reports.params AS params', 'reports.created AS created', 'report_templates.name AS report_template_name', 'report_templates.mime_type AS mime_type' ],
request, ['#', 'name', 'report_templates.name', 'description', 'created'], ['name'], 'created DESC', null, callback); request, ['#', 'name', 'report_templates.name', 'description', 'created'], ['name'], 'created DESC', null, callback);
}; };

View file

@ -10,7 +10,19 @@ module.exports.list = (source, fields, orderBy, start, limit, callback) => {
return callback(err); return callback(err);
} }
connection.query('SELECT SQL_CALC_FOUND_ROWS ' + fields.join(', ') + ' FROM ' + source + ' ORDER BY ' + orderBy + ' DESC LIMIT ? OFFSET ?', [limit, start], (err, rows) => { let limitQuery = '';
let limitValues = [];
if (limit) {
limitQuery = ' LIMIT ?';
limitValues.push(limit);
if (start) {
limitQuery += ' OFFSET ?';
limitValues.push(start);
}
}
connection.query('SELECT SQL_CALC_FOUND_ROWS ' + fields.join(', ') + ' FROM ' + source + ' ORDER BY ' + orderBy + ' DESC' + limitQuery, limitValues, (err, rows) => {
if (err) { if (err) {
connection.release(); connection.release();
return callback(err); return callback(err);
@ -56,8 +68,6 @@ module.exports.filter = (source, fields, request, columns, searchFields, default
values = values.concat(queryData.values || []); values = values.concat(queryData.values || []);
} }
log.info("tableHelpers", query);
connection.query(query, values, (err, total) => { connection.query(query, values, (err, total) => {
if (err) { if (err) {
connection.release(); connection.release();

View file

@ -748,4 +748,27 @@ router.get('/subscription/:id/import/:importId/failed', (req, res) => {
}); });
}); });
router.post('/quicklist/ajax', (req, res) => {
lists.filterQuicklist(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) => ({
"0": (Number(req.body.start) || 0) + 1 + i,
"1": '<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/lists/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
"2": row.subscribers,
"DT_RowId": row.id
}))
});
});
});
module.exports = router; module.exports = router;

View file

@ -62,7 +62,7 @@ router.get('/create', passport.csrfProtection, (req, res) => {
const wizard = req.query['type'] || ''; const wizard = req.query['type'] || '';
if (wizard == 'subscribers-all') { 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 (!('description' in data)) data.description = 'Generates a campaign report listing all subscribers along with their statistics.';
if (!('mimeType' in data)) data.mimeType = 'text/html'; if (!('mimeType' in data)) data.mimeType = 'text/html';
@ -124,7 +124,7 @@ router.get('/create', passport.csrfProtection, (req, res) => {
'</div>'; '</div>';
} else if (wizard == 'subscribers-grouped') { } 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 (!('description' in data)) data.description = 'Generates a campaign report with results are aggregated by some "Country" custom field.';
if (!('mimeType' in data)) data.mimeType = 'text/html'; if (!('mimeType' in data)) data.mimeType = 'text/html';
@ -142,13 +142,13 @@ router.get('/create', passport.csrfProtection, (req, res) => {
if (!('js' in data)) data.js = if (!('js' in data)) data.js =
'const reports = require("../lib/models/reports");\n' + 'const reports = require("../lib/models/reports");\n' +
'\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' + 'reports.getCampaignResults(inputs.campaign, ["custom_country", "count(*) AS count_all", "SUM(IF(tracker.count IS NULL, 0, 1)) AS count_opened"], "GROUP BY custom_country", (err, results) => {\n' +
' if (err) {\n' + ' if (err) {\n' +
' return callback(err);\n' + ' return callback(err);\n' +
' }\n' + ' }\n' +
'\n' + '\n' +
' for (let row of results) {\n' + ' for (let row of results) {\n' +
' row["percentage"] = Math.round((row.countOpened / row.countAll) * 100);\n' + ' row["percentage"] = Math.round((row.count_opened / row.count_all) * 100);\n' +
' }\n' + ' }\n' +
'\n' + '\n' +
' let data = {\n' + ' let data = {\n' +
@ -186,10 +186,10 @@ router.get('/create', passport.csrfProtection, (req, res) => {
' {{custom_zone}}\n' + ' {{custom_zone}}\n' +
' </th>\n' + ' </th>\n' +
' <td style="width: 20%;">\n' + ' <td style="width: 20%;">\n' +
' {{countOpened}}\n' + ' {{count_opened}}\n' +
' </td>\n' + ' </td>\n' +
' <td style="width: 20%;">\n' + ' <td style="width: 20%;">\n' +
' {{countAll}}\n' + ' {{count_all}}\n' +
' </td>\n' + ' </td>\n' +
' <td style="width: 20%;">\n' + ' <td style="width: 20%;">\n' +
' {{percentage}}%\n' + ' {{percentage}}%\n' +
@ -200,6 +200,43 @@ router.get('/create', passport.csrfProtection, (req, res) => {
' {{/if}}\n' + ' {{/if}}\n' +
' </table>\n' + ' </table>\n' +
'</div>'; '</div>';
} else if (wizard == 'export-list-csv') {
if (!('description' in data)) data.description = 'Exports a list as a CSV file.';
if (!('mimeType' in data)) data.mimeType = 'text/csv';
if (!('userFields' in data)) data.userFields =
'[\n' +
' {\n' +
' "id": "list",\n' +
' "name": "List",\n' +
' "type": "list",\n' +
' "minOccurences": 1,\n' +
' "maxOccurences": 1\n' +
' }\n' +
']';
if (!('js' in data)) data.js =
'const subscriptions = require("../lib/models/subscriptions");\n' +
'\n' +
'subscriptions.list(inputs.list.id,0,0, (err, results) => {\n' +
' if (err) {\n' +
' return callback(err);\n' +
' }\n' +
'\n' +
' let data = {\n' +
' title: "Sample Export of " + inputs.list.name,\n' +
' results: results\n' +
' };\n' +
'\n' +
' return callback(null, data);\n' +
'});';
if (!('hbs' in data)) data.hbs =
'{{#each results}}\n' +
'{{firstName}},{{lastName}},{{email}}\n' +
'{{/each}}';
} }
data.csrfToken = req.csrfToken(); data.csrfToken = req.csrfToken();

View file

@ -7,6 +7,7 @@ const _ = require('../lib/translate')._;
const reportTemplates = require('../lib/models/report-templates'); const reportTemplates = require('../lib/models/report-templates');
const reports = require('../lib/models/reports'); const reports = require('../lib/models/reports');
const campaigns = require('../lib/models/campaigns'); const campaigns = require('../lib/models/campaigns');
const lists = require('../lib/models/lists');
const tools = require('../lib/tools'); const tools = require('../lib/tools');
const util = require('util'); const util = require('util');
const htmlescape = require('escape-html'); const htmlescape = require('escape-html');
@ -30,6 +31,12 @@ router.get('/', (req, res) => {
}); });
router.post('/ajax', (req, res) => { router.post('/ajax', (req, res) => {
function getViewIcon(mimeType) {
let icon = 'search';
if (mimeType == 'text/csv') icon = 'download-alt';
return icon;
}
reports.filter(req.body, (err, data, total, filteredTotal) => { reports.filter(req.body, (err, data, total, filteredTotal) => {
if (err) { if (err) {
return res.json({ return res.json({
@ -48,7 +55,7 @@ router.post('/ajax', (req, res) => {
htmlescape(row.reportTemplateName || ''), htmlescape(row.reportTemplateName || ''),
htmlescape(striptags(row.description) || ''), htmlescape(striptags(row.description) || ''),
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>', '<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/view/' + row.id + '"><span class="glyphicon glyphicon-' + getViewIcon(row.mimeType) + '" aria-hidden="true"></span></a> ' +
'<a href="/reports/edit/' + row.id + '"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span></a>'] '<a href="/reports/edit/' + row.id + '"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span></a>']
) )
}); });
@ -123,15 +130,15 @@ router.get('/edit/:id', passport.csrfProtection, (req, res) => {
skip: ['layout'] skip: ['layout']
}); });
reports.get(req.params.id, (err, template) => { reports.get(req.params.id, (err, report) => {
if (err || !template) { if (err || !report) {
req.flash('danger', err && err.message || err || _('Could not find report with specified ID')); req.flash('danger', err && err.message || err || _('Could not find report with specified ID'));
return res.redirect('/reports'); return res.redirect('/reports');
} }
template.csrfToken = req.csrfToken(); report.csrfToken = req.csrfToken();
template.title = _('Edit Report'); report.title = _('Edit Report');
template.useEditor = true; report.useEditor = true;
reportTemplates.quicklist((err, items) => { reportTemplates.quicklist((err, items) => {
if (err) { if (err) {
@ -139,7 +146,7 @@ router.get('/edit/:id', passport.csrfProtection, (req, res) => {
return res.redirect('/'); return res.redirect('/');
} }
const reportTemplateId = template.reportTemplate; const reportTemplateId = report.reportTemplate;
items.forEach(item => { items.forEach(item => {
if (item.id === reportTemplateId) { if (item.id === reportTemplateId) {
@ -147,9 +154,9 @@ router.get('/edit/:id', passport.csrfProtection, (req, res) => {
} }
}); });
template.reportTemplates = items; report.reportTemplates = items;
addUserFields(reportTemplateId, reqData, template, (err, data) => { addUserFields(reportTemplateId, reqData, report, (err, data) => {
if (err) { if (err) {
req.flash('danger', err.message || err); req.flash('danger', err.message || err);
return res.redirect('/reports'); return res.redirect('/reports');
@ -201,18 +208,18 @@ router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) =
}); });
router.get('/view/:id', passport.csrfProtection, (req, res) => { router.get('/view/:id', passport.csrfProtection, (req, res) => {
reports.get(req.params.id, (err, template) => { reports.get(req.params.id, (err, report) => {
if (err || !template) { if (err || !report) {
req.flash('danger', err && err.message || err || _('Could not find report with specified ID')); req.flash('danger', err && err.message || err || _('Could not find report with specified ID'));
return res.redirect('/reports'); return res.redirect('/reports');
} }
reportTemplates.get(template.reportTemplate, (err, reportTemplate) => { reportTemplates.get(report.reportTemplate, (err, reportTemplate) => {
if (err) { if (err) {
return callback(err); return callback(err);
} }
resolveUserFields(reportTemplate.userFieldsObject, template.paramsObject, (err, inputs) => { resolveUserFields(reportTemplate.userFieldsObject, report.paramsObject, (err, inputs) => {
if (err) { if (err) {
req.flash('danger', err.message || err); req.flash('danger', err.message || err);
return res.redirect('/reports'); return res.redirect('/reports');
@ -228,31 +235,45 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
} }
const hbsTmpl = hbs.handlebars.compile(reportTemplate.hbs); const hbsTmpl = hbs.handlebars.compile(reportTemplate.hbs);
const report = hbsTmpl(outputs); const reportText = hbsTmpl(outputs);
if (reportTemplate.mimeType == 'text/html') {
const data = { const data = {
csrfToken: req.csrfToken(), csrfToken: req.csrfToken(),
report: new hbs.handlebars.SafeString(report), report: new hbs.handlebars.SafeString(reportText),
title: outputs.title title: outputs.title
}; };
res.render('reports/view', data); res.render('reports/view', data);
} else if (reportTemplate.mimeType == 'text/csv') {
res.set('Content-Disposition', 'attachment;filename=' + toFileName(report.name) + '.csv');
res.set('Content-Type', 'text/csv');
res.send(new Buffer(reportText));
} else {
req.flash('danger', _('Unknown type of template'));
return res.redirect('/reports');
}
} }
}; };
try {
const script = new vm.Script(reportTemplate.js); const script = new vm.Script(reportTemplate.js);
script.runInNewContext(sandbox, { displayErrors: true, timeout: 10000 }); 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) { function toFileName(name) {
return name.
trim().
toLowerCase().
replace(/[ .+/]/g, '-').
replace(/[^a-z0-9\-_]/gi, '');
}
function resolveEntities(getter, ids, callback) {
const idsRemaining = ids.slice(); const idsRemaining = ids.slice();
const resolved = []; const resolved = [];
@ -261,12 +282,12 @@ function resolveCampaigns(ids, callback) {
return callback(null, resolved); return callback(null, resolved);
} }
campaigns.get(idsRemaining.shift(), false, (err, campaign) => { getter(idsRemaining.shift(), (err, entity) => {
if (err) { if (err) {
return callback(err); return callback(err);
} }
resolved.push(campaign); resolved.push(entity);
return doWork(); return doWork();
}); });
} }
@ -274,6 +295,11 @@ function resolveCampaigns(ids, callback) {
setImmediate(doWork); setImmediate(doWork);
} }
const userFieldTypeToGetter = {
'campaign': (id, callback) => campaigns.get(id, false, callback),
'list': lists.get
};
function resolveUserFields(userFields, params, callback) { function resolveUserFields(userFields, params, callback) {
const userFieldsRemaining = userFields.slice(); const userFieldsRemaining = userFields.slice();
const resolved = {}; const resolved = {};
@ -284,25 +310,27 @@ function resolveUserFields(userFields, params, callback) {
} }
const spec = userFieldsRemaining.shift(); const spec = userFieldsRemaining.shift();
if (spec.type == 'campaign') { const getter = userFieldTypeToGetter[spec.type];
return resolveCampaigns(params[spec.id], (err, campaigns) => {
if (getter) {
return resolveEntities(getter, params[spec.id], (err, entities) => {
if (spec.minOccurences == 1 && spec.maxOccurences == 1) { if (spec.minOccurences == 1 && spec.maxOccurences == 1) {
resolved[spec.id] = campaigns[0]; resolved[spec.id] = entities[0];
} else { } else {
resolved[spec.id] = campaigns; resolved[spec.id] = entities;
} }
doWork(); doWork();
}); });
} } else {
return callback(new Error(_('Unknown user field type "' + spec.type + '".'))); return callback(new Error(_('Unknown user field type "' + spec.type + '".')));
} }
}
setImmediate(doWork); setImmediate(doWork);
} }
function addUserFields(reportTemplateId, reqData, template, callback) { function addUserFields(reportTemplateId, reqData, report, callback) {
reportTemplates.get(reportTemplateId, (err, reportTemplate) => { reportTemplates.get(reportTemplateId, (err, reportTemplate) => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -314,8 +342,8 @@ function addUserFields(reportTemplateId, reqData, template, callback) {
let value = ''; let value = '';
if ((spec.id + 'Selection') in reqData) { if ((spec.id + 'Selection') in reqData) {
value = reqData[spec.id + 'Selection']; value = reqData[spec.id + 'Selection'];
} else if (template && template.paramsObject && spec.id in template.paramsObject) { } else if (report && report.paramsObject && spec.id in report.paramsObject) {
value = template.paramsObject[spec.id].join(','); value = report.paramsObject[spec.id].join(',');
} }
userFields.push({ userFields.push({
@ -327,7 +355,7 @@ function addUserFields(reportTemplateId, reqData, template, callback) {
}); });
} }
const data = template ? template : reqData; const data = report ? report : reqData;
data.userFields = userFields; data.userFields = userFields;
callback(null, data); callback(null, data);

View file

@ -13,6 +13,7 @@
<li><a href="/report-templates/create">{{#translate}}Blank{{/translate}}</a></li> <li><a href="/report-templates/create">{{#translate}}Blank{{/translate}}</a></li>
<li><a href="/report-templates/create?type=subscribers-all">{{#translate}}All Subscribers{{/translate}}</a></li> <li><a href="/report-templates/create?type=subscribers-all">{{#translate}}All Subscribers{{/translate}}</a></li>
<li><a href="/report-templates/create?type=subscribers-grouped">{{#translate}}Grouped Subscribers{{/translate}}</a></li> <li><a href="/report-templates/create?type=subscribers-grouped">{{#translate}}Grouped Subscribers{{/translate}}</a></li>
<li><a href="/report-templates/create?type=export-list-csv">{{#translate}}Export List as CSV{{/translate}}</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -44,6 +44,30 @@
</div> </div>
</div> </div>
{{/case}} {{/case}}
{{#case "list"}}
<div class="form-group">
<label for="description" class="col-sm-2 control-label">{{name}}</label>
<div class="col-sm-10">
<div class="table-responsive">
<table data-topic-url="/lists/quicklist" data-sort-column="2" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax data-table-{{#if isMulti}}multi{{/if}}selectable display nowrap" width="100%" data-row-sort="0,1,1">
<thead>
<th class="col-md-1">
#
</th>
<th>
{{#translate}}Name{{/translate}}
</th>
<th>
{{#translate}}Subscribers{{/translate}}
</th>
</thead>
</table>
<input type="hidden" name="{{id}}Selection" value="{{value}}" />
</div>
<span class="help-block">{{#translate}}Select a campaign in the table above by clicking on the respective row number.{{/translate}}</span>
</div>
</div>
{{/case}}
{{/switch}} {{/switch}}
{{/each}} {{/each}}