From 2056645023551414c1f3986963c3f460943cd10e Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Mon, 17 Apr 2017 16:30:31 -0400 Subject: [PATCH] Added the option to select lists in report. Added an option to generate a CSV report. --- lib/models/lists.js | 4 + lib/models/reports.js | 2 +- lib/table-helpers.js | 16 ++- routes/lists.js | 23 +++++ routes/report-templates.js | 49 +++++++-- routes/reports.js | 108 ++++++++++++-------- views/report-templates/report-templates.hbs | 1 + views/reports/partials/report-fields.hbs | 24 +++++ 8 files changed, 177 insertions(+), 50 deletions(-) diff --git a/lib/models/lists.js b/lib/models/lists.js index dc1a0912..772a5820 100644 --- a/lib/models/lists.js +++ b/lib/models/lists.js @@ -17,6 +17,10 @@ module.exports.filter = (request, parent, 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 => { db.getConnection((err, connection) => { if (err) { diff --git a/lib/models/reports.js b/lib/models/reports.js index 57f3b1f5..d9dd3098 100644 --- a/lib/models/reports.js +++ b/lib/models/reports.js @@ -16,7 +16,7 @@ module.exports.list = (start, limit, callback) => { module.exports.filter = (request, callback) => { 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); }; diff --git a/lib/table-helpers.js b/lib/table-helpers.js index eaac0597..f5c2e6bc 100644 --- a/lib/table-helpers.js +++ b/lib/table-helpers.js @@ -10,7 +10,19 @@ module.exports.list = (source, fields, orderBy, start, limit, callback) => { 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) { connection.release(); return callback(err); @@ -56,8 +68,6 @@ module.exports.filter = (source, fields, request, columns, searchFields, default values = values.concat(queryData.values || []); } - log.info("tableHelpers", query); - connection.query(query, values, (err, total) => { if (err) { connection.release(); diff --git a/routes/lists.js b/routes/lists.js index 1ae18d7c..64c1352f 100644 --- a/routes/lists.js +++ b/routes/lists.js @@ -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": ' ' + htmlescape(row.name || '') + '', + "2": row.subscribers, + "DT_RowId": row.id + })) + }); + }); +}); + module.exports = router; diff --git a/routes/report-templates.js b/routes/report-templates.js index 37f9d6fd..72c74f1f 100644 --- a/routes/report-templates.js +++ b/routes/report-templates.js @@ -62,7 +62,7 @@ router.get('/create', passport.csrfProtection, (req, res) => { 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 (!('description' in data)) data.description = 'Generates a campaign report listing all subscribers along with their statistics.'; if (!('mimeType' in data)) data.mimeType = 'text/html'; @@ -124,7 +124,7 @@ router.get('/create', passport.csrfProtection, (req, res) => { ''; } 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'; @@ -142,13 +142,13 @@ router.get('/create', passport.csrfProtection, (req, res) => { 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' + + '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' + ' return callback(err);\n' + ' }\n' + '\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' + ' let data = {\n' + @@ -186,10 +186,10 @@ router.get('/create', passport.csrfProtection, (req, res) => { ' {{custom_zone}}\n' + ' \n' + ' \n' + - ' {{countOpened}}\n' + + ' {{count_opened}}\n' + ' \n' + ' \n' + - ' {{countAll}}\n' + + ' {{count_all}}\n' + ' \n' + ' \n' + ' {{percentage}}%\n' + @@ -200,6 +200,43 @@ router.get('/create', passport.csrfProtection, (req, res) => { ' {{/if}}\n' + ' \n' + ''; + + } 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(); diff --git a/routes/reports.js b/routes/reports.js index 922874d0..645c7bc9 100644 --- a/routes/reports.js +++ b/routes/reports.js @@ -7,6 +7,7 @@ const _ = require('../lib/translate')._; const reportTemplates = require('../lib/models/report-templates'); const reports = require('../lib/models/reports'); const campaigns = require('../lib/models/campaigns'); +const lists = require('../lib/models/lists'); const tools = require('../lib/tools'); const util = require('util'); const htmlescape = require('escape-html'); @@ -30,6 +31,12 @@ router.get('/', (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) => { if (err) { return res.json({ @@ -48,7 +55,7 @@ router.post('/ajax', (req, res) => { htmlescape(row.reportTemplateName || ''), htmlescape(striptags(row.description) || ''), '' + row.created.toISOString() + '', - ' ' + + ' ' + ''] ) }); @@ -123,15 +130,15 @@ router.get('/edit/:id', passport.csrfProtection, (req, res) => { skip: ['layout'] }); - reports.get(req.params.id, (err, template) => { - if (err || !template) { + reports.get(req.params.id, (err, report) => { + if (err || !report) { 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; + report.csrfToken = req.csrfToken(); + report.title = _('Edit Report'); + report.useEditor = true; reportTemplates.quicklist((err, items) => { if (err) { @@ -139,7 +146,7 @@ router.get('/edit/:id', passport.csrfProtection, (req, res) => { return res.redirect('/'); } - const reportTemplateId = template.reportTemplate; + const reportTemplateId = report.reportTemplate; items.forEach(item => { 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) { req.flash('danger', err.message || err); 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) => { - reports.get(req.params.id, (err, template) => { - if (err || !template) { + reports.get(req.params.id, (err, report) => { + if (err || !report) { req.flash('danger', err && err.message || err || _('Could not find report with specified ID')); return res.redirect('/reports'); } - reportTemplates.get(template.reportTemplate, (err, reportTemplate) => { + reportTemplates.get(report.reportTemplate, (err, reportTemplate) => { if (err) { return callback(err); } - resolveUserFields(reportTemplate.userFieldsObject, template.paramsObject, (err, inputs) => { + resolveUserFields(reportTemplate.userFieldsObject, report.paramsObject, (err, inputs) => { if (err) { req.flash('danger', err.message || err); return res.redirect('/reports'); @@ -228,31 +235,45 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => { } const hbsTmpl = hbs.handlebars.compile(reportTemplate.hbs); - const report = hbsTmpl(outputs); + const reportText = hbsTmpl(outputs); - const data = { - csrfToken: req.csrfToken(), - report: new hbs.handlebars.SafeString(report), - title: outputs.title - }; + if (reportTemplate.mimeType == 'text/html') { + const data = { + csrfToken: req.csrfToken(), + report: new hbs.handlebars.SafeString(reportText), + 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); - 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'); - } + const script = new vm.Script(reportTemplate.js); + script.runInNewContext(sandbox, { displayErrors: true, timeout: 10000 }); }); }); }); }); -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 resolved = []; @@ -261,12 +282,12 @@ function resolveCampaigns(ids, callback) { return callback(null, resolved); } - campaigns.get(idsRemaining.shift(), false, (err, campaign) => { + getter(idsRemaining.shift(), (err, entity) => { if (err) { return callback(err); } - resolved.push(campaign); + resolved.push(entity); return doWork(); }); } @@ -274,6 +295,11 @@ function resolveCampaigns(ids, callback) { setImmediate(doWork); } +const userFieldTypeToGetter = { + 'campaign': (id, callback) => campaigns.get(id, false, callback), + 'list': lists.get +}; + function resolveUserFields(userFields, params, callback) { const userFieldsRemaining = userFields.slice(); const resolved = {}; @@ -284,25 +310,27 @@ function resolveUserFields(userFields, params, callback) { } const spec = userFieldsRemaining.shift(); - if (spec.type == 'campaign') { - return resolveCampaigns(params[spec.id], (err, campaigns) => { + const getter = userFieldTypeToGetter[spec.type]; + + if (getter) { + return resolveEntities(getter, params[spec.id], (err, entities) => { if (spec.minOccurences == 1 && spec.maxOccurences == 1) { - resolved[spec.id] = campaigns[0]; + resolved[spec.id] = entities[0]; } else { - resolved[spec.id] = campaigns; + resolved[spec.id] = entities; } doWork(); }); + } else { + return callback(new Error(_('Unknown user field type "' + spec.type + '".'))); } - - return callback(new Error(_('Unknown user field type "' + spec.type + '".'))); } setImmediate(doWork); } -function addUserFields(reportTemplateId, reqData, template, callback) { +function addUserFields(reportTemplateId, reqData, report, callback) { reportTemplates.get(reportTemplateId, (err, reportTemplate) => { if (err) { return callback(err); @@ -314,8 +342,8 @@ function addUserFields(reportTemplateId, reqData, template, callback) { 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(','); + } else if (report && report.paramsObject && spec.id in report.paramsObject) { + value = report.paramsObject[spec.id].join(','); } 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; callback(null, data); diff --git a/views/report-templates/report-templates.hbs b/views/report-templates/report-templates.hbs index 68939e11..3250137d 100644 --- a/views/report-templates/report-templates.hbs +++ b/views/report-templates/report-templates.hbs @@ -13,6 +13,7 @@
  • {{#translate}}Blank{{/translate}}
  • {{#translate}}All Subscribers{{/translate}}
  • {{#translate}}Grouped Subscribers{{/translate}}
  • +
  • {{#translate}}Export List as CSV{{/translate}}
  • diff --git a/views/reports/partials/report-fields.hbs b/views/reports/partials/report-fields.hbs index ba24828b..df53d9bb 100644 --- a/views/reports/partials/report-fields.hbs +++ b/views/reports/partials/report-fields.hbs @@ -44,6 +44,30 @@ {{/case}} + {{#case "list"}} +
    + +
    +
    + + + + + + +
    + # + + {{#translate}}Name{{/translate}} + + {{#translate}}Subscribers{{/translate}} +
    + +
    + {{#translate}}Select a campaign in the table above by clicking on the respective row number.{{/translate}} +
    +
    + {{/case}} {{/switch}} {{/each}}