diff --git a/client/src/campaigns/CUD.js b/client/src/campaigns/CUD.js
index ad0f81b4..2782ac3e 100644
--- a/client/src/campaigns/CUD.js
+++ b/client/src/campaigns/CUD.js
@@ -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(
+
+
+ {lsts.length > 1 &&
+
+
+
+
+ {(campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) &&
+
+
+ {selectedList && this.getFormValue(prefix + 'useSegmentation') &&
+
+ }
+
+ }
+
+
+ );
+
+ lstOrderIdx += 1;
+ }
+
+ const lstsEdit =
+ ;
+
+
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 (
{canDelete &&
@@ -520,12 +681,7 @@ export default class CUD extends Component {
-
-
-
- {this.getFormValue('useSegmentation') &&
-
- }
+ {lstsEdit}
diff --git a/client/src/campaigns/styles.scss b/client/src/campaigns/styles.scss
new file mode 100644
index 00000000..5c314cfc
--- /dev/null
+++ b/client/src/campaigns/styles.scss
@@ -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;
+}
diff --git a/client/src/campaigns/triggers/CUD.js b/client/src/campaigns/triggers/CUD.js
index 462eff24..9168a60b 100644
--- a/client/src/campaigns/triggers/CUD.js
+++ b/client/src/campaigns/triggers/CUD.js
@@ -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 (
{isEdit &&
@@ -221,7 +223,7 @@ export default class CUD extends Component {
{entityKey === Entity.CAMPAIGN &&
}
{entityKey === Entity.CAMPAIGN &&
-
+
}
diff --git a/client/src/campaigns/triggers/List.js b/client/src/campaigns/triggers/List.js
index f40c319e..ef5913bc 100644
--- a/client/src/campaigns/triggers/List.js
+++ b/client/src/campaigns/triggers/List.js
@@ -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 = [];
diff --git a/config/default.yaml b/config/default.yaml
index 96239747..b7fa9f78 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -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:
diff --git a/lib/executor.js b/lib/executor.js
index dc9ace0e..894c1ccd 100644
--- a/lib/executor.js
+++ b/lib/executor.js
@@ -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, '..'),
diff --git a/lib/feedcheck.js b/lib/feedcheck.js
index c48fd35a..57a24a90 100644
--- a/lib/feedcheck.js
+++ b/lib/feedcheck.js
@@ -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, '..'),
diff --git a/lib/importer.js b/lib/importer.js
index a5547bec..ef59e421 100644
--- a/lib/importer.js
+++ b/lib/importer.js
@@ -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});
diff --git a/lib/senders.js b/lib/senders.js
index 65cfe36b..564a546f 100644
--- a/lib/senders.js
+++ b/lib/senders.js
@@ -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, '..'),
diff --git a/models/campaigns.js b/models/campaigns.js
index c4666b42..ae30a3d1 100644
--- a/models/campaigns.js
+++ b/models/campaigns.js
@@ -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,
diff --git a/models/lists.js b/models/lists.js
index 9d136502..7ed9e919 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -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()
}
]
diff --git a/models/triggers.js b/models/triggers.js
index bf1cc335..4434651b 100644
--- a/models/triggers.js
+++ b/models/triggers.js
@@ -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);
}
diff --git a/routes/rest/campaigns.js b/routes/rest/campaigns.js
index 0d4f6d5e..cb1791ce 100644
--- a/routes/rest/campaigns.js
+++ b/routes/rest/campaigns.js
@@ -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));
});
diff --git a/services/feedcheck.js b/services/feedcheck.js
index c3ccb411..792883bf 100644
--- a/services/feedcheck.js
+++ b/services/feedcheck.js
@@ -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,
diff --git a/services/sender-master.js b/services/sender-master.js
index 7fe59a51..35ab82e6 100644
--- a/services/sender-master.js
+++ b/services/sender-master.js
@@ -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));
}
diff --git a/setup/knex/migrations/20170506102634_v1_to_v2.js b/setup/knex/migrations/20170506102634_v1_to_v2.js
index c021b018..71b15a09 100644
--- a/setup/knex/migrations/20170506102634_v1_to_v2.js
+++ b/setup/knex/migrations/20170506102634_v1_to_v2.js
@@ -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]);