- "Channels" feature
- Shoutout config param rendered on the homepage
- "Clone" feature for campaigns
This commit is contained in:
Tomas Bures 2020-07-17 14:53:48 +02:00
parent 00432e6cfe
commit d170548cfa
25 changed files with 1009 additions and 525 deletions

View file

@ -357,7 +357,7 @@ async function rawGetByTx(tx, key, id) {
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
.groupBy('campaigns.id')
.select([
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.channel', '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',
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url', 'campaigns.scheduled',
'campaigns.delivered', 'campaigns.unsubscribed', 'campaigns.bounced', 'campaigns.complained', 'campaigns.blacklisted', 'campaigns.opened', 'campaigns.clicks',
@ -412,6 +412,7 @@ async function getByIdTx(tx, context, id, withPermissions = true, content = Cont
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
entity = {
id: entity.id,
channel: entity.channel,
send_configuration: entity.send_configuration,
data: {
@ -454,6 +455,8 @@ async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
}
enforce(Number.isInteger(entity.source));
@ -481,6 +484,10 @@ async function _createTx(tx, context, entity, content) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
if (entity.channel) {
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.channel, 'createCampaign');
}
let copyFilesFrom = null;
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
copyFilesFrom = {
@ -570,6 +577,13 @@ async function createRssTx(tx, context, entity) {
return await _createTx(tx, context, entity, Content.RSS_ENTRY);
}
async function _validateChannelMoveTx(tx, context, entity, existing) {
if (existing.channel !== entity.channel) {
await shares.enforceEntityPermission(context, 'channel', entity.channel, 'createCampaign');
await shares.enforceEntityPermission(context, 'campaign', entity.id, 'delete');
}
}
async function updateWithConsistencyCheck(context, entity, content) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit');
@ -585,11 +599,13 @@ async function updateWithConsistencyCheck(context, entity, content) {
let filteredEntity = filterObject(entity, allowedKeysUpdate);
if (content === Content.ALL) {
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'campaign', 'createCampaign', 'delete');
await _validateChannelMoveTx(tx, context, entity, existing);
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
filteredEntity.data.sourceCustom = existing.data.sourceCustom;
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'campaign', 'createCampaign', 'delete');
await _validateChannelMoveTx(tx, context, entity, existing);
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
const data = existing.data;

View file

@ -16,8 +16,8 @@ const dependencyHelpers = require('../lib/dependency-helpers');
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const allowedKeys = ['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source'];
const allowedKeys = new Set(['name', 'description', 'namespace', 'cpg_name', 'cpg_description',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url', 'source']);
function hash(entity) {
@ -43,12 +43,27 @@ async function listDTAjax(context, params) {
);
}
async function _getByTx(tx, key, id, withPermissions = true) {
async function listWithCreateCampaignPermissionDTAjax(context, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'channel', requiredOperations: ['createCampaign'] }],
params,
builder => {
builder = builder.from('channels')
.innerJoin('namespaces', 'namespaces.id', 'channels.namespace');
return builder;
},
['channels.id', 'channels.name', 'channels.cid', 'channels.description', 'namespaces.name']
);
}
async function _getByTx(tx, context, key, id, withPermissions = true) {
const entity = await tx('channels').where('channels.' + key, id)
.leftJoin('channel_lists', 'channels.id', 'channel_lists.channel')
.groupBy('channels.id')
.select([
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.source',
'channels.id', 'channels.name', 'channels.cid', 'channels.description', 'channels.namespace', 'channels.cpg_name', 'channels.cpg_description', 'channels.source',
'channels.send_configuration', 'channels.from_name_override', 'channels.from_email_override', 'channels.reply_to_override', 'channels.subject',
'channels.data', 'channels.click_tracking_disabled', 'channels.open_tracking_disabled', 'channels.unsubscribe_url',
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', channel_lists.list, channel_lists.segment) ORDER BY channel_lists.id SEPARATOR \';\') as lists`)
@ -82,7 +97,7 @@ async function _getByTx(tx, key, id, withPermissions = true) {
async function getByIdTx(tx, context, id, withPermissions = true) {
await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'view');
return await _getByTx(tx, 'id', id, withPermissions);
return await _getByTx(tx, context, 'id', id, withPermissions);
}
async function getById(context, id, withPermissions = true) {
@ -97,12 +112,13 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
if (entity.source !== null) {
enforce(Number.isInteger(entity.source));
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.data.sourceCampaign, 'view');
} else if (entity.source === CampaignSource.CUSTOM) {
enforce(allTagLanguages.includes(entity.data.sourceCustom.tag_language), `Invalid tag language '${entity.data.sourceCustom.tag_language}'`);
} else if (entity.source === CampaignSource.URL) {
} else {
enforce(false, 'Unknown channel source');
}
@ -156,7 +172,7 @@ async function updateWithConsistencyCheck(context, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.id, 'edit');
const existing = await _getByTx(tx, 'id', entity.id, false);
const existing = await _getByTx(tx, context, 'id', entity.id, false);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
@ -166,7 +182,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, context, entity, false);
let filteredEntity = filterObject(entity, allowedKeys);
await namespaceHelpers.validateMove(context, entity, existing, 'channel', 'createCampaign', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'channel', 'createCampaign', 'delete');
await tx('channel_lists').where('channel', entity.id).del();
await tx('channel_lists').insert(entity.lists.map(x => ({channel: entity.id, ...x})));
@ -197,8 +213,8 @@ async function remove(context, id) {
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listWithCreateCampaignPermissionDTAjax = listWithCreateCampaignPermissionDTAjax;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;
module.exports.create = create;

View file

@ -169,7 +169,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'customForm', 'createCustomForm', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'customForm', 'createCustomForm', 'delete');
const form = filterObject(entity, allowedFormKeys);
enforce(!Object.keys(checkForMjmlErrors(form)).length, 'Error(s) in form templates');

View file

@ -263,7 +263,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'list', 'createList', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'list', 'createList', 'delete');
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -89,7 +89,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
await tx('mosaico_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -198,7 +198,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
// namespaceHelpers.validateEntity is not needed here because it is part of the tree traversal check below
await namespaceHelpers.validateMove(context, entity, existing, 'namespace', 'createNamespace', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'namespace', 'createNamespace', 'delete');
let iter = entity;
while (iter.namespace != null) {

View file

@ -66,7 +66,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'reportTemplate', 'createReportTemplate', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'reportTemplate', 'createReportTemplate', 'delete');
await tx('report_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -102,7 +102,7 @@ async function updateWithConsistencyCheck(context, entity) {
}
await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'report', 'createReport', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'report', 'createReport', 'delete');
entity.params = JSON.stringify(entity.params);

View file

@ -155,7 +155,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'sendConfiguration', 'createSendConfiguration', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'sendConfiguration', 'createSendConfiguration', 'delete');
await tx('send_configurations').where('id', entity.id).update(filterObject(entity, allowedKeys));

View file

@ -132,7 +132,7 @@ async function updateWithConsistencyCheck(context, entity) {
await _validateAndPreprocess(tx, entity);
entity.data = JSON.stringify(entity.data);
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');
await namespaceHelpers.validateMoveTx(tx, context, entity, existing, 'template', 'createTemplate', 'delete');
const filteredEntity = filterObject(entity, allowedKeys);