Reports halfway through

Datatable now correctly handles the situation when user is not logged in and access protected resources
This commit is contained in:
Tomas Bures 2017-07-09 23:16:47 +02:00
parent aba42d94ac
commit 3f7b428546
28 changed files with 421 additions and 471 deletions

11
app.js
View file

@ -39,17 +39,17 @@ const blacklist = require('./routes/blacklist');
const editorapi = require('./routes/editorapi'); const editorapi = require('./routes/editorapi');
const grapejs = require('./routes/grapejs'); const grapejs = require('./routes/grapejs');
const mosaico = require('./routes/mosaico'); const mosaico = require('./routes/mosaico');
const reports = require('./routes/reports');
const namespaces = require('./routes/rest/namespaces'); const namespaces = require('./routes/rest/namespaces');
const users = require('./routes/rest/users'); const users = require('./routes/rest/users');
const account = require('./routes/rest/account'); const account = require('./routes/rest/account');
const reportTemplates = require('./routes/rest/report-templates'); const reportTemplates = require('./routes/rest/report-templates');
const reports = require('./routes/rest/reports');
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration'); const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
const usersLegacyIntegration = require('./routes/users-legacy-integration'); const usersLegacyIntegration = require('./routes/users-legacy-integration');
const accountLegacyIntegration = require('./routes/account-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'); const interoperableErrors = require('./shared/interoperable-errors');
@ -246,7 +246,7 @@ app.use('/namespaces', namespacesLegacyIntegration);
app.use('/account', accountLegacyIntegration); app.use('/account', accountLegacyIntegration);
if (config.reports && config.reports.enabled === true) { 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) { if (config.reports && config.reports.enabled === true) {
app.use('/rest', reportTemplates); app.use('/rest', reportTemplates);
} app.use('/rest', reports);
if (config.reports && config.reports.enabled === true) {
app.use('/reports', reports);
} }
// catch 404 and forward to error handler // catch 404 and forward to error handler

View file

@ -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.')); this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
} }
} catch (error) { } catch (error) {
if (error instanceof interoperableErrors.InvalidToken) { if (error instanceof interoperableErrors.InvalidTokenError) {
this.setFormStatusMessage('danger', this.setFormStatusMessage('danger',
<span> <span>
<strong>{t('Your password cannot be reset.')}</strong>{' '} <strong>{t('Your password cannot be reset.')}</strong>{' '}

View file

@ -9,6 +9,7 @@ import interoperableErrors from '../../../shared/interoperable-errors';
import { withPageHelpers } from './page' import { withPageHelpers } from './page'
import { withErrorHandling, withAsyncErrorHandler } from './error-handling'; import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
import { TreeTable, TreeSelectMode } from './tree'; import { TreeTable, TreeSelectMode } from './tree';
import { Table, TableSelectMode } from './table';
import brace from 'brace'; import brace from 'brace';
import AceEditor from 'react-ace'; import AceEditor from 'react-ace';
@ -62,6 +63,17 @@ class Form extends Component {
return; 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; throw error;
} }
} }
@ -368,7 +380,46 @@ class TreeTableSelect extends Component {
const htmlId = 'form_' + id; const htmlId = 'form_' + id;
return wrapInput(id, htmlId, owner, props.label, props.help, 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, ButtonRow,
Button, Button,
TreeTableSelect, TreeTableSelect,
TableSelect,
TableSelectMode,
ACEEditor, ACEEditor,
FormSendMethod FormSendMethod
} }

View file

@ -1,8 +1,8 @@
.mt-button-row > button { .mt-button-row > * {
margin-right: 15px; margin-right: 15px;
} }
.mt-button-row > button:last-child { .mt-button-row > *:last-child {
margin-right: 0px; margin-right: 0px;
} }

View file

@ -52,6 +52,7 @@ class Table extends Component {
columns: PropTypes.array, columns: PropTypes.array,
selectMode: PropTypes.number, selectMode: PropTypes.number,
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]), selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
selectionKeyIndex: PropTypes.number,
onSelectionChangedAsync: PropTypes.func, onSelectionChangedAsync: PropTypes.func,
actionLinks: PropTypes.array, actionLinks: PropTypes.array,
withHeader: PropTypes.bool 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; return this.props.selection !== nextProps.selection || this.props.data != nextProps.data || this.props.dataUrl != nextProps.dataUrl;
} }
static defaultProps = {
selectionKeyIndex: 0
}
componentDidMount() { componentDidMount() {
const columns = this.props.columns.slice(); const columns = this.props.columns.slice();
@ -108,10 +113,7 @@ class Table extends Component {
dtOptions.data = this.props.data; dtOptions.data = this.props.data;
} else { } else {
dtOptions.serverSide = true; dtOptions.serverSide = true;
dtOptions.ajax = { dtOptions.ajax = ::this.fetchData;
url: this.props.dataUrl,
type: 'POST'
};
} }
this.table = jQuery(this.domTable).DataTable(dtOptions); this.table = jQuery(this.domTable).DataTable(dtOptions);
@ -122,6 +124,13 @@ class Table extends Component {
this.updateSelection(); 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() { componentDidUpdate() {
if (this.props.data) { if (this.props.data) {
this.table.clear(); this.table.clear();
@ -132,23 +141,35 @@ class Table extends Component {
} }
updateSelection() { updateSelection() {
/* let selArray = [];
const tree = this.tree; if (this.selectMode === TableSelectMode.SINGLE) {
if (this.selectMode === TableSelectMode.MULTI) { selArray = [this.props.selection];
const selectSet = new Set(this.props.selection); } else if (this.selectMode === TableSelectMode.MULTI) {
selArray = 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);
} }
*/
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) { 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) { if (this.props.onSelectionChangedAsync) {
await this.props.onSelectionChangedAsync(sel); await this.props.onSelectionChangedAsync(sel);

View file

@ -144,11 +144,20 @@ export default class CUD extends Component {
} catch (error) { } catch (error) {
if (error instanceof interoperableErrors.LoopDetectedError) { if (error instanceof interoperableErrors.LoopDetectedError) {
this.disableForm();
this.setFormStatusMessage('danger', this.setFormStatusMessage('danger',
<span> <span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '} <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> </span>
); );
return; return;

View file

@ -3,10 +3,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate, Trans } from 'react-i18next'; import { translate, Trans } from 'react-i18next';
import { withPageHelpers, Title } from '../lib/page' 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 axios from '../lib/axios';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import { ModalDialog } from '../lib/bootstrap-components'; import { ModalDialog } from '../lib/bootstrap-components';
import moment from 'moment';
@translate() @translate()
@withForm @withForm
@ -31,193 +32,20 @@ export default class CUD extends Component {
@withAsyncErrorHandler @withAsyncErrorHandler
async loadFormValues() { async loadFormValues() {
await this.getFormValuesFromURL(`/rest/report-templates/${this.state.entityId}`); await this.getFormValuesFromURL(`/rest/reports/${this.state.entityId}`);
} }
componentDidMount() { componentDidMount() {
if (this.props.edit) { if (this.props.edit) {
this.loadFormValues(); 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 { } else {
this.populateFormValues({ this.populateFormValues({
report_template: null,
name: '', name: '',
description: '', description: ''
mime_type: 'text/html',
user_fields: '',
js: '',
hbs: ''
}); });
} }
} }
}
localValidateFormValues(state) { localValidateFormValues(state) {
const t = this.props.t; const t = this.props.t;
@ -229,41 +57,24 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], null); state.setIn(['name', 'error'], null);
} }
if (!state.getIn(['mime_type', 'value'])) { if (!state.getIn(['report_template', 'value'])) {
state.setIn(['mime_type', 'error'], t('MIME Type must be selected')); state.setIn(['report_template', 'error'], t('Report template must be selected'));
} else { } else {
state.setIn(['mime_type', 'error'], null); state.setIn(['report_template', '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'));
}
} }
} }
async submitAndStay() { async submitHandler() {
await Form.handleChangedError(this, async () => await this.doSubmit(true));
}
async submitAndLeave() {
await Form.handleChangedError(this, async () => await this.doSubmit(false));
}
async doSubmit(stay) {
const t = this.props.t; const t = this.props.t;
const edit = this.props.edit; const edit = this.props.edit;
let sendMethod, url; let sendMethod, url;
if (edit) { if (edit) {
sendMethod = FormSendMethod.PUT; sendMethod = FormSendMethod.PUT;
url = `/rest/report-templates/${this.state.entityId}` url = `/rest/reports/${this.state.entityId}`
} else { } else {
sendMethod = FormSendMethod.POST; sendMethod = FormSendMethod.POST;
url = '/rest/report-templates' url = '/rest/reports'
} }
this.disableForm(); this.disableForm();
@ -274,12 +85,7 @@ export default class CUD extends Component {
}); });
if (submitSuccessful) { if (submitSuccessful) {
if (stay) { this.navigateToWithFlashMessage('/reports', 'success', t('Report saved'));
this.enableForm();
this.setFormStatusMessage('success', t('Report template saved'));
} else {
this.navigateToWithFlashMessage('/report-templates', 'success', t('Report template saved'));
}
} else { } else {
this.enableForm(); this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); 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() { async showDeleteModal() {
this.navigateTo(`/report-templates/edit/${this.state.entityId}/delete`); this.navigateTo(`/reports/edit/${this.state.entityId}/delete`);
} }
async hideDeleteModal() { async hideDeleteModal() {
this.navigateTo(`/report-templates/edit/${this.state.entityId}`); this.navigateTo(`/reports/edit/${this.state.entityId}`);
} }
async performDelete() { async performDelete() {
@ -300,17 +106,23 @@ export default class CUD extends Component {
await this.hideDeleteModal(); await this.hideDeleteModal();
this.disableForm(); 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() { render() {
const t = this.props.t; const t = this.props.t;
const edit = this.props.edit; 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 ( return (
<div> <div>
@ -319,31 +131,22 @@ export default class CUD extends Component {
{ label: t('No'), className: 'btn-primary', onClickAsync: ::this.hideDeleteModal }, { label: t('No'), className: 'btn-primary', onClickAsync: ::this.hideDeleteModal },
{ label: t('Yes'), className: 'btn-danger', onClickAsync: ::this.performDelete } { 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> </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')}/> <InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/> <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 ? <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 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> <ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/> <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> </ButtonRow>
}
</Form> </Form>
</div> </div>
); );

View file

@ -2,8 +2,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { DropdownMenu } from '../lib/bootstrap-components'; import { Title, Toolbar, NavButton } from '../lib/page';
import { Title, Toolbar, DropdownLink } from '../lib/page';
import { Table } from '../lib/table'; import { Table } from '../lib/table';
import moment from 'moment'; import moment from 'moment';
@ -14,30 +13,27 @@ export default class List extends Component {
const actionLinks = [{ const actionLinks = [{
label: 'Edit', label: 'Edit',
link: data => '/report-templates/edit/' + data[0] link: data => '/reports/edit/' + data[0]
}]; }];
const columns = [ const columns = [
{ data: 0, title: "#" }, { data: 0, title: "#" },
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Description') }, { data: 2, title: t('Template') },
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() } { data: 3, title: t('Description') },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() }
]; ];
return ( return (
<div> <div>
<Toolbar> <Toolbar>
<DropdownMenu className="btn-primary" label={t('Create Report Template')}> <NavButton linkTo="/reports/create" className="btn-primary" icon="plus" label={t('Create Report')}/>
<DropdownLink to="/report-templates/create">{t('Blank')}</DropdownLink> <NavButton linkTo="/reports/templates" className="btn-primary" label={t('Report Templates')}/>
<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>
</Toolbar> </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> </div>
); );
} }

View file

@ -6,8 +6,10 @@ import { I18nextProvider } from 'react-i18next';
import i18n from '../lib/i18n'; import i18n from '../lib/i18n';
import { Section } from '../lib/page' import { Section } from '../lib/page'
import CUD from './CUD' import ReportsCUD from './CUD'
import List from './List' import ReportsList from './List'
import ReportTemplatesCUD from './templates/CUD'
import ReportTemplatesList from './templates/List'
const getStructure = t => { const getStructure = t => {
const subPaths = {}; const subPaths = {};
@ -17,31 +19,47 @@ const getStructure = t => {
title: t('Home'), title: t('Home'),
externalLink: '/', externalLink: '/',
children: { children: {
'report-templates': { 'reports': {
title: t('Report Templates'), title: t('Reports'),
link: '/report-templates', link: '/reports',
component: List, component: ReportsList,
children: {
edit: {
title: t('Edit Report'),
params: [':id', ':action?'],
render: props => (<ReportsCUD edit {...props} />)
},
create: {
title: t('Create Report'),
render: props => (<ReportsCUD {...props} />)
},
'templates': {
title: t('Templates'),
link: '/reports/templates',
component: ReportTemplatesList,
children: { children: {
edit: { edit: {
title: t('Edit Report Template'), title: t('Edit Report Template'),
params: [':id', ':action?'], params: [':id', ':action?'],
render: props => (<CUD edit {...props} />) render: props => (<ReportTemplatesCUD edit {...props} />)
}, },
create: { create: {
title: t('Create Report Template'), title: t('Create Report Template'),
params: [':wizard?'], params: [':wizard?'],
render: props => (<CUD {...props} />) render: props => (<ReportTemplatesCUD {...props} />)
} }
} }
} }
} }
},
}
} }
} }
}; };
export default function() { export default function() {
ReactDOM.render( ReactDOM.render(
<I18nextProvider i18n={ i18n }><Section root='/report-templates' structure={getStructure}/></I18nextProvider>, <I18nextProvider i18n={ i18n }><Section root='/reports' structure={getStructure}/></I18nextProvider>,
document.getElementById('root') document.getElementById('root')
); );
}; };

View file

@ -2,11 +2,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate, Trans } from 'react-i18next'; import { translate, Trans } from 'react-i18next';
import { withPageHelpers, Title } from '../lib/page' 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, Dropdown, ACEEditor, ButtonRow, Button } from '../../lib/form';
import axios from '../lib/axios'; import axios from '../../lib/axios';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import { ModalDialog } from '../lib/bootstrap-components'; import { ModalDialog } from '../../lib/bootstrap-components';
@translate() @translate()
@withForm @withForm

View file

@ -2,9 +2,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { DropdownMenu } from '../lib/bootstrap-components'; import { DropdownMenu } from '../../lib/bootstrap-components';
import { Title, Toolbar, DropdownLink } from '../lib/page'; import { Title, Toolbar, DropdownLink } from '../../lib/page';
import { Table } from '../lib/table'; import { Table } from '../../lib/table';
import moment from 'moment'; import moment from 'moment';
@translate() @translate()

View file

@ -6,7 +6,7 @@ module.exports = {
namespaces: ['babel-polyfill', './src/namespaces/root.js'], namespaces: ['babel-polyfill', './src/namespaces/root.js'],
users: ['babel-polyfill', './src/users/root.js'], users: ['babel-polyfill', './src/users/root.js'],
account: ['babel-polyfill', './src/account/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: { output: {
library: 'MailtrainReactBody', library: 'MailtrainReactBody',

View file

@ -126,8 +126,8 @@ server.on('listening', () => {
triggers(() => { triggers(() => {
spawnSenders(() => { spawnSenders(() => {
feedcheck(() => { feedcheck(() => {
postfixBounceServer(() => { postfixBounceServer(async () => {
reportProcessor.init(() => { await reportProcessor.init();
log.info('Service', 'All services started'); log.info('Service', 'All services started');
}); });
}); });
@ -137,7 +137,6 @@ server.on('listening', () => {
}); });
}); });
}); });
});
} }
if (config.reports && config.reports.enabled === true) { if (config.reports && config.reports.enabled === true) {

View file

@ -13,7 +13,7 @@ function _getConfig(context) {
} }
} }
function registerRootRoute(router, title, entryPoint) { function registerRootRoute(router, entryPoint, title) {
router.get('/*', passport.csrfProtection, (req, res) => { router.get('/*', passport.csrfProtection, (req, res) => {
res.render('react-root', { res.render('react-root', {
title, title,

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('npmlog');
const reports = require('./models/reports'); const reports = require('../models/reports');
const executor = require('./executor'); const executor = require('./executor');
let runningWorkersCount = 0; let runningWorkersCount = 0;
@ -11,12 +11,12 @@ let workers = {};
function startWorker(report) { 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); log.info('ReportProcessor', 'Worker process for "%s" started with tid %s. Current worker count is %s.', report.name, tid, runningWorkersCount);
workers[report.id] = tid; workers[report.id] = tid;
} }
function onFinished(code, signal) { async function onFinished(code, signal) {
runningWorkersCount--; 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); 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]; delete workers[report.id];
@ -24,21 +24,20 @@ function startWorker(report) {
const fields = {}; const fields = {};
if (code === 0) { if (code === 0) {
fields.state = reports.ReportState.FINISHED; fields.state = reports.ReportState.FINISHED;
fields.lastRun = new Date(); fields.last_run = new Date();
} else { } else {
fields.state = reports.ReportState.FAILED; fields.state = reports.ReportState.FAILED;
} }
reports.updateFields(report.id, fields, err => { try {
if (err) { await reports.update(report.id, fields);
setImmediate(tryStartWorkers);
} catch (err) {
log.error('ReportProcessor', err); log.error('ReportProcessor', err);
} }
setImmediate(startWorkers);
});
} }
function onFailed(msg) { async function onFailed(msg) {
runningWorkersCount--; 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); 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]; delete workers[report.id];
@ -47,13 +46,12 @@ function startWorker(report) {
state: reports.ReportState.FAILED state: reports.ReportState.FAILED
}; };
reports.updateFields(report.id, fields, err => { try {
if (err) { await reports.update(report.id, fields);
setImmediate(tryStartWorkers);
} catch (err) {
log.error('ReportProcessor', err); log.error('ReportProcessor', err);
} }
setImmediate(startWorkers);
});
} }
const reportData = { const reportData = {
@ -65,83 +63,68 @@ function startWorker(report) {
executor.start('report-processor-worker', reportData, onStarted, onFinished, onFailed); executor.start('report-processor-worker', reportData, onStarted, onFinished, onFailed);
} }
function startWorkers() { let isStartingWorkers = false;
reports.listWithState(reports.ReportState.SCHEDULED, 0, maxWorkersCount - runningWorkersCount, (err, reportList) => {
if (err) { async function tryStartWorkers() {
log.error('ReportProcessor', err);
if (isStartingWorkers) {
// Generally it is possible that this function is invoked simultaneously multiple times. This is to prevent it.
return; return;
} }
isStartingWorkers = true;
for (let report of reportList) { try {
reports.updateFields(report.id, { state: reports.ReportState.PROCESSING }, err => { while (runningWorkersCount < maxWorkersCount) {
if (err) { log.info('ReportProcessor', 'Trying to start worker because runningWorkersCount=%s maxWorkersCount=%s', runningWorkersCount, maxWorkersCount);
log.error('ReportProcessor', err);
return;
}
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); startWorker(report);
});
} else {
log.info('ReportProcessor', 'No more report to start a worker for');
break;
} }
});
} }
module.exports.start = (reportId, callback) => { } catch (err) {
log.error('ReportProcessor', err);
}
isStartingWorkers = false;
}
module.exports.start = async reportId => {
if (!workers[reportId]) { if (!workers[reportId]) {
log.info('ReportProcessor', 'Scheduling report id: %s', reportId); log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, lastRun: null}, err => { await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, lastRun: null});
if (err) { tryStartWorkers();
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);
});
} else { } else {
log.info('ReportProcessor', 'Worker for report id: %s is already running.', reportId); 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]; const tid = workers[reportId];
if (tid) { if (tid) {
log.info('ReportProcessor', 'Killing worker for report id: %s', reportId); log.info('ReportProcessor', 'Killing worker for report id: %s', reportId);
executor.stop(tid); executor.stop(tid);
reports.updateFields(reportId, { state: reports.ReportState.FAILED}, callback);
await reports.updateFields(reportId, { state: reports.ReportState.FAILED });
} else { } else {
log.info('ReportProcessor', 'No running worker found for report id: %s', reportId); log.info('ReportProcessor', 'No running worker found for report id: %s', reportId);
} }
}; };
module.exports.init = callback => { module.exports.init = async () => {
reports.listWithState(reports.ReportState.PROCESSING, 0, 0, (err, reportList) => { try {
if (err) { await reports.bulkChangeState(reports.ReportState.PROCESSING, reports.ReportState.SCHEDULED);
} catch (err) {
log.error('ReportProcessor', 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();
});
}; };

View file

@ -11,60 +11,74 @@ async function list() {
return await knex('namespaces'); return await knex('namespaces');
} }
function hash(ns) { function hash(entity) {
return hasher.hash(filterObject(ns, allowedKeys)); return hasher.hash(filterObject(entity, allowedKeys));
} }
async function getById(nsId) { async function getById(id) {
const ns = await knex('namespaces').where('id', nsId).first(); const entity = await knex('namespaces').where('id', id).first();
if (!ns) { if (!entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
return ns; return entity;
} }
async function create(ns) { async function create(entity) {
const nsId = await knex('namespaces').insert(filterObject(ns, allowedKeys)); await knex.transaction(async tx => {
return nsId; 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();
}
} }
async function updateWithConsistencyCheck(ns) { return id;
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 => { await knex.transaction(async tx => {
const existingNs = await tx('namespaces').where('id', ns.id).first(); const existing = await tx('namespaces').where('id', entity.id).first();
if (!ns) { if (!entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
const existingNsHash = hash(existingNs); const existingHash = hash(existing);
if (existingNsHash != ns.originalHash) { if (existingHash != entity.originalHash) {
throw new interoperableErrors.ChangedError(); throw new interoperableErrors.ChangedError();
} }
let iter = ns; let iter = entity;
while (iter.parent != null) { while (iter.parent != null) {
iter = await tx('namespaces').where('id', iter.parent).first(); 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(); 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) { async function remove(id) {
enforce(nsId !== 1, 'Cannot delete the root namespace.'); enforce(id !== 1, 'Cannot delete the root namespace.');
await knex.transaction(async tx => { 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) { if (childNs) {
throw new interoperableErrors.ChildDetectedError(); throw new interoperableErrors.ChildDetectedError();
} }
await tx('namespaces').where('id', nsId).del(); await tx('namespaces').where('id', id).del();
}); });
} }

View file

@ -8,37 +8,37 @@ const interoperableErrors = require('../shared/interoperable-errors');
const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs']); const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs']);
function hash(ns) { function hash(entity) {
return hasher.hash(filterObject(ns, allowedKeys)); return hasher.hash(filterObject(entity, allowedKeys));
} }
async function getById(templateId) { async function getById(id) {
const template = await knex('report_templates').where('id', templateId).first(); const entity = await knex('report_templates').where('id', id).first();
if (!template) { if (!entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
return template; return entity;
} }
async function listDTAjax(params) { 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('report_templates'), ['report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created']);
} }
async function create(template) { async function create(entity) {
const templateId = await knex('report_templates').insert(filterObject(template, allowedKeys)); const id = await knex('report_templates').insert(filterObject(entity, allowedKeys));
return templateId; return id;
} }
async function updateWithConsistencyCheck(template) { async function updateWithConsistencyCheck(template) {
await knex.transaction(async tx => { 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) { if (!template) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
const existingNsHash = hash(existingTemplate); const existingHash = hash(existing);
if (existingNsHash != template.originalHash) { if (existingHash != template.originalHash) {
throw new interoperableErrors.ChangedError(); throw new interoperableErrors.ChangedError();
} }
@ -46,8 +46,8 @@ async function updateWithConsistencyCheck(template) {
}); });
} }
async function remove(templateId) { async function remove(id) {
await knex('report_templates').where('id', templateId).del(); await knex('report_templates').where('id', id).del();
} }
module.exports = { module.exports = {

View file

@ -6,55 +6,93 @@ const { enforce, filterObject } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers'); const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors'); 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) { const ReportState = {
return hasher.hash(filterObject(ns, allowedKeys)); SCHEDULED: 0,
PROCESSING: 1,
FINISHED: 2,
FAILED: 3,
MAX: 4
};
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
} }
async function getById(templateId) { async function getById(id) {
const template = await knex('report_templates').where('id', templateId).first(); const entity = await knex('reports').where('id', id).first();
if (!template) { if (!entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
return template; return entity;
} }
async function listDTAjax(params) { 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) { async function create(entity) {
const templateId = await knex('report_templates').insert(filterObject(template, allowedKeys));
return templateId;
}
async function updateWithConsistencyCheck(template) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
const existingTemplate = await tx('report_templates').where('id', template.id).first(); const id = await tx('reports').insert(filterObject(entity, allowedKeys));
if (!template) {
throw new interoperableErrors.NotFoundError(); if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) {
throw new interoperableErrors.DependencyNotFoundError();
} }
const existingNsHash = hash(existingTemplate); return id;
if (existingNsHash != template.originalHash) {
throw new interoperableErrors.ChangedError();
}
await tx('report_templates').where('id', template.id).update(filterObject(template, allowedKeys));
}); });
} }
async function remove(templateId) { async function updateWithConsistencyCheck(entity) {
await knex('report_templates').where('id', templateId).del(); 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 = { module.exports = {
ReportState,
hash, hash,
getById, getById,
listDTAjax, listDTAjax,
create, create,
updateWithConsistencyCheck, updateWithConsistencyCheck,
remove remove,
updateFields,
listByState,
bulkChangeState
}; };

View file

@ -30,8 +30,8 @@ const hashKeys = new Set(['username', 'name', 'email']);
const passport = require('../lib/passport'); const passport = require('../lib/passport');
function hash(user) { function hash(entity) {
return hasher.hash(filterObject(user, hashKeys)); return hasher.hash(filterObject(entity, hashKeys));
} }
async function _getBy(key, value, extraColumns) { async function _getBy(key, value, extraColumns) {
@ -50,8 +50,8 @@ async function _getBy(key, value, extraColumns) {
return user; return user;
} }
async function getById(userId) { async function getById(id) {
return await _getBy('id', userId); return await _getBy('id', id);
} }
async function serverValidate(data, isOwnAccount) { async function serverValidate(data, isOwnAccount) {
@ -309,7 +309,7 @@ async function resetPassword(username, resetToken, password) {
reset_expire: null reset_expire: null
}); });
} else { } else {
throw new interoperableErrors.InvalidToken(); throw new interoperableErrors.InvalidTokenError();
} }
}); });
} }

View file

@ -10,6 +10,6 @@ router.get('/logout', (req, res) => {
res.redirect('/'); res.redirect('/');
}); });
clientHelpers.registerRootRoute(router, _('Account'), 'account'); clientHelpers.registerRootRoute(router, 'account', _('Account'));
module.exports = router; module.exports = router;

View file

@ -5,6 +5,6 @@ const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create(); const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, _('Namespaces'), 'namespaces'); clientHelpers.registerRootRoute(router, 'namespaces', _('Namespaces'));
module.exports = router; module.exports = router;

View file

@ -5,7 +5,7 @@ const passport = require('../lib/passport');
const router = new express.Router(); const router = new express.Router();
const _ = require('../lib/translate')._; const _ = require('../lib/translate')._;
const reportTemplates = require('../lib/models/report-templates'); const reportTemplates = require('../lib/models/report-templates');
const reports = require('../lib/models/reports'); const reports = require('../lib/models/reports-REMOVE');
const reportProcessor = require('../lib/report-processor'); const reportProcessor = require('../lib/report-processor');
const campaigns = require('../lib/models/campaigns'); const campaigns = require('../lib/models/campaigns');
const lists = require('../lib/models/lists'); const lists = require('../lib/models/lists');

View file

@ -5,6 +5,6 @@ const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create(); const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, _('Report Templates'), 'reportTemplates'); clientHelpers.registerRootRoute(router, 'reports', _('Reports'));
module.exports = router; module.exports = router;

View file

@ -27,7 +27,7 @@ router.putAsync('/report-templates/:reportTemplateId', passport.loggedIn, passpo
}); });
router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => { 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(); return res.json();
}); });

View file

@ -2,38 +2,50 @@
const passport = require('../../lib/passport'); const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._; 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(); const router = require('../../lib/router-async').create();
router.getAsync('/report-templates/:reportTemplateId', passport.loggedIn, async (req, res) => { router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => {
const reportTemplate = await reportTemplates.getById(req.params.reportTemplateId); const report = await reports.getById(req.params.reportId);
reportTemplate.hash = reportTemplates.hash(reportTemplate); report.hash = reports.hash(report);
return res.json(reportTemplate); return res.json(report);
}); });
router.postAsync('/report-templates', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/reports', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reportTemplates.create(req.body); await reports.create(req.body);
return res.json(); return res.json();
}); });
router.putAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.putAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const reportTemplate = req.body; const report = req.body;
reportTemplate.id = parseInt(req.params.reportTemplateId); report.id = parseInt(req.params.reportId);
await reportTemplates.updateWithConsistencyCheck(reportTemplate); await reports.updateWithConsistencyCheck(report);
return res.json(); return res.json();
}); });
router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.deleteAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reportTemplates.remove(req.context, req.params.reportTemplateId); await reports.remove(req.params.reportId);
return res.json(); return res.json();
}); });
router.postAsync('/report-templates-table', passport.loggedIn, async (req, res) => { router.postAsync('/reports-table', passport.loggedIn, async (req, res) => {
return res.json(await reportTemplates.listDTAjax(req.body)); 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; module.exports = router;

View file

@ -5,6 +5,6 @@ const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create(); const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, _('Users'), 'users'); clientHelpers.registerRootRoute(router, 'users', _('Users'));
module.exports = router; module.exports = router;

View file

@ -56,9 +56,15 @@ class IncorrectPasswordError extends InteroperableError {
} }
} }
class InvalidToken extends InteroperableError { class InvalidTokenError extends InteroperableError {
constructor(msg, data) { 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, DuplicitNameError,
DuplicitEmailError, DuplicitEmailError,
IncorrectPasswordError, IncorrectPasswordError,
InvalidToken InvalidTokenError,
DependencyNotFoundError
}; };
function deserialize(errorObj) { function deserialize(errorObj) {

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const reports = require('../../lib/models/reports'); const reports = require('../../lib/models/reports-REMOVE');
const reportTemplates = require('../../lib/models/report-templates'); const reportTemplates = require('../../lib/models/report-templates');
const lists = require('../../lib/models/lists'); const lists = require('../../lib/models/lists');
const subscriptions = require('../../lib/models/subscriptions'); const subscriptions = require('../../lib/models/subscriptions');