diff --git a/app.js b/app.js
index 14912aa6..7f0f9023 100644
--- a/app.js
+++ b/app.js
@@ -40,6 +40,9 @@ const editorapi = require('./routes/editorapi');
const grapejs = require('./routes/grapejs');
const mosaico = require('./routes/mosaico');
+// These are routes for the new React-based client
+const reports = require('./routes/reports');
+
const namespacesRest = require('./routes/rest/namespaces');
const usersRest = require('./routes/rest/users');
const accountRest = require('./routes/rest/account');
@@ -242,18 +245,22 @@ app.use('/editorapi', editorapi);
app.use('/grapejs', grapejs);
app.use('/mosaico', mosaico);
+
+if (config.reports && config.reports.enabled === true) {
+ app.use('/reports', reports);
+}
+
/* FIXME - this should be removed once we bind the ReactJS client to / */
app.use('/users', usersLegacyIntegration);
app.use('/namespaces', namespacesLegacyIntegration);
app.use('/account', accountLegacyIntegration);
if (config.reports && config.reports.enabled === true) {
+ app.use('/reports', reports);
app.use('/reports', reportsLegacyIntegration);
}
-
/* ------------------------------------------------------------------- */
-
app.all('/rest/*', (req, res, next) => {
req.needsJSONResponse = true;
next();
@@ -301,11 +308,16 @@ if (app.get('env') === 'development') {
res.status(err.status || 500).json(resp);
} else {
- res.status(err.status || 500);
- res.render('error', {
- message: err.message,
- error: err
- });
+ if (err instanceof interoperableErrors.NotLoggedInError) {
+ req.flash('danger', _('Need to be logged in to access restricted content'));
+ return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
+ } else {
+ res.status(err.status || 500);
+ res.render('error', {
+ message: err.message,
+ error: err
+ });
+ }
}
});
@@ -331,11 +343,16 @@ if (app.get('env') === 'development') {
res.status(err.status || 500).json(resp);
} else {
- res.status(err.status || 500);
- res.render('error', {
- message: err.message,
- error: {}
- });
+ if (err instanceof interoperableErrors.NotLoggedInError) {
+ req.flash('danger', _('Need to be logged in to access restricted content'));
+ return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
+ } else {
+ res.status(err.status || 500);
+ res.render('error', {
+ message: err.message,
+ error: {}
+ });
+ }
}
});
}
diff --git a/client/src/lib/form.js b/client/src/lib/form.js
index 2453ec32..468076f2 100644
--- a/client/src/lib/form.js
+++ b/client/src/lib/form.js
@@ -49,36 +49,6 @@ class Form extends Component {
};
}
- static async handleChangedError(owner, fn) {
- try {
- await fn();
- } catch (error) {
- if (error instanceof interoperableErrors.ChangedError) {
- owner.disableForm();
- owner.setFormStatusMessage('danger',
-
- {t('Your updates cannot be saved.')}{' '}
- {t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
-
- );
- return;
- }
-
- if (error instanceof interoperableErrors.NotFoundError) {
- owner.disableForm();
- owner.setFormStatusMessage('danger',
-
- {t('Your updates cannot be saved.')}{' '}
- {t('It seems that someone else has deleted the entity in the meantime.')}
-
- );
- return;
- }
-
- throw error;
- }
- }
-
@withAsyncErrorHandler
async onSubmit(evt) {
const t = this.props.t;
@@ -88,7 +58,7 @@ class Form extends Component {
evt.preventDefault();
if (this.props.onSubmitAsync) {
- await Form.handleChangedError(owner, async () => await this.props.onSubmitAsync(evt));
+ await this.formHandleChangedError(async () => await this.props.onSubmitAsync(evt));
}
}
@@ -520,6 +490,7 @@ class ACEEditor extends Component {
showPrintMargin={false}
value={owner.getFormValue(id)}
tabSize={2}
+ setOptions={{useWorker: false}} // This disables syntax check because it does not always work well (e.g. in case of JS code in report templates)
/>
);
}
@@ -839,6 +810,37 @@ function withForm(target) {
return this.state.formState.get('isDisabled');
};
+ inst.formHandleChangedError = async function(fn) {
+ const t = this.props.t;
+ try {
+ await fn();
+ } catch (error) {
+ if (error instanceof interoperableErrors.ChangedError) {
+ this.disableForm();
+ this.setFormStatusMessage('danger',
+
+ {t('Your updates cannot be saved.')}{' '}
+ {t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
+
+ );
+ return;
+ }
+
+ if (error instanceof interoperableErrors.NotFoundError) {
+ this.disableForm();
+ this.setFormStatusMessage('danger',
+
+ {t('Your updates cannot be saved.')}{' '}
+ {t('It seems that someone else has deleted the entity in the meantime.')}
+
+ );
+ return;
+ }
+
+ throw error;
+ }
+ };
+
return target;
}
diff --git a/client/src/lib/page.css b/client/src/lib/page.css
index fa84852d..029b6b67 100644
--- a/client/src/lib/page.css
+++ b/client/src/lib/page.css
@@ -11,11 +11,11 @@
padding-bottom: 5px;
}
-.mt-action-links > a {
+.mt-action-links > * {
margin-right: 8px;
}
-.mt-action-links > a:last-child {
+.mt-action-links > *:last-child {
margin-right: 0px;
}
diff --git a/client/src/lib/table.js b/client/src/lib/table.js
index 670015c4..8a1b63a2 100644
--- a/client/src/lib/table.js
+++ b/client/src/lib/table.js
@@ -29,7 +29,7 @@ const TableSelectMode = {
};
-@translate()
+@translate(null, { withRef: true })
@withPageHelpers
@withErrorHandling
class Table extends Component {
@@ -48,8 +48,9 @@ class Table extends Component {
selectionAsArray: PropTypes.bool,
onSelectionChangedAsync: PropTypes.func,
onSelectionDataAsync: PropTypes.func,
- actionLinks: PropTypes.array,
- withHeader: PropTypes.bool
+ actions: PropTypes.func,
+ withHeader: PropTypes.bool,
+ refreshInterval: PropTypes.number
}
static defaultProps = {
@@ -57,6 +58,12 @@ class Table extends Component {
selectionKeyIndex: 0
}
+ refresh() {
+ if (this.table) {
+ this.table.rows().draw('page');
+ }
+ }
+
getSelectionMap(props) {
let selArray = [];
if (props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
@@ -162,17 +169,56 @@ class Table extends Component {
componentDidMount() {
const columns = this.props.columns.slice();
- if (this.props.actionLinks) {
- const actionLinks = this.props.actionLinks;
-
+ if (this.props.actions) {
const createdCellFn = (td, data) => {
const linksContainer = jQuery('');
- for (const {label, link} of actionLinks) {
- const dest = link(data);
- const lnkHtml = ReactDOMServer.renderToStaticMarkup({label});
- const lnk = jQuery(lnkHtml);
- lnk.click((evt) => { evt.preventDefault(); this.navigateTo(dest) });
- linksContainer.append(lnk);
+
+ let actions = this.props.actions(data);
+ let options = {};
+
+ if (!Array.isArray(actions)) {
+ options = actions;
+ actions = actions.actions;
+ }
+
+ for (const action of actions) {
+ if (action.action) {
+ const html = ReactDOMServer.renderToStaticMarkup({action.label});
+ const elem = jQuery(html);
+ elem.click((evt) => { evt.preventDefault(); action.action(this) });
+ linksContainer.append(elem);
+
+ } else if (action.link) {
+ const html = ReactDOMServer.renderToStaticMarkup({action.label});
+ const elem = jQuery(html);
+ elem.click((evt) => { evt.preventDefault(); this.navigateTo(action.link) });
+ linksContainer.append(elem);
+
+ } else if (action.href) {
+ const html = ReactDOMServer.renderToStaticMarkup({action.label});
+ const elem = jQuery(html);
+ linksContainer.append(elem);
+
+ } else {
+ const html = ReactDOMServer.renderToStaticMarkup(action.label);
+ const elem = jQuery(html);
+ linksContainer.append(elem);
+ }
+ }
+
+ if (options.refreshTimeout) {
+ const currentMS = Date.now();
+
+ if (!this.refreshTimeoutAt || this.refreshTimeoutAt > currentMS + options.refreshTimeout) {
+ clearTimeout(this.refreshTimeoutId);
+
+ this.refreshTimeoutAt = currentMS + options.refreshTimeout;
+
+ this.refreshTimeoutId = setTimeout(() => {
+ this.refreshTimeoutAt = 0;
+ this.refresh();
+ }, options.refreshTimeout);
+ }
}
jQuery(td).html(linksContainer);
@@ -238,6 +284,15 @@ class Table extends Component {
this.table = jQuery(this.domTable).DataTable(dtOptions);
+ if (this.props.refreshInterval) {
+ this.refreshIntervalId = setInterval(() => this.refresh(), this.props.refreshInterval);
+ }
+
+ this.table.on('destroy.dt', () => {
+ clearInterval(this.refreshIntervalId);
+ clearTimeout(this.refreshTimeoutId);
+ });
+
this.fetchSelectionData();
}
@@ -306,6 +361,14 @@ class Table extends Component {
}
}
+/*
+ Refreshes the table. This method is provided to allow programmatic refresh from a handler outside the table.
+ The reference to the table can be obtained by ref.
+ */
+Table.prototype.refresh = function() {
+ this.getWrappedInstance().refresh()
+};
+
export {
Table,
TableSelectMode
diff --git a/client/src/lib/tree.js b/client/src/lib/tree.js
index 27e66dd1..edfbe497 100644
--- a/client/src/lib/tree.js
+++ b/client/src/lib/tree.js
@@ -65,7 +65,7 @@ class TreeTable extends Component {
selectMode: PropTypes.number,
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
onSelectionChangedAsync: PropTypes.func,
- actionLinks: PropTypes.array,
+ actions: PropTypes.func,
withHeader: PropTypes.bool
}
@@ -100,19 +100,18 @@ class TreeTable extends Component {
};
let createNodeFn;
- if (this.props.actionLinks) {
- const actionLinks = this.props.actionLinks;
-
+ if (this.props.actions) {
createNodeFn = (event, data) => {
const node = data.node;
const tdList = jQuery(node.tr).find(">td");
const linksContainer = jQuery('');
- for (const {label, link} of actionLinks) {
- const dest = link(node.key);
- const lnkHtml = ReactDOMServer.renderToStaticMarkup({label});
+
+ const actions = this.props.actions(node.key);
+ for (const {label, link} of actions) {
+ const lnkHtml = ReactDOMServer.renderToStaticMarkup({label});
const lnk = jQuery(lnkHtml);
- lnk.click((evt) => { evt.preventDefault(); this.navigateTo(dest) });
+ lnk.click((evt) => { evt.preventDefault(); this.navigateTo(link) });
linksContainer.append(lnk);
}
@@ -202,7 +201,7 @@ class TreeTable extends Component {
render() {
const t = this.props.t;
const props = this.props;
- const actionLinks = props.actionLinks;
+ const actions = props.actions;
const withHeader = props.withHeader;
let containerClass = 'mt-treetable-container';
@@ -223,14 +222,14 @@ class TreeTable extends Component {
{t('Name')} |
- {actionLinks && | }
+ {actions && | }
}
|
- {actionLinks && | }
+ {actions && | }
diff --git a/client/src/namespaces/List.js b/client/src/namespaces/List.js
index a16ab205..af107c27 100644
--- a/client/src/namespaces/List.js
+++ b/client/src/namespaces/List.js
@@ -10,10 +10,10 @@ export default class List extends Component {
render() {
const t = this.props.t;
- const actionLinks = [
+ const actions = key => [
{
label: 'Edit',
- link: key => '/namespaces/edit/' + key
+ link: '/namespaces/edit/' + key
}
];
@@ -25,7 +25,7 @@ export default class List extends Component {
{t('Namespaces')}
-
+
);
}
diff --git a/client/src/reports/List.js b/client/src/reports/List.js
index 08e271c5..1dc1de8f 100644
--- a/client/src/reports/List.js
+++ b/client/src/reports/List.js
@@ -4,24 +4,100 @@ import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { Title, Toolbar, NavButton } from '../lib/page';
import { Table } from '../lib/table';
+import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import moment from 'moment';
+import axios from '../lib/axios';
+import { ReportState } from '../../../shared/reports';
@translate()
+@withErrorHandling
export default class List extends Component {
+
+ @withAsyncErrorHandler
+ async stop(table, id) {
+ await axios.post(`/rest/report-stop/${id}`);
+ table.refresh();
+ }
+
+ @withAsyncErrorHandler
+ async start(table, id) {
+ await axios.post(`/rest/report-start/${id}`);
+ table.refresh();
+ }
+
render() {
const t = this.props.t;
- const actionLinks = [{
- label: 'Edit',
- link: data => '/reports/edit/' + data[0]
- }];
+ const actions = data => {
+ let view, startStop, refreshTimeout;
+
+ const state = data[5];
+ const id = data[0];
+ const mimeType = data[6];
+
+ if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) {
+ view = {
+ label: ,
+ };
+
+ startStop = {
+ label: ,
+ action: (table) => this.stop(table, id)
+ };
+
+ refreshTimeout = 1000;
+ } else if (state === ReportState.FINISHED) {
+ if (mimeType === 'text/html') {
+ view = {
+ label: ,
+ link: `reports/view/${id}`
+ };
+ } else if (mimeType === 'text/csv') {
+ view = {
+ label: ,
+ href: `reports/download/${id}`
+ };
+ }
+
+ startStop = {
+ label: ,
+ action: (table) => this.start(table, id)
+ };
+
+ } else if (state === ReportState.FAILED) {
+ view = {
+ label: ,
+ };
+
+ startStop = {
+ label: ,
+ action: (table) => this.start(table, id)
+ };
+ }
+
+ return {
+ refreshTimeout,
+ actions: [
+ view,
+ {
+ label: ,
+ link: `reports/output/${id}`
+ },
+ startStop,
+ {
+ label: ,
+ link: `/reports/edit/${id}`
+ }
+ ]
+ };
+ };
const columns = [
{ data: 0, title: "#" },
{ data: 1, title: t('Name') },
{ data: 2, title: t('Template') },
{ data: 3, title: t('Description') },
- { data: 4, title: t('Last Run'), render: data => data ? moment(data).fromNow() : t('Not run yet') }
+ { data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
];
return (
@@ -33,7 +109,7 @@ export default class List extends Component {
{t('Reports')}
-
+
);
}
diff --git a/client/src/reports/Output.js b/client/src/reports/Output.js
new file mode 100644
index 00000000..b3df049b
--- /dev/null
+++ b/client/src/reports/Output.js
@@ -0,0 +1,53 @@
+'use strict';
+
+import React, { Component } from 'react';
+import { translate } from 'react-i18next';
+import { withPageHelpers, Title } from '../lib/page'
+import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
+import axios from '../lib/axios';
+
+@translate()
+@withPageHelpers
+@withErrorHandling
+export default class Output extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ output: null
+ };
+ }
+
+ @withAsyncErrorHandler
+ async loadOutput() {
+ const id = parseInt(this.props.match.params.id);
+ const outputResp = await axios.get(`/rest/report-output/${id}`);
+ const reportResp = await axios.get(`/rest/reports/${id}`);
+
+ this.setState({
+ output: outputResp.data,
+ report: reportResp.data,
+ });
+ }
+
+ componentDidMount() {
+ this.loadOutput();
+ }
+
+ render() {
+ const t = this.props.t;
+
+ if (this.state.report) {
+ return (
+
+
{t('Output for report {{name}}', { name: this.state.report.name })}
+
+
{this.state.output}
+
+ );
+ } else {
+ return {t('Loading report output ...')}
;
+ }
+
+ }
+}
diff --git a/client/src/reports/View.js b/client/src/reports/View.js
new file mode 100644
index 00000000..902ec073
--- /dev/null
+++ b/client/src/reports/View.js
@@ -0,0 +1,57 @@
+'use strict';
+
+import React, { Component } from 'react';
+import { translate } from 'react-i18next';
+import { withPageHelpers, Title } from '../lib/page'
+import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
+import axios from '../lib/axios';
+import { ReportState } from '../../../shared/reports';
+
+@translate()
+@withPageHelpers
+@withErrorHandling
+export default class View extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ content: null
+ };
+ }
+
+ @withAsyncErrorHandler
+ async loadContent() {
+ const id = parseInt(this.props.match.params.id);
+ const contentResp = await axios.get(`/rest/report-content/${id}`);
+ const reportResp = await axios.get(`/rest/reports/${id}`);
+
+ this.setState({
+ content: contentResp.data,
+ report: reportResp.data
+ });
+ }
+
+ componentDidMount() {
+ this.loadContent();
+ }
+
+ render() {
+ const t = this.props.t;
+
+ if (this.state.report) {
+ if (this.state.report.state === ReportState.FINISHED) {
+ return (
+
+
{t('Report {{name}}', { name: this.state.report.name })}
+
+
+
+ );
+ } else {
+ return {t('Report not generated')}
;
+ }
+ } else {
+ return {t('Loading report ...')}
;
+ }
+ }
+}
diff --git a/client/src/reports/root.js b/client/src/reports/root.js
index 416b4c80..49f9145a 100644
--- a/client/src/reports/root.js
+++ b/client/src/reports/root.js
@@ -8,6 +8,8 @@ import i18n from '../lib/i18n';
import { Section } from '../lib/page'
import ReportsCUD from './CUD'
import ReportsList from './List'
+import ReportsView from './View'
+import ReportsOutput from './Output'
import ReportTemplatesCUD from './templates/CUD'
import ReportTemplatesList from './templates/List'
@@ -33,6 +35,16 @@ const getStructure = t => {
title: t('Create Report'),
render: props => ()
},
+ view: {
+ title: t('View Report'),
+ params: [':id' ],
+ render: props => ()
+ },
+ output: {
+ title: t('View Report Output'),
+ params: [':id' ],
+ render: props => ()
+ },
'templates': {
title: t('Templates'),
link: '/reports/templates',
diff --git a/client/src/reports/templates/CUD.js b/client/src/reports/templates/CUD.js
index 03add471..4934fc9a 100644
--- a/client/src/reports/templates/CUD.js
+++ b/client/src/reports/templates/CUD.js
@@ -57,17 +57,8 @@ export default class CUD extends Component {
' }\n' +
']',
js:
- 'campaigns.results(inputs.campaign, ["*"], "", (err, results) => {\n' +
- ' if (err) {\n' +
- ' return callback(err);\n' +
- ' }\n' +
- '\n' +
- ' const data = {\n' +
- ' results: results\n' +
- ' };\n' +
- '\n' +
- ' return callback(null, data);\n' +
- '});',
+ 'const results = await campaigns.getResults(inputs.campaign, ["*"]);\n' +
+ 'render({ results });',
hbs:
'{{title}}
\n' +
'\n' +
@@ -115,21 +106,17 @@ export default class CUD extends Component {
' }\n' +
']',
js:
- 'campaigns.results(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' +
+ 'const results = await campaigns.getResults(inputs.campaign, ["custom_country"], query =>\n' +
+ ' query.count("* AS count_all")\n' +
+ ' .select(knex.raw("SUM(IF(tracker.count IS NULL, 0, 1)) AS count_opened"))\n' +
+ ' .groupBy("custom_country")\n' +
+ ');\n' +
'\n' +
- ' for (let row of results) {\n' +
- ' row["percentage"] = Math.round((row.count_opened / row.count_all) * 100);\n' +
- ' }\n' +
+ 'for (const row of results) {\n' +
+ ' row.percentage = Math.round((row.count_opened / row.count_all) * 100);\n' +
+ '}\n' +
'\n' +
- ' let data = {\n' +
- ' results: results\n' +
- ' };\n' +
- '\n' +
- ' return callback(null, data);\n' +
- '});',
+ 'render({ results });',
hbs:
'{{title}}
\n' +
'\n' +
@@ -189,17 +176,8 @@ export default class CUD extends Component {
' }\n' +
']',
js:
- 'subscriptions.list(inputs.list.id,0,0, (err, results) => {\n' +
- ' if (err) {\n' +
- ' return callback(err);\n' +
- ' }\n' +
- '\n' +
- ' let data = {\n' +
- ' results: results\n' +
- ' };\n' +
- '\n' +
- ' return callback(null, data);\n' +
- '});',
+ 'const results = await subscriptions.list(inputs.list.id);\n' +
+ 'render({ results });',
hbs:
'{{#each results}}\n' +
'{{firstName}},{{lastName}},{{email}}\n' +
@@ -246,11 +224,11 @@ export default class CUD extends Component {
}
async submitAndStay() {
- await Form.handleChangedError(this, async () => await this.doSubmit(true));
+ await this.formHandleChangedError(async () => await this.doSubmit(true));
}
async submitAndLeave() {
- await Form.handleChangedError(this, async () => await this.doSubmit(false));
+ await this.formHandleChangedError(async () => await this.doSubmit(false));
}
async doSubmit(stay) {
@@ -273,6 +251,7 @@ export default class CUD extends Component {
if (submitSuccessful) {
if (stay) {
+ await this.loadFormValues();
this.enableForm();
this.setFormStatusMessage('success', t('Report template saved'));
} else {
diff --git a/client/src/reports/templates/List.js b/client/src/reports/templates/List.js
index 40604364..a1106316 100644
--- a/client/src/reports/templates/List.js
+++ b/client/src/reports/templates/List.js
@@ -12,10 +12,12 @@ export default class List extends Component {
render() {
const t = this.props.t;
- const actionLinks = [{
- label: 'Edit',
- link: data => '/reports/templates/edit/' + data[0]
- }];
+ const actions = data => [
+ {
+ label: 'Edit',
+ link: '/reports/templates/edit/' + data[0]
+ }
+ ];
const columns = [
{ data: 0, title: "#" },
@@ -37,7 +39,7 @@ export default class List extends Component {
{t('Report Templates')}
-
+
);
}
diff --git a/client/src/users/List.js b/client/src/users/List.js
index 3048d5b8..3da7104d 100644
--- a/client/src/users/List.js
+++ b/client/src/users/List.js
@@ -11,7 +11,7 @@ export default class List extends Component {
render() {
const t = this.props.t;
- let actionLinks;
+ let actions;
const columns = [
{ data: 0, title: "#" },
@@ -21,10 +21,12 @@ export default class List extends Component {
if (mailtrainConfig.isAuthMethodLocal) {
columns.push({ data: 2, title: "Full Name" });
- actionLinks = [{
- label: 'Edit',
- link: data => '/users/edit/' + data[0]
- }];
+ actions = data => [
+ {
+ label: 'Edit',
+ link: '/users/edit/' + data[0]
+ }
+ ];
}
return (
@@ -35,7 +37,7 @@ export default class List extends Component {
{t('Users')}
-
+
);
}
diff --git a/lib/models/reports-REMOVE.js b/lib/models/reports-REMOVE.js
deleted file mode 100644
index edfe665f..00000000
--- a/lib/models/reports-REMOVE.js
+++ /dev/null
@@ -1,262 +0,0 @@
-'use strict';
-
-const db = require('../db');
-const tableHelpers = require('../table-helpers');
-const fields = require('./fields');
-const reportTemplates = require('./report-templates');
-const tools = require('../tools');
-const _ = require('../translate')._;
-
-const allowedKeys = ['name', 'description', 'report_template', 'params'];
-
-const ReportState = {
- SCHEDULED: 0,
- PROCESSING: 1,
- FINISHED: 2,
- FAILED: 3,
- MAX: 4
-};
-
-module.exports.ReportState = ReportState;
-
-module.exports.list = (start, limit, callback) => {
- tableHelpers.list('reports', ['*'], 'name', null, start, limit, callback);
-};
-
-module.exports.listWithState = (state, start, limit, callback) => {
- tableHelpers.list('reports', ['*'], 'name', { where: 'state=?', values: [state] }, 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.state AS state', 'reports.report_template AS report_template', 'reports.params AS params', 'reports.last_run AS last_run', 'report_templates.name AS report_template_name', 'report_templates.mime_type AS mime_type' ],
- request, ['#', 'name', 'report_templates.name', 'description', 'last_run'], ['name'], 'name ASC', null, callback);
-};
-
-module.exports.get = (id, callback) => {
- id = Number(id) || 0;
-
- if (id < 1) {
- return callback(new Error(_('Missing report ID')));
- }
-
- db.getConnection((err, connection) => {
- if (err) {
- return callback(err);
- }
-
- connection.query('SELECT * FROM reports WHERE id=?', [id], (err, rows) => {
- connection.release();
- if (err) {
- return callback(err);
- }
-
- if (!rows || !rows.length) {
- return callback(null, false);
- }
-
- const template = tools.convertKeys(rows[0]);
-
- const params = template.params.trim();
- if (params !== '') {
- try {
- template.paramsObject = JSON.parse(params);
- } catch (err) {
- return callback(err);
- }
- } else {
- template.params = {};
- }
-
- return callback(null, template);
- });
- });
-};
-
-// This method is not supposed to be used for unsanitized inputs. It does not do any checks.
-module.exports.updateFields = (id, fieldValueMap, callback) => {
- db.getConnection((err, connection) => {
- if (err) {
- return callback(err);
- }
-
- const clauses = [];
- const values = [];
- for (let key of Object.keys(fieldValueMap)) {
- clauses.push(tools.toDbKey(key) + '=?');
- values.push(fieldValueMap[key]);
- }
-
- values.push(id);
-
- const query = 'UPDATE reports SET ' + clauses.join(', ') + ' WHERE id=? LIMIT 1';
- connection.query(query, values, (err, result) => {
- connection.release();
- if (err) {
- return callback(err);
- }
-
- return callback(null, result && result.affectedRows || false);
- });
- });
-};
-
-module.exports.createOrUpdate = (createMode, report, callback) => {
- report = report || {};
-
- const id = 'id' in report ? Number(report.id) : 0;
-
- if (!createMode && id < 1) {
- return callback(new Error(_('Missing report ID')));
- }
-
- const name = (report.name || '').toString().trim();
-
- if (!name) {
- return callback(new Error(_('Report name must be set')));
- }
-
- const reportTemplateId = Number(report.reportTemplate);
- reportTemplates.get(reportTemplateId, (err, reportTemplate) => {
- if (err) {
- return callback(err);
- }
-
- const params = report.paramsObject;
- for (const spec of reportTemplate.userFieldsObject) {
- if (params[spec.id].length < spec.minOccurences) {
- return callback(new Error(_('At least ' + spec.minOccurences + ' rows in "' + spec.name + '" have to be selected.')));
- }
-
- if (params[spec.id].length > spec.maxOccurences) {
- return callback(new Error(_('At most ' + spec.minOccurences + ' rows in "' + spec.name + '" can be selected.')));
- }
- }
-
- const keys = ['name', 'params'];
- const values = [name, JSON.stringify(params)];
-
-
- Object.keys(report).forEach(key => {
- let value = typeof report[key] === 'number' ? report[key] : (report[key] || '').toString().trim();
- key = tools.toDbKey(key);
-
- if (key === 'description') {
- value = tools.purifyHTML(value);
- }
-
- if (allowedKeys.indexOf(key) >= 0 && keys.indexOf(key) < 0) {
- keys.push(key);
- values.push(value);
- }
- });
-
- db.getConnection((err, connection) => {
- if (err) {
- return callback(err);
- }
-
- let query;
-
- if (createMode) {
- query = 'INSERT INTO reports (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
- } else {
- query = 'UPDATE reports SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1';
- values.push(id);
- }
-
- connection.query(query, values, (err, result) => {
- connection.release();
- if (err) {
- return callback(err);
- }
-
- if (createMode) {
- return callback(null, result && result.insertId || false);
- } else {
- return callback(null, result && result.affectedRows || false);
- }
- });
- });
- });
-};
-
-module.exports.delete = (id, callback) => {
- id = Number(id) || 0;
-
- if (id < 1) {
- return callback(new Error(_('Missing report ID')));
- }
-
- db.getConnection((err, connection) => {
- if (err) {
- return callback(err);
- }
-
- connection.query('DELETE FROM reports WHERE id=? LIMIT 1', [id], (err, result) => {
- connection.release();
- if (err) {
- return callback(err);
- }
-
- const affected = result && result.affectedRows || 0;
- return callback(err, affected);
- });
- });
-};
-
-const campaignFieldsMapping = {
- tracker_count: 'tracker.count',
- country: 'tracker.country',
- device_type: 'tracker.device_type',
- status: 'campaign.status',
- first_name: 'subscribers.first_name',
- last_name: 'subscribers.last_name',
- email: 'subscribers.email'
-};
-
-module.exports.getCampaignResults = (campaign, select, clause, callback) => {
- db.getConnection((err, connection) => {
- if (err) {
- return callback(err);
- }
-
- fields.list(campaign.list, (err, fieldList) => {
- if (err) {
- return callback(err);
- }
-
- const fieldsMapping = fieldList.reduce((map, field) => {
- map[customFieldName(field.key)] = 'subscribers.' + field.column;
- return map;
- }, Object.assign({}, campaignFieldsMapping));
-
- let selFields = [];
- for (let idx = 0; idx < select.length; idx++) {
- const item = select[idx];
- if (item in fieldsMapping) {
- selFields.push(fieldsMapping[item] + ' AS ' + item);
- } else if (item === '*') {
- selFields = selFields.concat(Object.keys(fieldsMapping).map(item => fieldsMapping[item] + ' AS ' + item));
- } else {
- selFields.push(item);
- }
- }
-
- const query = 'SELECT ' + selFields.join(', ') + ' FROM `subscription__' + campaign.list + '` subscribers INNER JOIN `campaign__' + campaign.id + '` campaign on subscribers.id=campaign.subscription LEFT JOIN `campaign_tracker__' + campaign.id + '` tracker on subscribers.id=tracker.subscriber ' + clause;
-
- connection.query(query, (err, results) => {
- connection.release();
- if (err) {
- return callback(err);
- }
-
- return callback(null, results);
- });
- });
- });
-};
-
-function customFieldName(id) {
- return id.replace(/MERGE_/, 'CUSTOM_').toLowerCase();
-}
diff --git a/lib/models/subscriptions.js b/lib/models/subscriptions.js
index f9b562c5..8192036e 100644
--- a/lib/models/subscriptions.js
+++ b/lib/models/subscriptions.js
@@ -10,14 +10,7 @@ let segments = require('./segments');
let _ = require('../translate')._;
let tableHelpers = require('../table-helpers');
-const Status = {
- SUBSCRIBED: 1,
- UNSUBSCRIBED: 2,
- BOUNCED: 3,
- COMPLAINED: 4,
- MAX: 5
-};
-
+const Status = require('../../models/subscriptions').Status;
module.exports.Status = Status;
module.exports.list = (listId, start, limit, callback) => {
diff --git a/lib/report-processor.js b/lib/report-processor.js
index dd909278..ede02beb 100644
--- a/lib/report-processor.js
+++ b/lib/report-processor.js
@@ -30,7 +30,7 @@ function startWorker(report) {
}
try {
- await reports.update(report.id, fields);
+ await reports.updateFields(report.id, fields);
setImmediate(tryStartWorkers);
} catch (err) {
log.error('ReportProcessor', err);
@@ -47,7 +47,7 @@ function startWorker(report) {
};
try {
- await reports.update(report.id, fields);
+ await reports.updateFields(report.id, fields);
setImmediate(tryStartWorkers);
} catch (err) {
log.error('ReportProcessor', err);
@@ -83,7 +83,7 @@ async function tryStartWorkers() {
log.info('ReportProcessor', 'Starting worker');
const report = reportList[0];
- await report.updateFields(report.id, {state: reports.ReportState.PROCESSING});
+ await reports.updateFields(report.id, {state: reports.ReportState.PROCESSING});
startWorker(report);
} else {
@@ -102,7 +102,7 @@ async function tryStartWorkers() {
module.exports.start = async reportId => {
if (!workers[reportId]) {
log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
- await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, lastRun: null});
+ await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, last_run: null});
tryStartWorkers();
} else {
log.info('ReportProcessor', 'Worker for report id: %s is already running.', reportId);
@@ -124,6 +124,7 @@ module.exports.stop = async reportId => {
module.exports.init = async () => {
try {
await reports.bulkChangeState(reports.ReportState.PROCESSING, reports.ReportState.SCHEDULED);
+ tryStartWorkers();
} catch (err) {
log.error('ReportProcessor', err);
}
diff --git a/models/campaigns.js b/models/campaigns.js
index 5a2b0202..fdfa3ef8 100644
--- a/models/campaigns.js
+++ b/models/campaigns.js
@@ -2,12 +2,22 @@
const knex = require('../lib/knex');
const dtHelpers = require('../lib/dt-helpers');
+const interoperableErrors = require('../shared/interoperable-errors');
async function listDTAjax(params) {
return await dtHelpers.ajaxList(params, tx => tx('campaigns'), ['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.status', 'campaigns.created']);
}
+async function getById(id) {
+ const entity = await knex('campaigns').where('id', id).first();
+ if (!entity) {
+ throw new interoperableErrors.NotFoundError();
+ }
+
+ return entity;
+}
module.exports = {
- listDTAjax
+ listDTAjax,
+ getById
};
\ No newline at end of file
diff --git a/models/fields.js b/models/fields.js
new file mode 100644
index 00000000..40d50404
--- /dev/null
+++ b/models/fields.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const knex = require('../lib/knex');
+const dtHelpers = require('../lib/dt-helpers');
+const interoperableErrors = require('../shared/interoperable-errors');
+const fieldsLegacy = require('../lib/models/fields');
+const bluebird = require('bluebird');
+
+module.exports = {
+ list: bluebird.promisify(fieldsLegacy.list)
+};
\ No newline at end of file
diff --git a/models/lists.js b/models/lists.js
index e2d85478..8b861649 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -2,12 +2,23 @@
const knex = require('../lib/knex');
const dtHelpers = require('../lib/dt-helpers');
+const interoperableErrors = require('../shared/interoperable-errors');
async function listDTAjax(params) {
return await dtHelpers.ajaxList(params, tx => tx('lists'), ['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description']);
}
+async function getById(id) {
+ const entity = await knex('lists').where('id', id).first();
+ if (!entity) {
+ throw new interoperableErrors.NotFoundError();
+ }
+
+ return entity;
+}
+
module.exports = {
- listDTAjax
+ listDTAjax,
+ getById
};
\ No newline at end of file
diff --git a/models/reports.js b/models/reports.js
index 3a634b7d..630cbb8b 100644
--- a/models/reports.js
+++ b/models/reports.js
@@ -5,24 +5,19 @@ const hasher = require('node-object-hash')();
const { enforce, filterObject } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors');
+const fields = require('./fields');
+
+const ReportState = require('../shared/reports').ReportState;
const allowedKeys = new Set(['name', 'description', 'report_template', 'params']);
-const ReportState = {
- SCHEDULED: 0,
- PROCESSING: 1,
- FINISHED: 2,
- FAILED: 3,
- MAX: 4
-};
-
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
}
-async function getByIdWithUserFields(id) {
- const entity = await knex('reports').where('reports.id', id).innerJoin('report_templates', 'reports.report_template', 'report_templates.id').select(['reports.id', 'reports.name', 'reports.description', 'reports.report_template', 'reports.params', 'report_templates.user_fields']).first();
+async function getByIdWithTemplate(id) {
+ const entity = await knex('reports').where('reports.id', id).innerJoin('report_templates', 'reports.report_template', 'report_templates.id').select(['reports.id', 'reports.name', 'reports.description', 'reports.report_template', 'reports.params', 'reports.state', 'report_templates.user_fields', 'report_templates.mime_type', 'report_templates.hbs', 'report_templates.js']).first();
if (!entity) {
throw new interoperableErrors.NotFoundError();
}
@@ -34,10 +29,11 @@ async function getByIdWithUserFields(id) {
}
async function listDTAjax(params) {
- return await dtHelpers.ajaxList(params, tx => tx('reports').innerJoin('report_templates', 'reports.report_template', 'report_templates.id'), ['reports.id', 'reports.name', 'report_templates.name', 'reports.description', 'reports.last_run', 'reports.state']);
+ return await dtHelpers.ajaxList(params, tx => tx('reports').innerJoin('report_templates', 'reports.report_template', 'report_templates.id'), ['reports.id', 'reports.name', 'report_templates.name', 'reports.description', 'reports.last_run', 'reports.state', 'report_templates.mime_type']);
}
async function create(entity) {
+ let id;
await knex.transaction(async tx => {
if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) {
throw new interoperableErrors.DependencyNotFoundError();
@@ -45,10 +41,12 @@ async function create(entity) {
entity.params = JSON.stringify(entity.params);
- const id = await tx('reports').insert(filterObject(entity, allowedKeys));
-
- return id;
+ id = await tx('reports').insert(filterObject(entity, allowedKeys));
});
+
+ const reportProcessor = require('../lib/report-processor');
+ await reportProcessor.start(id);
+ return id;
}
async function updateWithConsistencyCheck(entity) {
@@ -71,8 +69,15 @@ async function updateWithConsistencyCheck(entity) {
entity.params = JSON.stringify(entity.params);
- await tx('reports').where('id', entity.id).update(filterObject(entity, allowedKeys));
+ const filteredUpdates = filterObject(entity, allowedKeys);
+ filteredUpdates.state = ReportState.SCHEDULED;
+
+ await tx('reports').where('id', entity.id).update(filteredUpdates);
});
+
+ // This require is here to avoid cyclic dependency
+ const reportProcessor = require('../lib/report-processor');
+ await reportProcessor.start(entity.id);
}
async function remove(id) {
@@ -92,16 +97,68 @@ async function bulkChangeState(oldState, newState) {
}
+const campaignFieldsMapping = {
+ tracker_count: 'tracker.count',
+ country: 'tracker.country',
+ device_type: 'tracker.device_type',
+ status: 'campaign.status',
+ first_name: 'subscribers.first_name',
+ last_name: 'subscribers.last_name',
+ email: 'subscribers.email'
+};
+
+function customFieldName(id) {
+ return id.replace(/MERGE_/, 'CUSTOM_').toLowerCase();
+}
+
+async function getCampaignResults(campaign, select, extra) {
+ const fieldList = await fields.list(campaign.list);
+
+ const fieldsMapping = fieldList.reduce((map, field) => {
+ /* Dropdowns and checkboxes are aggregated. As such, they have field.column == null and the options are in field.options.
+ TODO - For the time being, we ignore groupped fields. */
+ if (field.column) {
+ map[customFieldName(field.key)] = 'subscribers.' + field.column;
+ }
+ return map;
+ }, Object.assign({}, campaignFieldsMapping));
+
+ let selFields = [];
+ for (let idx = 0; idx < select.length; idx++) {
+ const item = select[idx];
+ if (item in fieldsMapping) {
+ selFields.push(fieldsMapping[item] + ' AS ' + item);
+ } else if (item === '*') {
+ selFields = selFields.concat(Object.keys(fieldsMapping).map(item => fieldsMapping[item] + ' AS ' + item));
+ } else {
+ selFields.push(item);
+ }
+ }
+
+ let query = knex(`subscription__${campaign.list} AS subscribers`)
+ .innerJoin(`campaign__${campaign.id} AS campaign`, 'subscribers.id', 'campaign.subscription')
+ .leftJoin(`campaign_tracker__${campaign.id} AS tracker`, 'subscribers.id', 'tracker.subscriber')
+ .select(selFields);
+
+ if (extra) {
+ query = extra(query);
+ }
+
+ return await query;
+}
+
+
module.exports = {
ReportState,
hash,
- getByIdWithUserFields,
+ getByIdWithTemplate,
listDTAjax,
create,
updateWithConsistencyCheck,
remove,
updateFields,
listByState,
- bulkChangeState
+ bulkChangeState,
+ getCampaignResults
};
\ No newline at end of file
diff --git a/models/subscriptions.js b/models/subscriptions.js
new file mode 100644
index 00000000..3e2ab07f
--- /dev/null
+++ b/models/subscriptions.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const knex = require('../lib/knex');
+const dtHelpers = require('../lib/dt-helpers');
+const interoperableErrors = require('../shared/interoperable-errors');
+
+const Status = {
+ SUBSCRIBED: 1,
+ UNSUBSCRIBED: 2,
+ BOUNCED: 3,
+ COMPLAINED: 4,
+ MAX: 5
+};
+
+async function list(listId) {
+ return await knex(`subscription__${listId}`);
+}
+
+
+module.exports = {
+ Status,
+ list
+};
\ No newline at end of file
diff --git a/routes/reports-REMOVE.js b/routes/reports-REMOVE.js
deleted file mode 100644
index fe6c5df5..00000000
--- a/routes/reports-REMOVE.js
+++ /dev/null
@@ -1,406 +0,0 @@
-'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-REMOVE');
-const reportProcessor = require('../lib/report-processor');
-const campaigns = require('../lib/models/campaigns');
-const lists = require('../lib/models/lists');
-const tools = require('../lib/tools');
-const fileHelpers = require('../lib/file-helpers');
-const util = require('util');
-const htmlescape = require('escape-html');
-const striptags = require('striptags');
-const fs = require('fs');
-const hbs = require('hbs');
-
-router.all('/*', (req, res, next) => {
- if (!req.user) {
- req.flash('danger', _('Need to be logged in to access restricted content'));
- return res.redirect('/account/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) || ''),
- getRowLastRun(row),
- getRowActions(row)
- ])
- });
- });
-});
-
-router.get('/row/ajax/:id', (req, res) => {
- respondRowActions(req.params.id, res);
-});
-
-router.get('/start/ajax/:id', (req, res) => {
- reportProcessor.start(req.params.id, () => {
- respondRowActions(req.params.id, res);
- });
-});
-
-router.get('/stop/ajax/:id', (req, res) => {
- reportProcessor.stop(req.params.id, () => {
- respondRowActions(req.params.id, res);
- });
-});
-
-router.get('/create', passport.csrfProtection, (req, res) => {
- const reqData = req.query;
- 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));
- }
-
- reportProcessor.start(id, () => {
- req.flash('success', util.format(_('Report ā%sā created'), data.name));
- res.redirect('/reports');
- });
- });
- });
-});
-
-router.get('/edit/:id', passport.csrfProtection, (req, res) => {
- const reqData = req.query;
- 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');
- }
-
- report.csrfToken = req.csrfToken();
- report.title = _('Edit Report');
- report.useEditor = true;
-
- reportTemplates.quicklist((err, items) => {
- if (err) {
- req.flash('danger', err.message || err);
- return res.redirect('/');
- }
-
- const reportTemplateId = report.reportTemplate;
-
- items.forEach(item => {
- if (item.id === reportTemplateId) {
- item.selected = true;
- }
- });
-
- report.reportTemplates = items;
-
- addUserFields(reportTemplateId, reqData, report, (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', (req, res) => {
- 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(report.reportTemplate, (err, reportTemplate) => {
- if (err) {
- req.flash('danger', err && err.message || err || _('Could not find report template'));
- return res.redirect('/reports');
- }
-
- if (report.state == reports.ReportState.FINISHED) {
- if (reportTemplate.mimeType == 'text/html') {
-
- fs.readFile(fileHelpers.getReportContentFile(report), (err, reportContent) => {
- if (err) {
- req.flash('danger', err && err.message || err || _('Could not find report with specified ID'));
- return res.redirect('/reports');
- }
-
- const data = {
- report: new hbs.handlebars.SafeString(reportContent),
- title: report.name
- };
-
- res.render('reports/view', data);
- });
-
- } else if (reportTemplate.mimeType == 'text/csv') {
- const headers = {
- 'Content-Disposition': 'attachment;filename=' + fileHelpers.nameToFileName(report.name) + '.csv',
- 'Content-Type': 'text/csv'
- };
-
- res.sendFile(fileHelpers.getReportContentFile(report), {headers: headers});
-
- } else {
- req.flash('danger', _('Unknown type of template'));
- res.redirect('/reports');
- }
-
- } else {
- req.flash('danger', err && err.message || err || _('Could not find report with specified ID'));
- return res.redirect('/reports');
- }
- });
- });
-});
-
-router.get('/output/:id', (req, res) => {
- 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');
- }
-
- fs.readFile(fileHelpers.getReportOutputFile(report), (err, output) => {
- let data = {
- title: 'Output for report ' + report.name
- };
-
- if (err) {
- data.error = 'No output.';
- } else {
- data.output = output;
- }
-
- res.render('reports/output', data);
- });
- });
-});
-
-function getRowLastRun(row) {
- return '' + (row.lastRun ? '' + row.lastRun.toISOString() + '' : '') + '';
-}
-
-function getRowActions(row) {
- /* FIXME: add csrf protection to stop and refresh actions */
-
- let requestRefresh = false;
- let view, startStop;
- let topic = 'data-topic-id="' + row.id + '"';
-
- if (row.state == reports.ReportState.PROCESSING || row.state == reports.ReportState.SCHEDULED) {
- view = '';
- startStop = '';
- requestRefresh = true;
-
- } else if (row.state == reports.ReportState.FINISHED) {
- let icon = 'eye-open';
- if (row.mimeType == 'text/csv') icon = 'download-alt';
-
- view = '';
- startStop = '';
-
- } else if (row.state == reports.ReportState.FAILED) {
- view = '';
- startStop = '';
- }
-
- let actions = view;
- actions += '';
- actions += startStop;
- actions += '';
-
- return '' +
- actions +
- '';
-}
-
-function respondRowActions(id, res) {
- reports.get(id, (err, report) => {
- if (err) {
- return res.json({
- error: err,
- });
- }
-
- const data = {};
- data['#row-last-run-' + id] = getRowLastRun(report);
- data['#row-actions-' + id] = getRowActions(report);
-
- res.json(data);
- });
-}
-
-
-function addUserFields(reportTemplateId, reqData, report, 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 (report && report.paramsObject && spec.id in report.paramsObject) {
- value = report.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 = report ? report : 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;
diff --git a/routes/reports.js b/routes/reports.js
new file mode 100644
index 00000000..587dc0cf
--- /dev/null
+++ b/routes/reports.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const passport = require('../lib/passport');
+const _ = require('../lib/translate')._;
+const reports = require('../models/reports');
+const fileHelpers = require('../lib/file-helpers');
+
+const router = require('../lib/router-async').create();
+
+router.getAsync('/download/:id', passport.loggedIn, async (req, res) => {
+ const report = await reports.getByIdWithTemplate(req.params.id);
+
+ if (report.state == reports.ReportState.FINISHED) {
+ const headers = {
+ 'Content-Disposition': 'attachment;filename=' + fileHelpers.nameToFileName(report.name) + '.csv',
+ 'Content-Type': report.mime_type
+ };
+
+ res.sendFile(fileHelpers.getReportContentFile(report), {headers: headers});
+
+ } else {
+ return res.status(404).send(_('Report not found'));
+ }
+});
+
+module.exports = router;
diff --git a/routes/rest/reports.js b/routes/rest/reports.js
index 5d509a1d..ea1066f3 100644
--- a/routes/rest/reports.js
+++ b/routes/rest/reports.js
@@ -4,12 +4,13 @@ const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._;
const reports = require('../../models/reports');
const reportProcessor = require('../../lib/report-processor');
+const fileHelpers = require('../../lib/file-helpers');
const router = require('../../lib/router-async').create();
router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => {
- const report = await reports.getByIdWithUserFields(req.params.reportId);
+ const report = await reports.getByIdWithTemplate(req.params.reportId);
report.hash = reports.hash(report);
return res.json(report);
});
@@ -38,14 +39,23 @@ router.postAsync('/reports-table', passport.loggedIn, async (req, res) => {
router.postAsync('/report-start/:id', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reportProcessor.start(req.params.id);
- // TODO
+ res.json();
});
router.postAsync('/report-stop/:id', async (req, res) => {
await reportProcessor.stop(req.params.id);
- // TODO
+ res.json();
+});
+
+router.getAsync('/report-content/:id', async (req, res) => {
+ const report = await reports.getByIdWithTemplate(req.params.id);
+ res.sendFile(fileHelpers.getReportContentFile(report));
+});
+
+router.getAsync('/report-output/:id', async (req, res) => {
+ const report = await reports.getByIdWithTemplate(req.params.id);
+ res.sendFile(fileHelpers.getReportOutputFile(report));
});
-
module.exports = router;
\ No newline at end of file
diff --git a/services/executor.js b/services/executor.js
index 42b1294f..0632f887 100644
--- a/services/executor.js
+++ b/services/executor.js
@@ -129,3 +129,4 @@ process.on('message', msg => {
process.send({
type: 'executor-started'
});
+
diff --git a/shared/reports.js b/shared/reports.js
new file mode 100644
index 00000000..64b6cd05
--- /dev/null
+++ b/shared/reports.js
@@ -0,0 +1,13 @@
+'use strict';
+
+const ReportState = {
+ SCHEDULED: 0,
+ PROCESSING: 1,
+ FINISHED: 2,
+ FAILED: 3,
+ MAX: 4
+};
+
+module.exports = {
+ ReportState
+};
\ No newline at end of file
diff --git a/workers/reports/report-processor.js b/workers/reports/report-processor.js
index ff5d3a59..b93b4584 100644
--- a/workers/reports/report-processor.js
+++ b/workers/reports/report-processor.js
@@ -1,10 +1,10 @@
'use strict';
-const reports = require('../../lib/models/reports-REMOVE');
-const reportTemplates = require('../../lib/models/report-templates');
-const lists = require('../../lib/models/lists');
-const subscriptions = require('../../lib/models/subscriptions');
-const campaigns = require('../../lib/models/campaigns');
+const reports = require('../../models/reports');
+const reportTemplates = require('../../models/report-templates');
+const lists = require('../../models/lists');
+const subscriptions = require('../../models/subscriptions');
+const campaigns = require('../../models/campaigns');
const handlebars = require('handlebars');
const handlebarsHelpers = require('../../lib/handlebars-helpers');
const _ = require('../../lib/translate')._;
@@ -12,136 +12,82 @@ const hbs = require('hbs');
const vm = require('vm');
const log = require('npmlog');
const fs = require('fs');
+const knex = require('../../lib/knex');
+
handlebarsHelpers.registerHelpers(handlebars);
-let reportId = Number(process.argv[2]);
-let reportDir;
-
-function resolveEntities(getter, ids, callback) {
- const idsRemaining = ids.slice();
- const resolved = [];
-
- function doWork() {
- if (idsRemaining.length == 0) {
- return callback(null, resolved);
- }
-
- getter(idsRemaining.shift(), (err, entity) => {
- if (err) {
- return callback(err);
- }
-
- resolved.push(entity);
- return doWork();
- });
- }
-
- setImmediate(doWork);
-}
-
-const userFieldTypeToGetter = {
- 'campaign': (id, callback) => campaigns.get(id, false, callback),
- 'list': lists.get
+const userFieldGetters = {
+ 'campaign': campaigns.getById,
+ 'list': lists.getById
};
-function resolveUserFields(userFields, params, callback) {
- const userFieldsRemaining = userFields.slice();
- const resolved = {};
- function doWork() {
- if (userFieldsRemaining.length == 0) {
- return callback(null, resolved);
- }
+async function main() {
+ try {
+ const reportId = Number(process.argv[2]);
- const spec = userFieldsRemaining.shift();
- const getter = userFieldTypeToGetter[spec.type];
+ const report = await reports.getByIdWithTemplate(reportId);
- if (getter) {
- return resolveEntities(getter, params[spec.id], (err, entities) => {
- if (spec.minOccurences == 1 && spec.maxOccurences == 1) {
- resolved[spec.id] = entities[0];
- } else {
- resolved[spec.id] = entities;
- }
+ const inputs = {};
- doWork();
- });
- } else {
- return callback(new Error(_('Unknown user field type "' + spec.type + '".')));
- }
- }
-
- setImmediate(doWork);
-}
-
-function doneSuccess() {
- process.exit(0);
-}
-
-function doneFail() {
- process.exit(1)
-}
-
-
-
-reports.get(reportId, (err, report) => {
- if (err || !report) {
- log.error('reports', err && err.message || err || _('Could not find report with specified ID'));
- doneFail();
- }
-
- reportTemplates.get(report.reportTemplate, (err, reportTemplate) => {
- if (err) {
- log.error('reports', err && err.message || err || _('Could not find report template'));
- doneFail();
- }
-
- resolveUserFields(reportTemplate.userFieldsObject, report.paramsObject, (err, inputs) => {
- if (err) {
- log.error('reports', err.message || err);
- doneFail();
+ for (const spec of report.user_fields) {
+ const getter = userFieldGetters[spec.type];
+ if (!getter) {
+ throw new Error(_('Unknown user field type "' + spec.type + '".'));
}
- const campaignsProxy = {
- results: reports.getCampaignResults,
- list: campaigns.list,
- get: campaigns.get
- };
-
- const subscriptionsProxy = {
- list: subscriptions.list
- };
-
- const sandbox = {
- console,
- campaigns: campaignsProxy,
- subscriptions: subscriptionsProxy,
- inputs,
-
- callback: (err, outputs) => {
- if (err) {
- log.error('reports', err.message || err);
- doneFail();
- }
-
- const hbsTmpl = handlebars.compile(reportTemplate.hbs);
- const reportText = hbsTmpl(outputs);
-
- process.stdout.write(reportText);
- doneSuccess();
- }
- };
-
- const script = new vm.Script(reportTemplate.js);
-
- try {
- script.runInNewContext(sandbox, {displayErrors: true, timeout: 120000});
- } catch (err) {
- console.error(err);
- doneFail();
+ const entities = [];
+ for (const id of report.params[spec.id]) {
+ entities.push(await getter(id));
}
- });
- });
-});
+
+ if (spec.minOccurences == 1 && spec.maxOccurences == 1) {
+ inputs[spec.id] = entities[0];
+ } else {
+ inputs[spec.id] = entities;
+ }
+ }
+
+ const campaignsProxy = {
+ getResults: reports.getCampaignResults,
+ getById: campaigns.getById
+ };
+
+ const subscriptionsProxy = {
+ list: subscriptions.list
+ };
+
+ const sandbox = {
+ console,
+ campaigns: campaignsProxy,
+ subscriptions: subscriptionsProxy,
+ knex,
+ process,
+ inputs,
+
+ render: data => {
+ const hbsTmpl = handlebars.compile(report.hbs);
+ const reportText = hbsTmpl(data);
+
+ process.stdout.write(reportText);
+ }
+ };
+
+ const js =
+ '(async function() {' +
+ report.js +
+ '})().then(() => process.exit(0)).catch(err => { console.error(err); process.exit(1); })';
+
+ const script = new vm.Script(js);
+
+ script.runInNewContext(sandbox, {displayErrors: true, timeout: 120000});
+
+ } catch (err) {
+ console.error(err);
+ process.exit(1);
+ }
+}
+
+main();