Work in progress on Campaign Status
This commit is contained in:
parent
d1fa4f4211
commit
01d1a903a2
7 changed files with 116 additions and 84 deletions
|
@ -170,7 +170,7 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const overridable of campaignOverridables) {
|
for (const overridable of campaignOverridables) {
|
||||||
data[overridable + '_overriden'] = data[overridable + '_override'] === null;
|
data[overridable + '_overriden'] = data[overridable + '_override'] !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lsts = [];
|
const lsts = [];
|
||||||
|
|
|
@ -243,11 +243,11 @@ class SendControls extends Component {
|
||||||
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
|
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<AlignedRow label={t('Send status')}>
|
||||||
{t('Campaign is being sent out.')}
|
{t('Campaign is being sent out.')}
|
||||||
</div>
|
</AlignedRow>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button className="btn-primary" icon="stop" label={t('Stop')} onClickAsync={::this.pauseAsync}/>
|
<Button className="btn-primary" icon="stop" label={t('Stop')} onClickAsync={::this.stopAsync}/>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -255,9 +255,9 @@ class SendControls extends Component {
|
||||||
} else if (entity.status === CampaignStatus.FINISHED) {
|
} else if (entity.status === CampaignStatus.FINISHED) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<AlignedRow label={t('Send status')}>
|
||||||
{t('All messages sent! Hit "Continue" if you you want to send this campaign to new subscribers.')}
|
{t('All messages sent! Hit "Continue" if you you want to send this campaign to new subscribers.')}
|
||||||
</div>
|
</AlignedRow>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button className="btn-primary" icon="play" label={t('Continue')} onClickAsync={::this.startAsync}/>
|
<Button className="btn-primary" icon="play" label={t('Continue')} onClickAsync={::this.startAsync}/>
|
||||||
<Button className="btn-primary" icon="refresh" label={t('Reset')} onClickAsync={::this.resetAsync}/>
|
<Button className="btn-primary" icon="refresh" label={t('Reset')} onClickAsync={::this.resetAsync}/>
|
||||||
|
@ -356,8 +356,8 @@ export default class Status extends Component {
|
||||||
const listsColumns = [
|
const listsColumns = [
|
||||||
{ data: 1, title: t('Name') },
|
{ data: 1, title: t('Name') },
|
||||||
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
|
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
|
||||||
{ data: 3, title: t('Namespace') },
|
{ data: 4, title: t('Segment') },
|
||||||
{ data: 4, title: t('Segment') }
|
{ data: 3, title: t('List namespace') }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default class List extends Component {
|
||||||
const segments = this.props.segments;
|
const segments = this.props.segments;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ data: 1, title: t('CID') },
|
{ data: 1, title: t('ID'), render: data => <code>{data}</code> },
|
||||||
{ data: 2, title: t('Email') },
|
{ data: 2, title: t('Email') },
|
||||||
{ data: 3, title: t('Status'), render: (data, display, rowData) => this.subscriptionStatusLabels[data] + (rowData[5] ? ', ' + t('Blacklisted') : '') },
|
{ data: 3, title: t('Status'), render: (data, display, rowData) => this.subscriptionStatusLabels[data] + (rowData[5] ? ', ' + t('Blacklisted') : '') },
|
||||||
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
|
{ data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }
|
||||||
|
|
|
@ -8,7 +8,7 @@ const knex = require('knex')({
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: __dirname + '/../setup/knex/migrations'
|
directory: __dirname + '/../setup/knex/migrations'
|
||||||
}
|
}
|
||||||
, debug: true
|
//, debug: true
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = knex;
|
module.exports = knex;
|
||||||
|
|
|
@ -97,46 +97,66 @@ async function listTestUsersDTAjax(context, campaignId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'view');
|
||||||
|
|
||||||
const subscriptionsQueries = [];
|
/*
|
||||||
|
This is supposed to produce queries like this:
|
||||||
|
|
||||||
|
select * from (
|
||||||
|
(select `subscription__1`.`email`, 2 AS campaign_list_id, 1 AS list, NULL AS segment from `subscription__1` left join `campaign_messages` on
|
||||||
|
`campaign_messages`.`subscription` = `subscription__1`.`id` where `subscription__1`.`status` = 1 and `subscription__1`.`is_test` = true)
|
||||||
|
UNION ALL
|
||||||
|
(select `subscription__2`.`email`, 4 AS campaign_list_id, 2 AS list, NULL AS segment from `subscription__2` left join `campaign_messages` on
|
||||||
|
`campaign_messages`.`subscription` = `subscription__2`.`id` where `subscription__2`.`status` = 1 and `subscription__2`.`is_test` = true)
|
||||||
|
) as `test_subscriptions` inner join `lists` on `test_subscriptions`.`list` = `lists`.`id` inner join `segments` on `test_subscriptions`.`segment` = `segments`.`id`
|
||||||
|
inner join `namespaces` on `lists`.`namespace` = `namespaces`.`id`
|
||||||
|
|
||||||
|
This was too much for Knex, so we partially construct these queries directly as strings;
|
||||||
|
*/
|
||||||
|
|
||||||
|
const subsQrys = [];
|
||||||
const cpgLists = await tx('campaign_lists').where('campaign', campaignId);
|
const cpgLists = await tx('campaign_lists').where('campaign', campaignId);
|
||||||
|
|
||||||
for (const cpgList of cpgLists) {
|
for (const cpgList of cpgLists) {
|
||||||
const addSegmentQuery = cpgList.segment ? await segments.getQueryGeneratorTx(tx, cpgList.list, cpgList.segment) : () => {
|
const addSegmentQuery = cpgList.segment ? await segments.getQueryGeneratorTx(tx, cpgList.list, cpgList.segment) : () => {};
|
||||||
};
|
|
||||||
const subsTable = subscriptions.getSubscriptionTableName(cpgList.list);
|
const subsTable = subscriptions.getSubscriptionTableName(cpgList.list);
|
||||||
|
|
||||||
subscriptionsQueries.push(function () {
|
const sqlQry = knex.from(subsTable)
|
||||||
this.from(subsTable)
|
.where(subsTable + '.status', SubscriptionStatus.SUBSCRIBED)
|
||||||
.where(subsTable + '.status', SubscriptionStatus.SUBSCRIBED)
|
.where(subsTable + '.is_test', true)
|
||||||
.where(subsTable + '.is_test', true)
|
.where(function() {
|
||||||
.where(function() {
|
addSegmentQuery(this);
|
||||||
addSegmentQuery(this);
|
})
|
||||||
})
|
.select([subsTable + '.email', knex.raw('? AS campaign_list_id', [cpgList.id]), knex.raw('? AS list', [cpgList.list]), knex.raw('? AS segment', [cpgList.segment])])
|
||||||
.select([subsTable + '.email', knex.raw('? AS campaign_list_id', [cpgList.id]), knex.raw('? AS list', [cpgList.list]), knex.raw('? AS segment', [cpgList.segment])]);
|
.toSQL().toNative();
|
||||||
});
|
|
||||||
|
subsQrys.push(sqlQry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionsQueries.length > 0) {
|
if (subsQrys.length > 0) {
|
||||||
|
let subsQry;
|
||||||
|
|
||||||
|
if (subsQrys.length === 1) {
|
||||||
|
const subsUnionSql = '(' + subsQrys[0].sql + ') as `test_subscriptions`'
|
||||||
|
subsQry = knex.raw(subsUnionSql, subsQrys[0].bindings);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const subsUnionSql = '(' +
|
||||||
|
subsQrys.map(qry => '(' + qry.sql + ')').join(' UNION ALL ') +
|
||||||
|
') as `test_subscriptions`';
|
||||||
|
const subsUnionBindings = Array.prototype.concat(...subsQrys.map(qry => qry.bindings));
|
||||||
|
subsQry = knex.raw(subsUnionSql, subsUnionBindings);
|
||||||
|
}
|
||||||
|
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
context,
|
context,
|
||||||
[{ entityTypeId: 'list', requiredOperations: ['viewSubscriptions'] }],
|
[{ entityTypeId: 'list', requiredOperations: ['viewSubscriptions'] }],
|
||||||
params,
|
params,
|
||||||
builder => {
|
builder => {
|
||||||
let ret;
|
const qry = builder.from(subsQry)
|
||||||
if (subscriptionsQueries.length > 1) {
|
|
||||||
ret = builder.unionAll(subscriptionsQueries, true)
|
|
||||||
.as('test_subscriptions');
|
|
||||||
} else {
|
|
||||||
ret = builder.from(function () { subscriptionsQueries[0].apply(this); this.as('test_subscriptions'); })
|
|
||||||
.as('test_subscriptions');
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = ret
|
|
||||||
.innerJoin('lists', 'test_subscriptions.list', 'lists.id')
|
.innerJoin('lists', 'test_subscriptions.list', 'lists.id')
|
||||||
.innerJoin('segments', 'test_subscriptions.segment', 'segments.id')
|
.leftJoin('segments', 'test_subscriptions.segment', 'segments.id')
|
||||||
.innerJoin('namespaces', 'lists.namespace', 'namespaces.id');
|
.innerJoin('namespaces', 'lists.namespace', 'namespaces.id')
|
||||||
|
|
||||||
return ret;
|
return qry
|
||||||
},
|
},
|
||||||
['test_subscriptions.campaign_list_id', 'test_subscriptions.email', 'test_subscriptions.list', 'test_subscriptions.segment', 'lists.cid', 'lists.name', 'segments.name', 'namespaces.name']
|
['test_subscriptions.campaign_list_id', 'test_subscriptions.email', 'test_subscriptions.list', 'test_subscriptions.segment', 'lists.cid', 'lists.name', 'segments.name', 'namespaces.name']
|
||||||
);
|
);
|
||||||
|
@ -156,7 +176,7 @@ async function listTestUsersDTAjax(context, campaignId, params) {
|
||||||
|
|
||||||
async function rawGetByIdTx(tx, id) {
|
async function rawGetByIdTx(tx, id) {
|
||||||
const entity = await tx('campaigns').where('campaigns.id', id)
|
const entity = await tx('campaigns').where('campaigns.id', id)
|
||||||
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
||||||
.groupBy('campaigns.id')
|
.groupBy('campaigns.id')
|
||||||
.select([
|
.select([
|
||||||
'campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
|
'campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
|
||||||
|
@ -170,12 +190,16 @@ async function rawGetByIdTx(tx, id) {
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.lists = entity.lists.split(';').map(x => {
|
if (entity.lists) {
|
||||||
const entries = x.split(':');
|
entity.lists = entity.lists.split(';').map(x => {
|
||||||
const list = Number.parseInt(entries[0]);
|
const entries = x.split(':');
|
||||||
const segment = entries[1] ? Number.parseInt(entries[1]) : null;
|
const list = Number.parseInt(entries[0]);
|
||||||
return {list, segment};
|
const segment = entries[1] ? Number.parseInt(entries[1]) : null;
|
||||||
});
|
return {list, segment};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
entity.lists = [];
|
||||||
|
}
|
||||||
|
|
||||||
entity.data = JSON.parse(entity.data);
|
entity.data = JSON.parse(entity.data);
|
||||||
|
|
||||||
|
@ -546,54 +570,63 @@ async function changeStatusByMessage(context, message, subscriptionStatus, updat
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscribersQueryGeneratorTx(tx, campaignId, onlyUnsent, batchSize) {
|
async function getSubscribersQueryGeneratorTx(tx, campaignId, onlyUnsent, batchSize) {
|
||||||
const subscriptionsQueries = [];
|
/*
|
||||||
|
This is supposed to produce queries like this:
|
||||||
|
|
||||||
|
select count(*) as `subscriptionsToSend` from `campaign_lists` inner join (
|
||||||
|
select `email`, min(`campaign_list_id`) as `campaign_list_id`, max(`sent`) as `sent` from (
|
||||||
|
(select `subscription__1`.`email`, 2 AS campaign_list_id, campaign_messages.id IS NOT NULL AS sent from `subscription__1` left join `campaign_messages` on
|
||||||
|
`campaign_messages`.`subscription` = `subscription__1`.`id` where `campaign_messages`.`campaign` = 1 and `campaign_messages`.`list` = 1 and `subscription__1`.`status` = 1)
|
||||||
|
UNION ALL
|
||||||
|
(select `subscription__2`.`email`, 4 AS campaign_list_id, campaign_messages.id IS NOT NULL AS sent from `subscription__2` left join `campaign_messages` on
|
||||||
|
`campaign_messages`.`subscription` = `subscription__2`.`id` where `campaign_messages`.`campaign` = 1 and `campaign_messages`.`list` = 2 and `subscription__2`.`status` = 1)
|
||||||
|
)
|
||||||
|
as `pending_subscriptions_all` where `sent` = false group by `email`
|
||||||
|
) as `pending_subscriptions` on `campaign_lists`.`id` = `pending_subscriptions`.`campaign_list_id` where `campaign_lists`.`campaign` = 1 limit 1
|
||||||
|
|
||||||
|
This was too much for Knex, so we partially construct these queries directly as strings;
|
||||||
|
*/
|
||||||
|
|
||||||
|
const subsQrys = [];
|
||||||
const cpgLists = await tx('campaign_lists').where('campaign', campaignId);
|
const cpgLists = await tx('campaign_lists').where('campaign', campaignId);
|
||||||
|
|
||||||
for (const cpgList of cpgLists) {
|
for (const cpgList of cpgLists) {
|
||||||
const addSegmentQuery = cpgList.segment ? await segments.getQueryGeneratorTx(tx, cpgList.list, cpgList.segment) : () => {};
|
const addSegmentQuery = cpgList.segment ? await segments.getQueryGeneratorTx(tx, cpgList.list, cpgList.segment) : () => {};
|
||||||
const subsTable = subscriptions.getSubscriptionTableName(cpgList.list);
|
const subsTable = subscriptions.getSubscriptionTableName(cpgList.list);
|
||||||
|
|
||||||
subscriptionsQueries.push(function() {
|
const sqlQry = knex.from(subsTable)
|
||||||
this.from(subsTable)
|
.leftJoin('campaign_messages', 'campaign_messages.subscription', subsTable + '.id')
|
||||||
.leftJoin('campaign_messages', 'campaign_messages.subscription', subsTable + '.id')
|
.where('campaign_messages.campaign', cpgList.campaign)
|
||||||
.where('campaign_messages.campaign', cpgList.campaign)
|
.where('campaign_messages.list', cpgList.list)
|
||||||
.where('campaign_messages.list', cpgList.list)
|
.where(subsTable + '.status', SubscriptionStatus.SUBSCRIBED)
|
||||||
.where(subsTable + '.status', SubscriptionStatus.SUBSCRIBED)
|
.where(function() {
|
||||||
.where(function() {
|
addSegmentQuery(this);
|
||||||
addSegmentQuery(this);
|
})
|
||||||
})
|
.select([subsTable + '.email', knex.raw('? AS campaign_list_id', [cpgList.id]), knex.raw('campaign_messages.id IS NOT NULL AS sent')])
|
||||||
.select([subsTable + '.email', knex.raw('? AS campaign_list_id', [cpgList.id]), knex.raw('campaign_messages.id IS NOT NULL AS sent')]);
|
.toSQL().toNative();
|
||||||
});
|
|
||||||
|
subsQrys.push(sqlQry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionsQueries.length > 0) {
|
if (subsQrys.length > 0) {
|
||||||
|
let subsQry;
|
||||||
|
const unsentWhere = onlyUnsent ? ' where `sent` = false' : '';
|
||||||
|
|
||||||
|
if (subsQrys.length === 1) {
|
||||||
|
const subsUnionSql = '(select `email`, `campaign_list_id`, `sent` from (' + subsQrys[0].sql + ') as `pending_subscriptions_all`' + unsentWhere + ') as `pending_subscriptions`'
|
||||||
|
subsQry = knex.raw(subsUnionSql, subsQrys[0].bindings);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const subsUnionSql = '(select `email`, min(`campaign_list_id`) as `campaign_list_id`, max(`sent`) as `sent` from (' +
|
||||||
|
subsQrys.map(qry => '(' + qry.sql + ')').join(' UNION ALL ') +
|
||||||
|
') as `pending_subscriptions_all`' + unsentWhere + ' group by `email`) as `pending_subscriptions`';
|
||||||
|
const subsUnionBindings = Array.prototype.concat(...subsQrys.map(qry => qry.bindings));
|
||||||
|
subsQry = knex.raw(subsUnionSql, subsUnionBindings);
|
||||||
|
}
|
||||||
|
|
||||||
return knx => knx.from('campaign_lists')
|
return knx => knx.from('campaign_lists')
|
||||||
.where('campaign_lists.campaign', campaignId)
|
.where('campaign_lists.campaign', campaignId)
|
||||||
.innerJoin(
|
.innerJoin(subsQry, 'campaign_lists.id', 'pending_subscriptions.campaign_list_id');
|
||||||
function () {
|
|
||||||
let ret;
|
|
||||||
if (subscriptionsQueries.length > 1) {
|
|
||||||
ret = this.unionAll(subscriptionsQueries, true)
|
|
||||||
.groupBy('email')
|
|
||||||
.select(['email']).min('campaign_list_id AS campaign_list_id')
|
|
||||||
.select(['sent']).max('sent AS sent');
|
|
||||||
} else {
|
|
||||||
ret = this.from(function () { subscriptionsQueries[0].apply(this); this.as('pending_subscriptions'); })
|
|
||||||
.select(['email', 'sent', 'campaign_list_id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = ret.where('sent', false)
|
|
||||||
.as('pending_subscriptions');
|
|
||||||
|
|
||||||
if (batchSize) {
|
|
||||||
ret = ret.limit(retrieveBatchSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
'campaign_lists.id',
|
|
||||||
'pending_subscriptions.campaign_list_id'
|
|
||||||
);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -624,7 +657,7 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
|
||||||
|
|
||||||
|
|
||||||
async function start(context, campaignId, startAt) {
|
async function start(context, campaignId, startAt) {
|
||||||
await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.PAUSED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE or PAUSED state', startAt);
|
await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.PAUSED, CampaignStatus.FINISHED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE or PAUSED state', startAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stop(context, campaignId) {
|
async function stop(context, campaignId) {
|
||||||
|
|
|
@ -137,7 +137,6 @@ function prepareCsv(impt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _execImportRun(impt, handlers) {
|
async function _execImportRun(impt, handlers) {
|
||||||
// FIXME - handle STOPPING
|
|
||||||
try {
|
try {
|
||||||
let imptRun;
|
let imptRun;
|
||||||
|
|
||||||
|
|
|
@ -1062,7 +1062,7 @@ async function migrateTriggers(knex) {
|
||||||
|
|
||||||
for (const trigger of triggers) {
|
for (const trigger of triggers) {
|
||||||
const campaign = await knex('campaigns')
|
const campaign = await knex('campaigns')
|
||||||
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
|
.innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign') // We assume here that every campaign has a list.
|
||||||
.groupBy('campaigns.id')
|
.groupBy('campaigns.id')
|
||||||
.select(
|
.select(
|
||||||
knex.raw(`GROUP_CONCAT(campaign_lists.list SEPARATOR \';\') as lists`)
|
knex.raw(`GROUP_CONCAT(campaign_lists.list SEPARATOR \';\') as lists`)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue