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 = +
+ {lstsEditEntries} +
+
+
; + + 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]);