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"}}
+
+ {{/case}}
{{/switch}}
{{/each}}
|