,
action: () => this.deleteShare(entityTypeId, data[2])
});
}
@@ -72,8 +75,6 @@ export default class UserShares extends Component {
);
};
- const t = this.props.t;
-
return (
{t('Shares for user "{{username}}"', {username: this.props.user.username})}
diff --git a/client/src/users/CUD.js b/client/src/users/CUD.js
index 47045a5e..edfc8523 100644
--- a/client/src/users/CUD.js
+++ b/client/src/users/CUD.js
@@ -10,7 +10,7 @@ import interoperableErrors from '../../../shared/interoperable-errors';
import passwordValidator from '../../../shared/password-validator';
import mailtrainConfig from 'mailtrainConfig';
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
-import {DeleteModalDialog} from "../lib/delete";
+import {DeleteModalDialog} from "../lib/modals";
@translate()
@withForm
diff --git a/client/src/users/List.js b/client/src/users/List.js
index e688666d..8cdafe86 100644
--- a/client/src/users/List.js
+++ b/client/src/users/List.js
@@ -5,6 +5,7 @@ import { translate } from 'react-i18next';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
import { Table } from '../lib/table';
import mailtrainConfig from 'mailtrainConfig';
+import {Icon} from "../lib/bootstrap-components";
@translate()
@withPageHelpers
@@ -34,11 +35,11 @@ export default class List extends Component {
columns.push({
actions: data => [
{
- label: ,
+ label: ,
link: `/users/${data[0]}/edit`
},
{
- label: ,
+ label: ,
link: `/users/${data[0]}/shares`
}
]
diff --git a/models/fields.js b/models/fields.js
index abecc638..b446f48c 100644
--- a/models/fields.js
+++ b/models/fields.js
@@ -18,55 +18,82 @@ const hashKeys = allowedKeysCreate;
const fieldTypes = {};
+const Cardinality = {
+ SINGLE: 0,
+ MULTIPLE: 1
+};
+
fieldTypes.text = fieldTypes.website = {
validate: entity => {},
addColumn: (table, name) => table.string(name),
indexed: true,
- grouped: false
+ grouped: false,
+ enumerated: false,
+ cardinality: Cardinality.SINGLE
};
fieldTypes.longtext = fieldTypes.gpg = {
validate: entity => {},
addColumn: (table, name) => table.text(name),
indexed: false,
- grouped: false
+ grouped: false,
+ enumerated: false,
+ cardinality: Cardinality.SINGLE
};
fieldTypes.json = {
validate: entity => {},
addColumn: (table, name) => table.json(name),
indexed: false,
- grouped: false
+ grouped: false,
+ enumerated: false,
+ cardinality: Cardinality.SINGLE
};
fieldTypes.number = {
validate: entity => {},
addColumn: (table, name) => table.integer(name),
indexed: true,
- grouped: false
+ grouped: false,
+ enumerated: false,
+ cardinality: Cardinality.SINGLE
};
-fieldTypes.checkbox = fieldTypes['radio-grouped'] = fieldTypes['dropdown-grouped'] = {
+fieldTypes['checkbox-grouped'] = {
validate: entity => {},
indexed: true,
- grouped: true
+ grouped: true,
+ enumerated: false,
+ cardinality: Cardinality.MULTIPLE
+};
+
+fieldTypes['radio-grouped'] = fieldTypes['dropdown-grouped'] = {
+ validate: entity => {},
+ indexed: true,
+ grouped: true,
+ enumerated: false,
+ cardinality: Cardinality.SINGLE
};
fieldTypes['radio-enum'] = fieldTypes['dropdown-enum'] = {
validate: entity => {
enforce(entity.settings.options, 'Options missing in settings');
- enforce(Object.keys(entity.settings.options).includes(entity.default_value), 'Default value not present in options');
+ enforce(entity.default_value === null || entity.settings.options.find(x => x.key === entity.default_value), 'Default value not present in options');
},
addColumn: (table, name) => table.string(name),
indexed: true,
- grouped: false
+ grouped: false,
+ enumerated: true,
+ cardinality: Cardinality.SINGLE
};
fieldTypes.option = {
validate: entity => {},
addColumn: (table, name) => table.boolean(name),
indexed: true,
- grouped: false
+ grouped: false,
+ enumerated: false,
+ cardinality: Cardinality.SINGLE
};
fieldTypes['date'] = fieldTypes['birthday'] = {
@@ -75,11 +102,17 @@ fieldTypes['date'] = fieldTypes['birthday'] = {
},
addColumn: (table, name) => table.dateTime(name),
indexed: true,
- grouped: false
+ grouped: false,
+ enumerated: false,
+ cardinality: Cardinality.SINGLE
};
const groupedTypes = Object.keys(fieldTypes).filter(key => fieldTypes[key].grouped);
+function getFieldType(type) {
+ return fieldTypes[type];
+}
+
function hash(entity) {
return hasher.hash(filterObject(entity, hashKeys));
}
@@ -90,6 +123,8 @@ async function getById(context, listId, id) {
const entity = await tx('custom_fields').where({list: listId, id}).first();
+ entity.settings = JSON.parse(entity.settings);
+
const orderFields = {
order_list: 'orderListBefore',
order_subscribe: 'orderSubscribeBefore',
@@ -114,16 +149,55 @@ async function getById(context, listId, id) {
}
async function listTx(tx, listId) {
- return await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'key', 'column', 'order_list', 'order_subscribe', 'order_manage']).orderBy('id', 'asc');
+ return await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'key', 'column', 'order_list', 'settings', 'group', 'order_subscribe', 'order_manage']).orderBy(knex.raw('-order_list'), 'desc').orderBy('id', 'asc');
}
async function list(context, listId) {
return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['manageFields', 'manageSegments']);
return await listTx(tx, listId);
});
}
+async function listGroupedTx(tx, listId) {
+ const flds = await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'column', 'settings', 'group', 'default_value']).orderBy(knex.raw('-order_list'), 'desc').orderBy('id', 'asc');
+
+ const fldsById = {};
+ for (const fld of flds) {
+ fld.settings = JSON.parse(fld.settings);
+
+ fldsById[fld.id] = fld;
+
+ if (fieldTypes[fld.type].grouped) {
+ fld.settings.options = [];
+ fld.groupedOptions = {};
+ }
+ }
+
+ for (const fld of flds) {
+ if (fld.group) {
+ const group = fldsById[fld.group];
+ group.settings.options.push({ key: fld.column, label: fld.name });
+ group.groupedOptions[fld.column] = fld;
+ }
+ }
+
+ const groupedFlds = flds.filter(fld => !fld.group);
+
+ for (const fld of flds) {
+ delete fld.group;
+ }
+
+ return groupedFlds;
+}
+
+async function listGrouped(context, listId) {
+ return await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['manageSubscriptions']);
+ return await listGroupedTx(tx, listId);
+ });
+}
+
async function listByOrderListTx(tx, listId, extraColumns = []) {
return await tx('custom_fields').where({list: listId}).whereNotNull('order_list').select(['name', ...extraColumns]).orderBy('order_list', 'asc');
}
@@ -241,7 +315,7 @@ async function _validateAndPreprocess(tx, listId, entity, isCreate) {
enforce(validators.mergeTagValid(entity.key), 'Merge tag is not valid.');
- const existingWithKeyQuery = knex('custom_fields').where({
+ const existingWithKeyQuery = tx('custom_fields').where({
list: listId,
key: entity.key
});
@@ -349,6 +423,7 @@ async function updateWithConsistencyCheck(context, listId, entity) {
throw new interoperableErrors.NotFoundError();
}
+ existing.settings = JSON.parse(existing.settings);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError();
@@ -357,7 +432,7 @@ async function updateWithConsistencyCheck(context, listId, entity) {
enforce(entity.type === existing.type, 'Field type cannot be changed');
await _validateAndPreprocess(tx, listId, entity, false);
- await tx('custom_fields').where('id', entity.id).update(filterObject(entity, allowedKeysUpdate));
+ await tx('custom_fields').where({list: listId, id: entity.id}).update(filterObject(entity, allowedKeysUpdate));
await _sortIn(tx, listId, entity.id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
});
}
@@ -401,10 +476,14 @@ async function removeAllByListIdTx(tx, context, listId) {
// This is to handle circular dependency with segments.js
Object.assign(module.exports, {
+ Cardinality,
+ getFieldType,
hash,
getById,
list,
listTx,
+ listGrouped,
+ listGroupedTx,
listByOrderListTx,
listDTAjax,
listGroupedDTAjax,
diff --git a/models/segments.js b/models/segments.js
index be0fb9f8..82f4fe98 100644
--- a/models/segments.js
+++ b/models/segments.js
@@ -214,7 +214,7 @@ function hash(entity) {
async function listDTAjax(context, listId, params) {
return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageSegments']);
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
return await dtHelpers.ajaxListTx(
tx,
@@ -227,7 +227,7 @@ async function listDTAjax(context, listId, params) {
});
}
-async function list(context, listId) {
+async function listIdName(context, listId) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageSegments']);
@@ -237,7 +237,7 @@ async function list(context, listId) {
async function getById(context, listId, id) {
return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageSegments']);
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
const entity = await tx('segments').where({id, list: listId}).first();
entity.settings = JSON.parse(entity.settings);
return entity;
@@ -400,7 +400,7 @@ async function getQueryGeneratorTx(tx, listId, id) {
Object.assign(module.exports, {
hash,
listDTAjax,
- list,
+ listIdName,
getById,
create,
updateWithConsistencyCheck,
diff --git a/models/subscriptions.js b/models/subscriptions.js
index 21ac7592..c9b275ff 100644
--- a/models/subscriptions.js
+++ b/models/subscriptions.js
@@ -1,24 +1,149 @@
'use strict';
const knex = require('../lib/knex');
+const hasher = require('node-object-hash')();
+const shortid = require('shortid');
const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares');
const fields = require('./fields');
-const { SubscriptionStatus } = require('../shared/lists');
+const { SubscriptionStatus, getFieldKey } = require('../shared/lists');
const segments = require('./segments');
+const { enforce, filterObject } = require('../lib/helpers');
+const moment = require('moment');
+
+const allowedKeysBase = new Set(['email', 'tz', 'is_test', 'status']);
+
+function getTableName(listId) {
+ return `subscription__${listId}`;
+}
+
+async function getGroupedFieldsMap(tx, listId) {
+ const groupedFields = await fields.listGroupedTx(tx, listId);
+ const result = {};
+ for (const fld of groupedFields) {
+ result[getFieldKey(fld)] = fld;
+ }
+ return result;
+}
+
+function groupSubscription(groupedFieldsMap, entity) {
+ for (const fldKey in groupedFieldsMap) {
+ const fld = groupedFieldsMap[fldKey];
+ const fieldType = fields.getFieldType(fld.type);
+
+ if (fieldType.grouped) {
+
+ let value = null;
+
+ if (fieldType.cardinality === fields.Cardinality.SINGLE) {
+ for (const optionKey in fld.groupedOptions) {
+ const option = fld.groupedOptions[optionKey];
+
+ if (entity[option.column]) {
+ value = option.column;
+ }
+
+ delete entity[option.column];
+ }
+
+ } else {
+ value = [];
+ for (const optionKey in fld.groupedOptions) {
+ const option = fld.groupedOptions[optionKey];
+
+ if (entity[option.column]) {
+ value.push(option.column);
+ }
+
+ delete entity[option.column];
+ }
+ }
+
+ entity[fldKey] = value;
+
+ } else if (fieldType.enumerated) {
+ // This is enum-xxx type. We just make sure that the options we give out match the field settings.
+ // If the field settings gets changed, there can be discrepancies between the field and the subscription data.
+
+ const allowedKeys = new Set(fld.settings.options.map(x => x.key));
+
+ if (!allowedKeys.has(entity[fldKey])) {
+ entity[fldKey] = null;
+ }
+ }
+ }
+}
+
+function ungroupSubscription(groupedFieldsMap, entity) {
+ for (const fldKey in groupedFieldsMap) {
+ const fld = groupedFieldsMap[fldKey];
+
+ const fieldType = fields.getFieldType(fld.type);
+ if (fieldType.grouped) {
+
+ if (fieldType.cardinality === fields.Cardinality.SINGLE) {
+ const value = entity[fldKey];
+ for (const optionKey in fld.groupedOptions) {
+ const option = fld.groupedOptions[optionKey];
+ entity[option.column] = option.column === value;
+ }
+
+ } else {
+ const values = entity[fldKey];
+ for (const optionKey in fld.groupedOptions) {
+ const option = fld.groupedOptions[optionKey];
+ entity[option.column] = values.includes(option.column);
+ }
+ }
+
+ delete entity[fldKey];
+
+ } else if (fieldType.enumerated) {
+ // This is enum-xxx type. We just make sure that the options we give out match the field settings.
+ // If the field settings gets changed, there can be discrepancies between the field and the subscription data.
+
+ const allowedKeys = new Set(fld.settings.options.map(x => x.key));
+
+ if (!allowedKeys.has(entity[fldKey])) {
+ entity[fldKey] = null;
+ }
+ }
+ }
+}
-const allowedKeysBase = new Set(['cid', 'email']);
-
-function hash(entity) {
- const allowedKeys = allowedKeysBase.slice();
-
- // TODO add keys from custom fields
+function getAllowedKeys(groupedFieldsMap) {
+ return new Set([
+ ...allowedKeysBase,
+ ...Object.keys(groupedFieldsMap)
+ ]);
+}
+function hashByAllowedKeys(allowedKeys, entity) {
return hasher.hash(filterObject(entity, allowedKeys));
}
+async function hashByList(listId, entity) {
+ return await knex.transaction(async tx => {
+ const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
+ const allowedKeys = getAllowedKeys(groupedFieldsMap);
+ return hashByAllowedKeys(allowedKeys, entity);
+ });
+}
+
+async function getById(context, listId, id) {
+ return await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
+
+ const entity = await tx(getTableName(listId)).where('id', id).first();
+
+ const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
+ groupSubscription(groupedFieldsMap, entity);
+
+ return entity;
+ });
+}
async function listDTAjax(context, listId, segmentId, params) {
return await knex.transaction(async tx => {
@@ -31,13 +156,14 @@ async function listDTAjax(context, listId, segmentId, params) {
tx,
params,
builder => {
- const query = builder.from(`subscription__${listId}`);
+ const query = builder.from(getTableName(listId));
query.where(function() {
addSegmentQuery(this);
});
return query;
},
['id', 'cid', 'email', 'status', 'created', ...flds.map(fld => fld.column)]
+ // FIXME - adapt data in custom columns to render them properly
);
});
}
@@ -46,12 +172,189 @@ async function list(context, listId) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
- return await tx(`subscription__${listId}`);
+ const entities = await tx(getTableName(listId));
+
+ const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
+
+ for (const entity of entities) {
+ groupSubscription(groupedFieldsMap, entity);
+ }
+
+ return entities;
+ });
+}
+
+async function serverValidate(context, listId, data) {
+ return await knex.transaction(async tx => {
+ const result = {};
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
+
+ if (data.email) {
+ const existingKeyQuery = tx(getTableName(listId)).where('email', data.email);
+
+ if (data.id) {
+ existingKeyQuery.whereNot('id', data.id);
+ }
+
+ const existingKey = await existingKeyQuery.first();
+ result.key = {
+ exists: !!existingKey
+ };
+ }
+
+ return result;
+ });
+}
+
+async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCreate) {
+ enforce(entity.email, 'Email must be set');
+
+ const existingWithKeyQuery = tx(getTableName(listId)).where('email', entity.email);
+
+ if (!isCreate) {
+ existingWithKeyQuery.whereNot('id', entity.id);
+ }
+ const existingWithKey = await existingWithKeyQuery.first();
+ if (existingWithKey) {
+ throw new interoperableErrors.DuplicitEmailError();
+ }
+
+ enforce(entity.status >= 0 && entity.status < SubscriptionStatus.MAX, 'Invalid status');
+
+ for (const key in groupedFieldsMap) {
+ const fld = groupedFieldsMap[key];
+ if (fld.type === 'date' || fld.type === 'birthday') {
+ entity[getFieldKey(fld)] = moment(entity[getFieldKey(fld)]).toDate();
+ }
+ }
+}
+
+async function create(context, listId, entity) {
+ return await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
+
+ const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
+ const allowedKeys = getAllowedKeys(groupedFieldsMap);
+
+ await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, true);
+
+ const filteredEntity = filterObject(entity, allowedKeys);
+ filteredEntity.cid = shortid.generate();
+ filteredEntity.status_change = new Date();
+
+ ungroupSubscription(groupedFieldsMap, filteredEntity);
+
+ // FIXME - process:
+ // filteredEntity.opt_in_ip =
+ // filteredEntity.opt_in_country =
+ // filteredEntity.imported =
+
+ const ids = await tx(getTableName(listId)).insert(filteredEntity);
+ const id = ids[0];
+
+ if (entity.status === SubscriptionStatus.SUBSCRIBED) {
+ await tx('lists').where('id', listId).increment('subscribers', 1);
+ }
+
+ return id;
+ });
+}
+
+async function updateWithConsistencyCheck(context, listId, entity) {
+ await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
+
+ const existing = await tx(getTableName(listId)).where('id', entity.id).first();
+ if (!existing) {
+ throw new interoperableErrors.NotFoundError();
+ }
+
+ const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
+ const allowedKeys = getAllowedKeys(groupedFieldsMap);
+
+ groupSubscription(groupedFieldsMap, existing);
+
+ const existingHash = hashByAllowedKeys(allowedKeys, existing);
+ if (existingHash !== entity.originalHash) {
+ throw new interoperableErrors.ChangedError();
+ }
+
+ await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, false);
+
+ const filteredEntity = filterObject(entity, allowedKeys);
+
+ ungroupSubscription(groupedFieldsMap, filteredEntity);
+
+ if (existing.status !== entity.status) {
+ filteredEntity.status_change = new Date();
+ }
+
+ await tx(getTableName(listId)).where('id', entity.id).update(filteredEntity);
+
+
+ let countIncrement = 0;
+ if (existing.status === SubscriptionStatus.SUBSCRIBED && entity.status !== SubscriptionStatus.SUBSCRIBED) {
+ countIncrement = -1;
+ } else if (existing.status !== SubscriptionStatus.SUBSCRIBED && entity.status === SubscriptionStatus.SUBSCRIBED) {
+ countIncrement = 1;
+ }
+
+ if (countIncrement) {
+ await tx('lists').where('id', listId).increment('subscribers', countIncrement);
+ }
+ });
+}
+
+async function removeTx(tx, context, listId, id) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
+
+ const existing = await tx(getTableName(listId)).where('id', id).first();
+ if (!existing) {
+ throw new interoperableErrors.NotFoundError();
+ }
+
+ await tx(getTableName(listId)).where('id', id).del();
+
+ if (existing.status === SubscriptionStatus.SUBSCRIBED) {
+ await tx('lists').where('id', listId).decrement('subscribers', 1);
+ }
+}
+
+async function remove(context, listId, id) {
+ await knex.transaction(async tx => {
+ await removeTx(tx, context, listId, id);
+ });
+}
+
+async function unsubscribe(context, listId, id) {
+ await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
+
+ const existing = await tx(getTableName(listId)).where('id', id).first();
+ if (!existing) {
+ throw new interoperableErrors.NotFoundError();
+ }
+
+ if (existing.status === SubscriptionStatus.SUBSCRIBED) {
+ await tx(getTableName(listId)).where('id', id).update({
+ status: SubscriptionStatus.UNSUBSCRIBED
+ });
+
+ await tx('lists').where('id', listId).decrement('subscribers', 1);
+ }
});
}
+
module.exports = {
+ hashByList,
+ getById,
list,
- listDTAjax
+ listDTAjax,
+ serverValidate,
+ create,
+ updateWithConsistencyCheck,
+ remove,
+ unsubscribe
};
\ No newline at end of file
diff --git a/routes/rest/fields.js b/routes/rest/fields.js
index f00863db..c7101c1a 100644
--- a/routes/rest/fields.js
+++ b/routes/rest/fields.js
@@ -25,6 +25,11 @@ router.getAsync('/fields/:listId', passport.loggedIn, async (req, res) => {
return res.json(rows);
});
+router.getAsync('/fields-grouped/:listId', passport.loggedIn, async (req, res) => {
+ const rows = await fields.listGrouped(req.context, req.params.listId);
+ return res.json(rows);
+});
+
router.postAsync('/fields/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await fields.create(req.context, req.params.listId, req.body);
return res.json();
diff --git a/routes/rest/segments.js b/routes/rest/segments.js
index 8106d357..b25be6ed 100644
--- a/routes/rest/segments.js
+++ b/routes/rest/segments.js
@@ -11,7 +11,7 @@ router.postAsync('/segments-table/:listId', passport.loggedIn, async (req, res)
});
router.getAsync('/segments/:listId', passport.loggedIn, async (req, res) => {
- return res.json(await segments.list(req.context, req.params.listId));
+ return res.json(await segments.listIdName(req.context, req.params.listId));
});
router.getAsync('/segments/:listId/:segmentId', passport.loggedIn, async (req, res) => {
diff --git a/routes/rest/subscriptions.js b/routes/rest/subscriptions.js
index 81215922..c117a139 100644
--- a/routes/rest/subscriptions.js
+++ b/routes/rest/subscriptions.js
@@ -10,5 +10,38 @@ router.postAsync('/subscriptions-table/:listId/:segmentId?', passport.loggedIn,
return res.json(await subscriptions.listDTAjax(req.context, req.params.listId, req.params.segmentId, req.body));
});
+router.getAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, async (req, res) => {
+ const entity = await subscriptions.getById(req.context, req.params.listId, req.params.subscriptionId);
+ entity.hash = await subscriptions.hashByList(req.params.listId, entity);
+ return res.json(entity);
+});
+
+router.postAsync('/subscriptions/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ await subscriptions.create(req.context, req.params.listId, req.body);
+ return res.json();
+});
+
+router.putAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ const entity = req.body;
+ entity.id = parseInt(req.params.subscriptionId);
+
+ await subscriptions.updateWithConsistencyCheck(req.context, req.params.listId, entity);
+ return res.json();
+});
+
+router.deleteAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ await subscriptions.remove(req.context, req.params.listId, req.params.subscriptionId);
+ return res.json();
+});
+
+router.postAsync('/subscriptions-validate/:listId', passport.loggedIn, async (req, res) => {
+ return res.json(await subscriptions.serverValidate(req.context, req.params.listId, req.body));
+});
+
+router.postAsync('/subscriptions-unsubscribe/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ await subscriptions.unsubscribe(req.context, req.params.listId, req.params.subscriptionId);
+ return res.json();
+});
+
module.exports = router;
\ No newline at end of file
diff --git a/shared/lists.js b/shared/lists.js
index f472dbed..75a5f556 100644
--- a/shared/lists.js
+++ b/shared/lists.js
@@ -15,9 +15,14 @@ const SubscriptionStatus = {
BOUNCED: 3,
COMPLAINED: 4,
MAX: 5
+};
+
+function getFieldKey(field) {
+ return field.column || 'grouped_' + field.id;
}
module.exports = {
UnsubscriptionMode,
- SubscriptionStatus
+ SubscriptionStatus,
+ getFieldKey
};
\ No newline at end of file