Campaign UI and model adjusted to allow sending a campaign to multiple lists
This commit is contained in:
parent
130c953d94
commit
67d7129f7b
16 changed files with 334 additions and 78 deletions
|
@ -15,6 +15,7 @@ import {
|
|||
ButtonRow,
|
||||
CheckBox,
|
||||
Dropdown,
|
||||
Fieldset,
|
||||
Form,
|
||||
FormSendMethod,
|
||||
InputField,
|
||||
|
@ -39,6 +40,7 @@ import {
|
|||
} from '../templates/helpers';
|
||||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
import campaignsStyles from "./styles.scss";
|
||||
import {getUrl} from "../lib/urls";
|
||||
import {
|
||||
campaignOverridables,
|
||||
|
@ -100,6 +102,8 @@ export default class CUD extends Component {
|
|||
sendConfiguration: null
|
||||
};
|
||||
|
||||
this.nextListEntryId = 0;
|
||||
|
||||
this.initForm({
|
||||
onChange: {
|
||||
send_configuration: ::this.onSendConfigurationChanged
|
||||
|
@ -116,6 +120,12 @@ export default class CUD extends Component {
|
|||
type: PropTypes.number
|
||||
}
|
||||
|
||||
getNextListEntryId() {
|
||||
const id = this.nextListEntryId;
|
||||
this.nextListEntryId += 1;
|
||||
return id;
|
||||
}
|
||||
|
||||
onCustomTemplateTypeChanged(mutState, key, oldType, type) {
|
||||
if (type) {
|
||||
this.templateTypes[type].afterTypeChange(mutState);
|
||||
|
@ -156,12 +166,24 @@ export default class CUD extends Component {
|
|||
data.data_feedUrl = data.data.feedUrl;
|
||||
}
|
||||
|
||||
data.useSegmentation = !!data.segment;
|
||||
|
||||
for (const overridable of campaignOverridables) {
|
||||
data[overridable + '_overriden'] = !!data[overridable + '_override'];
|
||||
}
|
||||
|
||||
const lsts = [];
|
||||
for (const lst of data.lists) {
|
||||
const lstUid = this.getNextListEntryId();
|
||||
|
||||
const prefix = 'lists_' + lstUid + '_';
|
||||
|
||||
data[prefix + 'list'] = lst.list;
|
||||
data[prefix + 'segment'] = lst.segment;
|
||||
data[prefix + 'useSegmentation'] = !!lst.segment;
|
||||
|
||||
lsts.push(lstUid);
|
||||
}
|
||||
data.lists = lsts;
|
||||
|
||||
this.fetchSendConfiguration(data.send_configuration);
|
||||
});
|
||||
|
||||
|
@ -172,6 +194,9 @@ export default class CUD extends Component {
|
|||
data[overridable + '_overriden'] = false;
|
||||
}
|
||||
|
||||
const lstUid = this.getNextListEntryId();
|
||||
const lstPrefix = 'lists_' + lstUid + '_';
|
||||
|
||||
this.populateFormValues({
|
||||
...data,
|
||||
|
||||
|
@ -179,9 +204,12 @@ export default class CUD extends Component {
|
|||
|
||||
name: '',
|
||||
description: '',
|
||||
list: null,
|
||||
segment: null,
|
||||
useSegmentation: false,
|
||||
|
||||
[lstPrefix + 'list']: null,
|
||||
[lstPrefix + 'segment']: null,
|
||||
[lstPrefix + 'useSegmentation']: false,
|
||||
lists: [lstUid],
|
||||
|
||||
send_configuration: null,
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
|
||||
|
@ -227,14 +255,6 @@ export default class CUD extends Component {
|
|||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||
}
|
||||
|
||||
if (!state.getIn(['list', 'value'])) {
|
||||
state.setIn(['list', 'error'], t('List must be selected'));
|
||||
}
|
||||
|
||||
if (state.getIn(['useSegmentation', 'value']) && !state.getIn(['segment', 'value'])) {
|
||||
state.setIn(['segment', 'error'], t('Segment must be selected'));
|
||||
}
|
||||
|
||||
if (!state.getIn(['send_configuration', 'value'])) {
|
||||
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
|
||||
}
|
||||
|
@ -281,10 +301,25 @@ export default class CUD extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
for (const lstUid of state.getIn(['lists', 'value'])) {
|
||||
const prefix = 'lists_' + lstUid + '_';
|
||||
|
||||
if (!state.getIn([prefix + 'list', 'value'])) {
|
||||
state.setIn([prefix + 'list', 'error'], t('List must be selected'));
|
||||
}
|
||||
|
||||
if (campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) {
|
||||
if (state.getIn([prefix + 'useSegmentation', 'value']) && !state.getIn([prefix + 'segment', 'value'])) {
|
||||
state.setIn([prefix + 'segment', 'error'], t('Segment must be selected'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateNamespace(t, state);
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const isEdit = !!this.props.entity;
|
||||
const t = this.props.t;
|
||||
|
||||
let sendMethod, url;
|
||||
|
@ -302,11 +337,6 @@ export default class CUD extends Component {
|
|||
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
data.source = Number.parseInt(data.source);
|
||||
|
||||
if (!data.useSegmentation) {
|
||||
data.segment = null;
|
||||
}
|
||||
delete data.useSegmentation;
|
||||
|
||||
data.data = {};
|
||||
if (data.source === CampaignSource.TEMPLATE || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
data.data.sourceTemplate = data.data_sourceTemplate;
|
||||
|
@ -316,7 +346,7 @@ export default class CUD extends Component {
|
|||
data.data.sourceCampaign = data.data_sourceCampaign;
|
||||
}
|
||||
|
||||
if (data.source === CampaignSource.CUSTOM) {
|
||||
if (!isEdit && data.source === CampaignSource.CUSTOM) {
|
||||
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
|
||||
|
||||
data.data.sourceCustom = {
|
||||
|
@ -342,8 +372,21 @@ export default class CUD extends Component {
|
|||
delete data[overridable + '_overriden'];
|
||||
}
|
||||
|
||||
const lsts = [];
|
||||
for (const lstUid of data.lists) {
|
||||
const prefix = 'lists_' + lstUid + '_';
|
||||
|
||||
const useSegmentation = data[prefix + 'useSegmentation'] && (data.type === CampaignType.REGULAR || data.type === CampaignType.RSS);
|
||||
|
||||
lsts.push({
|
||||
list: data[prefix + 'list'],
|
||||
segment: useSegmentation ? data[prefix + 'segment'] : null
|
||||
});
|
||||
}
|
||||
data.lists = lsts;
|
||||
|
||||
for (const key in data) {
|
||||
if (key.startsWith('data_')) {
|
||||
if (key.startsWith('data_') || key.startsWith('lists_')) {
|
||||
delete data[key];
|
||||
}
|
||||
}
|
||||
|
@ -364,6 +407,47 @@ export default class CUD extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
onAddListEntry(orderBeforeIdx) {
|
||||
this.updateForm(mutState => {
|
||||
const lsts = mutState.getIn(['lists', 'value']);
|
||||
let paramId = 0;
|
||||
|
||||
const lstUid = this.getNextListEntryId();
|
||||
|
||||
const prefix = 'lists_' + lstUid + '_';
|
||||
|
||||
mutState.setIn([prefix + 'list', 'value'], null);
|
||||
mutState.setIn([prefix + 'segment', 'value'], null);
|
||||
mutState.setIn([prefix + 'useSegmentation', 'value'], false);
|
||||
|
||||
mutState.setIn(['lists', 'value'], [...lsts.slice(0, orderBeforeIdx), lstUid, ...lsts.slice(orderBeforeIdx)]);
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveListEntry(lstUid) {
|
||||
this.updateForm(mutState => {
|
||||
const lsts = this.getFormValue('lists');
|
||||
|
||||
const prefix = 'lists_' + lstUid + '_';
|
||||
|
||||
mutState.delete(prefix + 'list');
|
||||
mutState.delete(prefix + 'segment');
|
||||
mutState.delete(prefix + 'useSegmentation');
|
||||
|
||||
mutState.setIn(['lists', 'value'], lsts.filter(val => val !== lstUid));
|
||||
});
|
||||
}
|
||||
|
||||
onListEntryMoveUp(orderIdx) {
|
||||
const lsts = this.getFormValue('lists');
|
||||
this.updateFormValue('lists', [...lsts.slice(0, orderIdx - 1), lsts[orderIdx], lsts[orderIdx - 1], ...lsts.slice(orderIdx + 1)]);
|
||||
}
|
||||
|
||||
onListEntryMoveDown(orderIdx) {
|
||||
const lsts = this.getFormValue('lists');
|
||||
this.updateFormValue('lists', [...lsts.slice(0, orderIdx), lsts[orderIdx + 1], lsts[orderIdx], ...lsts.slice(orderIdx + 2)]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
@ -390,6 +474,81 @@ export default class CUD extends Component {
|
|||
{ data: 1, title: t('Name') }
|
||||
];
|
||||
|
||||
const lstsEditEntries = [];
|
||||
const lsts = this.getFormValue('lists') || [];
|
||||
let lstOrderIdx = 0;
|
||||
for (const lstUid of lsts) {
|
||||
const prefix = 'lists_' + lstUid + '_';
|
||||
const lstOrderIdxClosure = lstOrderIdx;
|
||||
|
||||
const selectedList = this.getFormValue(prefix + 'list');
|
||||
|
||||
lstsEditEntries.push(
|
||||
<div key={lstUid} className={campaignsStyles.entry + ' ' + campaignsStyles.entryWithButtons}>
|
||||
<div className={campaignsStyles.entryButtons}>
|
||||
{lsts.length > 1 &&
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="remove"
|
||||
title={t('Remove')}
|
||||
onClickAsync={() => this.onRemoveListEntry(lstUid)}
|
||||
/>
|
||||
}
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="plus"
|
||||
title={t('Insert new entry before this one')}
|
||||
onClickAsync={() => this.onAddListEntry(lstOrderIdxClosure)}
|
||||
/>
|
||||
{lstOrderIdx > 0 &&
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="chevron-up"
|
||||
title={t('Move up')}
|
||||
onClickAsync={() => this.onListEntryMoveUp(lstOrderIdxClosure)}
|
||||
/>
|
||||
}
|
||||
{lstOrderIdx < lsts.length - 1 &&
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="chevron-down"
|
||||
title={t('Move down')}
|
||||
onClickAsync={() => this.onListEntryMoveDown(lstOrderIdxClosure)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className={campaignsStyles.entryContent}>
|
||||
<TableSelect id={prefix + 'list'} label={t('List')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
|
||||
|
||||
{(campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) &&
|
||||
<div>
|
||||
<CheckBox id={prefix + 'useSegmentation'} label={t('Segment')} text={t('Use a particular segment')}/>
|
||||
{selectedList && this.getFormValue(prefix + 'useSegmentation') &&
|
||||
<TableSelect id={prefix + 'segment'} withHeader dropdown dataUrl={`rest/segments-table/${selectedList}`} columns={segmentsColumns} selectionLabelIndex={1} />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
lstOrderIdx += 1;
|
||||
}
|
||||
|
||||
const lstsEdit =
|
||||
<Fieldset label={t('Lists')}>
|
||||
{lstsEditEntries}
|
||||
<div key="newEntry" className={campaignsStyles.newEntry}>
|
||||
<Button
|
||||
className="btn-default"
|
||||
icon="plus"
|
||||
label={t('Add list')}
|
||||
onClickAsync={() => this.onAddListEntry(lsts.length)}
|
||||
/>
|
||||
</div>
|
||||
</Fieldset>;
|
||||
|
||||
|
||||
const sendConfigurationsColumns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
|
@ -495,6 +654,8 @@ export default class CUD extends Component {
|
|||
saveButtonLabel = t('Save and edit campaign');
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canDelete &&
|
||||
|
@ -520,12 +681,7 @@ export default class CUD extends Component {
|
|||
|
||||
<hr/>
|
||||
|
||||
<TableSelect id="list" label={t('List')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
|
||||
|
||||
<CheckBox id="useSegmentation" label={t('Segment')} text={t('Use a particular segment')}/>
|
||||
{this.getFormValue('useSegmentation') &&
|
||||
<TableSelect id="segment" withHeader dropdown dataUrl={`rest/segments-table/${this.getFormValue('list')}`} columns={segmentsColumns} selectionLabelIndex={1} />
|
||||
}
|
||||
{lstsEdit}
|
||||
|
||||
<hr/>
|
||||
|
||||
|
|
38
client/src/campaigns/styles.scss
Normal file
38
client/src/campaigns/styles.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
.entry {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-bottom: 15px;
|
||||
min-height: 91px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0px none;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.entryButtons {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 19px;
|
||||
|
||||
button {
|
||||
padding: 2px 3px;
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.entryWithButtons > .entryContent {
|
||||
margin-right: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.newEntry{
|
||||
text-align: right;
|
||||
margin-bottom: 15px;
|
||||
}
|
|
@ -191,6 +191,8 @@ export default class CUD extends Component {
|
|||
{ data: 5, title: t('Namespace') }
|
||||
];
|
||||
|
||||
const campaignLists = this.props.campaign.lists.map(x => x.list).join(';');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isEdit &&
|
||||
|
@ -221,7 +223,7 @@ export default class CUD extends Component {
|
|||
{entityKey === Entity.CAMPAIGN && <Dropdown id="campaignEvent" label={t('Event')} options={this.eventOptions[Entity.CAMPAIGN]} help={t('Select the event that triggers sending the campaign.')}/>}
|
||||
|
||||
{entityKey === Entity.CAMPAIGN &&
|
||||
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${this.props.campaign.list}`} columns={campaignsColumns} selectionLabelIndex={1} />
|
||||
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${campaignLists}`} columns={campaignsColumns} selectionLabelIndex={1} />
|
||||
}
|
||||
|
||||
<CheckBox id="enabled" text={t('Enabled')}/>
|
||||
|
|
|
@ -44,11 +44,10 @@ export default class List extends Component {
|
|||
const columns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('List') },
|
||||
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
|
||||
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
|
||||
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
|
||||
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
|
||||
{ data: 3, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
|
||||
{ data: 4, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[3]][data], searchable: false },
|
||||
{ data: 5, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
|
||||
{ data: 6, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
|
||||
{
|
||||
actions: data => {
|
||||
const actions = [];
|
||||
|
|
|
@ -129,7 +129,6 @@ nodemailer:
|
|||
|
||||
queue:
|
||||
# How many parallel sender processes to spawn
|
||||
# You can use more than 1 process only if you have Redis enabled
|
||||
processes: 1
|
||||
|
||||
cors:
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = {
|
|||
};
|
||||
|
||||
function spawn(callback) {
|
||||
log.info('Executor', 'Spawning executor process');
|
||||
log.verbose('Executor', 'Spawning executor process');
|
||||
|
||||
executorProcess = fork(path.join(__dirname, '..', 'services', 'executor.js'), [], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
|
|
|
@ -11,7 +11,7 @@ module.exports = {
|
|||
};
|
||||
|
||||
function spawn(callback) {
|
||||
log.info('Feed', 'Spawning feedcheck process');
|
||||
log.verbose('Feed', 'Spawning feedcheck process');
|
||||
|
||||
feedcheckProcess = fork(path.join(__dirname, '..', 'services', 'feedcheck.js'), [], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = {
|
|||
};
|
||||
|
||||
function spawn(callback) {
|
||||
log.info('Importer', 'Spawning importer process');
|
||||
log.verbose('Importer', 'Spawning importer process');
|
||||
|
||||
knex.transaction(async tx => {
|
||||
await tx('imports').where('status', ImportStatus.PREP_RUNNING).update({status: ImportStatus.PREP_SCHEDULED});
|
||||
|
|
|
@ -8,7 +8,7 @@ let messageTid = 0;
|
|||
let senderProcess;
|
||||
|
||||
function spawn(callback) {
|
||||
log.info('Senders', 'Spawning master sender process');
|
||||
log.verbose('Senders', 'Spawning master sender process');
|
||||
|
||||
senderProcess = fork(path.join(__dirname, '..', 'services', 'sender-master.js'), [], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
|
|
|
@ -15,7 +15,7 @@ const segments = require('./segments');
|
|||
const sendConfigurations = require('./send-configurations');
|
||||
const triggers = require('./triggers');
|
||||
|
||||
const allowedKeysCommon = ['name', 'description', 'list', 'segment', 'namespace',
|
||||
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||
|
||||
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
||||
|
@ -33,9 +33,11 @@ function hash(entity, content) {
|
|||
|
||||
if (content === Content.ALL) {
|
||||
filteredEntity = filterObject(entity, allowedKeysUpdate);
|
||||
filteredEntity.lists = entity.lists;
|
||||
|
||||
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
filteredEntity = filterObject(entity, allowedKeysUpdate);
|
||||
filteredEntity.lists = entity.lists;
|
||||
filteredEntity.data = {...filteredEntity.data};
|
||||
delete filteredEntity.data.sourceCustom;
|
||||
|
||||
|
@ -73,7 +75,7 @@ async function listWithContentDTAjax(context, params) {
|
|||
);
|
||||
}
|
||||
|
||||
async function listOthersByListDTAjax(context, campaignId, listId, params) {
|
||||
async function listOthersWhoseListsAreIncludedDTAjax(context, campaignId, listIds, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||
|
@ -81,17 +83,44 @@ async function listOthersByListDTAjax(context, campaignId, listId, params) {
|
|||
builder => builder.from('campaigns')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||
.whereNot('campaigns.id', campaignId)
|
||||
.where('campaigns.list', listId),
|
||||
.whereNotExists(qry => qry.from('campaign_lists').whereRaw('campaign_lists.campaign = campaigns.id').whereNotIn('campaign_lists.list', listIds)),
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function getByIdTx(tx, context, id, withPermissions = true, content = Content.ALL) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
|
||||
let entity = await tx('campaigns').where('id', id).first();
|
||||
async function rawGetByIdTx(tx, id) {
|
||||
const entity = await tx('campaigns').where('campaigns.id', id)
|
||||
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
||||
.groupBy('campaigns.id')
|
||||
.select([
|
||||
'campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
|
||||
'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject_override',
|
||||
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url',
|
||||
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', campaign_lists.list, campaign_lists.segment) ORDER BY campaign_lists.id SEPARATOR \';\') as lists`)
|
||||
])
|
||||
.first();
|
||||
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
entity.lists = entity.lists.split(';').map(x => {
|
||||
const entries = x.split(':');
|
||||
const list = Number.parseInt(entries[0]);
|
||||
const segment = entries[1] ? Number.parseInt(entries[1]) : null;
|
||||
return {list, segment};
|
||||
});
|
||||
|
||||
entity.data = JSON.parse(entity.data);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function getByIdTx(tx, context, id, withPermissions = true, content = Content.ALL) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
|
||||
|
||||
let entity = await rawGetByIdTx(tx, id);
|
||||
|
||||
if (content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
delete entity.data.sourceCustom;
|
||||
|
||||
|
@ -135,11 +164,13 @@ async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
|
|||
enforce(entity.source >= CampaignSource.MIN && entity.source <= CampaignSource.MAX, 'Unknown campaign source');
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.list, 'view');
|
||||
for (const lstSeg of entity.lists) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', lstSeg.list, 'view');
|
||||
|
||||
if (entity.segment) {
|
||||
// Check that the segment under the list exists
|
||||
await segments.getByIdTx(tx, context, entity.list, entity.segment);
|
||||
if (lstSeg.segment) {
|
||||
// Check that the segment under the list exists
|
||||
await segments.getByIdTx(tx, context, lstSeg.list, lstSeg.segment);
|
||||
}
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', entity.send_configuration, 'viewPublic');
|
||||
|
@ -218,6 +249,8 @@ async function _createTx(tx, context, entity, content) {
|
|||
const ids = await tx('campaigns').insert(filteredEntity);
|
||||
const id = ids[0];
|
||||
|
||||
await tx('campaign_lists').insert(entity.lists.map(x => ({campaign: id, ...x})));
|
||||
|
||||
await knex.schema.raw('CREATE TABLE `campaign__' + id + '` (\n' +
|
||||
' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
|
||||
' `list` int(10) unsigned NOT NULL,\n' +
|
||||
|
@ -279,12 +312,8 @@ async function updateWithConsistencyCheck(context, entity, content) {
|
|||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit');
|
||||
|
||||
const existing = await tx('campaigns').where('id', entity.id).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
const existing = await rawGetByIdTx(tx, entity.id);
|
||||
|
||||
existing.data = JSON.parse(existing.data);
|
||||
const existingHash = hash(existing, content);
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
|
@ -311,6 +340,9 @@ async function updateWithConsistencyCheck(context, entity, content) {
|
|||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
await tx('campaigns').where('id', entity.id).update(filteredEntity);
|
||||
|
||||
await tx('campaign_lists').where('campaign', entity.id).del();
|
||||
await tx('campaign_lists').insert(entity.lists.map(x => ({campaign: entity.id, ...x})));
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id });
|
||||
});
|
||||
}
|
||||
|
@ -345,7 +377,7 @@ Object.assign(module.exports, {
|
|||
hash,
|
||||
listDTAjax,
|
||||
listWithContentDTAjax,
|
||||
listOthersByListDTAjax,
|
||||
listOthersWhoseListsAreIncludedDTAjax,
|
||||
getByIdTx,
|
||||
getById,
|
||||
create,
|
||||
|
|
|
@ -34,10 +34,11 @@ async function listDTAjax(context, params) {
|
|||
['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name',
|
||||
{ query: builder =>
|
||||
builder.from('campaigns')
|
||||
.whereRaw('campaigns.list = lists.id')
|
||||
.innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`)
|
||||
.where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers')
|
||||
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
||||
.innerJoin('triggers', 'campaigns.id', 'triggers.campaign')
|
||||
.innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`)
|
||||
.whereRaw('campaign_lists.list = lists.id')
|
||||
.where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers')
|
||||
.count()
|
||||
}
|
||||
]
|
||||
|
|
|
@ -35,9 +35,8 @@ async function listByCampaignDTAjax(context, campaignId, params) {
|
|||
builder => builder
|
||||
.from('triggers')
|
||||
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
|
||||
.innerJoin('lists', 'lists.id', 'campaigns.list')
|
||||
.where('triggers.campaign', campaignId),
|
||||
[ 'triggers.id', 'triggers.name', 'triggers.description', 'lists.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled' ]
|
||||
[ 'triggers.id', 'triggers.name', 'triggers.description', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled' ]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -50,7 +49,8 @@ async function listByListDTAjax(context, listId, params) {
|
|||
builder => builder
|
||||
.from('triggers')
|
||||
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
|
||||
.where('campaigns.list', listId),
|
||||
.innerJoin('campaign_lists', 'campaign_lists.campaign', 'campaigns.id')
|
||||
.where('campaign_lists.list', listId),
|
||||
[ 'triggers.id', 'triggers.name', 'triggers.description', 'campaigns.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled', 'triggers.campaign' ]
|
||||
);
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ async function remove(context, campaignId, id) {
|
|||
}
|
||||
|
||||
async function removeAllByCampaignIdTx(tx, context, campaignId) {
|
||||
const entities = await tx('triggers').where('list', campaignId).select(['id']);
|
||||
const entities = await tx('triggers').where('campaign', campaignId).select(['id']);
|
||||
for (const entity of entities) {
|
||||
await removeTx(tx, context, campaignId, entity.id);
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req,
|
|||
return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/campaigns-others-by-list-table/:campaignId/:listId', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.listOthersByListDTAjax(req.context, req.params.campaignId, req.params.listId, req.body));
|
||||
router.postAsync('/campaigns-others-by-list-table/:campaignId/:listIds', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.listOthersWhoseListsAreIncludedDTAjax(req.context, req.params.campaignId, req.params.listIds.split(';'), req.body));
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -51,20 +51,20 @@ async function run() {
|
|||
|
||||
running = true;
|
||||
|
||||
let rssCampaign;
|
||||
let rssCampaignIdRow;
|
||||
|
||||
while (rssCampaign = await knex('campaigns')
|
||||
while (rssCampaignIdRow = await knex('campaigns')
|
||||
.where('type', CampaignType.RSS)
|
||||
.where('status', CampaignStatus.ACTIVE)
|
||||
.where(qry => qry.whereNull('last_check').orWhere('last_check', '<', new Date(Date.now() - feedCheckInterval)))
|
||||
// 'SELECT `id`, `source_url`, `from`, `address`, `subject`, `list`, `segment`, `html`, `open_tracking_disabled`, `click_tracking_disabled`
|
||||
.select('id')
|
||||
.first()) {
|
||||
|
||||
const rssCampaign = campaigns.getById(contextHelpers.getAdminContext(), rssCampaignIdRow.id);
|
||||
|
||||
let checkStatus = null;
|
||||
|
||||
try {
|
||||
rssCampaign.data = JSON.parse(rssCampaign.data);
|
||||
|
||||
const entries = await fetch(rssCampaign.data.feedUrl);
|
||||
|
||||
let added = 0;
|
||||
|
@ -95,8 +95,7 @@ async function run() {
|
|||
type: CampaignType.RSS_ENTRY,
|
||||
source,
|
||||
name: entry.title || `RSS entry ${entry.guid.substr(0, 67)}`,
|
||||
list: rssCampaign.list,
|
||||
segment: rssCampaign.segment,
|
||||
lists: rssCampaign.lists,
|
||||
namespace: rssCampaign.namespace,
|
||||
send_configuration: rssCampaign.send_configuration,
|
||||
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const fork = require('child_process').fork;
|
||||
const log = require('npmlog');
|
||||
const path = require('path');
|
||||
const knex = require('../lib/knex');
|
||||
|
||||
let messageTid = 0;
|
||||
let workerProcesses = new Map();
|
||||
|
||||
const numOfWorkerProcesses = 5;
|
||||
|
||||
let running = false;
|
||||
|
||||
/*
|
||||
const knex = require('../lib/knex');
|
||||
const path = require('path');
|
||||
const log = require('npmlog');
|
||||
const fsExtra = require('fs-extra-promise');
|
||||
|
@ -28,9 +27,16 @@ const shares = require('../models/shares');
|
|||
const _ = require('../lib/translate')._;
|
||||
*/
|
||||
|
||||
|
||||
async function processCampaign(campaignId) {
|
||||
const campaignSubscribersTable = 'campaign__' + campaignId;
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function spawnWorker(workerId) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
log.info('Senders', `Spawning worker process ${workerId}`);
|
||||
log.verbose('Senders', `Spawning worker process ${workerId}`);
|
||||
|
||||
const senderProcess = fork(path.join(__dirname, 'sender-worker.js'), [workerId], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
|
@ -79,7 +85,7 @@ function sendToWorker(workerId, msgType, data) {
|
|||
async function init() {
|
||||
const spawnWorkerFutures = [];
|
||||
let workerId;
|
||||
for (workerId = 0; workerId < numOfWorkerProcesses; workerId++) {
|
||||
for (workerId = 0; workerId < config.queue.processes; workerId++) {
|
||||
spawnWorkerFutures.push(spawnWorker(workerId));
|
||||
}
|
||||
|
||||
|
|
|
@ -841,8 +841,8 @@ async function migrateCampaigns(knex) {
|
|||
X | parent | int(10) unsigned | YES | MUL | NULL | |
|
||||
OK | name | varchar(255) | NO | MUL | | |
|
||||
OK | description | text | YES | | NULL | |
|
||||
OK | list | int(10) unsigned | NO | | NULL | |
|
||||
OK | segment | int(10) unsigned | YES | | NULL | |
|
||||
X | list | int(10) unsigned | NO | | NULL | |
|
||||
X | segment | int(10) unsigned | YES | | NULL | |
|
||||
X | template | int(10) unsigned | NO | | NULL | |
|
||||
X | source_url | varchar(255) | YES | | NULL | |
|
||||
X | editor_name | varchar(50) | YES | | | |
|
||||
|
@ -884,8 +884,16 @@ async function migrateCampaigns(knex) {
|
|||
scheduled - used only for campaign type NORMAL
|
||||
|
||||
parent - discarded because it duplicates the info in table `rss`. `rss` can be used to establish a db link between RSS campaign and its entries
|
||||
list, segment - held in campaign_lists table
|
||||
*/
|
||||
|
||||
await knex.schema.createTable('campaign_lists', table => {
|
||||
table.increments('id').primary();
|
||||
table.integer('campaign').unsigned().notNullable().references('campaigns.id').onDelete('CASCADE');
|
||||
table.integer('list').unsigned().notNullable().references('lists.id').onDelete('CASCADE');
|
||||
table.integer('segment').unsigned().references('segments.id').onDelete('CASCADE');
|
||||
});
|
||||
|
||||
await knex.schema.table('campaigns', table => {
|
||||
table.text('data', 'longtext');
|
||||
table.integer('source').unsigned().notNullable();
|
||||
|
@ -940,10 +948,18 @@ async function migrateCampaigns(knex) {
|
|||
|
||||
campaign.data = JSON.stringify(data);
|
||||
|
||||
await knex('campaign_lists').insert({
|
||||
campaign: campaign.id,
|
||||
list: campaign.list,
|
||||
segment: campaign.segment || null
|
||||
});
|
||||
|
||||
await knex('campaigns').where('id', campaign.id).update(campaign);
|
||||
}
|
||||
|
||||
await knex.schema.table('campaigns', table => {
|
||||
table.dropColumn('list');
|
||||
table.dropColumn('segment');
|
||||
table.dropColumn('template');
|
||||
table.dropColumn('source_url');
|
||||
table.dropColumn('editor_name');
|
||||
|
@ -1001,9 +1017,17 @@ async function migrateTriggers(knex) {
|
|||
const triggers = await knex('triggers');
|
||||
|
||||
for (const trigger of triggers) {
|
||||
const campaign = await knex('campaigns').where('id', trigger.campaign).first();
|
||||
const campaign = await knex('campaigns')
|
||||
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
||||
.groupBy('campaigns.id')
|
||||
.select(
|
||||
knex.raw(`GROUP_CONCAT(campaign_lists.list SEPARATOR \';\') as lists`)
|
||||
)
|
||||
.where('id', trigger.campaign).first();
|
||||
|
||||
enforce(campaign.list === trigger.list, 'The list of trigger and campaign have to be the same.');
|
||||
campaign.lists = campaign.lists.split(';').map(x => Number.parseInt(x));
|
||||
|
||||
enforce(campaign.lists.includes(trigger.list), 'The list of trigger and campaign have to be the same.');
|
||||
|
||||
enforce(trigger.entity in TriggerEntityVals);
|
||||
enforce(trigger.event in TriggerEventVals[trigger.entity]);
|
||||
|
|
Loading…
Reference in a new issue