Campaign UI and model adjusted to allow sending a campaign to multiple lists

This commit is contained in:
Tomas Bures 2018-09-02 20:17:42 +02:00
parent 130c953d94
commit 67d7129f7b
16 changed files with 334 additions and 78 deletions

View file

@ -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/>

View 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;
}

View file

@ -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')}/>

View file

@ -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 = [];

View file

@ -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:

View file

@ -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, '..'),

View file

@ -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, '..'),

View file

@ -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});

View file

@ -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, '..'),

View file

@ -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,

View file

@ -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()
}
]

View file

@ -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);
}

View file

@ -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));
});

View file

@ -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,

View file

@ -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));
}

View file

@ -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]);