Reports halfway through
Datatable now correctly handles the situation when user is not logged in and access protected resources
This commit is contained in:
parent
aba42d94ac
commit
3f7b428546
28 changed files with 421 additions and 471 deletions
11
app.js
11
app.js
|
@ -39,17 +39,17 @@ const blacklist = require('./routes/blacklist');
|
|||
const editorapi = require('./routes/editorapi');
|
||||
const grapejs = require('./routes/grapejs');
|
||||
const mosaico = require('./routes/mosaico');
|
||||
const reports = require('./routes/reports');
|
||||
|
||||
const namespaces = require('./routes/rest/namespaces');
|
||||
const users = require('./routes/rest/users');
|
||||
const account = require('./routes/rest/account');
|
||||
const reportTemplates = require('./routes/rest/report-templates');
|
||||
const reports = require('./routes/rest/reports');
|
||||
|
||||
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
||||
const usersLegacyIntegration = require('./routes/users-legacy-integration');
|
||||
const accountLegacyIntegration = require('./routes/account-legacy-integration');
|
||||
const reportTemplatesLegacyIntegration = require('./routes/report-templates-legacy-integration');
|
||||
const reportsLegacyIntegration = require('./routes/reports-legacy-integration');
|
||||
|
||||
const interoperableErrors = require('./shared/interoperable-errors');
|
||||
|
||||
|
@ -246,7 +246,7 @@ app.use('/namespaces', namespacesLegacyIntegration);
|
|||
app.use('/account', accountLegacyIntegration);
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/report-templates', reportTemplatesLegacyIntegration);
|
||||
app.use('/reports', reportsLegacyIntegration);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------- */
|
||||
|
@ -263,10 +263,7 @@ app.use('/rest', account);
|
|||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/rest', reportTemplates);
|
||||
}
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/reports', reports);
|
||||
app.use('/rest', reports);
|
||||
}
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
|
|
|
@ -101,7 +101,7 @@ export default class Account extends Component {
|
|||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.InvalidToken) {
|
||||
if (error instanceof interoperableErrors.InvalidTokenError) {
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your password cannot be reset.')}</strong>{' '}
|
||||
|
|
|
@ -9,6 +9,7 @@ import interoperableErrors from '../../../shared/interoperable-errors';
|
|||
import { withPageHelpers } from './page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import { TreeTable, TreeSelectMode } from './tree';
|
||||
import { Table, TableSelectMode } from './table';
|
||||
|
||||
import brace from 'brace';
|
||||
import AceEditor from 'react-ace';
|
||||
|
@ -62,6 +63,17 @@ class Form extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if (error instanceof interoperableErrors.NotFoundError) {
|
||||
owner.disableForm();
|
||||
owner.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('It seems that someone else has deleted the entity in the meantime.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -368,7 +380,46 @@ class TreeTableSelect extends Component {
|
|||
const htmlId = 'form_' + id;
|
||||
|
||||
return wrapInput(id, htmlId, owner, props.label, props.help,
|
||||
<TreeTable data={this.props.data} dataUrl={this.props.dataUrl} selectMode={TreeSelectMode.SINGLE} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||
<TreeTable data={props.data} dataUrl={props.dataUrl} selectMode={TreeSelectMode.SINGLE} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TableSelect extends Component {
|
||||
static propTypes = {
|
||||
dataUrl: PropTypes.string,
|
||||
data: PropTypes.array,
|
||||
columns: PropTypes.array,
|
||||
selectionKeyIndex: PropTypes.number,
|
||||
selectMode: PropTypes.number,
|
||||
withHeader: PropTypes.bool,
|
||||
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
selectMode: TableSelectMode.SINGLE
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
formStateOwner: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
async onSelectionChangedAsync(sel) {
|
||||
const owner = this.context.formStateOwner;
|
||||
owner.updateFormValue(this.props.id, sel);
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
|
||||
return wrapInput(id, htmlId, owner, props.label, props.help,
|
||||
<Table data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} withHeader={props.withHeader} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -727,6 +778,8 @@ export {
|
|||
ButtonRow,
|
||||
Button,
|
||||
TreeTableSelect,
|
||||
TableSelect,
|
||||
TableSelectMode,
|
||||
ACEEditor,
|
||||
FormSendMethod
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.mt-button-row > button {
|
||||
.mt-button-row > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.mt-button-row > button:last-child {
|
||||
.mt-button-row > *:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ class Table extends Component {
|
|||
columns: PropTypes.array,
|
||||
selectMode: PropTypes.number,
|
||||
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||
selectionKeyIndex: PropTypes.number,
|
||||
onSelectionChangedAsync: PropTypes.func,
|
||||
actionLinks: PropTypes.array,
|
||||
withHeader: PropTypes.bool
|
||||
|
@ -61,6 +62,10 @@ class Table extends Component {
|
|||
return this.props.selection !== nextProps.selection || this.props.data != nextProps.data || this.props.dataUrl != nextProps.dataUrl;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
selectionKeyIndex: 0
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
const columns = this.props.columns.slice();
|
||||
|
@ -108,10 +113,7 @@ class Table extends Component {
|
|||
dtOptions.data = this.props.data;
|
||||
} else {
|
||||
dtOptions.serverSide = true;
|
||||
dtOptions.ajax = {
|
||||
url: this.props.dataUrl,
|
||||
type: 'POST'
|
||||
};
|
||||
dtOptions.ajax = ::this.fetchData;
|
||||
}
|
||||
|
||||
this.table = jQuery(this.domTable).DataTable(dtOptions);
|
||||
|
@ -122,6 +124,13 @@ class Table extends Component {
|
|||
this.updateSelection();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async fetchData(data, callback) {
|
||||
// This custom ajax fetch function allows us to properly handle the case when the user is not authenticated.
|
||||
const response = await axios.post(this.props.dataUrl, data);
|
||||
callback(response.data);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.data) {
|
||||
this.table.clear();
|
||||
|
@ -132,23 +141,35 @@ class Table extends Component {
|
|||
}
|
||||
|
||||
updateSelection() {
|
||||
/*
|
||||
const tree = this.tree;
|
||||
if (this.selectMode === TableSelectMode.MULTI) {
|
||||
const selectSet = new Set(this.props.selection);
|
||||
|
||||
tree.enableUpdate(false);
|
||||
tree.visit(node => node.setSelected(selectSet.has(node.key)));
|
||||
tree.enableUpdate(true);
|
||||
|
||||
} else if (this.selectMode === TableSelectMode.SINGLE) {
|
||||
this.tree.activateKey(this.props.selection);
|
||||
let selArray = [];
|
||||
if (this.selectMode === TableSelectMode.SINGLE) {
|
||||
selArray = [this.props.selection];
|
||||
} else if (this.selectMode === TableSelectMode.MULTI) {
|
||||
selArray = this.props.selection;
|
||||
}
|
||||
*/
|
||||
|
||||
const selSet = new Set(selArray);
|
||||
|
||||
const selectionKeyIndex = this.props.selectionKeyIndex;
|
||||
|
||||
this.table.rows({ selected: true }).every(function() {
|
||||
const key = this.data()[selectionKeyIndex];
|
||||
if (!selSet.has(key)) {
|
||||
this.deselect();
|
||||
}
|
||||
|
||||
selSet.delete(key);
|
||||
});
|
||||
|
||||
this.table.rows((idx, data, node) => selSet.has(data[selectionKeyIndex])).select();
|
||||
}
|
||||
|
||||
async onSelect(event, data) {
|
||||
const sel = this.table.rows( { selected: true } ).data();
|
||||
let sel = this.table.rows( { selected: true } ).data().toArray().map(item => item[this.props.selectionKeyIndex]);
|
||||
|
||||
if (this.selectMode === TableSelectMode.SINGLE) {
|
||||
sel = sel.length ? sel[0] : null;
|
||||
}
|
||||
|
||||
if (this.props.onSelectionChangedAsync) {
|
||||
await this.props.onSelectionChangedAsync(sel);
|
||||
|
|
|
@ -144,11 +144,20 @@ export default class CUD extends Component {
|
|||
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.LoopDetectedError) {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('There has been a loop detected in the assignment of the parent namespace. This is most likely because someone else has changed the parent of some namespace in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')}
|
||||
{t('There has been a loop detected in the assignment of the parent namespace. This is most likely because someone else has changed the parent of some namespace in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof interoperableErrors.DependencyNotFoundError) {
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('It seems that the parent namespace has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
import React, { Component } from 'react';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../lib/form';
|
||||
import { withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, TableSelectMode, ButtonRow, Button } from '../lib/form';
|
||||
import axios from '../lib/axios';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import { ModalDialog } from '../lib/bootstrap-components';
|
||||
import moment from 'moment';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -31,191 +32,18 @@ export default class CUD extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/report-templates/${this.state.entityId}`);
|
||||
await this.getFormValuesFromURL(`/rest/reports/${this.state.entityId}`);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
|
||||
} else {
|
||||
const wizard = this.props.match.params.wizard;
|
||||
|
||||
if (wizard === 'subscribers-all') {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
description: 'Generates a campaign report listing all subscribers along with their statistics.',
|
||||
mime_type: 'text/html',
|
||||
user_fields:
|
||||
'[\n' +
|
||||
' {\n' +
|
||||
' "id": "campaign",\n' +
|
||||
' "name": "Campaign",\n' +
|
||||
' "type": "campaign",\n' +
|
||||
' "minOccurences": 1,\n' +
|
||||
' "maxOccurences": 1\n' +
|
||||
' }\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' +
|
||||
'});',
|
||||
hbs:
|
||||
'<h2>{{title}}</h2>\n' +
|
||||
'\n' +
|
||||
'<div class="table-responsive">\n' +
|
||||
' <table class="table table-bordered table-hover data-table display nowrap" width="100%" data-row-sort="1,1" data-paging="false">\n' +
|
||||
' <thead>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Email{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Tracker Count{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' </thead>\n' +
|
||||
' {{#if results}}\n' +
|
||||
' <tbody>\n' +
|
||||
' {{#each results}}\n' +
|
||||
' <tr>\n' +
|
||||
' <th scope="row">\n' +
|
||||
' {{email}}\n' +
|
||||
' </th>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{tracker_count}}\n' +
|
||||
' </td>\n' +
|
||||
' </tr>\n' +
|
||||
' {{/each}}\n' +
|
||||
' </tbody>\n' +
|
||||
' {{/if}}\n' +
|
||||
' </table>\n' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
} else if (wizard === 'subscribers-grouped') {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
description: 'Generates a campaign report with results are aggregated by some "Country" custom field.',
|
||||
mime_type: 'text/html',
|
||||
user_fields:
|
||||
'[\n' +
|
||||
' {\n' +
|
||||
' "id": "campaign",\n' +
|
||||
' "name": "Campaign",\n' +
|
||||
' "type": "campaign",\n' +
|
||||
' "minOccurences": 1,\n' +
|
||||
' "maxOccurences": 1\n' +
|
||||
' }\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' +
|
||||
'\n' +
|
||||
' for (let 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' +
|
||||
'});',
|
||||
hbs:
|
||||
'<h2>{{title}}</h2>\n' +
|
||||
'\n' +
|
||||
'<div class="table-responsive">\n' +
|
||||
' <table class="table table-bordered table-hover data-table display nowrap" width="100%" data-row-sort="1,1,1,1" data-paging="false">\n' +
|
||||
' <thead>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Country{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Opened{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}All{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' {{#translate}}Percentage{{/translate}}\n' +
|
||||
' </th>\n' +
|
||||
' </thead>\n' +
|
||||
' {{#if results}}\n' +
|
||||
' <tbody>\n' +
|
||||
' {{#each results}}\n' +
|
||||
' <tr>\n' +
|
||||
' <th scope="row">\n' +
|
||||
' {{custom_country}}\n' +
|
||||
' </th>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{count_opened}}\n' +
|
||||
' </td>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{count_all}}\n' +
|
||||
' </td>\n' +
|
||||
' <td style="width: 20%;">\n' +
|
||||
' {{percentage}}%\n' +
|
||||
' </td>\n' +
|
||||
' </tr>\n' +
|
||||
' {{/each}}\n' +
|
||||
' </tbody>\n' +
|
||||
' {{/if}}\n' +
|
||||
' </table>\n' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
} else if (wizard === 'export-list-csv') {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
description: 'Exports a list as a CSV file.',
|
||||
mime_type: 'text/csv',
|
||||
user_fields:
|
||||
'[\n' +
|
||||
' {\n' +
|
||||
' "id": "list",\n' +
|
||||
' "name": "List",\n' +
|
||||
' "type": "list",\n' +
|
||||
' "minOccurences": 1,\n' +
|
||||
' "maxOccurences": 1\n' +
|
||||
' }\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' +
|
||||
'});',
|
||||
hbs:
|
||||
'{{#each results}}\n' +
|
||||
'{{firstName}},{{lastName}},{{email}}\n' +
|
||||
'{{/each}}'
|
||||
});
|
||||
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
description: '',
|
||||
mime_type: 'text/html',
|
||||
user_fields: '',
|
||||
js: '',
|
||||
hbs: ''
|
||||
});
|
||||
}
|
||||
this.populateFormValues({
|
||||
report_template: null,
|
||||
name: '',
|
||||
description: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,41 +57,24 @@ export default class CUD extends Component {
|
|||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
if (!state.getIn(['mime_type', 'value'])) {
|
||||
state.setIn(['mime_type', 'error'], t('MIME Type must be selected'));
|
||||
if (!state.getIn(['report_template', 'value'])) {
|
||||
state.setIn(['report_template', 'error'], t('Report template must be selected'));
|
||||
} else {
|
||||
state.setIn(['mime_type', 'error'], null);
|
||||
}
|
||||
|
||||
try {
|
||||
const userFields = JSON.parse(state.getIn(['user_fields', 'value']));
|
||||
state.setIn(['user_fields', 'error'], null);
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
state.setIn(['user_fields', 'error'], t('Syntax error in the user fields specification'));
|
||||
}
|
||||
state.setIn(['report_template', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
async submitAndStay() {
|
||||
await Form.handleChangedError(this, async () => await this.doSubmit(true));
|
||||
}
|
||||
|
||||
async submitAndLeave() {
|
||||
await Form.handleChangedError(this, async () => await this.doSubmit(false));
|
||||
}
|
||||
|
||||
async doSubmit(stay) {
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
let sendMethod, url;
|
||||
if (edit) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/report-templates/${this.state.entityId}`
|
||||
url = `/rest/reports/${this.state.entityId}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/report-templates'
|
||||
url = '/rest/reports'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
|
@ -274,12 +85,7 @@ export default class CUD extends Component {
|
|||
});
|
||||
|
||||
if (submitSuccessful) {
|
||||
if (stay) {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('success', t('Report template saved'));
|
||||
} else {
|
||||
this.navigateToWithFlashMessage('/report-templates', 'success', t('Report template saved'));
|
||||
}
|
||||
this.navigateToWithFlashMessage('/reports', 'success', t('Report saved'));
|
||||
} else {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
|
@ -287,11 +93,11 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
async showDeleteModal() {
|
||||
this.navigateTo(`/report-templates/edit/${this.state.entityId}/delete`);
|
||||
this.navigateTo(`/reports/edit/${this.state.entityId}/delete`);
|
||||
}
|
||||
|
||||
async hideDeleteModal() {
|
||||
this.navigateTo(`/report-templates/edit/${this.state.entityId}`);
|
||||
this.navigateTo(`/reports/edit/${this.state.entityId}`);
|
||||
}
|
||||
|
||||
async performDelete() {
|
||||
|
@ -300,17 +106,23 @@ export default class CUD extends Component {
|
|||
await this.hideDeleteModal();
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Deleting report template...'));
|
||||
this.setFormStatusMessage('info', t('Deleting report...'));
|
||||
|
||||
await axios.delete(`/rest/report-templates/${this.state.entityId}`);
|
||||
await axios.delete(`/rest/reports/${this.state.entityId}`);
|
||||
|
||||
this.navigateToWithFlashMessage('/report-templates', 'success', t('Report template deleted'));
|
||||
this.navigateToWithFlashMessage('/reports', 'success', t('Report deleted'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
const userId = this.getFormValue('id');
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -319,31 +131,22 @@ export default class CUD extends Component {
|
|||
{ label: t('No'), className: 'btn-primary', onClickAsync: ::this.hideDeleteModal },
|
||||
{ label: t('Yes'), className: 'btn-danger', onClickAsync: ::this.performDelete }
|
||||
]}>
|
||||
{t('Are you sure you want to delete report template "{{name}}"?', {name: this.getFormValue('name')})}
|
||||
{t('Are you sure you want to delete "{{name}}"?', {name: this.getFormValue('name')})}
|
||||
</ModalDialog>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit Report Template') : t('Create Report Template')}</Title>
|
||||
<Title>{edit ? t('Edit Report') : t('Create Report')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<Dropdown id="mime_type" label={t('Type')} options={[{key: 'text/html', label: t('HTML')}, {key: 'text/csv', label: t('CSV')}]}/>
|
||||
<ACEEditor id="user_fields" height="250px" mode="json" label={t('User selectable fields')} help={t('JSON specification of user selectable fields.')}/>
|
||||
<ACEEditor id="js" height="700px" mode="javascript" label={t('Data processing code')} help={<Trans>Write the body of the JavaScript function with signature <code>function(inputs, callback)</code> that returns an object to be rendered by the Handlebars template below.</Trans>}/>
|
||||
<ACEEditor id="hbs" height="700px" mode="handlebars" label={t('Rendering template')} help={<Trans>Use HTML with Handlebars syntax. See documentation <a href="http://handlebarsjs.com/">here</a>.</Trans>}/>
|
||||
|
||||
{edit ?
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
|
||||
<Button className="btn-danger" icon="remove" label={t('Delete Template')} onClickAsync={::this.showDeleteModal}/>
|
||||
</ButtonRow>
|
||||
:
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
</ButtonRow>
|
||||
}
|
||||
<TableSelect id="report_template" label={t('Report Template')} withHeader dataUrl="/rest/report-templates-table" columns={columns} />
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <Button className="btn-danger" icon="remove" label={t('Delete Report')} onClickAsync={::this.showDeleteModal}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { DropdownMenu } from '../lib/bootstrap-components';
|
||||
import { Title, Toolbar, DropdownLink } from '../lib/page';
|
||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
||||
import { Table } from '../lib/table';
|
||||
import moment from 'moment';
|
||||
|
||||
|
@ -14,30 +13,27 @@ export default class List extends Component {
|
|||
|
||||
const actionLinks = [{
|
||||
label: 'Edit',
|
||||
link: data => '/report-templates/edit/' + data[0]
|
||||
link: data => '/reports/edit/' + data[0]
|
||||
}];
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() }
|
||||
{ data: 2, title: t('Template') },
|
||||
{ data: 3, title: t('Description') },
|
||||
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<DropdownMenu className="btn-primary" label={t('Create Report Template')}>
|
||||
<DropdownLink to="/report-templates/create">{t('Blank')}</DropdownLink>
|
||||
<DropdownLink to="/report-templates/create/subscribers-all">{t('All Subscribers')}</DropdownLink>
|
||||
<DropdownLink to="/report-templates/create/subscribers-grouped">{t('Grouped Subscribers')}</DropdownLink>
|
||||
<DropdownLink to="/report-templates/create/export-list-csv">{t('Export List as CSV')}</DropdownLink>
|
||||
</DropdownMenu>
|
||||
<NavButton linkTo="/reports/create" className="btn-primary" icon="plus" label={t('Create Report')}/>
|
||||
<NavButton linkTo="/reports/templates" className="btn-primary" label={t('Report Templates')}/>
|
||||
</Toolbar>
|
||||
|
||||
<Title>{t('Users')}</Title>
|
||||
<Title>{t('Reports')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/report-templates-table" columns={columns} actionLinks={actionLinks} />
|
||||
<Table withHeader dataUrl="/rest/reports-table" columns={columns} actionLinks={actionLinks} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import { I18nextProvider } from 'react-i18next';
|
|||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page'
|
||||
import CUD from './CUD'
|
||||
import List from './List'
|
||||
import ReportsCUD from './CUD'
|
||||
import ReportsList from './List'
|
||||
import ReportTemplatesCUD from './templates/CUD'
|
||||
import ReportTemplatesList from './templates/List'
|
||||
|
||||
const getStructure = t => {
|
||||
const subPaths = {};
|
||||
|
@ -17,23 +19,39 @@ const getStructure = t => {
|
|||
title: t('Home'),
|
||||
externalLink: '/',
|
||||
children: {
|
||||
'report-templates': {
|
||||
title: t('Report Templates'),
|
||||
link: '/report-templates',
|
||||
component: List,
|
||||
'reports': {
|
||||
title: t('Reports'),
|
||||
link: '/reports',
|
||||
component: ReportsList,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit Report Template'),
|
||||
title: t('Edit Report'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<CUD edit {...props} />)
|
||||
render: props => (<ReportsCUD edit {...props} />)
|
||||
},
|
||||
create: {
|
||||
title: t('Create Report Template'),
|
||||
params: [':wizard?'],
|
||||
render: props => (<CUD {...props} />)
|
||||
title: t('Create Report'),
|
||||
render: props => (<ReportsCUD {...props} />)
|
||||
},
|
||||
'templates': {
|
||||
title: t('Templates'),
|
||||
link: '/reports/templates',
|
||||
component: ReportTemplatesList,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit Report Template'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<ReportTemplatesCUD edit {...props} />)
|
||||
},
|
||||
create: {
|
||||
title: t('Create Report Template'),
|
||||
params: [':wizard?'],
|
||||
render: props => (<ReportTemplatesCUD {...props} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +59,7 @@ const getStructure = t => {
|
|||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={ i18n }><Section root='/report-templates' structure={getStructure}/></I18nextProvider>,
|
||||
<I18nextProvider i18n={ i18n }><Section root='/reports' structure={getStructure}/></I18nextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../lib/form';
|
||||
import axios from '../lib/axios';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import { ModalDialog } from '../lib/bootstrap-components';
|
||||
import { withPageHelpers, Title } from '../../lib/page'
|
||||
import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../../lib/form';
|
||||
import axios from '../../lib/axios';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import { ModalDialog } from '../../lib/bootstrap-components';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { DropdownMenu } from '../lib/bootstrap-components';
|
||||
import { Title, Toolbar, DropdownLink } from '../lib/page';
|
||||
import { Table } from '../lib/table';
|
||||
import { DropdownMenu } from '../../lib/bootstrap-components';
|
||||
import { Title, Toolbar, DropdownLink } from '../../lib/page';
|
||||
import { Table } from '../../lib/table';
|
||||
import moment from 'moment';
|
||||
|
||||
@translate()
|
||||
|
|
|
@ -6,7 +6,7 @@ module.exports = {
|
|||
namespaces: ['babel-polyfill', './src/namespaces/root.js'],
|
||||
users: ['babel-polyfill', './src/users/root.js'],
|
||||
account: ['babel-polyfill', './src/account/root.js'],
|
||||
reportTemplates: ['babel-polyfill', './src/report-templates/root.js']
|
||||
reports: ['babel-polyfill', './src/reports/root.js']
|
||||
},
|
||||
output: {
|
||||
library: 'MailtrainReactBody',
|
||||
|
|
7
index.js
7
index.js
|
@ -126,10 +126,9 @@ server.on('listening', () => {
|
|||
triggers(() => {
|
||||
spawnSenders(() => {
|
||||
feedcheck(() => {
|
||||
postfixBounceServer(() => {
|
||||
reportProcessor.init(() => {
|
||||
log.info('Service', 'All services started');
|
||||
});
|
||||
postfixBounceServer(async () => {
|
||||
await reportProcessor.init();
|
||||
log.info('Service', 'All services started');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ function _getConfig(context) {
|
|||
}
|
||||
}
|
||||
|
||||
function registerRootRoute(router, title, entryPoint) {
|
||||
function registerRootRoute(router, entryPoint, title) {
|
||||
router.get('/*', passport.csrfProtection, (req, res) => {
|
||||
res.render('react-root', {
|
||||
title,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const log = require('npmlog');
|
||||
const reports = require('./models/reports');
|
||||
const reports = require('../models/reports');
|
||||
const executor = require('./executor');
|
||||
|
||||
let runningWorkersCount = 0;
|
||||
|
@ -11,12 +11,12 @@ let workers = {};
|
|||
|
||||
function startWorker(report) {
|
||||
|
||||
function onStarted(tid) {
|
||||
async function onStarted(tid) {
|
||||
log.info('ReportProcessor', 'Worker process for "%s" started with tid %s. Current worker count is %s.', report.name, tid, runningWorkersCount);
|
||||
workers[report.id] = tid;
|
||||
}
|
||||
|
||||
function onFinished(code, signal) {
|
||||
async function onFinished(code, signal) {
|
||||
runningWorkersCount--;
|
||||
log.info('ReportProcessor', 'Worker process for "%s" (tid %s) exited with code %s signal %s. Current worker count is %s.', report.name, workers[report.id], code, signal, runningWorkersCount);
|
||||
delete workers[report.id];
|
||||
|
@ -24,21 +24,20 @@ function startWorker(report) {
|
|||
const fields = {};
|
||||
if (code === 0) {
|
||||
fields.state = reports.ReportState.FINISHED;
|
||||
fields.lastRun = new Date();
|
||||
fields.last_run = new Date();
|
||||
} else {
|
||||
fields.state = reports.ReportState.FAILED;
|
||||
}
|
||||
|
||||
reports.updateFields(report.id, fields, err => {
|
||||
if (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
|
||||
setImmediate(startWorkers);
|
||||
});
|
||||
try {
|
||||
await reports.update(report.id, fields);
|
||||
setImmediate(tryStartWorkers);
|
||||
} catch (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
}
|
||||
|
||||
function onFailed(msg) {
|
||||
async function onFailed(msg) {
|
||||
runningWorkersCount--;
|
||||
log.error('ReportProcessor', 'Executing worker process for "%s" (tid %s) failed with message "%s". Current worker count is %s.', report.name, workers[report.id], msg, runningWorkersCount);
|
||||
delete workers[report.id];
|
||||
|
@ -47,13 +46,12 @@ function startWorker(report) {
|
|||
state: reports.ReportState.FAILED
|
||||
};
|
||||
|
||||
reports.updateFields(report.id, fields, err => {
|
||||
if (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
|
||||
setImmediate(startWorkers);
|
||||
});
|
||||
try {
|
||||
await reports.update(report.id, fields);
|
||||
setImmediate(tryStartWorkers);
|
||||
} catch (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
}
|
||||
|
||||
const reportData = {
|
||||
|
@ -65,83 +63,68 @@ function startWorker(report) {
|
|||
executor.start('report-processor-worker', reportData, onStarted, onFinished, onFailed);
|
||||
}
|
||||
|
||||
function startWorkers() {
|
||||
reports.listWithState(reports.ReportState.SCHEDULED, 0, maxWorkersCount - runningWorkersCount, (err, reportList) => {
|
||||
if (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
return;
|
||||
}
|
||||
let isStartingWorkers = false;
|
||||
|
||||
for (let report of reportList) {
|
||||
reports.updateFields(report.id, { state: reports.ReportState.PROCESSING }, err => {
|
||||
if (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
return;
|
||||
}
|
||||
async function tryStartWorkers() {
|
||||
|
||||
if (isStartingWorkers) {
|
||||
// Generally it is possible that this function is invoked simultaneously multiple times. This is to prevent it.
|
||||
return;
|
||||
}
|
||||
isStartingWorkers = true;
|
||||
|
||||
try {
|
||||
while (runningWorkersCount < maxWorkersCount) {
|
||||
log.info('ReportProcessor', 'Trying to start worker because runningWorkersCount=%s maxWorkersCount=%s', runningWorkersCount, maxWorkersCount);
|
||||
|
||||
const reportList = await reports.listByState(reports.ReportState.SCHEDULED, 1);
|
||||
|
||||
if (reportList.length > 0) {
|
||||
log.info('ReportProcessor', 'Starting worker');
|
||||
|
||||
const report = reportList[0];
|
||||
await report.updateFields(report.id, {state: reports.ReportState.PROCESSING});
|
||||
startWorker(report);
|
||||
});
|
||||
|
||||
} else {
|
||||
log.info('ReportProcessor', 'No more report to start a worker for');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
|
||||
isStartingWorkers = false;
|
||||
}
|
||||
|
||||
module.exports.start = (reportId, callback) => {
|
||||
module.exports.start = async reportId => {
|
||||
if (!workers[reportId]) {
|
||||
log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
|
||||
reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, lastRun: null}, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (runningWorkersCount < maxWorkersCount) {
|
||||
log.info('ReportProcessor', 'Starting worker because runningWorkersCount=%s maxWorkersCount=%s', runningWorkersCount, maxWorkersCount);
|
||||
|
||||
startWorkers();
|
||||
} else {
|
||||
log.info('ReportProcessor', 'Not starting worker because runningWorkersCount=%s maxWorkersCount=%s', runningWorkersCount, maxWorkersCount);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, lastRun: null});
|
||||
tryStartWorkers();
|
||||
} else {
|
||||
log.info('ReportProcessor', 'Worker for report id: %s is already running.', reportId);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.stop = (reportId, callback) => {
|
||||
module.exports.stop = async reportId => {
|
||||
const tid = workers[reportId];
|
||||
if (tid) {
|
||||
log.info('ReportProcessor', 'Killing worker for report id: %s', reportId);
|
||||
executor.stop(tid);
|
||||
reports.updateFields(reportId, { state: reports.ReportState.FAILED}, callback);
|
||||
|
||||
await reports.updateFields(reportId, { state: reports.ReportState.FAILED });
|
||||
} else {
|
||||
log.info('ReportProcessor', 'No running worker found for report id: %s', reportId);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.init = callback => {
|
||||
reports.listWithState(reports.ReportState.PROCESSING, 0, 0, (err, reportList) => {
|
||||
if (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
|
||||
function scheduleReport() {
|
||||
if (reportList.length > 0) {
|
||||
const report = reportList.shift();
|
||||
|
||||
reports.updateFields(report.id, { state: reports.ReportState.SCHEDULED}, err => {
|
||||
if (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
|
||||
scheduleReport();
|
||||
});
|
||||
}
|
||||
|
||||
startWorkers();
|
||||
return callback();
|
||||
}
|
||||
|
||||
scheduleReport();
|
||||
});
|
||||
module.exports.init = async () => {
|
||||
try {
|
||||
await reports.bulkChangeState(reports.ReportState.PROCESSING, reports.ReportState.SCHEDULED);
|
||||
} catch (err) {
|
||||
log.error('ReportProcessor', err);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,60 +11,74 @@ async function list() {
|
|||
return await knex('namespaces');
|
||||
}
|
||||
|
||||
function hash(ns) {
|
||||
return hasher.hash(filterObject(ns, allowedKeys));
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
}
|
||||
|
||||
async function getById(nsId) {
|
||||
const ns = await knex('namespaces').where('id', nsId).first();
|
||||
if (!ns) {
|
||||
async function getById(id) {
|
||||
const entity = await knex('namespaces').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
return ns;
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function create(ns) {
|
||||
const nsId = await knex('namespaces').insert(filterObject(ns, allowedKeys));
|
||||
return nsId;
|
||||
async function create(entity) {
|
||||
await knex.transaction(async tx => {
|
||||
const id = await tx('namespaces').insert(filterObject(entity, allowedKeys));
|
||||
|
||||
if (entity.parent) {
|
||||
if (!await tx('namespaces').select(['id']).where('id', entity.parent).first()) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(ns) {
|
||||
enforce(ns.id !== 1 || ns.parent === null, 'Cannot assign a parent to the root namespace.');
|
||||
async function updateWithConsistencyCheck(entity) {
|
||||
enforce(entity.id !== 1 || entity.parent === null, 'Cannot assign a parent to the root namespace.');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const existingNs = await tx('namespaces').where('id', ns.id).first();
|
||||
if (!ns) {
|
||||
const existing = await tx('namespaces').where('id', entity.id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingNsHash = hash(existingNs);
|
||||
if (existingNsHash != ns.originalHash) {
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
let iter = ns;
|
||||
let iter = entity;
|
||||
while (iter.parent != null) {
|
||||
iter = await tx('namespaces').where('id', iter.parent).first();
|
||||
if (iter.id == ns.id) {
|
||||
|
||||
if (!iter) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
|
||||
if (iter.id == entity.id) {
|
||||
throw new interoperableErrors.LoopDetectedError();
|
||||
}
|
||||
}
|
||||
|
||||
await tx('namespaces').where('id', ns.id).update(filterObject(ns, allowedKeys));
|
||||
await tx('namespaces').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
});
|
||||
}
|
||||
|
||||
async function remove(nsId) {
|
||||
enforce(nsId !== 1, 'Cannot delete the root namespace.');
|
||||
async function remove(id) {
|
||||
enforce(id !== 1, 'Cannot delete the root namespace.');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const childNs = await tx('namespaces').where('parent', nsId).first();
|
||||
const childNs = await tx('namespaces').where('parent', id).first();
|
||||
if (childNs) {
|
||||
throw new interoperableErrors.ChildDetectedError();
|
||||
}
|
||||
|
||||
await tx('namespaces').where('id', nsId).del();
|
||||
await tx('namespaces').where('id', id).del();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,37 +8,37 @@ const interoperableErrors = require('../shared/interoperable-errors');
|
|||
|
||||
const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs']);
|
||||
|
||||
function hash(ns) {
|
||||
return hasher.hash(filterObject(ns, allowedKeys));
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
}
|
||||
|
||||
async function getById(templateId) {
|
||||
const template = await knex('report_templates').where('id', templateId).first();
|
||||
if (!template) {
|
||||
async function getById(id) {
|
||||
const entity = await knex('report_templates').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
return template;
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function listDTAjax(params) {
|
||||
return await dtHelpers.ajaxList(params, tx => tx('report_templates'), ['report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created']);
|
||||
}
|
||||
|
||||
async function create(template) {
|
||||
const templateId = await knex('report_templates').insert(filterObject(template, allowedKeys));
|
||||
return templateId;
|
||||
async function create(entity) {
|
||||
const id = await knex('report_templates').insert(filterObject(entity, allowedKeys));
|
||||
return id;
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(template) {
|
||||
await knex.transaction(async tx => {
|
||||
const existingTemplate = await tx('report_templates').where('id', template.id).first();
|
||||
const existing = await tx('report_templates').where('id', template.id).first();
|
||||
if (!template) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingNsHash = hash(existingTemplate);
|
||||
if (existingNsHash != template.originalHash) {
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != template.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
@ -46,8 +46,8 @@ async function updateWithConsistencyCheck(template) {
|
|||
});
|
||||
}
|
||||
|
||||
async function remove(templateId) {
|
||||
await knex('report_templates').where('id', templateId).del();
|
||||
async function remove(id) {
|
||||
await knex('report_templates').where('id', id).del();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -6,55 +6,93 @@ const { enforce, filterObject } = require('../lib/helpers');
|
|||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs']);
|
||||
const allowedKeys = new Set(['name', 'description', 'report_template', 'params']);
|
||||
|
||||
function hash(ns) {
|
||||
return hasher.hash(filterObject(ns, allowedKeys));
|
||||
const ReportState = {
|
||||
SCHEDULED: 0,
|
||||
PROCESSING: 1,
|
||||
FINISHED: 2,
|
||||
FAILED: 3,
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
}
|
||||
|
||||
async function getById(templateId) {
|
||||
const template = await knex('report_templates').where('id', templateId).first();
|
||||
if (!template) {
|
||||
async function getById(id) {
|
||||
const entity = await knex('reports').where('id', id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
return template;
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function listDTAjax(params) {
|
||||
return await dtHelpers.ajaxList(params, tx => tx('report_templates'), ['report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created']);
|
||||
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']);
|
||||
}
|
||||
|
||||
async function create(template) {
|
||||
const templateId = await knex('report_templates').insert(filterObject(template, allowedKeys));
|
||||
return templateId;
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(template) {
|
||||
async function create(entity) {
|
||||
await knex.transaction(async tx => {
|
||||
const existingTemplate = await tx('report_templates').where('id', template.id).first();
|
||||
if (!template) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
const id = await tx('reports').insert(filterObject(entity, allowedKeys));
|
||||
|
||||
if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
|
||||
const existingNsHash = hash(existingTemplate);
|
||||
if (existingNsHash != template.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
await tx('report_templates').where('id', template.id).update(filterObject(template, allowedKeys));
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function remove(templateId) {
|
||||
await knex('report_templates').where('id', templateId).del();
|
||||
async function updateWithConsistencyCheck(entity) {
|
||||
await knex.transaction(async tx => {
|
||||
const existing = await tx('reports').where('id', entity.id).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
|
||||
await tx('reports').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
});
|
||||
}
|
||||
|
||||
async function remove(id) {
|
||||
await knex('reports').where('id', id).del();
|
||||
}
|
||||
|
||||
async function updateFields(id, fields) {
|
||||
return await knex('reports').where('id', id).update(fields);
|
||||
}
|
||||
|
||||
async function listByState(state, limit) {
|
||||
return await knex('reports').where('state', state).limit(limit);
|
||||
}
|
||||
|
||||
async function bulkChangeState(oldState, newState) {
|
||||
return await knex('reports').where('state', oldState).update('state', newState);
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
ReportState,
|
||||
hash,
|
||||
getById,
|
||||
listDTAjax,
|
||||
create,
|
||||
updateWithConsistencyCheck,
|
||||
remove
|
||||
remove,
|
||||
updateFields,
|
||||
listByState,
|
||||
bulkChangeState
|
||||
};
|
|
@ -30,8 +30,8 @@ const hashKeys = new Set(['username', 'name', 'email']);
|
|||
const passport = require('../lib/passport');
|
||||
|
||||
|
||||
function hash(user) {
|
||||
return hasher.hash(filterObject(user, hashKeys));
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, hashKeys));
|
||||
}
|
||||
|
||||
async function _getBy(key, value, extraColumns) {
|
||||
|
@ -50,8 +50,8 @@ async function _getBy(key, value, extraColumns) {
|
|||
return user;
|
||||
}
|
||||
|
||||
async function getById(userId) {
|
||||
return await _getBy('id', userId);
|
||||
async function getById(id) {
|
||||
return await _getBy('id', id);
|
||||
}
|
||||
|
||||
async function serverValidate(data, isOwnAccount) {
|
||||
|
@ -309,7 +309,7 @@ async function resetPassword(username, resetToken, password) {
|
|||
reset_expire: null
|
||||
});
|
||||
} else {
|
||||
throw new interoperableErrors.InvalidToken();
|
||||
throw new interoperableErrors.InvalidTokenError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,6 +10,6 @@ router.get('/logout', (req, res) => {
|
|||
res.redirect('/');
|
||||
});
|
||||
|
||||
clientHelpers.registerRootRoute(router, _('Account'), 'account');
|
||||
clientHelpers.registerRootRoute(router, 'account', _('Account'));
|
||||
|
||||
module.exports = router;
|
|
@ -5,6 +5,6 @@ const clientHelpers = require('../lib/client-helpers');
|
|||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, _('Namespaces'), 'namespaces');
|
||||
clientHelpers.registerRootRoute(router, 'namespaces', _('Namespaces'));
|
||||
|
||||
module.exports = router;
|
|
@ -5,7 +5,7 @@ const passport = require('../lib/passport');
|
|||
const router = new express.Router();
|
||||
const _ = require('../lib/translate')._;
|
||||
const reportTemplates = require('../lib/models/report-templates');
|
||||
const reports = require('../lib/models/reports');
|
||||
const reports = require('../lib/models/reports-REMOVE');
|
||||
const reportProcessor = require('../lib/report-processor');
|
||||
const campaigns = require('../lib/models/campaigns');
|
||||
const lists = require('../lib/models/lists');
|
||||
|
|
|
@ -5,6 +5,6 @@ const clientHelpers = require('../lib/client-helpers');
|
|||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, _('Report Templates'), 'reportTemplates');
|
||||
clientHelpers.registerRootRoute(router, 'reports', _('Reports'));
|
||||
|
||||
module.exports = router;
|
|
@ -27,7 +27,7 @@ router.putAsync('/report-templates/:reportTemplateId', passport.loggedIn, passpo
|
|||
});
|
||||
|
||||
router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await reportTemplates.remove(req.context, req.params.reportTemplateId);
|
||||
await reportTemplates.remove(req.params.reportTemplateId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
|
|
@ -2,38 +2,50 @@
|
|||
|
||||
const passport = require('../../lib/passport');
|
||||
const _ = require('../../lib/translate')._;
|
||||
const reportTemplates = require('../../models/report-templates');
|
||||
const reports = require('../../models/reports');
|
||||
const reportProcessor = require('../../lib/report-processor');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
|
||||
|
||||
router.getAsync('/report-templates/:reportTemplateId', passport.loggedIn, async (req, res) => {
|
||||
const reportTemplate = await reportTemplates.getById(req.params.reportTemplateId);
|
||||
reportTemplate.hash = reportTemplates.hash(reportTemplate);
|
||||
return res.json(reportTemplate);
|
||||
router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => {
|
||||
const report = await reports.getById(req.params.reportId);
|
||||
report.hash = reports.hash(report);
|
||||
return res.json(report);
|
||||
});
|
||||
|
||||
router.postAsync('/report-templates', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await reportTemplates.create(req.body);
|
||||
router.postAsync('/reports', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await reports.create(req.body);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.putAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const reportTemplate = req.body;
|
||||
reportTemplate.id = parseInt(req.params.reportTemplateId);
|
||||
router.putAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const report = req.body;
|
||||
report.id = parseInt(req.params.reportId);
|
||||
|
||||
await reportTemplates.updateWithConsistencyCheck(reportTemplate);
|
||||
await reports.updateWithConsistencyCheck(report);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await reportTemplates.remove(req.context, req.params.reportTemplateId);
|
||||
router.deleteAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await reports.remove(req.params.reportId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.postAsync('/report-templates-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await reportTemplates.listDTAjax(req.body));
|
||||
router.postAsync('/reports-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await reports.listDTAjax(req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/report-start/:id', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await reportProcessor.start(req.params.id);
|
||||
// TODO
|
||||
});
|
||||
|
||||
router.postAsync('/report-stop/:id', async (req, res) => {
|
||||
await reportProcessor.stop(req.params.id);
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -5,6 +5,6 @@ const clientHelpers = require('../lib/client-helpers');
|
|||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, _('Users'), 'users');
|
||||
clientHelpers.registerRootRoute(router, 'users', _('Users'));
|
||||
|
||||
module.exports = router;
|
|
@ -56,9 +56,15 @@ class IncorrectPasswordError extends InteroperableError {
|
|||
}
|
||||
}
|
||||
|
||||
class InvalidToken extends InteroperableError {
|
||||
class InvalidTokenError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidToken', msg, data);
|
||||
super('InvalidTokenError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DependencyNotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DependencyNotFound', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +78,8 @@ const errorTypes = {
|
|||
DuplicitNameError,
|
||||
DuplicitEmailError,
|
||||
IncorrectPasswordError,
|
||||
InvalidToken
|
||||
InvalidTokenError,
|
||||
DependencyNotFoundError
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const reports = require('../../lib/models/reports');
|
||||
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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue